Custom targets
@atomprd/codegen is target-agnostic. The reference is @atomprd/codegen-senlang; you can write your own.
Skeleton
Section titled “Skeleton”import type { CodegenContext, CodegenTemplate, OutputFile,} from "@atomprd/codegen";
export const myTemplate: CodegenTemplate = { name: "my-stack",
async scaffold(ctx) { return [ { path: "package.json", contents: JSON.stringify({ name: "generated-app", dependencies: { "vue": "^3.4.0" } }, null, 2), generated: true }, ]; },
async structural(ctx) { const out: OutputFile[] = []; for (const atom of ctx.doc.manifest.atoms) { if (atom.kind === "entity") { out.push({ path: `src/entities/${atom.name}.ts`, contents: emitEntity(atom), generated: true, }); } if (atom.kind === "feature" && atom.behavior) { out.push({ path: `src/features/${slug(atom.name)}.ts`, contents: emitFeature(atom), generated: true, }); } } return out; },
async behavioral(ctx) { return []; // Defer or call ctx.llm },};Run it
Section titled “Run it”import { runTemplate } from "@atomprd/codegen";import { myTemplate } from "./my-template";import { PrdDocument } from "@atomprd/core";import manifest from "./atoms/manifest.json" assert { type: "json" };
const doc = PrdDocument.load(manifest);const result = await runTemplate(myTemplate, { doc, outDir: "./out",});console.log(`Wrote ${result.files.length} files`);Lowering Layer 2
Section titled “Lowering Layer 2”The 5 atom kinds carrying Layer 2 are where most of your structural work lives. Pseudocode for a Vue + Pinia target:
function lowerStepToVue(step: Step): string { switch (step.kind) { case "validate": return `if (!validators.${step.rule}(state)) throw new Error(${JSON.stringify(step.errorMessage ?? "")});`; case "call": return `const ${step.assignTo ?? "_"} = await api.${step.method?.toLowerCase() ?? "get"}(\`${step.endpoint}\`, ${JSON.stringify(step.body ?? null)});`; case "dispatch": return `await features.${slug(step.feature)}(${JSON.stringify(step.args ?? {})});`; case "setState": return `state.${step.path.replace(/^state\./, "")} = ${JSON.stringify(step.value)};`; case "navigate": return `router.push({ path: ${JSON.stringify(step.to)}, query: ${JSON.stringify(step.params ?? {})} });`; case "log": return `console.${step.level}(${JSON.stringify(step.msg)});`; case "if": return `if (${step.condition}) { ${step.then.map(lowerStepToVue).join(" ")} }${step.else ? ` else { ${step.else.map(lowerStepToVue).join(" ")} }` : ""}`; }}Same pattern for Section → Vue SFC, EnforcedAt → Pinia store guard, before_after → Vitest scenario.
Idempotency
Section titled “Idempotency”Mark deterministic output generated: true. The driver respects user edits on generated: false files (e.g. package.json after a user added a custom dep). Re-run is safe.
Testing your codegen
Section titled “Testing your codegen”The reference round-trip fixture is atomprd/examples/habit-tracker/ — 21 atoms, fully filled Layer 2, demonstrating all 4 patterns.
cd atomprd/examples/habit-trackerbun run gen # runs codegen-senlang, writes out/diff -r out/ <expected-snapshot>For your own codegen, mirror this folder structure:
my-fixture/├── manifest.json # the atom set├── gen.ts # imports your template, calls runTemplate└── expected/ # snapshot to diff againstRun bun run gen in CI; fail on diff.
See also
Section titled “See also”- Architecture — the 3-layer template + driver.
- SenLang bridge — the reference target.
- Spec → Layer 2 schemas — the structures you’re lowering.