@metaobjectsdev/cli 1.0.3 → 1.0.4-rc.1
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/README.md +13 -5
- package/dist/src/commands/eject.d.ts +2 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +235 -75
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +119 -29
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +5 -9
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +80 -384
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +40 -14
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +36 -14
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +10 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/args.d.ts +18 -2
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +15 -3
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +92 -0
- package/dist/src/lib/catalog-listing.d.ts.map +1 -0
- package/dist/src/lib/catalog-listing.js +268 -0
- package/dist/src/lib/catalog-listing.js.map +1 -0
- package/dist/src/lib/catalog.d.ts +41 -0
- package/dist/src/lib/catalog.d.ts.map +1 -0
- package/dist/src/lib/catalog.js +89 -0
- package/dist/src/lib/catalog.js.map +1 -0
- package/dist/src/lib/collection-load-options.d.ts +1 -1
- package/dist/src/lib/collection-load-options.d.ts.map +1 -1
- package/dist/src/lib/collection-load-options.js +6 -0
- package/dist/src/lib/collection-load-options.js.map +1 -1
- package/dist/src/lib/install-set.d.ts +31 -0
- package/dist/src/lib/install-set.d.ts.map +1 -0
- package/dist/src/lib/install-set.js +103 -0
- package/dist/src/lib/install-set.js.map +1 -0
- package/dist/src/lib/library-eject.d.ts +79 -0
- package/dist/src/lib/library-eject.d.ts.map +1 -0
- package/dist/src/lib/library-eject.js +241 -0
- package/dist/src/lib/library-eject.js.map +1 -0
- package/dist/src/lib/library-listing.d.ts +63 -0
- package/dist/src/lib/library-listing.d.ts.map +1 -0
- package/dist/src/lib/library-listing.js +178 -0
- package/dist/src/lib/library-listing.js.map +1 -0
- package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
- package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
- package/dist/src/lib/library-prefix-advisory.js +67 -0
- package/dist/src/lib/library-prefix-advisory.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +11 -17
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts +17 -2
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +51 -9
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/eject.ts +304 -90
- package/src/commands/gen.ts +128 -28
- package/src/commands/init.ts +82 -390
- package/src/commands/verify.ts +47 -15
- package/src/index.ts +36 -14
- package/src/lib/advisory.ts +13 -0
- package/src/lib/args.ts +29 -5
- package/src/lib/catalog-listing.ts +369 -0
- package/src/lib/catalog.ts +107 -0
- package/src/lib/collection-load-options.ts +7 -1
- package/src/lib/install-set.ts +116 -0
- package/src/lib/library-eject.ts +298 -0
- package/src/lib/library-listing.ts +258 -0
- package/src/lib/library-prefix-advisory.ts +78 -0
- package/src/lib/load-metaobjects-config.ts +15 -21
- package/src/lib/requirement-check.ts +71 -11
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// `meta gen --list` — the catalog as one machine-readable document.
|
|
2
|
+
//
|
|
3
|
+
// The governing rule (opt-in-codegen design §1): stop hard-coding selection decisions
|
|
4
|
+
// into the CLI. The tool's job is to describe what it can do, accurately, and to make
|
|
5
|
+
// each choice cheap to act on. Deciding WHICH generators an application needs belongs
|
|
6
|
+
// to whoever is building it — increasingly an LLM working in the repo, which is well
|
|
7
|
+
// able to make that call given a truthful catalog and is badly served by a default that
|
|
8
|
+
// pre-empts it.
|
|
9
|
+
//
|
|
10
|
+
// Two questions this answers, kept apart on purpose (§D5):
|
|
11
|
+
//
|
|
12
|
+
// APPLICABILITY — "would this emit anything for MY model?" — is `--probe`, which
|
|
13
|
+
// cannot drift, because it does not describe the generators, it RUNS them.
|
|
14
|
+
// COMPATIBILITY — "what does this need in order to work?" — is declared
|
|
15
|
+
// (`requires`, `runtimePeers`) and therefore has to be gated separately.
|
|
16
|
+
//
|
|
17
|
+
// Conflating the two is how a catalog goes quietly wrong as frameworks are added.
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
22
|
+
import {
|
|
23
|
+
runGen,
|
|
24
|
+
type GeneratorRegistryEntry,
|
|
25
|
+
type Layer,
|
|
26
|
+
type MetaobjectsGenConfig,
|
|
27
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
28
|
+
import * as coreTpl from "@metaobjectsdev/codegen-ts";
|
|
29
|
+
import * as reactTpl from "@metaobjectsdev/codegen-ts-react";
|
|
30
|
+
import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
|
|
31
|
+
import { listCatalog, packageOf } from "./catalog.js";
|
|
32
|
+
import {
|
|
33
|
+
buildLibraryRows, renderLibraryText,
|
|
34
|
+
type LibraryCatalogRow, type LibraryProjectContext,
|
|
35
|
+
} from "./library-listing.js";
|
|
36
|
+
import { installSetFor } from "./install-set.js";
|
|
37
|
+
import { readPackageManifest, declaredDependencyNames } from "./package-manifest.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One `--list` row. The cross-port subset is name / layer / tier / description.
|
|
41
|
+
*
|
|
42
|
+
* `kind` is the discriminator: FR-043 adds `kind: "library"` rows to THIS table rather
|
|
43
|
+
* than a parallel one, because an agent about to model a capability should meet the
|
|
44
|
+
* library that already declares it in the list it was already reading.
|
|
45
|
+
*/
|
|
46
|
+
export type CatalogRow = GeneratorCatalogRow | LibraryCatalogRow;
|
|
47
|
+
|
|
48
|
+
export type { LibraryCatalogRow };
|
|
49
|
+
|
|
50
|
+
export interface GeneratorCatalogRow {
|
|
51
|
+
name: string;
|
|
52
|
+
kind: "generator";
|
|
53
|
+
layer: Layer;
|
|
54
|
+
framework?: string;
|
|
55
|
+
tier: "native" | "neutral";
|
|
56
|
+
package: string;
|
|
57
|
+
description: string;
|
|
58
|
+
/** From the reference template's `use-when:` header. Absent for a non-ejectable entry. */
|
|
59
|
+
useWhen?: string;
|
|
60
|
+
/** From the reference template's `emits:` header. Absent for a non-ejectable entry. */
|
|
61
|
+
emits?: string;
|
|
62
|
+
requires?: readonly string[];
|
|
63
|
+
configKeys?: readonly string[];
|
|
64
|
+
install: { dev: string[]; runtime: string[] };
|
|
65
|
+
source: {
|
|
66
|
+
kind: "reference-template" | "package-only";
|
|
67
|
+
ejectable: boolean;
|
|
68
|
+
/** Whether this project already owns a copy. `null` with no project. */
|
|
69
|
+
owned: boolean | null;
|
|
70
|
+
};
|
|
71
|
+
project?: {
|
|
72
|
+
/** Wired in this project's `generators: [...]`. */
|
|
73
|
+
wired: boolean;
|
|
74
|
+
/** This generator's framework is a declared dependency. `null` when neutral or unknown. */
|
|
75
|
+
frameworkDetected: boolean | null;
|
|
76
|
+
/** Files this generator WOULD emit for this model. `null` unless --probe. */
|
|
77
|
+
wouldEmit: number | null;
|
|
78
|
+
/** Why `wouldEmit` is null despite --probe. Absent when it is a number. */
|
|
79
|
+
probeError?: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Where an ejected copy lands — mirrors `eject.ts`'s constant of the same name. */
|
|
84
|
+
const OWNED_GENERATORS_DIR = "codegen/generators";
|
|
85
|
+
|
|
86
|
+
/** The npm package a framework token names, for `frameworkDetected`. */
|
|
87
|
+
const FRAMEWORK_PACKAGE: Record<string, string> = {
|
|
88
|
+
fastify: "fastify",
|
|
89
|
+
hono: "hono",
|
|
90
|
+
react: "react",
|
|
91
|
+
tanstack: "@tanstack/react-query",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// reference-template headers
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** The three packages that ship reference templates, and their roots. */
|
|
99
|
+
const TEMPLATE_ROOTS: ReadonlyArray<readonly [readonly string[], () => string]> = [
|
|
100
|
+
[coreTpl.REFERENCE_GENERATOR_NAMES, coreTpl.resolveReferenceRoot],
|
|
101
|
+
[reactTpl.REFERENCE_GENERATOR_NAMES, reactTpl.resolveReferenceRoot],
|
|
102
|
+
[tanstackTpl.REFERENCE_GENERATOR_NAMES, tanstackTpl.resolveReferenceRoot],
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A `// <facet>: <text>` header line and its indented continuations, unwrapped.
|
|
107
|
+
*
|
|
108
|
+
* Read from the template rather than restated in the registry entry, for the same
|
|
109
|
+
* reason `eject` reads its import line from the header: the template is the thing an
|
|
110
|
+
* adopter opens, so a second copy of its own summary in the registry is a second thing
|
|
111
|
+
* to keep in step. A test asserts every ejectable entry's row actually carries these.
|
|
112
|
+
*/
|
|
113
|
+
function headerFacet(source: string, facet: string): string | undefined {
|
|
114
|
+
const lines = source.split("\n");
|
|
115
|
+
const start = lines.findIndex((l) => l.startsWith(`// ${facet}:`));
|
|
116
|
+
if (start === -1) return undefined;
|
|
117
|
+
const parts = [lines[start]!.slice(`// ${facet}:`.length).trim()];
|
|
118
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
119
|
+
const line = lines[i]!;
|
|
120
|
+
// A continuation is a comment line indented past the facet column and NOT itself a
|
|
121
|
+
// new `facet:` line. Anything else ends the block.
|
|
122
|
+
if (!/^\/\/ {3,}\S/.test(line) || /^\/\/ {3,}[a-z-]+: /.test(line)) break;
|
|
123
|
+
parts.push(line.replace(/^\/\/\s+/, "").trim());
|
|
124
|
+
}
|
|
125
|
+
return parts.join(" ").trim() || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readTemplate(name: string): string | undefined {
|
|
129
|
+
for (const [names, root] of TEMPLATE_ROOTS) {
|
|
130
|
+
if (!names.includes(name)) continue;
|
|
131
|
+
try {
|
|
132
|
+
return readFileSync(join(root(), `${name}.ts`), "utf8");
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// the probe
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The project facts a `--list` row can carry. All optional: `--list` with no project at
|
|
146
|
+
* all is the documented shape (`meta types` behaves the same way), because "what CAN
|
|
147
|
+
* this engine do" is a question about the installed engine, not about a repo.
|
|
148
|
+
*/
|
|
149
|
+
export interface CatalogProject {
|
|
150
|
+
projectRoot: string;
|
|
151
|
+
config: MetaobjectsGenConfig;
|
|
152
|
+
/** Stable names of the wired generators, as far as they can be identified. */
|
|
153
|
+
wiredNames: ReadonlySet<string>;
|
|
154
|
+
/** Owned copies present under `codegen/generators/`. */
|
|
155
|
+
ownedNames: ReadonlySet<string>;
|
|
156
|
+
/** Declared dependency names from package.json, for `frameworkDetected`. */
|
|
157
|
+
declaredDeps: ReadonlySet<string> | undefined;
|
|
158
|
+
/** FR-043 — the project's `libraries` selection, for the `kind: "library"` rows.
|
|
159
|
+
* Defaults to none, so a caller that predates libraries reports "not opted in"
|
|
160
|
+
* rather than crashing. */
|
|
161
|
+
libraries?: readonly string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface CatalogListingOpts {
|
|
165
|
+
project?: CatalogProject;
|
|
166
|
+
/** Run every generator in memory and report a real file count. Needs `project` + `metadata`. */
|
|
167
|
+
probe?: { metadata: MetaData; scope?: (fqn: string) => boolean };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Dry-run ONE generator and count what it would emit.
|
|
172
|
+
*
|
|
173
|
+
* One `runGen` per generator rather than one run over all of them, for isolation: a
|
|
174
|
+
* generator that throws (or one whose real use requires options `--list` cannot supply,
|
|
175
|
+
* like `template` and `shared-model`) must report itself and leave every other row
|
|
176
|
+
* intact. `dryRun` touches nothing on disk.
|
|
177
|
+
*/
|
|
178
|
+
async function probeOne(
|
|
179
|
+
entry: GeneratorRegistryEntry,
|
|
180
|
+
project: CatalogProject,
|
|
181
|
+
metadata: MetaData,
|
|
182
|
+
scope: ((fqn: string) => boolean) | undefined,
|
|
183
|
+
): Promise<{ count: number } | { error: string }> {
|
|
184
|
+
try {
|
|
185
|
+
const result = await runGen({
|
|
186
|
+
config: { ...project.config, generators: [entry.factory()] },
|
|
187
|
+
metadata,
|
|
188
|
+
projectRoot: project.projectRoot,
|
|
189
|
+
dryRun: true,
|
|
190
|
+
...(scope !== undefined ? { scope } : {}),
|
|
191
|
+
});
|
|
192
|
+
return { count: result.files.length };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return { error: (err as Error).message };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// the listing
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promise<CatalogRow[]> {
|
|
203
|
+
const rows: CatalogRow[] = [];
|
|
204
|
+
|
|
205
|
+
for (const entry of listCatalog()) {
|
|
206
|
+
const template = entry.ejectable ? readTemplate(entry.name) : undefined;
|
|
207
|
+
const install = installSetFor([entry]);
|
|
208
|
+
const pkg = packageOf(entry.name) ?? "";
|
|
209
|
+
|
|
210
|
+
const row: GeneratorCatalogRow = {
|
|
211
|
+
name: entry.name,
|
|
212
|
+
kind: entry.kind,
|
|
213
|
+
layer: entry.layer,
|
|
214
|
+
...(entry.framework !== undefined ? { framework: entry.framework } : {}),
|
|
215
|
+
tier: entry.tier,
|
|
216
|
+
package: pkg,
|
|
217
|
+
description: entry.description,
|
|
218
|
+
...(template !== undefined && headerFacet(template, "use-when") !== undefined
|
|
219
|
+
? { useWhen: headerFacet(template, "use-when")! }
|
|
220
|
+
: {}),
|
|
221
|
+
...(template !== undefined && headerFacet(template, "emits") !== undefined
|
|
222
|
+
? { emits: headerFacet(template, "emits")! }
|
|
223
|
+
: {}),
|
|
224
|
+
...(entry.requires !== undefined ? { requires: entry.requires } : {}),
|
|
225
|
+
...(entry.configKeys !== undefined ? { configKeys: entry.configKeys } : {}),
|
|
226
|
+
install: { dev: install.dev, runtime: install.runtime },
|
|
227
|
+
source: {
|
|
228
|
+
kind: entry.ejectable ? "reference-template" : "package-only",
|
|
229
|
+
ejectable: entry.ejectable,
|
|
230
|
+
owned: opts.project === undefined ? null : opts.project.ownedNames.has(entry.name),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (opts.project !== undefined) {
|
|
235
|
+
const project = opts.project;
|
|
236
|
+
const fw = entry.framework;
|
|
237
|
+
const frameworkDetected =
|
|
238
|
+
fw === undefined || project.declaredDeps === undefined
|
|
239
|
+
? null
|
|
240
|
+
: project.declaredDeps.has(FRAMEWORK_PACKAGE[fw] ?? fw);
|
|
241
|
+
|
|
242
|
+
let wouldEmit: number | null = null;
|
|
243
|
+
let probeError: string | undefined;
|
|
244
|
+
if (opts.probe !== undefined) {
|
|
245
|
+
const outcome = await probeOne(entry, project, opts.probe.metadata, opts.probe.scope);
|
|
246
|
+
if ("count" in outcome) wouldEmit = outcome.count;
|
|
247
|
+
else probeError = outcome.error;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
row.project = {
|
|
251
|
+
wired: project.wiredNames.has(entry.name),
|
|
252
|
+
frameworkDetected,
|
|
253
|
+
wouldEmit,
|
|
254
|
+
...(probeError !== undefined ? { probeError } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
rows.push(row);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// FR-043 — the library rows, after the generators. Same table, same `--probe`.
|
|
262
|
+
const libraryCtx: LibraryProjectContext | undefined =
|
|
263
|
+
opts.project === undefined
|
|
264
|
+
? undefined
|
|
265
|
+
: {
|
|
266
|
+
libraries: opts.project.libraries ?? [],
|
|
267
|
+
wiredNames: opts.project.wiredNames,
|
|
268
|
+
...(opts.probe !== undefined ? { metadata: opts.probe.metadata } : {}),
|
|
269
|
+
};
|
|
270
|
+
rows.push(...(await buildLibraryRows(libraryCtx)));
|
|
271
|
+
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The stable names wired in a config.
|
|
277
|
+
*
|
|
278
|
+
* `generators: [...]` holds constructed Generator objects (and, since ADR-0021 #1,
|
|
279
|
+
* bare stable-name strings). A constructed generator carries its own kebab-case `name`,
|
|
280
|
+
* which IS the stable name for every catalog entry — the registry's factory is what
|
|
281
|
+
* produced it. A generator whose name is not a catalog key is an owned or third-party
|
|
282
|
+
* one and simply contributes nothing here.
|
|
283
|
+
*/
|
|
284
|
+
export function wiredGeneratorNames(config: MetaobjectsGenConfig): Set<string> {
|
|
285
|
+
const names = new Set<string>();
|
|
286
|
+
for (const spec of config.generators ?? []) {
|
|
287
|
+
if (typeof spec === "string") names.add(spec);
|
|
288
|
+
else if (typeof spec?.name === "string") names.add(spec.name);
|
|
289
|
+
}
|
|
290
|
+
return names;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Owned copies present under `codegen/generators/` in this project. */
|
|
294
|
+
export function ownedGeneratorNames(projectRoot: string): Set<string> {
|
|
295
|
+
const owned = new Set<string>();
|
|
296
|
+
for (const entry of listCatalog()) {
|
|
297
|
+
if (!entry.ejectable) continue;
|
|
298
|
+
try {
|
|
299
|
+
readFileSync(join(projectRoot, OWNED_GENERATORS_DIR, `${entry.name}.ts`), "utf8");
|
|
300
|
+
owned.add(entry.name);
|
|
301
|
+
} catch {
|
|
302
|
+
// absent — not owned
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return owned;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Declared dependency names, or undefined when there is no readable manifest. */
|
|
309
|
+
export function declaredDepsOf(projectRoot: string): Set<string> | undefined {
|
|
310
|
+
const pkg = readPackageManifest(projectRoot);
|
|
311
|
+
return pkg === undefined ? undefined : declaredDependencyNames(pkg);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// text rendering
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const LAYER_BLURB: Record<Layer, string> = {
|
|
319
|
+
model: "the entity modules and the constants beside them",
|
|
320
|
+
persistence: "how rows are read and written",
|
|
321
|
+
api: "the HTTP surface — pick ONE framework",
|
|
322
|
+
client: "the browser tier — form + hooks + grid compose, they do not conflict",
|
|
323
|
+
docs: "on by default; the canonical door is `meta docs`",
|
|
324
|
+
capability: "chosen by your MODEL, not by browsing — run --probe",
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** The human rendering: grouped by layer, because layer is the axis you select by. */
|
|
328
|
+
export function renderCatalogText(rows: CatalogRow[], probed: boolean): string {
|
|
329
|
+
const lines: string[] = [];
|
|
330
|
+
lines.push("Generator catalog — nothing runs until you wire it in `generators: [...]`.");
|
|
331
|
+
lines.push("");
|
|
332
|
+
|
|
333
|
+
const generators = rows.filter((r): r is GeneratorCatalogRow => r.kind === "generator");
|
|
334
|
+
const width = Math.max(...generators.map((r) => r.name.length));
|
|
335
|
+
let layer: string | undefined;
|
|
336
|
+
for (const r of generators) {
|
|
337
|
+
if (r.layer !== layer) {
|
|
338
|
+
layer = r.layer;
|
|
339
|
+
lines.push(`${layer} — ${LAYER_BLURB[r.layer]}`);
|
|
340
|
+
}
|
|
341
|
+
const marks: string[] = [];
|
|
342
|
+
if (r.framework !== undefined) marks.push(r.framework);
|
|
343
|
+
if (r.tier === "neutral") marks.push("neutral");
|
|
344
|
+
if (r.project?.wired) marks.push("WIRED");
|
|
345
|
+
if (r.source.owned) marks.push("owned");
|
|
346
|
+
if (probed && r.project?.wouldEmit !== null && r.project?.wouldEmit !== undefined) {
|
|
347
|
+
marks.push(`would emit ${r.project.wouldEmit}`);
|
|
348
|
+
}
|
|
349
|
+
const suffix = marks.length > 0 ? ` [${marks.join(", ")}]` : "";
|
|
350
|
+
lines.push(` ${r.name.padEnd(width)} — ${r.description}${suffix}`);
|
|
351
|
+
if (r.requires !== undefined && r.requires.length > 0) {
|
|
352
|
+
lines.push(` ${" ".repeat(width)} requires: ${r.requires.join(", ")}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The library section sits between the generators and the footer: the footer's
|
|
357
|
+
// `meta eject` line applies to both kinds, and putting libraries after it buries
|
|
358
|
+
// the one row an agent modelling a capability most needs to see.
|
|
359
|
+
lines.push(...renderLibraryText(rows.filter((r): r is LibraryCatalogRow => r.kind === "library")));
|
|
360
|
+
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("`meta eject <name...>` copies a generator into codegen/generators/ and prints");
|
|
363
|
+
lines.push("the import to add, the entry to wire, and what to install.");
|
|
364
|
+
if (!probed) {
|
|
365
|
+
lines.push("Add --probe to see how many files each would emit for YOUR model.");
|
|
366
|
+
}
|
|
367
|
+
lines.push("`meta gen --list --format json` is the same catalog, machine-readable.");
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The TypeScript port's generator CATALOG — the one place all three registry slices
|
|
2
|
+
// are visible at once (ADR-0021 D3; opt-in-codegen design §D2a).
|
|
3
|
+
//
|
|
4
|
+
// `codegen-ts` owns the entry type and its own entries, but cannot import
|
|
5
|
+
// `codegen-ts-react` / `codegen-ts-tanstack` (dependency direction), so each of those
|
|
6
|
+
// exports its own slice and the CLI unions them here — exactly how `eject.ts` already
|
|
7
|
+
// composes its `SOURCES` table over the same three packages.
|
|
8
|
+
//
|
|
9
|
+
// Before this existed, `meta gen --list` read the codegen-ts registry and
|
|
10
|
+
// `meta eject --list` read the reference-template lists, so an agent asking "what can I
|
|
11
|
+
// turn on" got two different answers. After it, they are one table with two views.
|
|
12
|
+
//
|
|
13
|
+
// (The Angular generators stay out until ADR-0048's promotion bar is met — a
|
|
14
|
+
// source-only package is not a catalog entry.)
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
generatorRegistry,
|
|
18
|
+
GENERATOR_LAYERS,
|
|
19
|
+
type GeneratorRegistryEntry,
|
|
20
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
21
|
+
import { reactGeneratorRegistry } from "@metaobjectsdev/codegen-ts-react";
|
|
22
|
+
import { tanstackGeneratorRegistry } from "@metaobjectsdev/codegen-ts-tanstack";
|
|
23
|
+
|
|
24
|
+
/** The slices, in install-boundary order: the engine, then its framework packages. */
|
|
25
|
+
const SLICES: ReadonlyArray<readonly [string, Record<string, GeneratorRegistryEntry>]> = [
|
|
26
|
+
["@metaobjectsdev/codegen-ts", generatorRegistry],
|
|
27
|
+
["@metaobjectsdev/codegen-ts-react", reactGeneratorRegistry],
|
|
28
|
+
["@metaobjectsdev/codegen-ts-tanstack", tanstackGeneratorRegistry],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which npm package a catalog entry comes from.
|
|
33
|
+
*
|
|
34
|
+
* This is the fact composition ADDS: a slice cannot know its own package name without
|
|
35
|
+
* hard-coding it in every entry, and the package is precisely what `meta eject` has to
|
|
36
|
+
* name in an install command. Undefined for a name no slice registers.
|
|
37
|
+
*/
|
|
38
|
+
export function packageOf(name: string): string | undefined {
|
|
39
|
+
return SLICES.find(([, slice]) => name in slice)?.[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every `@metaobjectsdev` package that contributes catalog entries. */
|
|
43
|
+
export function catalogPackages(): string[] {
|
|
44
|
+
return SLICES.map(([pkg]) => pkg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The composed catalog, keyed by stable name.
|
|
49
|
+
*
|
|
50
|
+
* Throws on a name registered by two packages. A stable name identifies ONE generator
|
|
51
|
+
* across the whole catalog — that is the entire content of ADR-0021 D3 — so a collision
|
|
52
|
+
* is a build-time defect, not something to resolve by precedence. Silently letting the
|
|
53
|
+
* last slice win would make `meta gen --list` describe one generator and `meta gen` run
|
|
54
|
+
* another.
|
|
55
|
+
*/
|
|
56
|
+
export function composeCatalog(): Record<string, GeneratorRegistryEntry> {
|
|
57
|
+
return composeSlices(SLICES);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The composition itself, over any slice list.
|
|
62
|
+
*
|
|
63
|
+
* Separate from {@link composeCatalog} so the REFUSAL is reachable from a test. With the
|
|
64
|
+
* live `SLICES` closed over, the only thing a test could assert is that today's three
|
|
65
|
+
* packages happen not to collide — which stays true if the check above is deleted, so
|
|
66
|
+
* the guard that makes a collision a build failure was never covered by anything.
|
|
67
|
+
*/
|
|
68
|
+
export function composeSlices(
|
|
69
|
+
slices: ReadonlyArray<readonly [string, Record<string, GeneratorRegistryEntry>]>,
|
|
70
|
+
): Record<string, GeneratorRegistryEntry> {
|
|
71
|
+
const out: Record<string, GeneratorRegistryEntry> = {};
|
|
72
|
+
const from: Record<string, string> = {};
|
|
73
|
+
for (const [pkg, slice] of slices) {
|
|
74
|
+
for (const [name, entry] of Object.entries(slice)) {
|
|
75
|
+
if (name in out) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`generator "${name}" is registered by both ${from[name]} and ${pkg} — a stable ` +
|
|
78
|
+
"name identifies ONE generator across the whole catalog (ADR-0021 D3). Rename " +
|
|
79
|
+
"one of them, or delete the duplicate registration.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
out[name] = entry;
|
|
83
|
+
from[name] = pkg;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Every catalog entry, grouped by layer in {@link GENERATOR_LAYERS} order and
|
|
91
|
+
* alphabetical within a layer.
|
|
92
|
+
*
|
|
93
|
+
* Layer order, not tier order: `layer` is the axis an adopter selects by, so it is the
|
|
94
|
+
* axis the listing is organized along. `tier` still travels on each row.
|
|
95
|
+
*/
|
|
96
|
+
export function listCatalog(): GeneratorRegistryEntry[] {
|
|
97
|
+
return Object.values(composeCatalog()).sort(
|
|
98
|
+
(a, b) =>
|
|
99
|
+
GENERATOR_LAYERS.indexOf(a.layer) - GENERATOR_LAYERS.indexOf(b.layer) ||
|
|
100
|
+
a.name.localeCompare(b.name),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve one catalog entry by stable name, or undefined. */
|
|
105
|
+
export function catalogEntry(name: string): GeneratorRegistryEntry | undefined {
|
|
106
|
+
return composeCatalog()[name];
|
|
107
|
+
}
|
|
@@ -26,11 +26,17 @@ import type { Collection, LoadMemoryOptions } from "@metaobjectsdev/sdk";
|
|
|
26
26
|
*/
|
|
27
27
|
export function collectionLoadOptions(
|
|
28
28
|
collection: Collection,
|
|
29
|
-
): Required<Pick<LoadMemoryOptions, "files" | "fileIds" | "importedPackages" | "importedNodes">> {
|
|
29
|
+
): Required<Pick<LoadMemoryOptions, "files" | "fileIds" | "importedPackages" | "importedNodes" | "libraries">> {
|
|
30
30
|
return {
|
|
31
31
|
files: collection.files,
|
|
32
32
|
fileIds: collection.fileIds,
|
|
33
33
|
importedPackages: collection.importedPackages,
|
|
34
34
|
importedNodes: collection.importedNodes,
|
|
35
|
+
// FR-043 — the shipped-library selection moved here from metaobjects.config.ts, so
|
|
36
|
+
// it now rides the same helper as everything else the collection contributes. That
|
|
37
|
+
// is the point of the move as much as the neutrality is: `libraries` used to be
|
|
38
|
+
// threaded by `loadMemoryOptionsFrom`, a SECOND helper, and #333 is on record as the
|
|
39
|
+
// bug where one of the two reached every command and the other reached none.
|
|
40
|
+
libraries: collection.libraries,
|
|
35
41
|
};
|
|
36
42
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// What a catalog entry costs to install — the one derivation behind both
|
|
2
|
+
// `meta gen --list --format json` and `meta eject --format json`.
|
|
3
|
+
//
|
|
4
|
+
// Two kinds of package, resolved two different ways:
|
|
5
|
+
//
|
|
6
|
+
// dev the `@metaobjectsdev/codegen-*` package the generator lives in. Its range
|
|
7
|
+
// is the CLI's own version: the engine and the CLI ship in lockstep, so a
|
|
8
|
+
// generator from a different minor is a version mismatch, not a choice.
|
|
9
|
+
// runtime the `@metaobjectsdev` runtime the EMITTED code imports, plus the
|
|
10
|
+
// third-party packages it imports. The runtime's range is the CLI's version
|
|
11
|
+
// for the same reason; the third-party RANGES are read from that runtime
|
|
12
|
+
// package's own `peerDependencies` and are never written here.
|
|
13
|
+
//
|
|
14
|
+
// That last rule is the point of this module. A second copy of a peer range in the CLI
|
|
15
|
+
// is a second thing to keep in step, and the failure mode of drift is an adopter
|
|
16
|
+
// installing a major nothing was tested against — the ERESOLVE trap FR-040 §4.4
|
|
17
|
+
// documented, closed by construction rather than by a warning. If a range cannot be
|
|
18
|
+
// read, the package is reported WITHOUT one rather than guessed at.
|
|
19
|
+
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import type { GeneratorRegistryEntry } from "@metaobjectsdev/codegen-ts";
|
|
24
|
+
import { cliVersion } from "./version.js";
|
|
25
|
+
import { packageOf } from "./catalog.js";
|
|
26
|
+
import type { PackageManifest } from "./package-manifest.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A package's declared peer ranges, or `{}` if they cannot be read.
|
|
30
|
+
*
|
|
31
|
+
* Resolves the package's ENTRY and walks up to the nearest manifest, rather than asking
|
|
32
|
+
* for `"<pkg>/package.json"` directly. The direct form is the obvious one and it is
|
|
33
|
+
* WRONG: a package's `exports` map gates every subpath, and `@metaobjectsdev/runtime-ts`
|
|
34
|
+
* exports `.` / `./drivers` / `./fastify` / `./drizzle-fastify` / `./hono` and no
|
|
35
|
+
* `./package.json`, so under real Node it throws ERR_PACKAGE_PATH_NOT_EXPORTED. It
|
|
36
|
+
* appeared to work in-repo only because bun resolves a symlinked source tree; against
|
|
37
|
+
* the published package it failed silently on the first try. Resolving the entry is
|
|
38
|
+
* never gated — `.` is the one subpath every package exports.
|
|
39
|
+
*/
|
|
40
|
+
export function peerRangesOf(packageName: string): Record<string, string> {
|
|
41
|
+
try {
|
|
42
|
+
const req = createRequire(import.meta.url);
|
|
43
|
+
let dir = dirname(req.resolve(packageName));
|
|
44
|
+
// The entry sits under dist/; the manifest is at the package root above it.
|
|
45
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
46
|
+
const candidate = join(dir, "package.json");
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
const manifest = JSON.parse(readFileSync(candidate, "utf8")) as PackageManifest & {
|
|
49
|
+
name?: string;
|
|
50
|
+
};
|
|
51
|
+
// Guard against stopping at a nested manifest that is not the package itself.
|
|
52
|
+
if (manifest.name === packageName) {
|
|
53
|
+
return (manifest.peerDependencies ?? {}) as Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const up = dirname(dir);
|
|
57
|
+
if (up === dir) break;
|
|
58
|
+
dir = up;
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface InstallSet {
|
|
67
|
+
/** Build-time packages, `name@range`. */
|
|
68
|
+
dev: string[];
|
|
69
|
+
/** Application-runtime packages, `name@range` (or bare `name` if no range is known). */
|
|
70
|
+
runtime: string[];
|
|
71
|
+
/** A paste-ready shell line, or "" when there is nothing to install. */
|
|
72
|
+
command: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function spec(name: string, range: string | undefined): string {
|
|
76
|
+
return range === undefined ? name : `${name}@${range}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The consolidated install set for a group of catalog entries.
|
|
81
|
+
*
|
|
82
|
+
* Consolidated, not per-entry: ejecting `hooks` and `grid` needs
|
|
83
|
+
* `@metaobjectsdev/codegen-ts-tanstack` ONCE, and an adopter handed the same package
|
|
84
|
+
* twice reasonably wonders which one to run.
|
|
85
|
+
*/
|
|
86
|
+
export function installSetFor(entries: readonly GeneratorRegistryEntry[]): InstallSet {
|
|
87
|
+
const version = cliVersion();
|
|
88
|
+
const dev = new Set<string>();
|
|
89
|
+
const runtime = new Set<string>();
|
|
90
|
+
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const pkg = packageOf(entry.name);
|
|
93
|
+
if (pkg !== undefined) dev.add(spec(pkg, `^${version}`));
|
|
94
|
+
|
|
95
|
+
for (const rt of entry.runtimePackages ?? []) runtime.add(spec(rt, `^${version}`));
|
|
96
|
+
|
|
97
|
+
if (entry.runtimePeers !== undefined && entry.runtimePeers.length > 0) {
|
|
98
|
+
// Ranges come from whichever runtime package declares these as peers — the union
|
|
99
|
+
// over this generator's runtimes, since a generator emitting against two of them
|
|
100
|
+
// may take a peer from either. A generator with third-party peers but no runtime
|
|
101
|
+
// package of its own has none to read, so its peers are named UNPINNED: honest,
|
|
102
|
+
// and better than a made-up bound.
|
|
103
|
+
const ranges: Record<string, string> = {};
|
|
104
|
+
for (const rt of entry.runtimePackages ?? []) Object.assign(ranges, peerRangesOf(rt));
|
|
105
|
+
for (const peer of entry.runtimePeers) runtime.add(spec(peer, ranges[peer]));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const devList = [...dev].sort();
|
|
110
|
+
const runtimeList = [...runtime].sort();
|
|
111
|
+
const parts: string[] = [];
|
|
112
|
+
if (devList.length > 0) parts.push(`npm i -D ${devList.join(" ")}`);
|
|
113
|
+
if (runtimeList.length > 0) parts.push(`npm i ${runtimeList.join(" ")}`);
|
|
114
|
+
|
|
115
|
+
return { dev: devList, runtime: runtimeList, command: parts.join(" && ") };
|
|
116
|
+
}
|