@metaobjectsdev/cli 0.24.4 → 0.24.5

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.
@@ -0,0 +1,38 @@
1
+ /** Every ejectable name, in registry order (stable — matches `meta eject --list`). */
2
+ export declare function ejectableNames(): string[];
3
+ export interface EjectOptions {
4
+ cwd: string;
5
+ name: string;
6
+ /** Overwrite an already-ejected file. Without it, eject NEVER clobbers — matching
7
+ * `writeOwnedGenerators`'s unconditional preserve-if-present rule in init.ts. */
8
+ force?: boolean;
9
+ }
10
+ export interface EjectResult {
11
+ path: string;
12
+ importLine: string;
13
+ /** The symbol the template exports — the binding to REPLACE in the config. */
14
+ exportName: string;
15
+ /** The package the generator currently comes from, i.e. the import to remove. */
16
+ packageName: string;
17
+ /** Advisory lines about packages the ejected file imports but the project lacks. */
18
+ dependencyNotes: string[];
19
+ status: "created" | "preserved";
20
+ }
21
+ /**
22
+ * An ejected file is ordinary source in the adopter's repo, so its imports must be
23
+ * declared dependencies or their `tsc` reports TS2307 on the file we just told them
24
+ * they own — and under a strict (pnpm/npm) node_modules layout `meta gen` cannot
25
+ * resolve it either. `meta init` already calls this out by ADDING the two packages its
26
+ * four scaffolded generators need; the on-demand templates import two more
27
+ * (codegen-ts-react, codegen-ts-tanstack) that nothing declares.
28
+ *
29
+ * This REPORTS rather than edits: init is a scaffolder writing a whole project and has
30
+ * a manifest in hand, while eject copies one file into a repo whose dependency policy
31
+ * (workspace protocol, catalog, pinned ranges) is the adopter's. Naming the exact
32
+ * missing package and the version to match is the useful half; silently rewriting
33
+ * someone's manifest is not.
34
+ */
35
+ export declare function dependencyNotesForTemplate(cwd: string, templateSource: string): Promise<string[]>;
36
+ export declare function ejectGenerator(opts: EjectOptions): Promise<EjectResult>;
37
+ export declare function ejectCommand(args: string[], cwd: string): Promise<number>;
38
+ //# sourceMappingURL=eject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"AA4DA,sFAAsF;AACtF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AA2CD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb;sFACkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;CACjC;AAoBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkBvG;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA8B7E;AAeD,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0D/E"}
@@ -0,0 +1,233 @@
1
+ // FR-040 §4.2(a) — `meta eject <generator>` takes ownership of any reference-template
2
+ // generator, in any package, at any time after `meta init`. ADR-0034 scaffold-and-own
3
+ // has `init` copy four of them eagerly (entity, queries, routes, barrel); this is the
4
+ // SAME copy operation, generalised to every ejectable name and callable on demand — for
5
+ // a generator you skipped at init time, or one a package gained since.
6
+ import { mkdir, writeFile, stat, readFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { cliVersion } from "../lib/version.js";
9
+ import * as coreTpl from "@metaobjectsdev/codegen-ts";
10
+ import * as reactTpl from "@metaobjectsdev/codegen-ts-react";
11
+ import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
12
+ import { parseEjectArgs } from "../lib/args.js";
13
+ import { log } from "../lib/log.js";
14
+ import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js";
15
+ // Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
16
+ // same never-clobber-without-consent contract. Kept as its own local constant rather
17
+ // than shared: eject is a standalone operation on ANY name, not a byproduct of init,
18
+ // and the two call sites have no other state in common worth coupling over one string.
19
+ const OWNED_GENERATORS_DIR = "codegen/generators";
20
+ // One registry, three packages — a package that gains templates later registers itself
21
+ // here and `meta eject` picks it up with no other change.
22
+ //
23
+ // Each entry exposes its reference ROOT rather than a read function. The packages' own
24
+ // `readReferenceTemplate` narrows its parameter to a literal union, so calling it with
25
+ // a CLI-supplied `string` used to need a generic `asserts name is N` helper — ~20 lines
26
+ // to re-establish, for the compiler, a fact `resolveSource` has ALREADY established at
27
+ // runtime by selecting this entry via `names.includes(name)`. Reading from the root
28
+ // deletes that machinery without weakening anything: membership is still checked, once,
29
+ // where the untrusted value enters.
30
+ const SOURCES = [
31
+ {
32
+ packageName: "@metaobjectsdev/codegen-ts",
33
+ names: coreTpl.REFERENCE_GENERATOR_NAMES,
34
+ root: coreTpl.resolveReferenceRoot,
35
+ },
36
+ {
37
+ packageName: "@metaobjectsdev/codegen-ts-react",
38
+ names: reactTpl.REFERENCE_GENERATOR_NAMES,
39
+ root: reactTpl.resolveReferenceRoot,
40
+ },
41
+ {
42
+ packageName: "@metaobjectsdev/codegen-ts-tanstack",
43
+ names: tanstackTpl.REFERENCE_GENERATOR_NAMES,
44
+ root: tanstackTpl.resolveReferenceRoot,
45
+ },
46
+ ];
47
+ function resolveSource(name) {
48
+ return SOURCES.find((s) => s.names.includes(name));
49
+ }
50
+ /** Every ejectable name, in registry order (stable — matches `meta eject --list`). */
51
+ export function ejectableNames() {
52
+ return SOURCES.flatMap((s) => s.names);
53
+ }
54
+ // Every reference template's header documents its own paste-ready import line, e.g.
55
+ // codegen-ts/src/reference/entity.ts:
56
+ // // Then import it LOCALLY in metaobjects.config.ts:
57
+ // // import { entityFile } from "./codegen/generators/entity.js";
58
+ // Extracting it here — rather than re-deriving an export symbol from the file name —
59
+ // means eject can never drift from what the template itself already tells a human to
60
+ // paste, and needs no per-name export-symbol map: a generator's exported symbol does
61
+ // NOT follow its file name (`hooks.ts` exports `tanstackQuery`, `routes-hono.ts`
62
+ // exports `routesFileHono`, `grid.ts` exports `tanstackGrid`).
63
+ const HEADER_IMPORT_RE = /^\/\/\s+(import \{ \w+ \} from "\.\/codegen\/generators\/[\w.-]+\.js";)\s*$/m;
64
+ /** The bound symbol out of the already-validated import line — same single source of
65
+ * truth as the line itself, so the "replace this binding" message can never name a
66
+ * symbol the template does not actually export. */
67
+ function extractExportName(importLine, name) {
68
+ const match = /^import \{ (\w+) \}/.exec(importLine);
69
+ if (!match?.[1]) {
70
+ throw new Error(`reference template "${name}" has an unparseable import line: ${importLine}`);
71
+ }
72
+ return match[1];
73
+ }
74
+ function extractImportLine(templateSource, name) {
75
+ const match = HEADER_IMPORT_RE.exec(templateSource);
76
+ if (!match?.[1]) {
77
+ throw new Error(`reference template "${name}" has no documented "// import { ... }" header line — ` +
78
+ "cannot report the import line to paste.");
79
+ }
80
+ return match[1];
81
+ }
82
+ async function fileExists(p) {
83
+ try {
84
+ return (await stat(p)).isFile();
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /** The `@metaobjectsdev/*` packages an ejected template imports, read from the file
91
+ * itself rather than from a per-name table — the template is the only thing that
92
+ * knows, and a table would drift from it the moment a template gains an import. */
93
+ function requiredPackages(templateSource) {
94
+ const found = new Set();
95
+ // The optional trailing group matches a SUBPATH and is deliberately not captured: what
96
+ // has to be installed is the package, and `@metaobjectsdev/metadata/constants` is a
97
+ // real, documented subpath this codebase already uses (the browser-safe pure-constants
98
+ // entry). Requiring the closing quote straight after the package name — as this did —
99
+ // made any such import match nothing at all, so a template gaining one would get no
100
+ // note. That is the drift this function exists to prevent, reappearing inside the
101
+ // function itself.
102
+ for (const m of templateSource.matchAll(/from\s+"(@metaobjectsdev\/[\w-]+)(?:\/[\w./-]+)?"/g)) {
103
+ if (m[1] !== undefined)
104
+ found.add(m[1]);
105
+ }
106
+ return [...found].sort();
107
+ }
108
+ /**
109
+ * An ejected file is ordinary source in the adopter's repo, so its imports must be
110
+ * declared dependencies or their `tsc` reports TS2307 on the file we just told them
111
+ * they own — and under a strict (pnpm/npm) node_modules layout `meta gen` cannot
112
+ * resolve it either. `meta init` already calls this out by ADDING the two packages its
113
+ * four scaffolded generators need; the on-demand templates import two more
114
+ * (codegen-ts-react, codegen-ts-tanstack) that nothing declares.
115
+ *
116
+ * This REPORTS rather than edits: init is a scaffolder writing a whole project and has
117
+ * a manifest in hand, while eject copies one file into a repo whose dependency policy
118
+ * (workspace protocol, catalog, pinned ranges) is the adopter's. Naming the exact
119
+ * missing package and the version to match is the useful half; silently rewriting
120
+ * someone's manifest is not.
121
+ */
122
+ export async function dependencyNotesForTemplate(cwd, templateSource) {
123
+ const required = requiredPackages(templateSource);
124
+ if (required.length === 0)
125
+ return [];
126
+ const pkg = readPackageManifest(cwd);
127
+ if (pkg === undefined) {
128
+ // No readable manifest — say what the file needs and let the adopter place it.
129
+ return [`This file imports: ${required.join(", ")}. Make sure each is installed.`];
130
+ }
131
+ const declared = declaredDependencyNames(pkg);
132
+ const missing = required.filter((p) => !declared.has(p));
133
+ if (missing.length === 0)
134
+ return [];
135
+ return [
136
+ `The ejected file imports ${missing.join(", ")}, which your package.json does not ` +
137
+ "declare — your typecheck will report TS2307 until it does. Install with:",
138
+ ` npm i -D ${missing.map((p) => `${p}@^${cliVersion()}`).join(" ")}`,
139
+ ];
140
+ }
141
+ export async function ejectGenerator(opts) {
142
+ const source = resolveSource(opts.name);
143
+ if (source === undefined) {
144
+ throw new Error(`unknown generator "${opts.name}". Ejectable generators: ${ejectableNames().join(", ")}. ` +
145
+ "Run `meta eject --list` to see them grouped by package.");
146
+ }
147
+ const templateSource = await readFile(join(source.root(), `${opts.name}.ts`), "utf8");
148
+ const importLine = extractImportLine(templateSource, opts.name);
149
+ const exportName = extractExportName(importLine, opts.name);
150
+ const rel = `${OWNED_GENERATORS_DIR}/${opts.name}.ts`;
151
+ const abs = join(opts.cwd, rel);
152
+ const notes = await dependencyNotesForTemplate(opts.cwd, templateSource);
153
+ const common = {
154
+ path: rel,
155
+ importLine,
156
+ exportName,
157
+ packageName: source.packageName,
158
+ dependencyNotes: notes,
159
+ };
160
+ if (!opts.force && (await fileExists(abs))) {
161
+ return { ...common, status: "preserved" };
162
+ }
163
+ await mkdir(join(opts.cwd, OWNED_GENERATORS_DIR), { recursive: true });
164
+ await writeFile(abs, templateSource, "utf8");
165
+ return { ...common, status: "created" };
166
+ }
167
+ function listOutput() {
168
+ const lines = [];
169
+ lines.push("Ejectable generators (copy any of these into codegen/generators/ and own it):");
170
+ lines.push("");
171
+ for (const source of SOURCES) {
172
+ lines.push(`${source.packageName}:`);
173
+ lines.push(` ${source.names.join(", ")}`);
174
+ lines.push("");
175
+ }
176
+ lines.push("Run: meta eject <name>");
177
+ return lines.join("\n");
178
+ }
179
+ export async function ejectCommand(args, cwd) {
180
+ let flags;
181
+ try {
182
+ flags = parseEjectArgs(args);
183
+ }
184
+ catch (err) {
185
+ log.error(err.message);
186
+ return 2;
187
+ }
188
+ if (flags.list) {
189
+ log.info(listOutput());
190
+ return 0;
191
+ }
192
+ if (flags.name === undefined) {
193
+ log.error("meta eject requires a generator name, or --list to see what's ejectable.");
194
+ return 2;
195
+ }
196
+ try {
197
+ const result = await ejectGenerator({ cwd, name: flags.name, force: flags.force });
198
+ if (result.status === "preserved") {
199
+ log.info(`${result.path} already exists — left untouched (pass --force to overwrite).`);
200
+ }
201
+ else {
202
+ log.info(`Ejected "${flags.name}" -> ${result.path}. You own it now (ADR-0034 scaffold-and-own).`);
203
+ }
204
+ // REPLACE, never "paste". A generator reaches `generators: [...]` under ONE binding,
205
+ // so a reader told to "paste" gets a duplicate identifier at best — and at worst
206
+ // deletes nothing, keeps `formFile()` in the array bound to the PACKAGE import, and
207
+ // silently runs the packaged generator while editing the ejected file. That failure
208
+ // is invisible and is the exact one ejecting exists to prevent.
209
+ //
210
+ // But eject reads no config, so it cannot know WHICH of the three states this project
211
+ // is in, and stating one of them as fact is wrong in the other two — including for the
212
+ // four `meta init` scaffolds, whose config already imports from ./codegen/generators/,
213
+ // which is precisely the `meta eject <name> --force` re-sync case. Name the goal, then
214
+ // the three branches; the reader knows which one they are looking at.
215
+ log.info(`In metaobjects.config.ts, "${result.exportName}" must resolve to this file:`);
216
+ log.info(` ${result.importLine}`);
217
+ log.info(` - If it is imported from "${result.packageName}", REPLACE that import with the ` +
218
+ "line above. Adding a second one leaves `generators` bound to the PACKAGED " +
219
+ "generator, and your edits to this file do nothing.");
220
+ log.info(" - If it is already imported from ./codegen/generators/ (what `meta init` " +
221
+ "scaffolds), it points here already — nothing to change.");
222
+ log.info(` - If ${result.exportName}() is not in \`generators\` yet, add the import above ` +
223
+ "AND the entry.");
224
+ for (const line of result.dependencyNotes)
225
+ log.info(line);
226
+ return 0;
227
+ }
228
+ catch (err) {
229
+ log.error(err.message);
230
+ return 1;
231
+ }
232
+ }
233
+ //# sourceMappingURL=eject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eject.js","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,16 @@ export interface InitResult {
26
26
  warnings: string[];
27
27
  }
28
28
  export declare function init(opts: InitOptions): Promise<InitResult>;
29
- export declare function nextStepsBlock(): string;
29
+ /**
30
+ * The post-init message.
31
+ *
32
+ * @param dbStubWritten whether THIS run wrote {@link DB_STUB_REL_PATH}. Required rather
33
+ * than defaulted, because a default would silently restore the bug this parameter
34
+ * exists to close: the note used to be part of one static string and so claimed the
35
+ * stub on every run, including the `meta init --force` in a project keeping its own
36
+ * `metaobjects.config.ts`, where the stub is deliberately not written. Callers pass
37
+ * `result.created.includes(DB_STUB_REL_PATH)` — the same list the message describes.
38
+ */
39
+ export declare function nextStepsBlock(dbStubWritten: boolean): string;
30
40
  export declare function initCommand(args: string[], cwd: string): Promise<number>;
31
41
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AA8HA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4KAA4K;IAC5K,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAoQD,wBAAsB,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAmIjE;AA2JD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAoBD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuD9E"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AA8NA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4KAA4K;IAC5K,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAyQD,wBAAsB,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA8KjE;AA0KD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,CAE7D;AAoBD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuD9E"}
@@ -8,11 +8,30 @@ import { resolveStack } from "../lib/detect-stack.js";
8
8
  import { parseInitArgs } from "../lib/args.js";
9
9
  import { log } from "../lib/log.js";
10
10
  import { cliVersion } from "../lib/version.js";
11
+ import { declaredDependencyNames } from "../lib/package-manifest.js";
11
12
  import { findWranglerConfig, parseWranglerConfig } from "@metaobjectsdev/migrate-ts";
12
- import { readReferenceTemplate, REFERENCE_GENERATOR_NAMES } from "@metaobjectsdev/codegen-ts";
13
+ import { readReferenceTemplate } from "@metaobjectsdev/codegen-ts";
13
14
  // ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into
14
15
  // the consumer's repo so they OWN them; metaobjects.config.ts imports them locally.
15
16
  const OWNED_GENERATORS_DIR = "codegen/generators";
17
+ // The FOUR reference generators `meta init` copies EAGERLY — deliberately an explicit
18
+ // literal, not derived from @metaobjectsdev/codegen-ts's REFERENCE_GENERATOR_NAMES (the
19
+ // full list of everything `meta eject` can copy). Looping over that array unconditionally
20
+ // used to mean init scaffolded whatever it contained: when a later task registered
21
+ // "routes-hono" there, init started writing an unwired Hono generator nothing in the
22
+ // scaffolded config imports, on every fresh project, silently. This constant is the
23
+ // scaffolded metaobjects.config.ts's import list (buildMetaobjectsConfigBody below) made
24
+ // explicit and checkable — anything else is eject-on-demand via `meta eject <name>`, which
25
+ // exists exactly so eager copying isn't the only way to take ownership of a template.
26
+ const SCAFFOLDED_GENERATOR_NAMES = ["entity", "queries", "routes", "barrel"];
27
+ // The scaffolded config's outDir + dbImport, as named constants so the throwing-stub
28
+ // path below is DERIVED from the same values the config template embeds rather than
29
+ // duplicated as a second literal that could drift from it.
30
+ const SCAFFOLD_OUT_DIR = "src/generated";
31
+ const SCAFFOLD_DB_IMPORT = "../db";
32
+ // "src/generated" + "../db" -> "src/db" -> "src/db.ts" (dbImport resolves relative
33
+ // to outDir, same as the module specifier a generated route file emits).
34
+ const DB_STUB_REL_PATH = `${join(SCAFFOLD_OUT_DIR, SCAFFOLD_DB_IMPORT)}.ts`;
16
35
  const META_COMMON_JSON = JSON.stringify({
17
36
  metadata: {
18
37
  package: "",
@@ -75,9 +94,18 @@ import { routesFile } from "./codegen/generators/routes.js";
75
94
  import { barrel } from "./codegen/generators/barrel.js";
76
95
 
77
96
  export default defineConfig({
78
- outDir: "src/generated",
79
- extStyle: "js", // ".js"-extensioned relative imports — safe under Node ESM / tsc nodenext AND bundlers
80
- dbImport: "../db",
97
+ outDir: "${SCAFFOLD_OUT_DIR}",
98
+ extStyle: "js", // ".js"-extensioned relative imports — correct for Node ESM and \`tsc\` with
99
+ // nodenext, which is what a fresh project has. BUNDLERS DISAGREE: this fails
100
+ // outright under Turbopack (even between two generated files, so the whole
101
+ // generated tree goes unresolvable), while Vite and esbuild accept it and
102
+ // webpack needs \`resolve.extensionAlias\`. If a generated import fails to
103
+ // resolve, set "none" and retest — do not assume this line covers your bundler.
104
+ dbImport: "${SCAFFOLD_DB_IMPORT}", // routesFile() below emits \`import { db } from …\` — meta init
105
+ // scaffolded ${DB_STUB_REL_PATH} as a THROWING STUB (types clean, no
106
+ // driver chosen) so meta gen and tsc pass; replace it with your real
107
+ // Drizzle connection before running the app.
108
+ // (queriesFile() takes db as a parameter and never reads this.)
81
109
  dialect: "${dialect}",
82
110
  apiPrefix: "", // set to "/api" if your routes mount under /api
83
111
  generators: [
@@ -94,10 +122,73 @@ export default defineConfig({
94
122
  });
95
123
  `;
96
124
  }
97
- const NEXT_STEPS = `
125
+ // The throwing-stub scaffolded at `dbImport`'s resolved path (DB_STUB_REL_PATH,
126
+ // "src/db.ts" by default). It exists so `meta gen` and a fresh project's FIRST
127
+ // `tsc` both succeed with no driver chosen and no dependency added — deliberately
128
+ // NOT a real connection. Every generated route only ever passes `db` straight
129
+ // through to `mountCrudRoutes(...)`; nothing reads a property off it at import
130
+ // time, so a value typed `unknown` (not `any`) satisfies every call site while
131
+ // making a genuine runtime use (mountCrudRoutes calling `db.select()` etc.) throw
132
+ // immediately with an actionable message instead of failing to resolve at all.
133
+ // Built as an array of plain single-quoted lines (not a template literal) so the
134
+ // backticks and quotes inside the comment/message need no escaping.
135
+ const DB_STUB_BODY = [
136
+ "// `meta init` scaffolded this file because the generated Fastify routes",
137
+ '// `import { db } from "../db.js"` (see `dbImport` in metaobjects.config.ts) —',
138
+ "// a module that has to exist for `meta gen` and `tsc` to succeed. MetaObjects",
139
+ "// cannot fill it in for real without choosing a database driver on your",
140
+ "// behalf (better-sqlite3 vs @libsql/client vs pg vs postgres.js) and adding a",
141
+ "// dependency you may not want, so this is a STUB, not a connection.",
142
+ "//",
143
+ "// It type-checks and satisfies every generated import, but throws the first",
144
+ "// time anything actually touches `db` at runtime. Replace the export below",
145
+ "// with your real Drizzle connection, e.g.:",
146
+ "//",
147
+ '// import { drizzle } from "drizzle-orm/better-sqlite3";',
148
+ '// import Database from "better-sqlite3";',
149
+ '// export const db = drizzle(new Database("dev.sqlite"));',
150
+ "//",
151
+ "// (swap the driver import for your dialect — see",
152
+ "// https://github.com/metaobjectsdev/metaobjects/blob/main/docs/recipes/wiring-generated-queries.md",
153
+ "// for SQLite/libsql, Cloudflare D1, Postgres and multi-tenant setups.)",
154
+ "",
155
+ "const UNWIRED_MESSAGE =",
156
+ ' "src/db.ts is still the scaffolded stub meta init wrote — it cannot choose " +',
157
+ " \"a database driver for you. Replace 'export const db = ...' below with \" +",
158
+ ' "your real Drizzle connection, e.g.:\\n\\n" +',
159
+ " \" import { drizzle } from 'drizzle-orm/better-sqlite3';\\n\" +",
160
+ " \" import Database from 'better-sqlite3';\\n\" +",
161
+ " \" export const db = drizzle(new Database('dev.sqlite'));\\n\";",
162
+ "",
163
+ "function unwired(): never {",
164
+ " throw new Error(UNWIRED_MESSAGE);",
165
+ "}",
166
+ "",
167
+ "/**",
168
+ " * Stand-in for your real Drizzle database connection. Generated code only",
169
+ " * ever passes `db` straight through to `mountCrudRoutes(...)` — it never",
170
+ " * reads a property off it at import time — so this typechecks everywhere",
171
+ " * `db` is used, and throws the message above the first time anything really",
172
+ " * touches it.",
173
+ " */",
174
+ "export const db: unknown = new Proxy({}, { get: unwired });",
175
+ "",
176
+ ].join("\n");
177
+ // Printed only when the stub was ACTUALLY written this run. It is gated on having just
178
+ // written the scaffolded config (see the db-stub block in `init`), so a re-run in a
179
+ // project that keeps its own config writes nothing — and a block claiming otherwise is
180
+ // the same "asserting things about its own scaffold that aren't true" defect the rest of
181
+ // this file was corrected for.
182
+ const DB_STUB_NOTE = `Also scaffolded ${DB_STUB_REL_PATH}: a THROWING STUB standing in for your database
183
+ connection, so the generated routes' \`db\` import resolves and the first tsc is clean.
184
+ Replace it with a real connection before running the app — until you do, the first
185
+ request that touches \`db\` throws with instructions.
186
+ `;
187
+ const SCAFFOLD_SUMMARY = `
98
188
  Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
99
189
  Codegen generators copied to codegen/generators/ — they're YOURS to edit (ADR-0034 scaffold-and-own).
100
-
190
+ `;
191
+ const NEXT_STEPS = `
101
192
  Next steps:
102
193
  0. Everything here is ESM — package.json needs "type": "module" (init sets it
103
194
  unless the project has CommonJS sources; without it the first tsc fails).
@@ -279,11 +370,16 @@ async function wireRootMemory(cwd, result, dryRun = false) {
279
370
  * `codegen/generators/<name>.ts` so they own them. Each file is written only if absent,
280
371
  * so a re-run with --force never clobbers a hand-edited generator. The scaffolded
281
372
  * metaobjects.config.ts imports these local copies (not the package `/generators` export).
373
+ *
374
+ * Copies SCAFFOLDED_GENERATOR_NAMES only — the four the scaffolded config actually
375
+ * wires — not every name @metaobjectsdev/codegen-ts happens to register. Anything else
376
+ * (routes-hono, and any UI-tier template from codegen-ts-react/-tanstack) is reached with
377
+ * `meta eject <name>`, not by eager copying.
282
378
  */
283
379
  async function writeOwnedGenerators(opts, result) {
284
380
  const dir = join(opts.cwd, OWNED_GENERATORS_DIR);
285
381
  await mkdir(dir, { recursive: true });
286
- for (const name of REFERENCE_GENERATOR_NAMES) {
382
+ for (const name of SCAFFOLDED_GENERATOR_NAMES) {
287
383
  const rel = `${OWNED_GENERATORS_DIR}/${name}.ts`;
288
384
  const abs = join(dir, `${name}.ts`);
289
385
  if (await fileExists(abs)) {
@@ -405,9 +501,9 @@ export async function init(opts) {
405
501
  result.created.push(d);
406
502
  result.created.push("metaobjects/meta.common.json", ".metaobjects/config.json", ".metaobjects/.gitignore", `.metaobjects/${PACKAGE_MANIFEST_FILE}`);
407
503
  result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
408
- for (const name of REFERENCE_GENERATOR_NAMES)
504
+ for (const name of SCAFFOLDED_GENERATOR_NAMES)
409
505
  result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
410
- result.created.push("metaobjects.config.ts", ".gitignore");
506
+ result.created.push("metaobjects.config.ts", DB_STUB_REL_PATH, ".gitignore");
411
507
  return result;
412
508
  }
413
509
  for (const d of dirs) {
@@ -450,10 +546,52 @@ export async function init(opts) {
450
546
  await writeOwnedGenerators(opts, result);
451
547
  // Scaffold metaobjects.config.ts at the project root. Never overwrite if it exists.
452
548
  const forgeConfigPath = join(opts.cwd, "metaobjects.config.ts");
453
- if (!(await fileExists(forgeConfigPath))) {
549
+ const wroteScaffoldedConfig = !(await fileExists(forgeConfigPath));
550
+ if (wroteScaffoldedConfig) {
454
551
  await writeFile(forgeConfigPath, buildMetaobjectsConfigBody(opts.d1 ? "d1" : "sqlite"), "utf8");
455
552
  result.created.push("metaobjects.config.ts");
456
553
  }
554
+ // Scaffold the `dbImport` throwing stub at DB_STUB_REL_PATH ("src/db.ts" by
555
+ // default) — ONLY if absent, so a re-run never clobbers a user's real db module
556
+ // (same "write once" precedent as writeOwnedGenerators above). Without this, the
557
+ // scaffolded config declares `dbImport: "../db"` pointing at a module `meta init`
558
+ // never creates, and a fresh project's FIRST `tsc` fails to resolve it.
559
+ //
560
+ // Gated on having just WRITTEN that config, not merely on the stub being absent.
561
+ // DB_STUB_REL_PATH is derived from SCAFFOLD_OUT_DIR + SCAFFOLD_DB_IMPORT — the
562
+ // scaffold's own constants — so it describes where the SCAFFOLDED config points and
563
+ // nowhere else. Re-running `meta init` in a project that already has a config with
564
+ // its own `outDir`/`dbImport` preserves that config (above) and would otherwise still
565
+ // drop a src/db.ts that nothing in the project references: a stray file, in the
566
+ // adopter's application source, answering a question they had already answered.
567
+ const dbStubPath = join(opts.cwd, DB_STUB_REL_PATH);
568
+ const dbStubExists = await fileExists(dbStubPath);
569
+ if (wroteScaffoldedConfig && !dbStubExists) {
570
+ await mkdir(dirname(dbStubPath), { recursive: true });
571
+ await writeFile(dbStubPath, DB_STUB_BODY, "utf8");
572
+ result.created.push(DB_STUB_REL_PATH);
573
+ }
574
+ else if (dbStubExists) {
575
+ // "preserved" means a file we would have written was left alone. Skipping because
576
+ // this project keeps its own config is not preservation — there is nothing there.
577
+ result.preserved.push(DB_STUB_REL_PATH);
578
+ }
579
+ else {
580
+ // Neither written nor preserved: this project keeps its own config AND has no stub.
581
+ // Usually correct — its `dbImport` points at a real module somewhere else. But it is
582
+ // ALSO what a scaffolded project looks like after someone deletes or moves src/db.ts,
583
+ // and `meta init --force` cannot tell those apart: `wroteScaffoldedConfig` is only
584
+ // "no config existed", so it is false for the config init itself wrote. Silently
585
+ // doing nothing there leaves the scaffolded `dbImport: "../db"` and the generated
586
+ // routes' `import { db } from "../db.js"` pointing at nothing, and the adopter meets
587
+ // it as a TS2307 from `tsc` with no word from the command that could have said so.
588
+ // Say it here rather than writing: dropping a file into a project that owns its
589
+ // config is what the branch above deliberately refuses.
590
+ result.warnings.push(`${DB_STUB_REL_PATH} was not scaffolded — this project has its own ` +
591
+ "metaobjects.config.ts, so init leaves the database module to it. If that config's " +
592
+ `\`dbImport\` resolves to ${DB_STUB_REL_PATH}, create it or the generated routes will ` +
593
+ "not resolve.");
594
+ }
457
595
  // Scaffold a minimal root .gitignore ONLY when the project has none — never
458
596
  // clobber a user's existing one (they may have their own rules).
459
597
  const rootGitignorePath = join(opts.cwd, ".gitignore");
@@ -513,6 +651,7 @@ async function ensureEsmPackageType(cwd, result) {
513
651
  "code in a sub-directory with its own package.json declaring `\"type\": \"module\"`.");
514
652
  return;
515
653
  }
654
+ const declaredType = pkg.type; // read BEFORE the mutation below overwrites it
516
655
  pkg.type = "module";
517
656
  const added = addScaffoldDevDependencies(pkg);
518
657
  // Preserve the file's existing indentation rather than reformatting someone's manifest.
@@ -521,8 +660,15 @@ async function ensureEsmPackageType(cwd, result) {
521
660
  // Past tense, deliberately: this reports an edit already made. The imperative
522
661
  // ("set `\"type\": \"module\"`") read as a TODO on the one line a newcomer sees
523
662
  // last, so a scaffold that had just done the right thing looked like it had failed.
524
- result.warnings.push('package.json declared no module system — set `"type": "module"` for you, because ' +
525
- "MetaObjects scaffolds and generates ESM, which a CommonJS project cannot compile.");
663
+ //
664
+ // And it must not claim the manifest was SILENT on the point: `npm init -y` writes
665
+ // `"type": "commonjs"` explicitly (npm 11.x), which is the dominant first-touch path,
666
+ // so "declared no module system" was false exactly where it is read most. Report what
667
+ // was actually there.
668
+ const previous = typeof declaredType === "string" ? declaredType : undefined;
669
+ result.warnings.push(`package.json ${previous === undefined ? "declared no module system" : `declared "type": "${previous}"`} — ` +
670
+ 'set `"type": "module"` for you, because MetaObjects scaffolds and generates ESM, ' +
671
+ "which a CommonJS project cannot compile.");
526
672
  if (added.length > 0) {
527
673
  result.warnings.push(`added ${added.join(" + ")} to devDependencies — the scaffolded ` +
528
674
  "codegen/generators/ are YOUR source now (ADR-0034) and import them. " +
@@ -553,10 +699,17 @@ function addScaffoldDevDependencies(pkg) {
553
699
  "@metaobjectsdev/metadata": `^${version}`,
554
700
  };
555
701
  const dev = (pkg.devDependencies ?? {});
556
- const deps = (pkg.dependencies ?? {});
702
+ // "Already declared" spans all four dependency fields — the shared rule, so this
703
+ // cannot drift from what `meta eject` means by the same words. It used to ask only
704
+ // dependencies + devDependencies, which meant a project declaring codegen-ts as a
705
+ // PEER dependency (correct for a library whose consumer supplies the version) got it
706
+ // added to devDependencies as well: the same package pinned twice in one manifest,
707
+ // and a second physical copy is the class-identity split this repo has been bitten
708
+ // by twice.
709
+ const declared = declaredDependencyNames(pkg);
557
710
  const added = [];
558
711
  for (const [name, range] of Object.entries(wanted)) {
559
- if (dev[name] !== undefined || deps[name] !== undefined)
712
+ if (declared.has(name))
560
713
  continue;
561
714
  dev[name] = range;
562
715
  added.push(name);
@@ -619,8 +772,18 @@ function buildD1MigrateBlock(cwd) {
619
772
  }
620
773
  return block;
621
774
  }
622
- export function nextStepsBlock() {
623
- return NEXT_STEPS;
775
+ /**
776
+ * The post-init message.
777
+ *
778
+ * @param dbStubWritten whether THIS run wrote {@link DB_STUB_REL_PATH}. Required rather
779
+ * than defaulted, because a default would silently restore the bug this parameter
780
+ * exists to close: the note used to be part of one static string and so claimed the
781
+ * stub on every run, including the `meta init --force` in a project keeping its own
782
+ * `metaobjects.config.ts`, where the stub is deliberately not written. Callers pass
783
+ * `result.created.includes(DB_STUB_REL_PATH)` — the same list the message describes.
784
+ */
785
+ export function nextStepsBlock(dbStubWritten) {
786
+ return SCAFFOLD_SUMMARY + (dbStubWritten ? DB_STUB_NOTE : "") + NEXT_STEPS;
624
787
  }
625
788
  async function dirExists(p) {
626
789
  try {
@@ -688,7 +851,7 @@ export async function initCommand(args, cwd) {
688
851
  log.warn(w);
689
852
  }
690
853
  else {
691
- log.info(nextStepsBlock());
854
+ log.info(nextStepsBlock(result.created.includes(DB_STUB_REL_PATH)));
692
855
  // Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
693
856
  // discovery warning) — these are otherwise dropped on the normal init path.
694
857
  for (const w of result.warnings)