UNPKG

@macroforge/mcp-server

Version:

MCP server for Macroforge documentation and code analysis

195 lines (138 loc) • 5.91 kB
# ts\_macro `#[ts_macro]` registers a function-like macro: the TypeScript equivalent of Rust's `#[proc_macro]`. Users invoke it as `$name(args)` in their source; the macro receives the argument text as a `TsStream` and emits a replacement `TsStream` that takes over the call site. ## Basic Syntax Rust ``` use macroforge_ts::macros::ts_macro; use macroforge_ts::ts_syn::{MacroforgeError, TsStream}; #[ts_macro(stringify, description = "Quote the argument source as a string literal")] pub fn stringify_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {     let src = input.source().trim();     Ok(TsStream::from_string(format!("\\"{src}\\""))) } ``` ## Call Site Consumers invoke the macro with a `$` prefix: TypeScript ``` /** import macro { $stringify } from "@my/macro-package" */ export const quoted = $stringify(1 + 2 * 3); //    ↓ expands to export const quoted = "1 + 2 * 3"; ``` Info The `$` prefix at call sites distinguishes macro invocations from regular function calls. The prefix is purely syntactic: the macro name itself (as declared in `#[ts_macro(name)]`) does not contain it. ## Attribute Options ### name (required) The first argument is the macro name, a bare identifier: Rust ``` #[ts_macro(sql)]              // users write $sql(...) #[ts_macro(stringify)]        // users write $stringify(...) ``` ### description Documentation surfaced in IDE tooling: Rust ``` #[ts_macro(     sql,     description = "Build-time SQL validation" )] ``` ### kind `#[ts_macro]` defaults to `kind = "call"`. You can change the kind explicitly (for example to share the same macro in multiple positions), but `"call"` is the natural default. Rust ``` #[ts_macro(name, kind = "call")]   // default #[ts_macro(name, kind = "attribute")]  // same as #[ts_macro_attribute] #[ts_macro(name, kind = "derive")]     // same as #[ts_macro_derive] ``` ## Input and Output The input `TsStream` contains the raw source text of the arguments between the parentheses. Access it via `input.source()`: Rust ``` #[ts_macro(concat_names)] pub fn concat_names_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {     let src = input.source();                    // e.g. "foo, bar"     let (left, right) = src.split_once(',').ok_or_else(|| {         MacroforgeError::new_global("expected two args")     })?;     Ok(TsStream::from_string(format!(         "\\"{}_{}\\"",         left.trim(),         right.trim()     ))) } ``` Call-macro expansion works by replacing the entire `$name(...)` span with the returned stream's source. The engine inlines the output verbatim, with no auto-wrapping. ## What a Call Macro Sees `input.source()` is the text between the parentheses. The context is minimal: its target is `TargetIR::Other`, and the file name, config and type registry are empty, so a call macro works from its arguments alone. Imports the macro adds with `add_import` and its siblings are written at the top of the file, like a derive's; see [Output and Imports](../../docs/custom-macros/output#imports). Its warnings and errors are reported at the call. ## Nested Macro Calls Expansion runs outside-in, as in Rust. A macro receives its arguments unexpanded, so `$stringify($concat_names(a, b))` sees the text `$concat_names(a, b)`. Macro calls in a macro's output are expanded afterwards, resolved where the original call was written, so a macro can emit another macro's call and have it expand. Output that keeps emitting calls stops with an error after 64 levels. A macro that declares a name at file level, such as a type its output refers to, should derive that name from `input.context().expansion_id` rather than from a source position. The id is unique within the file, including expansions of calls found in another macro's output, which have no position of their own. ## No-op Exports Each `#[ts_macro]` auto-generates a no-op identity function in the WASM build so consumers can type-check their code before expansion: TypeScript ``` // Generated automatically for #[ts_macro(stringify)] export function stringify(value: any): any; // The \`macroforge build\` CLI also appends: export { stringify as $stringify }; ``` Consumers can import the `$`-prefixed alias from the generated package and both the runtime (as a passthrough) and the build-time expansion will work. ## Importing Call Macros in Consumer Code Callable macros from external packages are advertised to the expander via the `import macro` JSDoc comment, including the `$` prefix: TypeScript ``` /** import macro { $stringify, $concat_names } from "@my/macros" */ const s = $stringify(1 + 2 * 3); const k = $concat_names(user, name); ``` Note Unlike the declarative `macroRules` system, proc macros ship inside a Rust crate compiled to WebAssembly. Publish the crate with `macroforge build` so the `$` aliases are exported correctly. ## Complete Examples ### $stringify: quote source text Rust ``` #[ts_macro(stringify)] pub fn stringify_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {     let src = input.source().trim();     let escaped = src         .replace('\\\\', "\\\\\\\\")         .replace('"', "\\\\\\"");     Ok(TsStream::from_string(format!("\\"{escaped}\\""))) } ``` ### $state: Svelte-runes-style reactive signal Rust ``` #[ts_macro(state)] pub fn state_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {     let args = input.source().trim();     Ok(TsStream::from_string(format!("createSignal({})", args))) } // Call site: let count = $state(0); // Expanded:  let count = createSignal(0); ```