# macroforge
> **Warning:** This is a work in progress and probably won't work for you. Use at your own risk!
TypeScript macro expansion engine powered by Rust and SWC.
## Overview
`macroforge` is a native Node.js module that enables compile-time code generation for TypeScript through a Rust-like derive macro system. It parses TypeScript using SWC, expands macros written in Rust, and outputs transformed TypeScript code with full source mapping support.
## Installation
```bash
npm install macroforge
```
The package includes pre-built binaries for:
- macOS (x64, arm64)
- Linux (x64, arm64)
- Windows (x64, arm64)
## Quick Start
### Using Built-in Macros
```typescript
import { Derive, Debug, Clone, Eq } from "macroforge";
/** @derive(Debug, Clone, Eq) */
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// After macro expansion, the class gains:
// - toString(): string (from Debug)
// - clone(): User (from Clone)
// - equals(other: User): boolean (from Eq)
```
### Programmatic API
```typescript
import { expandSync, NativePlugin } from "macroforge";
// One-shot expansion
const result = expandSync(sourceCode, "file.ts", {
keepDecorators: false
});
console.log(result.code); // Transformed TypeScript
console.log(result.diagnostics); // Any warnings/errors
console.log(result.metadata); // Macro IR metadata (JSON)
// Cached expansion (for language servers)
const plugin = new NativePlugin();
const cached = plugin.processFile("file.ts", sourceCode, {
version: "1.0.0" // Cache key
});
```
## API Reference
### Core Functions
#### `expandSync(code, filepath, options?)`
Expands macros in TypeScript code synchronously.
```typescript
function expandSync(
code: string,
filepath: string,
options?: ExpandOptions
): ExpandResult;
interface ExpandOptions {
keepDecorators?: boolean; // Keep @derive decorators in output (default: false)
}
interface ExpandResult {
code: string; // Transformed TypeScript
types?: string; // Generated .d.ts content
metadata?: string; // Macro IR as JSON
diagnostics: MacroDiagnostic[]; // Warnings and errors
sourceMapping?: SourceMappingResult; // Position mapping data
}
```
#### `transformSync(code, filepath)`
Lower-level transform that returns additional metadata.
```typescript
function transformSync(code: string, filepath: string): TransformResult;
interface TransformResult {
code: string;
map?: string; // Source map (not yet implemented)
types?: string; // Generated declarations
metadata?: string;
}
```
#### `checkSyntax(code, filepath)`
Validates TypeScript syntax without macro expansion.
```typescript
function checkSyntax(code: string, filepath: string): SyntaxCheckResult;
interface SyntaxCheckResult {
ok: boolean;
error?: string;
}
```
#### `parseImportSources(code, filepath)`
Extracts import information from TypeScript code.
```typescript
function parseImportSources(code: string, filepath: string): ImportSourceResult[];
interface ImportSourceResult {
local: string; // Local identifier name
module: string; // Module specifier
}
```
### Classes
#### `NativePlugin`
Stateful plugin with caching for language server integration.
```typescript
class NativePlugin {
constructor();
// Process file with version-based caching
processFile(
filepath: string,
code: string,
options?: ProcessFileOptions
): ExpandResult;
// Get position mapper for a cached file
// Map diagnostics from expanded to original positions
mapDiagnostics(filepath: string, diags: JsDiagnostic[]): JsDiagnostic[];
// Logging utilities
log(message: string): void;
setLogFile(path: string): void;
}
```
#### `NativeMapper` / `PositionMapper`
Maps positions between original and expanded code.
```typescript
class NativeMapper {
constructor(mapping: SourceMappingResult);
isEmpty(): boolean;
originalToExpanded(pos: number): number;
mapSpanToOriginal(start: number, length: number): SpanResult | null;
mapSpanToExpanded(start: number, length: number): SpanResult;
isInGenerated(pos: number): boolean;
}
```
### Built-in Decorators
| `@Derive(...features)` | Class decorator that triggers macro expansion |
| `@Debug` | Generates `toString(): string` method |
| `@Clone` | Generates `clone(): T` method |
| `@Eq` | Generates `equals(other: T): boolean` method |
## Writing Custom Macros
Custom macros are written in Rust and compiled to native Node.js addons. See the [playground/macro](../../playground/macro) directory for examples.
### Minimal Macro Crate
**Cargo.toml:**
```toml
[package]
name = "my-macros"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
macroforge_ts = "0.1.0"
napi = { version = "3.5.2", features = ["napi8", "compat-mode"] }
napi-derive = "3.3.3"
[build-dependencies]
napi-build = "2.3.1"
```
**src/lib.rs:**
```rust
use macroforge_ts::macros::{ts_macro_derive, body};
use macroforge_ts::ts_syn::{
Data, DeriveInput, MacroforgeError, TsStream, parse_ts_macro_input,
};
#[ts_macro_derive(
JSON,
description = "Generates toJSON() returning a plain object"
)]
pub fn derive_json(mut input: TsStream) -> Result<TsStream, MacroforgeError> {
let input = parse_ts_macro_input!(input as DeriveInput);
match &input.data {
Data::Class(class) => {
Ok(body! {
toJSON(): Record<string, unknown> {
return {
{#for field in class.field_names()}
@{field}: this.@{field},
{/for}
};
}
})
}
_ => Err(MacroforgeError::new(
input.decorator_span(),
"@derive(JSON) only works on classes",
)),
}
}
```
### Template Syntax
The `ts_template!` and `body!` macros support:
| `@{expr}` | Interpolate Rust expression as identifier/code |
| `{#for x in iter}...{/for}` | Loop over iterables |
| `{$let name = expr}` | Local variable binding |
| `{#if cond}...{/if}` | Conditional blocks |
### Re-exported Crates
`macroforge_ts` re-exports everything needed for macro development:
```rust
// TypeScript syntax types
use macroforge_ts::ts_syn::*;
// Macro attributes and quote templates
use macroforge_ts::macros::{ts_macro_derive, body, ts_template, above, below};
// SWC modules (for advanced use)
use macroforge_ts::swc_core;
use macroforge_ts::swc_common;
use macroforge_ts::swc_ecma_ast;
```
## Integration
### Vite Plugin
```typescript
// vite.config.ts
import macroforge from "@macroforge/vite-plugin";
export default defineConfig({
plugins: [
macroforge({
typesOutputDir: ".macroforge/types",
metadataOutputDir: ".macroforge/meta",
generateTypes: true,
emitMetadata: true,
}),
],
});
```
### TypeScript Plugin
Add to `tsconfig.json` for IDE support:
```json
{
"compilerOptions": {
"plugins": [
{
"name": "@macroforge/typescript-plugin"
}
]
}
}
```
## Debug API
For debugging macro registration:
```typescript
import {
__macroforgeGetManifest,
__macroforgeGetMacroNames,
__macroforgeIsMacroPackage,
__macroforgeDebugDescriptors,
__macroforgeDebugLookup,
} from "macroforge";
// Get all registered macros
const manifest = __macroforgeGetManifest();
console.log(manifest.macros);
// Check if current package exports macros
console.log(__macroforgeIsMacroPackage());
// List macro names
console.log(__macroforgeGetMacroNames());
// Debug lookup
console.log(__macroforgeDebugLookup("@my/macros", "JSON"));
```
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Node.js / Vite │
├─────────────────────────────────────────────────────────┤
│ NAPI-RS Bindings │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ ts_syn │ │ ts_quote │ │ts_macro_derive│ │
│ │ (parsing) │ │ (templating) │ │ (proc-macro) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
├─────────────────────────────────────────────────────────┤
│ SWC Core │
│ (TypeScript parsing & codegen) │
└─────────────────────────────────────────────────────────┘
```
## Performance
- **Thread-safe expansion**: Each expansion runs in an isolated thread with a 32MB stack to handle deep AST recursion
- **Caching**: `NativePlugin` caches expansion results by file version
- **Binary search**: Position mapping uses O(log n) lookups
- **Zero-copy parsing**: SWC's arena allocator minimizes allocations
## License
MIT