@metaobjectsdev/codegen-ts 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enum-shared.d.ts +9 -2
- package/dist/enum-shared.d.ts.map +1 -1
- package/dist/enum-shared.js +12 -3
- package/dist/enum-shared.js.map +1 -1
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +11 -1
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +31 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +4 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/shared-model-file.d.ts +23 -0
- package/dist/generators/shared-model-file.d.ts.map +1 -0
- package/dist/generators/shared-model-file.js +235 -0
- package/dist/generators/shared-model-file.js.map +1 -0
- package/dist/runner.d.ts +9 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +15 -1
- package/dist/runner.js.map +1 -1
- package/dist/templates/enums-file.d.ts +11 -1
- package/dist/templates/enums-file.d.ts.map +1 -1
- package/dist/templates/enums-file.js +10 -2
- package/dist/templates/enums-file.js.map +1 -1
- package/package.json +6 -6
- package/src/enum-shared.ts +17 -3
- package/src/generator-registry.ts +11 -0
- package/src/generator.ts +31 -1
- package/src/generators/entity-file.ts +7 -1
- package/src/generators/index.ts +11 -0
- package/src/generators/shared-model-file.ts +292 -0
- package/src/reference/entity.ts +10 -1
- package/src/runner.ts +28 -1
- package/src/templates/enums-file.ts +13 -2
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// FR-023 §4.3 — sharedModelFile(): the publisher's flattened shared-model artifact.
|
|
2
|
+
//
|
|
3
|
+
// A publisher project wires this generator to select a subset of its own metadata
|
|
4
|
+
// (by the same include/exclude scope-pattern grammar `scope` already uses) and emit
|
|
5
|
+
// it as ONE canonical-JSON `metadata.root` document plus a manifest — the artifact a
|
|
6
|
+
// consumer's `meta deps sync` copies into its own repo (design §2.2, §3.2, §3.5).
|
|
7
|
+
//
|
|
8
|
+
// The algorithm (design §4.3), in order:
|
|
9
|
+
// 1. Standalone load of `files` (default: the run's own source files, `ctx.sourceFiles`)
|
|
10
|
+
// with `ctx.registry` (the run's composed registry — the SAME vocabulary the
|
|
11
|
+
// project's own metadata loaded with), strict.
|
|
12
|
+
// 2. Select top-level nodes: never `requirement.*` / `template.*` (a publisher's
|
|
13
|
+
// ledger and prompts are not a consumer's shapes), and matching the compiled
|
|
14
|
+
// include/exclude scope.
|
|
15
|
+
// 3. Closure check: every `extends` target and every REF_BEARING_ATTR_NAMES value
|
|
16
|
+
// of a selected node (and of its own descendants) must resolve to a SELECTED
|
|
17
|
+
// node, else the selection is not self-contained and the build fails naming the
|
|
18
|
+
// (referrer, target) pairs.
|
|
19
|
+
// 4. Serialize the selection with `serializeSharedDocument` — raw own-layer form,
|
|
20
|
+
// `extends` preserved, sorted by resolution key.
|
|
21
|
+
// 5. Re-load the emitted document standalone with CORE providers only, strict — a
|
|
22
|
+
// consumer loads it with ITS providers, and a Phase 1 export must not need the
|
|
23
|
+
// publisher's non-core vocabulary.
|
|
24
|
+
// 6. Emit `<name>.metaobjects.json` + `metaobjects.pkg.json` (design §3.2).
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import {
|
|
29
|
+
type MetaData,
|
|
30
|
+
type MetaRoot,
|
|
31
|
+
MetaDataLoader,
|
|
32
|
+
InMemoryStringSource,
|
|
33
|
+
compileScope,
|
|
34
|
+
matchesScope,
|
|
35
|
+
REF_BEARING_ATTR_NAMES,
|
|
36
|
+
resolveObjectRef,
|
|
37
|
+
serializeSharedDocument,
|
|
38
|
+
packageOfResolutionKey,
|
|
39
|
+
PACKAGE_SEPARATOR,
|
|
40
|
+
CHILD_REF_SEPARATOR,
|
|
41
|
+
TYPE_REQUIREMENT,
|
|
42
|
+
TYPE_TEMPLATE,
|
|
43
|
+
METAMODEL_VERSION,
|
|
44
|
+
} from "@metaobjectsdev/metadata";
|
|
45
|
+
import { FileSource } from "@metaobjectsdev/metadata/core";
|
|
46
|
+
import { oncePerRun, type Generator, type GenContext, type EmittedFile } from "../generator.js";
|
|
47
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
48
|
+
|
|
49
|
+
// The FR-023 manifest/artifact tool-file conventions (design §3.2, §4.3, "Hash
|
|
50
|
+
// format"). Their single canonical source is `sdk/src/dependencies.ts`
|
|
51
|
+
// (`MANIFEST_FILE`, `ARTIFACT_SUFFIX`, `INTEGRITY_PREFIX`) — codegen-ts cannot
|
|
52
|
+
// import sdk (the dependency runs the other way: `cli` depends on both, design
|
|
53
|
+
// §4.3's closing note), so these mirror those values locally. Keep in sync —
|
|
54
|
+
// exported (not merely local) so `cli`, which depends on both packages, can
|
|
55
|
+
// assert equality against `sdk`'s exports in a test; see
|
|
56
|
+
// `cli/test/shared-model-constants-parity.test.ts`.
|
|
57
|
+
export const MANIFEST_FILE = "metaobjects.pkg.json";
|
|
58
|
+
export const ARTIFACT_SUFFIX = ".metaobjects.json";
|
|
59
|
+
export const INTEGRITY_PREFIX = "sha256-";
|
|
60
|
+
/** design §3.2 — the manifest schema's own version, distinct from `metamodelVersion`. */
|
|
61
|
+
const MANIFEST_SCHEMA_VERSION = 1;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Separator for `checkClosure`'s dedupe key. A resolution key never contains a NUL,
|
|
65
|
+
* so this cannot collide with either half it joins — unlike a printable separator
|
|
66
|
+
* (`->`, `→`), which would need a separate argument for why no legal FQN could ever
|
|
67
|
+
* contain it. Written as the `\0` ESCAPE, never a raw control byte: a raw 0x00 in
|
|
68
|
+
* this source file previously made `git` treat it as binary, which (a) makes
|
|
69
|
+
* `git diff`/GitHub show no content for the file at all, and (b) more seriously,
|
|
70
|
+
* makes `.githooks/pre-commit`'s public-repo-hygiene grep skip its added lines
|
|
71
|
+
* entirely (a binary file's diff is never text-enumerated) — silently defeating that
|
|
72
|
+
* guard for any future edit to this file. The escape produces the byte-identical
|
|
73
|
+
* runtime string; only the SOURCE FILE's byte content changes (readable text instead
|
|
74
|
+
* of an embedded control byte).
|
|
75
|
+
*/
|
|
76
|
+
const DEDUPE_KEY_SEPARATOR = "\0";
|
|
77
|
+
|
|
78
|
+
export interface SharedModelFileOpts {
|
|
79
|
+
/** The dependency key a consumer will `meta deps sync` under. Also the artifact's
|
|
80
|
+
* basename: emits `<name>.metaobjects.json`. */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Scope-pattern include list (the same `acme::platform::**` grammar as `scope`). */
|
|
83
|
+
include: readonly string[];
|
|
84
|
+
/** Applied after `include`. */
|
|
85
|
+
exclude?: readonly string[];
|
|
86
|
+
/** Which of the publisher's own source files may contribute. Default: the run's
|
|
87
|
+
* own source files (`ctx.sourceFiles` — everything `files?` narrows). */
|
|
88
|
+
files?: readonly string[];
|
|
89
|
+
/** The published version. Default: the nearest `package.json` `version` walking
|
|
90
|
+
* up from `ctx.projectRoot`. */
|
|
91
|
+
version?: string;
|
|
92
|
+
/** Named output target, as on every other generator. */
|
|
93
|
+
target?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Everything before the first `CHILD_REF_SEPARATOR` that follows the LAST
|
|
97
|
+
* `PACKAGE_SEPARATOR` — the object part of a possibly dotted ref (ADR-0029's
|
|
98
|
+
* addressing model: package segments never fold onto a non-root child, so a
|
|
99
|
+
* `.` before the final `::`-segment can't occur). Mirrors the private
|
|
100
|
+
* `splitChildTail` in `naming-refs.ts` (not exported); `extends` and the
|
|
101
|
+
* origin `@from`/`@of`/`@via` attrs are the only REF_BEARING_ATTR_NAMES that
|
|
102
|
+
* can carry a dotted child tail — every other ref-bearing attr is already a
|
|
103
|
+
* bare object ref, and stripping a tail that isn't there is a no-op. */
|
|
104
|
+
function refOwner(ref: string): string {
|
|
105
|
+
const lastSep = ref.lastIndexOf(PACKAGE_SEPARATOR);
|
|
106
|
+
const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
|
|
107
|
+
const dotInSeg = ref.indexOf(CHILD_REF_SEPARATOR, segStart);
|
|
108
|
+
return dotInSeg === -1 ? ref : ref.slice(0, dotInSeg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Depth-first walk of a node and its OWN descendants, node itself first. ADR-0039
|
|
112
|
+
* sanctioned own-only case: `serializeSharedDocument` emits each selected node's
|
|
113
|
+
* OWN layer only (`extends` preserved, not flattened — design §3.5), so the
|
|
114
|
+
* closure check must walk exactly what will be emitted, never what `extends`
|
|
115
|
+
* would additionally contribute (that is the base's own layer, checked
|
|
116
|
+
* separately if the base is itself selected). */
|
|
117
|
+
function walkOwn(node: MetaData): MetaData[] {
|
|
118
|
+
const out: MetaData[] = [node];
|
|
119
|
+
for (const child of node.ownChildren()) out.push(...walkOwn(child));
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ClosurePair {
|
|
124
|
+
readonly referrer: string;
|
|
125
|
+
readonly target: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** design §4.3 step 3. `standaloneRoot` already loaded successfully (step 1), so
|
|
129
|
+
* every ref found here is guaranteed to RESOLVE to some node in it — the loader's
|
|
130
|
+
* own validation passes reject a dangling `extends`/object-ref before this ever
|
|
131
|
+
* runs. What is NOT guaranteed is that the resolved node is in `selectedKeys`. */
|
|
132
|
+
function checkClosure(
|
|
133
|
+
standaloneRoot: MetaRoot,
|
|
134
|
+
selected: readonly MetaData[],
|
|
135
|
+
selectedKeys: ReadonlySet<string>,
|
|
136
|
+
): ClosurePair[] {
|
|
137
|
+
const pairs: ClosurePair[] = [];
|
|
138
|
+
const seen = new Set<string>();
|
|
139
|
+
for (const top of selected) {
|
|
140
|
+
const referrerKey = top.resolutionKey();
|
|
141
|
+
for (const node of walkOwn(top)) {
|
|
142
|
+
const referrerPkg = effectivePackage(node) ?? "";
|
|
143
|
+
const candidates: string[] = [];
|
|
144
|
+
if (node.superRef !== undefined) candidates.push(node.superRef);
|
|
145
|
+
// ADR-0039 sanctioned own-only case (same rationale as walkOwn() above): an
|
|
146
|
+
// INHERITED ref-bearing attr belongs to the base node's own layer, not this
|
|
147
|
+
// one's, and is checked when the base itself is walked — reading it via the
|
|
148
|
+
// resolving `attr()` here would attribute the base's reference to this node.
|
|
149
|
+
for (const attrName of REF_BEARING_ATTR_NAMES) {
|
|
150
|
+
const value = node.ownAttr(attrName);
|
|
151
|
+
if (typeof value === "string") candidates.push(value);
|
|
152
|
+
}
|
|
153
|
+
for (const raw of candidates) {
|
|
154
|
+
const resolved = resolveObjectRef(standaloneRoot, refOwner(raw), referrerPkg).node;
|
|
155
|
+
if (resolved === undefined) continue; // already validated at load — defensive only
|
|
156
|
+
const targetKey = resolved.resolutionKey();
|
|
157
|
+
if (selectedKeys.has(targetKey)) continue;
|
|
158
|
+
const dedupeKey = `${referrerKey}${DEDUPE_KEY_SEPARATOR}${targetKey}`;
|
|
159
|
+
if (seen.has(dedupeKey)) continue;
|
|
160
|
+
seen.add(dedupeKey);
|
|
161
|
+
pairs.push({ referrer: referrerKey, target: targetKey });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return pairs.sort((a, b) =>
|
|
166
|
+
a.referrer === b.referrer ? a.target.localeCompare(b.target) : a.referrer.localeCompare(b.referrer),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** design §2.2 — "version defaults to the host package's version". Walks up from
|
|
171
|
+
* `startDir` for the nearest `package.json` carrying a string `version`. */
|
|
172
|
+
function nearestPackageVersion(startDir: string): string | undefined {
|
|
173
|
+
let dir = startDir;
|
|
174
|
+
for (let i = 0; i < 64; i++) {
|
|
175
|
+
const pkgPath = join(dir, "package.json");
|
|
176
|
+
if (existsSync(pkgPath)) {
|
|
177
|
+
try {
|
|
178
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: unknown };
|
|
179
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
180
|
+
} catch {
|
|
181
|
+
// An unparsable package.json is not a version source — keep walking up.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const parent = dirname(dir);
|
|
185
|
+
if (parent === dir) return undefined;
|
|
186
|
+
dir = parent;
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function sharedModelFile(opts: SharedModelFileOpts): Generator {
|
|
192
|
+
const generator: Generator = {
|
|
193
|
+
name: "shared-model",
|
|
194
|
+
generate: oncePerRun(async (_entities, ctx: GenContext): Promise<EmittedFile[]> => {
|
|
195
|
+
const files = opts.files ?? ctx.sourceFiles ?? [];
|
|
196
|
+
if (files.length === 0) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`sharedModelFile("${opts.name}"): no source files to load — pass \`files\`, or run ` +
|
|
199
|
+
`through \`meta gen\`, which supplies the project's own files as \`ctx.sourceFiles\`.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Step 1 — standalone load, the run's own composed registry (so the publisher's
|
|
204
|
+
// own consumer-supplied providers are honoured here exactly as they were for
|
|
205
|
+
// the project's main load), strict.
|
|
206
|
+
const loader = new MetaDataLoader({
|
|
207
|
+
...(ctx.registry !== undefined ? { registry: ctx.registry } : {}),
|
|
208
|
+
strict: true,
|
|
209
|
+
});
|
|
210
|
+
const loaded = await loader.load(files.map((p) => new FileSource(p)));
|
|
211
|
+
if (loaded.errors.length > 0) {
|
|
212
|
+
throw loaded.errors[0];
|
|
213
|
+
}
|
|
214
|
+
const standaloneRoot = loaded.root;
|
|
215
|
+
|
|
216
|
+
// Step 2 — select top-level nodes. ADR-0039 sanctioned own-only case: `MetaRoot`
|
|
217
|
+
// has no super chain (it never `extends`), so its own children ARE its effective
|
|
218
|
+
// children — the same case `MetaRoot.objects()`/`findObject()` document.
|
|
219
|
+
const compiled = compileScope({ include: opts.include, exclude: opts.exclude ?? [] });
|
|
220
|
+
const selected = standaloneRoot
|
|
221
|
+
.ownChildren()
|
|
222
|
+
.filter((node) => node.type !== TYPE_REQUIREMENT && node.type !== TYPE_TEMPLATE)
|
|
223
|
+
.filter((node) => matchesScope(node.resolutionKey(), compiled));
|
|
224
|
+
if (selected.length === 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`sharedModelFile("${opts.name}"): selected no nodes (include/exclude, always ` +
|
|
227
|
+
`excluding requirement.* and template.*) — nothing to publish. Check \`include\`.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 3 — closure.
|
|
232
|
+
const selectedKeys = new Set(selected.map((n) => n.resolutionKey()));
|
|
233
|
+
const closureFailures = checkClosure(standaloneRoot, selected, selectedKeys);
|
|
234
|
+
if (closureFailures.length > 0) {
|
|
235
|
+
const lines = closureFailures.map((p) => ` ${p.referrer} → ${p.target}`).join("\n");
|
|
236
|
+
throw new Error(
|
|
237
|
+
`sharedModelFile("${opts.name}"): the selection is not closed — these references ` +
|
|
238
|
+
`point outside it; include it or exclude the referrer:\n${lines}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Step 4 — serialize (already sorted by resolution key; the callee also
|
|
243
|
+
// throws if a selected node has no package, per its own contract).
|
|
244
|
+
const artifactContent = serializeSharedDocument(selected);
|
|
245
|
+
|
|
246
|
+
// Step 5 — re-load standalone with CORE providers only (registry omitted ⇒
|
|
247
|
+
// MetaDataLoader's own default registry), strict. A consumer loads this
|
|
248
|
+
// artifact with ITS OWN providers, never the publisher's — so an export
|
|
249
|
+
// needing publisher-only vocabulary must fail HERE, not at the consumer.
|
|
250
|
+
const artifactPath = `${opts.name}${ARTIFACT_SUFFIX}`;
|
|
251
|
+
const coreLoader = new MetaDataLoader({ strict: true });
|
|
252
|
+
const coreLoaded = await coreLoader.load([
|
|
253
|
+
new InMemoryStringSource(artifactContent, { id: artifactPath }),
|
|
254
|
+
]);
|
|
255
|
+
if (coreLoaded.errors.length > 0) {
|
|
256
|
+
const first = coreLoaded.errors[0]!;
|
|
257
|
+
throw new Error(
|
|
258
|
+
`sharedModelFile("${opts.name}"): the export needs a provider this toolchain does ` +
|
|
259
|
+
`not ship; Phase 1 exports must load with core vocabulary: ${first.message}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Step 6 — emit the artifact + manifest.
|
|
264
|
+
const integrity = `${INTEGRITY_PREFIX}${createHash("sha256").update(artifactContent, "utf-8").digest("hex")}`;
|
|
265
|
+
const packages = [...new Set(selected.map((n) => packageOfResolutionKey(n.resolutionKey())))].sort();
|
|
266
|
+
const nodes = selected.map((n) => n.resolutionKey()).sort();
|
|
267
|
+
const version = opts.version ?? nearestPackageVersion(ctx.projectRoot ?? process.cwd());
|
|
268
|
+
if (version === undefined) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`sharedModelFile("${opts.name}"): no version available — pass \`version\` explicitly, ` +
|
|
271
|
+
`or run inside a project with a package.json version at or above the project root.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const manifest = {
|
|
275
|
+
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
276
|
+
name: opts.name,
|
|
277
|
+
version,
|
|
278
|
+
metamodelVersion: METAMODEL_VERSION,
|
|
279
|
+
artifact: artifactPath,
|
|
280
|
+
integrity,
|
|
281
|
+
packages,
|
|
282
|
+
nodes,
|
|
283
|
+
};
|
|
284
|
+
return [
|
|
285
|
+
{ path: artifactPath, content: artifactContent },
|
|
286
|
+
{ path: MANIFEST_FILE, content: JSON.stringify(manifest, null, 2) + "\n" },
|
|
287
|
+
];
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
if (opts.target !== undefined) generator.target = opts.target;
|
|
291
|
+
return generator;
|
|
292
|
+
}
|
package/src/reference/entity.ts
CHANGED
|
@@ -175,7 +175,16 @@ export const entityFile = function entityFile(opts?: EntityFileOpts): Generator
|
|
|
175
175
|
generate: async (ctx: GenContext): Promise<EmittedFile[]> => {
|
|
176
176
|
const files = await perEntityEmit(ctx);
|
|
177
177
|
// FR-019: emit the shared-enums module once per run (null → no file).
|
|
178
|
-
|
|
178
|
+
// FR-023 §11.1 item 2: this is a REFERENCE TEMPLATE `meta init` copies into
|
|
179
|
+
// every scaffolded project's codegen/generators/ (ADR-0034) — the DEFAULT
|
|
180
|
+
// path, not an opt-in one. `select` (never `ctx.matches`, which never runs
|
|
181
|
+
// for a whole-root render like this one) excludes an enum used only by an
|
|
182
|
+
// imported, out-of-scope entity; without it every `meta init` project would
|
|
183
|
+
// silently emit shared enums the library path correctly excludes.
|
|
184
|
+
const sharedEnums = renderSharedEnumsFile(
|
|
185
|
+
ctx.loadedRoot,
|
|
186
|
+
ctx.select !== undefined ? { select: ctx.select } : undefined,
|
|
187
|
+
);
|
|
179
188
|
if (sharedEnums !== null) {
|
|
180
189
|
files.push({ path: `${SHARED_ENUMS_BASENAME}.ts`, content: await formatTs(sharedEnums) });
|
|
181
190
|
}
|
package/src/runner.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isMetaRoot, OBJECT_SUBTYPE_VALUE, FIELD_SUBTYPE_TIMESTAMP, FIELD_ATTR_FILTERABLE,
|
|
11
|
+
composeRegistry, coreProviders,
|
|
12
|
+
} from "@metaobjectsdev/metadata";
|
|
10
13
|
import { assignEmittedNames } from "./naming/collision-names.js";
|
|
11
14
|
import { isAbstract } from "./instance-artifacts.js";
|
|
12
15
|
import { dbEmittingObjects, missingDialectMessage } from "./db-emitting.js";
|
|
@@ -106,6 +109,15 @@ export interface RunGenOpts {
|
|
|
106
109
|
* they pass straight through.
|
|
107
110
|
*/
|
|
108
111
|
scope?: (fqn: string) => boolean;
|
|
112
|
+
/**
|
|
113
|
+
* FR-023 §4.3 — the collection's own source files (`Collection.ownFiles`; never
|
|
114
|
+
* a dependency's snapshot artifact), threaded onto `GenContext.sourceFiles` for
|
|
115
|
+
* `sharedModelFile()`'s default `files` selection. The CLI's `gen` command
|
|
116
|
+
* always passes `genCollection.ownFiles`; a programmatic caller that never
|
|
117
|
+
* wires `sharedModelFile()` (or always passes that generator's own `files`
|
|
118
|
+
* option explicitly) can omit this with no behavior change elsewhere.
|
|
119
|
+
*/
|
|
120
|
+
sourceFiles?: readonly string[];
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
export interface RunGenResult {
|
|
@@ -374,6 +386,13 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
374
386
|
// 2. Resolve targets + entity-module target.
|
|
375
387
|
const config = normalizeConfig(opts.config);
|
|
376
388
|
|
|
389
|
+
// FR-023 §4.3 — the run's composed registry, threaded onto every GenContext.
|
|
390
|
+
// Mirrors sdk's `loadMemory` exactly (`defaultLoadMemoryProviders` = core
|
|
391
|
+
// providers, then the project's own `config.providers` appended) so
|
|
392
|
+
// `sharedModelFile()`'s standalone re-load of a `files` subset sees the SAME
|
|
393
|
+
// vocabulary `opts.metadata` was originally loaded with.
|
|
394
|
+
const registry = composeRegistry([...coreProviders, ...(config.providers ?? [])]);
|
|
395
|
+
|
|
377
396
|
// The compile break (`value` → `fetcher` on the provider) sends an adopter to the right
|
|
378
397
|
// line; this names the value to put there, once, for the projects that had one.
|
|
379
398
|
if (shouldNoteBaseUrlMove(config.apiPrefix, recordedEngine)) {
|
|
@@ -605,6 +624,12 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
605
624
|
entities: safeEntities,
|
|
606
625
|
loadedRoot: root,
|
|
607
626
|
matches: (e) => generator.filter?.(e) ?? true,
|
|
627
|
+
// FR-023 §11.1 item 2 — the SAME `scope` predicate that already narrowed
|
|
628
|
+
// `entities` above, re-exposed for a template that renders once over the
|
|
629
|
+
// WHOLE loaded root (the shared-enums module, so far the only one) rather
|
|
630
|
+
// than per matched entity, where `matches` never runs. Absent scope ⇒ no
|
|
631
|
+
// `select` at all, byte-identical to a project with no `scope` declared.
|
|
632
|
+
...(scope !== undefined && { select: scope }),
|
|
608
633
|
config: {
|
|
609
634
|
outDir: selfTarget.outDir,
|
|
610
635
|
extStyle: config.extStyle,
|
|
@@ -617,6 +642,8 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
617
642
|
},
|
|
618
643
|
renderContext,
|
|
619
644
|
...(projectRoot !== undefined && { projectRoot }),
|
|
645
|
+
registry,
|
|
646
|
+
...(opts.sourceFiles !== undefined && { sourceFiles: opts.sourceFiles }),
|
|
620
647
|
warn: (msg) => warnings.push(`[${generator.name}] ${msg}`),
|
|
621
648
|
};
|
|
622
649
|
|
|
@@ -37,9 +37,20 @@ export const ${sharedEnumZodConstName(e.name)} = ${z}.enum([${members}]);
|
|
|
37
37
|
/**
|
|
38
38
|
* The full shared-enums module body, or null when the model has no materialized
|
|
39
39
|
* shared enums (so the generator emits no file at all).
|
|
40
|
+
*
|
|
41
|
+
* `opts.select` (FR-023 §11.1 item 2) — the model-wide scope predicate over a
|
|
42
|
+
* CONSUMING entity's `resolutionKey()`: an enum extended only by entities
|
|
43
|
+
* `select` excludes is not materialized. This module renders once per run from
|
|
44
|
+
* the WHOLE loaded root (never per-generator-filtered — see #266), so it is the
|
|
45
|
+
* one shared-enums call site that must be threaded the scope predicate
|
|
46
|
+
* separately from `ctx.matches`. Omitted ⇒ every entity counts, byte-identical
|
|
47
|
+
* to a caller that never passes `opts` at all.
|
|
40
48
|
*/
|
|
41
|
-
export function renderSharedEnumsFile(
|
|
42
|
-
|
|
49
|
+
export function renderSharedEnumsFile(
|
|
50
|
+
root: MetaRoot,
|
|
51
|
+
opts?: { select?: (fqn: string) => boolean },
|
|
52
|
+
): string | null {
|
|
53
|
+
const enums = materializedSharedEnums(root, opts?.select);
|
|
43
54
|
if (enums.length === 0) return null;
|
|
44
55
|
|
|
45
56
|
const body = joinCode(enums.map(renderOneSharedEnum), { on: "\n" }).toString();
|