@metaobjectsdev/cli 1.0.9-rc.2 → 1.0.9-rc.4

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.
Files changed (59) hide show
  1. package/dist/src/commands/eject.d.ts +3 -0
  2. package/dist/src/commands/eject.d.ts.map +1 -1
  3. package/dist/src/commands/eject.js +10 -2
  4. package/dist/src/commands/eject.js.map +1 -1
  5. package/dist/src/commands/gen.d.ts +0 -5
  6. package/dist/src/commands/gen.d.ts.map +1 -1
  7. package/dist/src/commands/gen.js +18 -2
  8. package/dist/src/commands/gen.js.map +1 -1
  9. package/dist/src/commands/migrate.d.ts.map +1 -1
  10. package/dist/src/commands/migrate.js +5 -1
  11. package/dist/src/commands/migrate.js.map +1 -1
  12. package/dist/src/commands/verify.d.ts +13 -1
  13. package/dist/src/commands/verify.d.ts.map +1 -1
  14. package/dist/src/commands/verify.js +64 -11
  15. package/dist/src/commands/verify.js.map +1 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +4 -0
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lib/args.d.ts +12 -0
  20. package/dist/src/lib/args.d.ts.map +1 -1
  21. package/dist/src/lib/args.js +6 -0
  22. package/dist/src/lib/args.js.map +1 -1
  23. package/dist/src/lib/catalog-listing.d.ts +10 -0
  24. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  25. package/dist/src/lib/catalog-listing.js +43 -7
  26. package/dist/src/lib/catalog-listing.js.map +1 -1
  27. package/dist/src/lib/codegen-drift.d.ts +10 -0
  28. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  29. package/dist/src/lib/codegen-drift.js +8 -0
  30. package/dist/src/lib/codegen-drift.js.map +1 -1
  31. package/dist/src/lib/config.d.ts +8 -0
  32. package/dist/src/lib/config.d.ts.map +1 -1
  33. package/dist/src/lib/config.js +11 -0
  34. package/dist/src/lib/config.js.map +1 -1
  35. package/dist/src/lib/gen-migrate-advice.d.ts +17 -0
  36. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -0
  37. package/dist/src/lib/gen-migrate-advice.js +105 -0
  38. package/dist/src/lib/gen-migrate-advice.js.map +1 -0
  39. package/dist/src/lib/install-set.d.ts +5 -1
  40. package/dist/src/lib/install-set.d.ts.map +1 -1
  41. package/dist/src/lib/install-set.js +9 -1
  42. package/dist/src/lib/install-set.js.map +1 -1
  43. package/dist/src/lib/output.d.ts +5 -0
  44. package/dist/src/lib/output.d.ts.map +1 -1
  45. package/dist/src/lib/output.js +1 -1
  46. package/dist/src/lib/output.js.map +1 -1
  47. package/package.json +10 -10
  48. package/src/commands/eject.ts +12 -2
  49. package/src/commands/gen.ts +20 -2
  50. package/src/commands/migrate.ts +5 -0
  51. package/src/commands/verify.ts +66 -10
  52. package/src/index.ts +4 -0
  53. package/src/lib/args.ts +16 -0
  54. package/src/lib/catalog-listing.ts +66 -9
  55. package/src/lib/codegen-drift.ts +17 -0
  56. package/src/lib/config.ts +14 -0
  57. package/src/lib/gen-migrate-advice.ts +129 -0
  58. package/src/lib/install-set.ts +11 -1
  59. package/src/lib/output.ts +6 -1
@@ -0,0 +1,129 @@
1
+ // The "now migrate" next step `meta gen` prints — WHEN a schema change is plausible,
2
+ // and in the project's own dialect.
3
+ //
4
+ // It used to be one hardcoded line on every run that wrote a file:
5
+ // `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`.
6
+ // That was wrong twice over for a D1 project (whose migrate takes `--dialect d1 --d1
7
+ // <binding>` and applies through wrangler), and it was noise after any change that
8
+ // cannot reach the database — a value object or a prompt payload has no table.
9
+ //
10
+ // The decision, most precise evidence first:
11
+ // 1. No table-backed object in the model → no advice (nothing to migrate, ever).
12
+ // 2. Nothing written this run → no advice.
13
+ // 3. A committed schema snapshot exists → advise exactly when the expected schema's
14
+ // TABLES differ from it (the same diff `meta migrate` runs offline).
15
+ // 4. No snapshot (a D1 project, or before the first migrate) → advise when a file
16
+ // generated for a table-backed object, or the shared enums module (enum members
17
+ // are CHECK constraints), changed this run. Files of value objects, prompts and
18
+ // other sourceless declarations do not count.
19
+ import { existsSync } from "node:fs";
20
+ import { basename, resolve as resolvePath } from "node:path";
21
+ import type { MetaRoot } from "@metaobjectsdev/metadata";
22
+ import {
23
+ buildExpectedSchemaWithProvenance,
24
+ diff,
25
+ findWranglerConfig,
26
+ parseWranglerConfig,
27
+ readSnapshot,
28
+ snapshotPath,
29
+ type Change,
30
+ type D1Binding,
31
+ } from "@metaobjectsdev/migrate-ts";
32
+
33
+ /** The FQN separator; an object's short name is the segment after the last one. */
34
+ const FQN_SEPARATOR = "::";
35
+ /** The shared enums module `entityFile()` emits (its members back CHECK constraints). */
36
+ const SHARED_ENUMS_BASENAME = "enums.ts";
37
+
38
+ const VIEW_CHANGE_KINDS: ReadonlySet<Change["kind"]> = new Set(["create-view", "drop-view", "replace-view"]);
39
+
40
+ export interface GenMigrateAdviceInput {
41
+ metadata: MetaRoot;
42
+ /** `metaobjects.config.ts` `dialect` — undefined when no DB code is generated. */
43
+ dialect: "sqlite" | "postgres" | undefined;
44
+ /** `.metaobjects/config.json` `migrate.dialect`, when set (may say `d1`). */
45
+ migrateDialect: "sqlite" | "postgres" | "d1" | undefined;
46
+ columnNamingStrategy: "snake_case" | "literal" | "kebab-case" | undefined;
47
+ projectRoot: string;
48
+ /** The migrations directory `meta migrate` uses (holds the committed snapshot). */
49
+ migrateOutDir: string;
50
+ /** Paths (any form) of the files this run wrote, merged or removed. */
51
+ changedFiles: readonly string[];
52
+ }
53
+
54
+ /** The next-step line, or undefined when this run cannot have changed the schema. */
55
+ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<string | undefined> {
56
+ if (input.dialect === undefined || input.changedFiles.length === 0) return undefined;
57
+
58
+ const d1 = d1BindingFor(input);
59
+ const schemaDialect = d1 !== undefined ? "d1" : input.dialect;
60
+ let built;
61
+ try {
62
+ built = buildExpectedSchemaWithProvenance(input.metadata, {
63
+ dialect: schemaDialect,
64
+ ...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
65
+ });
66
+ } catch {
67
+ // A model `meta migrate` would refuse (it says why); gen is not the place to report it.
68
+ return undefined;
69
+ }
70
+ const expected = { tables: built.snapshot.tables, views: [] };
71
+ if (expected.tables.length === 0) return undefined;
72
+
73
+ const snapPath = snapshotPath(resolvePath(input.projectRoot, input.migrateOutDir), schemaDialect);
74
+ const snapshot = existsSync(snapPath) ? await readSnapshot(snapPath) : null;
75
+ if (snapshot !== null) {
76
+ const actual = { ...snapshot, views: [] };
77
+ try {
78
+ const d = await diff({ expected, actual, dialect: schemaDialect });
79
+ if (d.changes.every((c) => VIEW_CHANGE_KINDS.has(c.kind))) return undefined;
80
+ } catch {
81
+ // A diff the engine refuses (a primary-key move, say) is still a schema change.
82
+ }
83
+ } else if (!touchesTableBackedObject(input.changedFiles, built.provenance)) {
84
+ return undefined;
85
+ }
86
+
87
+ if (d1 !== undefined) {
88
+ const db = d1.database_name.length > 0 ? d1.database_name : "<database>";
89
+ return `write the schema change as a D1 migration with \`meta migrate --dialect d1 --d1 ${d1.binding} ` +
90
+ `--slug <name>\`, then apply it with \`wrangler d1 migrations apply ${db} --local\``;
91
+ }
92
+ return snapshot === null
93
+ ? `create your database tables with \`meta migrate --from-db --db <url> --dialect ${input.dialect} --slug init --apply\``
94
+ : `migrate the schema change with \`meta migrate --db <url> --dialect ${input.dialect} --slug <name> --apply\``;
95
+ }
96
+
97
+ /** The project's D1 binding when it migrates through D1, else undefined. */
98
+ function d1BindingFor(input: GenMigrateAdviceInput): D1Binding | undefined {
99
+ if (input.dialect !== "sqlite" && input.migrateDialect !== "d1") return undefined;
100
+ const path = findWranglerConfig(input.projectRoot);
101
+ if (path === undefined) return undefined;
102
+ try {
103
+ return parseWranglerConfig(path).d1Bindings[0];
104
+ } catch {
105
+ return undefined;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Whether a changed file was generated for a table-backed object: its basename starts
111
+ * with that object's short name followed by a `.` (`Pet.ts`, `Pet.queries.ts`), or it
112
+ * is the shared enums module.
113
+ */
114
+ function touchesTableBackedObject(
115
+ changedFiles: readonly string[],
116
+ provenance: ReadonlyMap<string, string>,
117
+ ): boolean {
118
+ const shortName = (fqn: string): string => {
119
+ const at = fqn.lastIndexOf(FQN_SEPARATOR);
120
+ return at === -1 ? fqn : fqn.slice(at + FQN_SEPARATOR.length);
121
+ };
122
+ const names = new Set([...provenance.values()].map(shortName));
123
+ return changedFiles.some((f) => {
124
+ const base = basename(f);
125
+ if (base === SHARED_ENUMS_BASENAME) return true;
126
+ const dot = base.indexOf(".");
127
+ return dot > 0 && names.has(base.slice(0, dot));
128
+ });
129
+ }
@@ -108,7 +108,13 @@ function specs(from: Map<string, string | undefined>): string[] {
108
108
  * `@metaobjectsdev/codegen-ts-tanstack` ONCE, and an adopter handed the same package
109
109
  * twice reasonably wonders which one to run.
110
110
  */
111
- export function installSetFor(entries: readonly GeneratorRegistryEntry[]): InstallSet {
111
+ export function installSetFor(
112
+ entries: readonly GeneratorRegistryEntry[],
113
+ /** `@metaobjectsdev/*` packages an ejected generator FILE imports at gen time (the
114
+ * catalog does not know them). Build-time, so dev — unless the runtime set already
115
+ * installs the package, which satisfies the build too. */
116
+ templatePackages: readonly string[] = [],
117
+ ): InstallSet {
112
118
  const version = cliVersion();
113
119
  const dev = new Map<string, string | undefined>();
114
120
  const runtime = new Map<string, string | undefined>();
@@ -131,6 +137,10 @@ export function installSetFor(entries: readonly GeneratorRegistryEntry[]): Insta
131
137
  }
132
138
  }
133
139
 
140
+ for (const p of templatePackages) {
141
+ if (!runtime.has(p)) addPackage(dev, p, `^${version}`);
142
+ }
143
+
134
144
  const devList = specs(dev);
135
145
  const runtimeList = specs(runtime);
136
146
  const parts: string[] = [];
package/src/lib/output.ts CHANGED
@@ -70,6 +70,11 @@ export interface GenResultShape {
70
70
  * already correct. The output's own first line (`gen: []`) already knew.
71
71
  */
72
72
  generatorCount: number;
73
+ /**
74
+ * The "now migrate" next step, worded for this project's dialect — or absent when
75
+ * this run cannot have changed the database schema (see gen-migrate-advice.ts).
76
+ */
77
+ migrateAdvice?: string;
73
78
  }
74
79
 
75
80
  const GEN_GLYPHS: Record<GenFileStatus, string> = {
@@ -283,7 +288,7 @@ export function genResultToData(result: GenResultShape): {
283
288
  ? "no generators are wired in metaobjects.config.ts, so nothing was generated"
284
289
  : `no entities to generate in ${result.outDir}`;
285
290
  const help = result.files.length !== 0
286
- ? ["typecheck the generated code with `npx tsc`", "create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`"]
291
+ ? ["typecheck the generated code with `npx tsc`", ...(result.migrateAdvice !== undefined ? [result.migrateAdvice] : [])]
287
292
  : noGenerators
288
293
  ? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta eject --list` names every generator you can own, and `meta init` scaffolds the usual set"]
289
294
  : ["author entities in this project's metadata sources then re-run `meta gen`"];