Skip to content

Commit 6cb094c

Browse files
dmealingclaude
andcommitted
feat(codegen-ts): add perPackage scope helper + perModel (alias oncePerRun)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
1 parent a4e8ccb commit 6cb094c

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

server/typescript/packages/codegen-ts/src/generator.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export function perEntity(
7373
};
7474
}
7575

76-
/** Called once with all matching entities. Use for barrels and cross-entity files. */
76+
/** Called once with all matching entities. Use for barrels and cross-entity files.
77+
* @deprecated Use {@link perModel} — "run" is ambiguous under multi-target output
78+
* (it reads as "per target"); `perModel` names the data scope (the whole model). */
7779
export function oncePerRun(
7880
fn: (entities: MetaObject[], ctx: GenContext) =>
7981
| EmittedFile
@@ -86,3 +88,35 @@ export function oncePerRun(
8688
return Array.isArray(result) ? result : [result];
8789
};
8890
}
91+
92+
/** App-scope convenience — run `fn` once over the whole model (all matched
93+
* entities). The canonical name for the one-shot scope (replaces `oncePerRun`). */
94+
export const perModel = oncePerRun;
95+
96+
/** One-file-per-package convenience. Groups matched entities by `package`, runs
97+
* `fn` once per package — packages ascending, entities keeping `ctx.entities`
98+
* order. The package scope from codegen-concepts §10 (object + model already
99+
* exist via perEntity + perModel). */
100+
export function perPackage(
101+
fn: (pkg: string, entities: MetaObject[], ctx: GenContext) =>
102+
| EmittedFile
103+
| EmittedFile[]
104+
| Promise<EmittedFile | EmittedFile[]>,
105+
): (ctx: GenContext) => Promise<EmittedFile[]> {
106+
return async (ctx) => {
107+
const matched = ctx.entities.filter(ctx.matches);
108+
const byPkg = new Map<string, MetaObject[]>();
109+
for (const e of matched) {
110+
const pkg = e.package ?? "";
111+
let bucket = byPkg.get(pkg);
112+
if (bucket === undefined) { bucket = []; byPkg.set(pkg, bucket); }
113+
bucket.push(e);
114+
}
115+
const out: EmittedFile[] = [];
116+
for (const pkg of [...byPkg.keys()].sort()) {
117+
const r = await fn(pkg, byPkg.get(pkg)!, ctx);
118+
out.push(...(Array.isArray(r) ? r : [r]));
119+
}
120+
return out;
121+
};
122+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, test, expect } from "bun:test";
2+
import { perPackage, perModel, oncePerRun } from "../../src/generator.js";
3+
import type { GenContext, EmittedFile } from "../../src/generator.js";
4+
import { MetaDataLoader } from "@metaobjectsdev/metadata";
5+
import { FileSource } from "@metaobjectsdev/metadata/core";
6+
import { resolve } from "node:path";
7+
8+
const FIXTURE = resolve(import.meta.dir, "../fixtures/single-entity.json");
9+
10+
async function ctxFor(path: string): Promise<GenContext> {
11+
const loader = new MetaDataLoader();
12+
const res = await loader.load([new FileSource(path)]);
13+
expect(res.errors).toEqual([]);
14+
const entities = res.root.objects();
15+
return {
16+
entities, loadedRoot: res.root, matches: () => true,
17+
config: {} as GenContext["config"], warn: () => {},
18+
};
19+
}
20+
21+
describe("perPackage", () => {
22+
test("runs fn once per distinct package, packages sorted", async () => {
23+
const ctx = await ctxFor(FIXTURE);
24+
const seen: string[] = [];
25+
const gen = perPackage((pkg, ents) => {
26+
seen.push(pkg);
27+
return { path: `${pkg || "_"}/out.txt`, content: `${ents.length}` } as EmittedFile;
28+
});
29+
const files = await gen(ctx);
30+
const pkgs = [...new Set(ctx.entities.map((e) => e.package ?? ""))].sort();
31+
expect(seen).toEqual(pkgs);
32+
expect(files.length).toBe(pkgs.length);
33+
});
34+
});
35+
36+
describe("perModel / oncePerRun alias", () => {
37+
test("perModel runs fn once with all matched entities", async () => {
38+
const ctx = await ctxFor(FIXTURE);
39+
let calls = 0;
40+
const gen = perModel((ents) => { calls++; return { path: "all.txt", content: `${ents.length}` }; });
41+
const files = await gen(ctx);
42+
expect(calls).toBe(1);
43+
expect(files.length).toBe(1);
44+
});
45+
test("oncePerRun is the same function as perModel (alias)", () => {
46+
expect(oncePerRun).toBe(perModel);
47+
});
48+
});

0 commit comments

Comments
 (0)