macroforge
v0.1.24
Published
TypeScript macro expansion engine powered by Rust and SWC
Maintainers
Readme
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
npm install macroforgeThe package includes pre-built binaries for:
- macOS (x64, arm64)
- Linux (x64, arm64)
- Windows (x64, arm64)
Quick Start
Using Built-in Macros
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
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.
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.
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.
function checkSyntax(code: string, filepath: string): SyntaxCheckResult;
interface SyntaxCheckResult {
ok: boolean;
error?: string;
}parseImportSources(code, filepath)
Extracts import information from TypeScript code.
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.
class NativePlugin {
constructor();
// Process file with version-based caching
processFile(
filepath: string,
code: string,
options?: ProcessFileOptions
): ExpandResult;
// Get position mapper for a cached file
getMapper(filepath: string): NativeMapper | null;
// 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.
class NativeMapper {
constructor(mapping: SourceMappingResult);
isEmpty(): boolean;
originalToExpanded(pos: number): number;
expandedToOriginal(pos: number): number | null;
generatedBy(pos: number): string | null;
mapSpanToOriginal(start: number, length: number): SpanResult | null;
mapSpanToExpanded(start: number, length: number): SpanResult;
isInGenerated(pos: number): boolean;
}Built-in Decorators
| Decorator | Description |
|-----------|-------------|
| @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 directory for examples.
Minimal Macro Crate
Cargo.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:
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:
| Syntax | Description |
|--------|-------------|
| @{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:
// 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
// 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:
{
"compilerOptions": {
"plugins": [
{
"name": "@macroforge/typescript-plugin"
}
]
}
}Debug API
For debugging macro registration:
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:
NativePlugincaches 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
