@macroforge/mcp-server
Version:
MCP server for Macroforge documentation and code analysis
195 lines (138 loc) • 5.91 kB
Markdown
# ts\_macro
`` registers a function-like macro: the TypeScript equivalent of Rust's ``.
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};
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 ``) does not contain it.
## Attribute Options
### name (required)
The first argument is the macro name, a bare identifier:
Rust
```
// users write $sql(...)
// users write $stringify(...)
```
### description
Documentation surfaced in IDE tooling:
Rust
```
```
### kind
`` 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
```
// default
// same as #[ts_macro_attribute]
// 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
```
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 `` 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
```
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
```
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);
```