macroforge_ts 0.1.12

TypeScript macro expansion engine - write compile-time macros in Rust
Documentation

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 macroforge

The 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: 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