Accessing generated content
The official pattern for reading .mandu/generated/ artifacts from user code — runtime registry, not direct imports.
On this page
- Why direct imports are forbidden
- The official API
- getGenerated<K>(key) — typed accessor
- tryGetGenerated<K>(key) — optional variant
- getManifest() — convenience wrapper
- getRouteById(id) — targeted lookup
- Decision tree
- Extending the registry
- Testing
- FAQ
- "I just want to read one file from .mandu/generated/. Is there no escape hatch?"
- "Can I use import.meta.resolve(...) to get the generated path?"
- "What about the existing barrel re-export workaround?"
- AI block
- 🤖 Agent Prompt
- Related
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:
- 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.
- Compiled-binary determinism —
bun build --compileembeds a fixed manifest. Direct imports bypass the embedded copy and fail at runtime with "module not found." - 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.
- 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
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.
Related
For Agents
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.
- 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