UNPKG

@macroforge/mcp-server

Version:

MCP server for Macroforge documentation and code analysis

171 lines (127 loc) • 4.97 kB
# ts\_macro\_attribute `#[ts_macro_attribute]` registers an attribute macro — the analog of Rust's `#[proc_macro_attribute]`. Users invoke it with a JSDoc `@name` decorator on a declaration; the macro rewrites the entire declaration. ## Basic Syntax Rust ``` use macroforge_ts::macros::ts_macro_attribute; use macroforge_ts::ts_syn::{     MacroforgeError, Patch, TargetIR, TsStream, }; #[ts_macro_attribute(traced, description = "Count calls to the decorated function")] pub fn traced_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {     let ctx = input         .context()         .ok_or_else(|| MacroforgeError::new_global("no macro context"))?;     let TargetIR::Function(f) = &ctx.target else {         return Err(MacroforgeError::new_global(             "@traced can only be applied to functions",         ));     };     let full = input.source();     let open = full.find('{').unwrap();     let close = full.rfind('}').unwrap();     let sig = &full[..open];     let body = &full[open + 1..close].trim();     let replacement = format!(         "{sig}{{\\n    (globalThis as any).__traced ??= {{}};\\n    \\          (globalThis as any).__traced[{name:?}] = \\          ((globalThis as any).__traced[{name:?}] || 0) + 1;\\n    {body}\\n}}",         sig = sig,         name = f.name,         body = body,     );     let mut out = TsStream::from_string(String::new());     out.runtime_patches.push(Patch::Replace {         span: f.span,         code: replacement,         source_macro: Some("traced".to_string()),     });     Ok(out) } ``` ## Call Site Consumers decorate a declaration with a JSDoc `@name`: TypeScript ``` /** import macro { traced } from "@my/macro-package" */ /** @traced */ export function add(a: number, b: number): number {     return a + b; } ``` ## Patch-Based Output Warning Attribute macros must emit their rewrite as an explicit `Patch::Replace` over the target span. Unlike derive macros, the expander does **not** auto-convert a returned `TsStream`'s tokens into patches for attribute macros — your macro is responsible for describing the edit. Minimal pattern: Rust ``` let ctx = input.context().ok_or_else(|| ...)?; let TargetIR::Function(f) = &ctx.target else { ... }; // Build the new declaration text... let replacement = format!("{sig}{{ /* new body */ }}", sig = signature); // Emit a Replace patch covering the function's full span: let mut out = TsStream::from_string(String::new()); out.runtime_patches.push(Patch::Replace {     span: f.span,     code: replacement,     source_macro: Some("my_macro".to_string()), }); Ok(out) ``` ## Target Kinds Attribute macros can be applied to any top-level declaration. `ctx.target` is a `TargetIR` enum; the structured `IR` types give you access to names, spans, parameters, fields, etc. | Decoration | TargetIR variant | | ---------- | ---------------------------------- | | Function | `TargetIR::Function(FunctionIR)` | | Class | `TargetIR::Class(ClassIR)` | | Interface | `TargetIR::Interface(InterfaceIR)` | | Enum | `TargetIR::Enum(EnumIR)` | | Type alias | `TargetIR::TypeAlias(TypeAliasIR)` | ## Attribute Options ### name (required) Rust ``` #[ts_macro_attribute(reactive)]   // users write /** @reactive */ #[ts_macro_attribute(traced)]     // users write /** @traced */ ``` ### description Rust ``` #[ts_macro_attribute(     reactive,     description = "Add reactivity tracking to a function" )] ``` ## No-op Exports Each `#[ts_macro_attribute]` auto-generates a no-op void export in the WASM build so TypeScript can resolve the decorator name during type-checking: TypeScript ``` // Generated automatically for #[ts_macro_attribute(traced)] export function traced(): void; ``` ## Example Walkthrough The `@traced` macro in `tooling/playground/macro/src/attrs.rs` wraps a function so every call increments a counter on `globalThis.__traced[fnName]`: TypeScript ``` /** import macro { traced } from "@playground/macro" */ /** @traced */ export function add(a: number, b: number): number {     return a + b; } // Expanded output: export function add(a: number, b: number): number {     (globalThis as any).__traced ??= {};     (globalThis as any).__traced["add"] =         ((globalThis as any).__traced["add"] || 0) + 1;     return a + b; } ``` The wrapper preserves the original signature (including `export`, `async`, parameters, and return type) and only injects new statements at the top of the body.