API Stability Notice
Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.
Command Line Interface
macroforge v0.5.0
Command-line interface for expanding Macroforge macros
Installation
The CLI is a Rust binary. You can install it using Cargo:
cargo install macroforge_tsOr build from source:
git clone https://gitlab.com/macroforge-ts/macroforge-ts.git
cd macroforge-ts/crates
cargo install --path macroforge_ts
# The binary is installed globallyCommands
Each command works on a project: the nearest directory at or above its
target that holds a macroforge.config.*, package.json, deno.json or deno.jsonc. The target is the file or directory given to expand, otherwise the current directory. The project's state
lives in its .macroforge/ directory. expand on a file outside any project expands it on its
own and writes no state; the other commands report that no project was found.
macroforge expand
Expands macros in a TypeScript file and outputs the transformed code.
macroforge expand <input> [options]Arguments
| Argument | Description |
|---|---|
<input> | Path to the TypeScript or TSX file to expand |
Options
| Option | Description |
|---|---|
--out <path> | Write the expanded JavaScript/TypeScript to a file |
--types-out <path> | Write the generated .d.ts declarations to a file |
--print | Print output to stdout even when --out is specified |
--scan | Scan directory for TypeScript files with macros |
--include-ignored | Include files ignored by .gitignore when scanning |
-q, --quiet | Suppress output when no macros are found |
Examples
Expand a file (writes a sibling src/user.expanded.ts):
macroforge expand src/user.tsPrint the expansion to stdout instead:
macroforge expand src/user.ts --printExpand and write to a file:
macroforge expand src/user.ts --out dist/user.jsExpand with both runtime output and type declarations:
macroforge expand src/user.ts --out dist/user.js --types-out dist/user.d.tsnode_modules via FFI,
so run the CLI from your project root when your code uses them.macroforge tsc
Runs TypeScript type checking with macro expansion. This wraps tsc --noEmit and expands macros before type checking, so your generated methods are properly
type-checked.
macroforge tsc [options]Options
| Option | Description |
|---|---|
-p, --project <path> | Path to tsconfig.json (defaults to tsconfig.json in current directory) |
Examples
Type check with default tsconfig.json:
macroforge tscType check with a specific config:
macroforge tsc -p tsconfig.build.jsonmacroforge svelte-check
Runs svelte-check with macro expansion, so Svelte components using
macros are properly type-checked.
macroforge svelte-check [options]Options
| Option | Description |
|---|---|
--workspace <path> | Workspace directory (defaults to current directory) |
--tsconfig <path> | Path to tsconfig.json |
--output <format> | Output format: human, human-verbose, machine, machine-verbose |
--fail-on-warnings | Exit with error on warnings (not just errors) |
macroforge svelte-package
Runs svelte-package with macro expansion, so a published Svelte library
ships fully expanded source and type declarations.
macroforge svelte-package [options]Options
| Option | Description |
|---|---|
-i, --input <path> | Source directory (defaults to src/lib) |
-o, --output <path> | Output directory (defaults to dist) |
--tsconfig <path> | Path to tsconfig.json |
--no-types | Skip generating type declarations |
--full-rebuild | Ignore the previous build and repackage everything |
Incremental builds
Packaging is incremental. Each run records what it consumed and produced
under .macroforge/svelte-package/, and a run whose inputs all match
the previous one exits without repackaging. When a rebuild is needed, only
the files that changed are re-expanded. The rest keep the expanded output
from last time.
A file that differs only in formatting (trailing whitespace, runs of blank
lines) does not count as a change. Note that .ts is transpiled
on the way into the package so its layout is discarded anyway, but .svelte and .js are copied through verbatim: a formatting-only edit to
those will not reach the package until the next real change or a --full-rebuild.
Any of these forces a full rebuild on its own:
- a changed macroforge version,
macroforge.config.*, or external macro binary - a changed
svelte.config.*,package.json, or tsconfig - a changed
@sveltejs/package,macroforge, or@macroforge/svelte-preprocessorversion - different command-line options
- a changed project source outside the input directory
- an output directory that was deleted or modified behind the CLI's back
A locally rebuilt linked package whose version did not change is the one
thing this cannot see; --full-rebuild is the escape hatch. macroforge refresh also discards the build state along with the expansion
cache.
Expansion failures fail the build. A module that cannot be expanded has no correct packaged form, and shipping its unexpanded source publishes a library whose generated runtime is silently missing.
macroforge watch
Watches source files and maintains the macro expansion cache, keeping it up to date as files change.
macroforge watch [root] [options]Options
| Option | Description |
|---|---|
--debounce-ms <ms> | Debounce interval in milliseconds (default: 100) |
macroforge cache
Builds the .macroforge/cache directory once for all source files.
Useful for CI or pre-build steps.
macroforge cache [root] [options]macroforge refresh
Deletes and rebuilds the macro cache from scratch.
macroforge refresh [root] [options]macroforge init
Declares #macroforge/config in the project's package.json, or its deno.json when there is no package.json. Generated code imports
foreign-type handlers from that specifier. See Expanded Config.
macroforge init [root]macroforge sync
Writes the generated files a type check reads, without expanding anything: the type and
declarative registries under .macroforge/ and the expanded config under .macroforge/config/. Run it before tsc or svelte-check in
CI, as a SvelteKit project runs svelte-kit sync.
macroforge sync [root]macroforge build
Builds a macro crate to WebAssembly with wasm-bindgen and
post-processes the output to add $-prefixed re-exports for
function-like (Call) macros. Used when distributing your own macro packages.
macroforge build [crate_dir] [options]Steps:
cargo build --release --target wasm32-unknown-unknown- Runs
wasm-bindgen --target nodejsintopkg/(or the directory given via-o) - Parses the generated
.d.tsto discover Call macros and appendsexport { state as $state }-style aliases so consumers can import both forms.
Options
| Option | Description |
|---|---|
[crate_dir] | Path to the macro crate (defaults to .) |
-o, --out <out> | Output directory for the WASM package (defaults to <crate_dir>/pkg) |
Examples
# Build the current macro crate
macroforge build
# Build a specific crate into a custom directory
macroforge build ./packages/my-macros -o dist/wasmOutput Format
Expanded Code
When expanding a file like this:
/** @derive(Debug) */
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}The CLI outputs the expanded code with the generated methods:
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
[Symbol.for("nodejs.util.inspect.custom")](): string {
return `User { name: ${this.name}, age: ${this.age} }`;
}
}Diagnostics
Errors and warnings are printed to stderr in a readable format:
[macroforge] error at src/user.ts:5:1: Unknown derive macro: InvalidMacro
[macroforge] warning at src/user.ts:10:3: Field 'unused' is never usedUse Cases
CI/CD Type Checking
Use macroforge tsc in your CI pipeline to type-check with macro expansion:
# package.json
{
"scripts": {
"typecheck": "macroforge tsc"
}
}Debugging Macro Output
Use macroforge expand to inspect what code your macros generate:
macroforge expand src/models/user.ts --print | lessBuild Pipeline
Generate expanded files as part of a custom build:
#!/bin/bash
for file in src/**/*.ts; do
outfile="dist/$(basename "$file" .ts).js"
macroforge expand "$file" --out "$outfile"
done