UNPKG

@macroforge/mcp-server

Version:

MCP server for Macroforge documentation and code analysis

161 lines (120 loc) • 6.04 kB
# Buildtime Evaluation `@buildtime` runs a piece of TypeScript **during the build** and replaces it with its result. Reading a schema file, hashing a manifest, or stamping a build time all become build-time constants rather than runtime work. ```typescript import { buildtime } from '@macroforge/core/buildtime'; /** @buildtime */ const BUILT_AT = buildtime.time.iso(); /** @buildtime */ const SCHEMA = buildtime.fs.readJson('./schema.json'); ``` After expansion the imports and the `buildtime` calls are gone, and the output contains only literals: ```typescript const BUILT_AT = '2026-07-21T10:30:00.000Z'; const SCHEMA = { 'version': 1, 'fields': [] }; ``` Evaluation happens in a sandboxed JavaScript engine (Boa). Like Zig's `comptime`, build-time code is pure apart from what you grant it: it can read the files and environment variables you allow, and it can never write files or reach the network. ## The three tiers What `@buildtime` does depends on what it's attached to. ### Tier 1: `const` expression The expression is evaluated and its result encoded to a TypeScript literal. ```typescript /** @buildtime */ const VERSION_HASH = buildtime.crypto.sha256(buildtime.fs.readText('./package.json')); ``` ### Tier 2: function The function is called with no arguments. If it returns a **string**, that string is spliced into the output verbatim as source code, which is how you generate declarations. Any other return value is serialized as `const NAME = <literal>`. ```typescript /** @buildtime */ function generateGuards() { const schema = buildtime.fs.readJson('./schema.json') as { fields: Field[] }; return schema.fields .map((f) => `export function is${f.name}(v: unknown): v is ${f.type} { return typeof v === "${f.type}"; }` ) .join('\n'); } ``` ### Tier 3: `type` alias The expression must return a **string containing TypeScript type syntax**. Returning anything else is an error. ```typescript /** @buildtime */ type SchemaKeys = (() => { const schema = buildtime.fs.readJson("./schema.json") as { fields: { name: string }[] }; return schema.fields.map((f) => JSON.stringify(f.name)).join(" | "); })(); ``` Declarations must be **top-level**. `export` visibility is preserved. ## API | Namespace | Members | | -------------------- | ------------------------------------------------------------------- | | `buildtime.fs` | `readText(path)`, `readJson(path)`, `exists(path)`, `listDir(path)` | | `buildtime.crypto` | `sha256(input)`, `sha512(input)` | | `buildtime.time` | `now()`, `unix()`, `iso()` | | `buildtime.location` | `file`, `line`, `column` | | `buildtime.env` | Allow-listed environment variables | | `buildtime.flags` | `has(name)`, `get(name)` | The same object is also exported as `$buildtime`. Calling any of these outside a build pass throws. ## Rebuild invalidation Every file read through `buildtime.fs` is recorded and returned as `buildtimeDependencies` on the expansion result. The Vite plugin uses this to re-expand a module when a file it read at build time changes, so editing `schema.json` re-runs the generator without a manual restart. ## Sandbox configuration Build-time code runs sandboxed. Configure it with a `buildtime` block: ```typescript // macroforge.config.ts export default { buildtime: { capabilities: { timeout: 5000, // ms maxHeap: 256, // MiB (advisory) filesystem: { read: ['src/**'] }, env: ['NODE_ENV', 'CI'] }, flags: { RELEASE: '1', CHANNEL: 'beta' } } }; ``` Capability keys may also be written flat (`buildtime.timeout`) if you prefer a shorter config; the nested form is canonical because it's the path sandbox diagnostics point you at. | Option | Default | Effect | | ----------------- | -------- | ------------------------------------------------------------------------------------------------- | | `timeout` | `5000` | Wall-clock budget in ms, **enforced**: an infinite loop fails the build rather than hanging it | | `maxHeap` | `256` | Heap ceiling in MiB. Advisory: Boa exposes no memory-limit hook, so it's carried but not enforced | | `filesystem.read` | `["**"]` | Globs readable via `buildtime.fs` | | `env` | `[]` | Names exposed as `buildtime.env.NAME`; anything not listed is `undefined` | | `flags` | `{}` | Values returned by `buildtime.flags` | A partial block only overrides what it names. Omitting `buildtime` entirely keeps every default. There is no write or network capability to grant: a config that sets `filesystem.write` or `network` is rejected. ## Environment variables `buildtime.env` is deny-by-default: a variable is only readable when you name it in `env`. ```typescript // macroforge.config.ts export default { buildtime: { capabilities: { env: ['NODE_ENV'] } } }; ``` ```typescript /** @buildtime */ const MODE = buildtime.env.NODE_ENV ?? 'development'; ``` ## Build flags `flags` gives you a build-time switch that doesn't depend on the process environment: ```typescript // macroforge.config.ts export default { buildtime: { flags: { CHANNEL: 'beta' } } }; ``` ```typescript /** @buildtime */ const CHANNEL = buildtime.flags.get('CHANNEL') ?? 'stable'; /** @buildtime */ const IS_BETA = buildtime.flags.has('CHANNEL'); ``` `get()` returns `undefined` for an unset flag, and `has()` returns `false`. Flag values are strings; booleans and numbers written in config are stringified.