LangENKO

Accessing generated content

The official pattern for reading .mandu/generated/ artifacts from user code — runtime registry, not direct imports.

since v0.24
On this page

Accessing generated content

User code must not import anything under .mandu/generated/, __generated__/, or any path that contains /generated/. The guard rule INVALID_GENERATED_IMPORT catches this at build time:

Direct __generated__/ imports are forbidden: <path>.
Use the runtime registry: see https://mandujs.com/docs/architect/generated-access

This page explains why the restriction exists and what to do instead.

Why direct imports are forbidden

Four failure modes, all observed in real codebases:

  1. Hot reload staleness — in dev, generated modules are rebuilt and re-imported on every file change. A direct ESM import caches the first version; your code reads stale data until the process restarts.
  2. Compiled-binary determinismbun build --compile embeds a fixed manifest. Direct imports bypass the embedded copy and fail at runtime with "module not found."
  3. ESM cache invalidation — transitive generated modules got stuck on stale copies during hot reload (issue #184). The registry is the single choke point the bundled importer invalidates cleanly.
  4. Path brittleness — generated paths change across versions. The registry API is stable; the on-disk layout is not.

The official API

getGenerated<K>(key) — typed accessor

import { getGenerated } from "@mandujs/core/runtime";

const manifest = getGenerated("routes");
for (const route of manifest.routes) {
  console.log(route.id, route.pattern);
}

getGenerated() throws a clear error if the manifest has not been registered yet. In normal app boot, registerManifestHandlers() from @mandujs/cli seeds the registry — the throw only fires in tests that forgot to seed fixtures or in broken boot sequences.

tryGetGenerated<K>(key) — optional variant

import { tryGetGenerated } from "@mandujs/core/runtime";

const collections = tryGetGenerated("collections");
if (!collections) {
  return []; // no collections emitted — that's fine
}

Use tryGetGenerated() for artifacts that might not be emitted. For required artifacts, prefer getGenerated() so boot-order errors surface immediately.

getManifest() — convenience wrapper

import { getManifest } from "@mandujs/core/runtime";

const manifest = getManifest(); // same as getGenerated("routes")

getRouteById(id) — targeted lookup

import { getRouteById } from "@mandujs/core/runtime";

const route = getRouteById("users-list");
if (route) {
  console.log(route.pattern); // "/api/users"
}

Decision tree

"I need to access…" Use
The route manifest at runtime getManifest() or getGenerated("routes")
A specific route by ID getRouteById("my-route")
An optional/conditional artifact tryGetGenerated("my-key")
Generated content in tests registerManifest("routes", fixture) in beforeEach
A generated type (not a runtime value) import type from .mandu/generated/types/** is allowed — types are erased at build time
A dev-only shim Explicit re-export from src/shared/**/index.ts — the barrel surfaces the shim and Guard's preset accepts it

Extending the registry

GeneratedRegistry is an interface you can augment. If you emit your own generated artifact (a collection index, a DB schema snapshot, a vendor bundle manifest, etc.), declare the key shape in a .d.ts and register it during boot.

// src/shared/types/generated.d.ts
declare module "@mandujs/core/runtime" {
  interface GeneratedRegistry {
    collections: Record<string, { entries: string[] }>;
  }
}
// boot glue — typically an adapter plugin
import { registerManifest } from "@mandujs/core/runtime";
import collections from ".mandu/generated/collections"; // OK: this file IS the generator output

registerManifest("collections", collections);

The one file that is allowed to import from .mandu/generated/ is the glue that wires the registry during boot. The framework ships this glue for routes via registerManifestHandlers(). Third-party generators emit their own glue.

Testing

Seed the registry in beforeEach and clear it in afterEach:

import { beforeEach, afterEach } from "bun:test";
import { registerManifest, clearGeneratedRegistry } from "@mandujs/core/runtime";

beforeEach(() => {
  clearGeneratedRegistry();
  registerManifest("routes", {
    version: 1,
    routes: [
      { id: "home", pattern: "/", kind: "page", module: "stub" },
    ],
  });
});

afterEach(() => clearGeneratedRegistry());

FAQ

"I just want to read one file from .mandu/generated/. Is there no escape hatch?"

No. Every case measured either (a) wants a typed view of the route manifest — use getManifest() — or (b) wants a feature that should live in its own registry key via module augmentation. If you think you have a third case, open an issue before adding a workaround.

"Can I use import.meta.resolve(...) to get the generated path?"

Yes, for non-import use cases: static analysis, CLI tools, debug output. import.meta.resolve() returns a URL string and does not trigger the ESM graph, so it sidesteps hot-reload. The guard rule only flags literal import ... from '…generated…' statements.

"What about the existing barrel re-export workaround?"

Re-exporting generated modules through a src/shared/**/index.ts barrel technically bypasses the guard. This is a workaround, not an endorsement. It hits all four failure modes above (hot reload staleness, compile-time lookup, ESM cache, path brittleness). Migrate to getGenerated().

AI block

{
  "pillar": "runtime",
  "access_rule": "never import from /generated/; use @mandujs/core/runtime registry",
  "entry_points": [
    {
      "name": "getGenerated",
      "source": "packages/core/src/runtime/registry.ts",
      "signature": "<K extends GeneratedKey>(key: K) => GeneratedShape<K>",
      "throws": "when key has not been registered"
    },
    {
      "name": "tryGetGenerated",
      "source": "packages/core/src/runtime/registry.ts",
      "signature": "<K extends GeneratedKey>(key: K) => GeneratedShape<K> | undefined"
    },
    {
      "name": "getManifest",
      "source": "packages/core/src/runtime/registry.ts",
      "signature": "() => RoutesManifest",
      "throws": "when the routes manifest has not been registered"
    },
    {
      "name": "getRouteById",
      "source": "packages/core/src/runtime/registry.ts",
      "signature": "(id: string) => RouteSpec | undefined"
    },
    {
      "name": "registerManifest",
      "source": "packages/core/src/runtime/registry.ts",
      "signature": "<K extends GeneratedKey>(key: K, value: GeneratedShape<K>) => void",
      "note": "called by framework boot via registerManifestHandlers(); user code only calls this in tests or when wiring a third-party generator"
    }
  ],
  "extension_point": "declare module '@mandujs/core/runtime' { interface GeneratedRegistry { ... } }",
  "guard_rule": "INVALID_GENERATED_IMPORT",
  "references": [
    "/docs/architect/guard",
    "/docs/architect/overview"
  ]
}

🤖 Agent Prompt

🤖 Agent Prompt — Accessing generated content
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/generated-access to my project.

Summary of the page:
Direct imports from .mandu/generated/ or any /generated/ path are forbidden by the guard rule INVALID_GENERATED_IMPORT. The official access path is the runtime registry: getGenerated(key) / getManifest() / getRouteById(id) / tryGetGenerated(key) exported from @mandujs/core/runtime. The registry is seeded during server boot by registerManifestHandlers() from @mandujs/cli. User code that wants a new generated artifact extends the GeneratedRegistry interface via module augmentation.

Required invariants — must hold after your changes:
- Files under app/, src/, and packages/ cannot import from any path containing /generated/
- The runtime registry is the single source of truth for generated artifacts at runtime
- getGenerated() throws a helpful error with docs link if called before manifest registration
- registerManifestHandlers() (from @mandujs/cli) is the only glue that seeds the routes manifest; third-party artifacts need their own glue
- Barrel re-export workarounds through src/shared/**/index.ts bypass the guard but are not endorsed

Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
   in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
  • Guard — the invariant catalog that owns INVALID_GENERATED_IMPORT
  • Source: packages/core/src/runtime/registry.ts
  • Source: packages/core/src/guard/check.ts (rule impl)
  • Issue: #200

For Agents

AI hint

Direct imports from .mandu/generated/ or any /generated/ path are forbidden by the guard rule INVALID_GENERATED_IMPORT. The official access path is the runtime registry: getGenerated(key) / getManifest() / getRouteById(id) / tryGetGenerated(key) exported from @mandujs/core/runtime. The registry is seeded during server boot by registerManifestHandlers() from @mandujs/cli. User code that wants a new generated artifact extends the GeneratedRegistry interface via module augmentation.

Invariants
  • Files under app/, src/, and packages/ cannot import from any path containing /generated/
  • The runtime registry is the single source of truth for generated artifacts at runtime
  • getGenerated() throws a helpful error with docs link if called before manifest registration
  • registerManifestHandlers() (from @mandujs/cli) is the only glue that seeds the routes manifest; third-party artifacts need their own glue
  • Barrel re-export workarounds through src/shared/**/index.ts bypass the guard but are not endorsed
Guard scope
guard