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.

Attribute Macros

Attribute macros are JSDoc annotations that transform or gate a declaration, rather than generating methods for it. They’re configured from macroforge.config.ts and run as a pre-pass, before declarative and derive macros.

They apply to top-level function, const, type and class declarations.

/** @cfg({ feature: "experimental" }) */
export function experimentalApi() { }

/** @deprecated("Use parseV2 instead", { since: "0.2.0" }) */
export function parse() { }

/** @mustUse("Ignoring the result discards validation errors") */
export function validate(input: string) { }

/** @nonExhaustive */
export type Status = "active" | "inactive";

@cfg — conditional compilation

@cfg({ … }) keeps a declaration only when its predicate matches. All keys must match (they’re ANDed together).

/** @cfg({ feature: "beta" }) */
export function betaOnly() { }

/** @cfg({ target: "node", debugAssertions: true }) */
export function nodeDebugHelper() { }
KeyMatches against
featureMembership in cfg.features
targetExact match with cfg.target
debugAssertionsBoolean match with cfg.debugAssertions
anything elseExact match against cfg.custom[key]

On a match, the annotation’s JSDoc comment is stripped and the declaration is kept. On a mismatch, the entire declaration is deleted — not just commented out. Downstream code referencing it will fail to compile, which is the intended behavior for a feature gate.

The predicate accepts JS-ish literals: single quotes, unquoted identifier keys, and trailing commas are all fine.

@deprecated — mark and optionally fail

/** @deprecated("Use parseV2 instead", { since: "0.2.0" }) */
export function parse() { }

The annotation is rewritten to a plain /** @deprecated … */ comment, which is what makes TypeScript itself surface the strikethrough and warning in editors.

With failOnUse enabled, using the symbol becomes a hard macro-expansion error instead of an editor hint.

runtimeWarn does nothing today. It defaults to true and is parsed, but no runtime console.warn is injected. Don’t rely on it.

@mustUse — flag discarded results

Emits an error diagnostic when a call’s return value is thrown away.

/** @mustUse("Ignoring this discards validation errors") */
export function validate(input: string): Result { }

validate(x);              // error: unused result
const r = validate(x);    // fine

The current implementation has real limits, worth knowing before you rely on it:

  • Same-file call sites only.
  • Top-level expression statements only — it does not descend into function bodies.
  • Method calls are not caught. obj.validate() is missed; only bare function calls are checked.

@nonExhaustive — prevent exhaustive narrowing

Brands a type alias so consumers can’t write an exhaustive switch that breaks when you add a variant:

/** @nonExhaustive */
export type Status = "active" | "inactive";

becomes

export type Status = ("active" | "inactive") & {
  readonly __nonExhaustive: unique symbol;
};

It applies to type aliases only — using it on anything else is an error. The brand field name is configurable.

Configuration

// macroforge.config.ts
export default {
  cfg: {
    features: ["beta", "experimental"],
    target: "node",
    debugAssertions: false,
    custom: { tier: "pro" }
  },
  deprecated: {
    runtimeWarn: true,   // parsed, currently no effect
    failOnUse: false
  },
  mustUse: { mode: "lint" },
  nonExhaustive: { brand: "__nonExhaustive" }
};
OptionTypeDefault
cfg.featuresstring \| string[][]
cfg.targetstringnone
cfg.debugAssertionsbooleanfalse
cfg.customobject{}
deprecated.runtimeWarnbooleantrue (no effect)
deprecated.failOnUsebooleanfalse
mustUse.mode"lint""lint"
nonExhaustive.brandstring"__nonExhaustive"