firebase-contract 0.1.0-rc.0 → 0.1.0-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.
@@ -54,6 +54,18 @@ export declare const generateAll: (entryPath: string, options?: {
54
54
  context?: GeneratorContext;
55
55
  loader?: ModuleLoader;
56
56
  }) => GenerateAllResult;
57
+ export interface FileDrift {
58
+ /** Files whose on-disk content differs from the generated content. */
59
+ changed: string[];
60
+ /** Files that generation produces but do not exist on disk yet. */
61
+ missing: string[];
62
+ }
63
+ /**
64
+ * Compare generated files against what is currently on disk — the dry-run
65
+ * behind `fbc generate --check`, catching "the contract changed but the
66
+ * committed outputs were not regenerated" drift without touching the tree.
67
+ */
68
+ export declare const diffFiles: (files: GeneratedFile[]) => FileDrift;
57
69
  export interface GenerateResult {
58
70
  ir: Ir;
59
71
  diagnostics: Diagnostic[];
@@ -1 +1 @@
1
- {"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAG3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,EAAE,UAAS,cAAmB,KAAG,aAUzE,CAAA;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC/B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAAC,MAAM,CAAC,EAAE,YAAY,CAAA;CAAO,KACjH,iBA8BF,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,SAAS,eAAe,KAAG,cAuCtE,CAAA"}
1
+ {"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAG3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,EAAE,UAAS,cAAmB,KAAG,aAUzE,CAAA;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC/B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAAC,MAAM,CAAC,EAAE,YAAY,CAAA;CAAO,KACjH,iBA8BF,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,aAAa,EAAE,KAAG,SAUlD,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,SAAS,eAAe,KAAG,cAuCtE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { error, hasErrors } from '../diagnostics.js';
4
4
  import { buildIr } from '../ir/build-ir.js';
@@ -62,6 +62,23 @@ export const generateAll = (entryPath, options = {}) => {
62
62
  }
63
63
  return { targets, diagnostics: collected, ok };
64
64
  };
65
+ /**
66
+ * Compare generated files against what is currently on disk — the dry-run
67
+ * behind `fbc generate --check`, catching "the contract changed but the
68
+ * committed outputs were not regenerated" drift without touching the tree.
69
+ */
70
+ export const diffFiles = (files) => {
71
+ const drift = { changed: [], missing: [] };
72
+ for (const file of files) {
73
+ if (!existsSync(file.path)) {
74
+ drift.missing.push(file.path);
75
+ }
76
+ else if (readFileSync(file.path, 'utf8') !== file.content) {
77
+ drift.changed.push(file.path);
78
+ }
79
+ }
80
+ return drift;
81
+ };
65
82
  /**
66
83
  * Compile then emit code. Generation is skipped when compilation produced any
67
84
  * error, so generators never see an invalid IR. The CLI is a thin wrapper over
@@ -15,6 +15,13 @@ export interface HostedDeps {
15
15
  * declarations can be emitted top-down without forward references.
16
16
  */
17
17
  export declare const collectDeps: (ir: Ir, roots: IrModel[]) => HostedDeps;
18
+ /**
19
+ * Assign a unique module basename to every table. Two models may share a
20
+ * physical table name across services (e.g. both declare
21
+ * `reconciliation_operations`); the first keeps the table-derived name and
22
+ * later ones fall back to their model name so files never overwrite each other.
23
+ */
24
+ export declare const assignModuleNames: (tables: IrModel[]) => Map<string, string>;
18
25
  export interface SplitGroups {
19
26
  tables: IrModel[];
20
27
  /** enum name → hosting table model name */
@@ -1 +1 @@
1
- {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/support/split.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,EAAE,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAIvD,qEAAqE;AACrE,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,KAAG,OAAiE,CAAA;AAE1G,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,MAAyD,CAAA;AAEtG,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,MAA6C,CAAA;AAavF,MAAM,WAAW,UAAU;IACzB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,EAAE,EAAE,OAAO,OAAO,EAAE,KAAG,UAuBtD,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,EAAE,CAAA;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,qDAAqD;IACrD,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,oDAAoD;IACpD,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,8CAA8C;IAC9C,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,EAAE,KAAG,WAepC,CAAA"}
1
+ {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/support/split.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,EAAE,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAIvD,qEAAqE;AACrE,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,KAAG,OAAiE,CAAA;AAE1G,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,MAAyD,CAAA;AAEtG,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,KAAG,MAA6C,CAAA;AAavF,MAAM,WAAW,UAAU;IACzB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,EAAE,EAAE,OAAO,OAAO,EAAE,KAAG,UAuBtD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,OAAO,EAAE,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAavE,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,EAAE,CAAA;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,qDAAqD;IACrD,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,oDAAoD;IACpD,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,8CAA8C;IAC9C,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,EAAE,KAAG,WAepC,CAAA"}
@@ -49,6 +49,28 @@ export const collectDeps = (ir, roots) => {
49
49
  visit(root);
50
50
  return { enums, models };
51
51
  };
52
+ /**
53
+ * Assign a unique module basename to every table. Two models may share a
54
+ * physical table name across services (e.g. both declare
55
+ * `reconciliation_operations`); the first keeps the table-derived name and
56
+ * later ones fall back to their model name so files never overwrite each other.
57
+ */
58
+ export const assignModuleNames = (tables) => {
59
+ const used = new Set();
60
+ const names = new Map();
61
+ for (const table of tables) {
62
+ let name = kebabCase(tableNameOf(table));
63
+ if (used.has(name))
64
+ name = kebabCase(pluralize(snakeCase(table.name)));
65
+ let candidate = name;
66
+ let suffix = 2;
67
+ while (used.has(candidate))
68
+ candidate = `${name}-${suffix++}`;
69
+ used.add(candidate);
70
+ names.set(table.name, candidate);
71
+ }
72
+ return names;
73
+ };
52
74
  /**
53
75
  * Assign every enum and embedded model to the first table model that
54
76
  * (transitively) references it; the rest become shared leftovers.
@@ -1 +1 @@
1
- {"version":3,"file":"typescript-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AA2F5E;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,QAAO,SAyEhD,CAAA"}
1
+ {"version":3,"file":"typescript-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AA2F5E;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,QAAO,SA0EhD,CAAA"}
@@ -2,7 +2,7 @@ import { findEnum, findModel } from '../../ir/ir.js';
2
2
  import { headerBlocks } from '../support/header.js';
3
3
  import { constantCase, singleQuote } from '../support/naming.js';
4
4
  import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
5
- import { collectDeps, kebabCase, splitGroups, tableNameOf } from '../support/split.js';
5
+ import { assignModuleNames, collectDeps, splitGroups } from '../support/split.js';
6
6
  const SCALAR_TS = {
7
7
  string: 'string',
8
8
  int: 'number',
@@ -95,6 +95,7 @@ export const createTypeScriptSplitGenerator = () => ({
95
95
  description: 'TypeScript interfaces, one file per table (types/<table>.ts + types.ts barrel)',
96
96
  generate(ir, context) {
97
97
  const groups = splitGroups(ir);
98
+ const moduleNames = assignModuleNames(groups.tables);
98
99
  const files = [];
99
100
  const fileOf = new Map(); // symbol -> module basename (no ext)
100
101
  const sharedNeeded = usesJson(ir.models) || groups.leftoverEnums.length > 0 || groups.leftoverModels.length > 0;
@@ -105,7 +106,7 @@ export const createTypeScriptSplitGenerator = () => ({
105
106
  for (const name of groups.leftoverModels)
106
107
  fileOf.set(name, '_shared');
107
108
  for (const table of groups.tables) {
108
- const moduleName = kebabCase(tableNameOf(table));
109
+ const moduleName = moduleNames.get(table.name) ?? table.name;
109
110
  fileOf.set(table.name, moduleName);
110
111
  for (const [name, home] of groups.enumHome)
111
112
  if (home === table.name)
@@ -156,7 +157,7 @@ export const createTypeScriptSplitGenerator = () => ({
156
157
  emit('_shared', enums, models, usesJson(ir.models));
157
158
  }
158
159
  for (const table of groups.tables) {
159
- const moduleName = kebabCase(tableNameOf(table));
160
+ const moduleName = moduleNames.get(table.name) ?? table.name;
160
161
  const deps = collectDeps(ir, [table]);
161
162
  const enums = deps.enums
162
163
  .filter(name => groups.enumHome.get(name) === table.name)
@@ -1 +1 @@
1
- {"version":3,"file":"zod-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAkE5E;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,SAkEzC,CAAA"}
1
+ {"version":3,"file":"zod-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAkE5E;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,SAmEzC,CAAA"}
@@ -3,7 +3,7 @@ import { zodConstraints } from '../support/constraints.js';
3
3
  import { headerBlocks } from '../support/header.js';
4
4
  import { singleQuote } from '../support/naming.js';
5
5
  import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
6
- import { collectDeps, kebabCase, splitGroups, tableNameOf } from '../support/split.js';
6
+ import { assignModuleNames, collectDeps, splitGroups } from '../support/split.js';
7
7
  const SCALAR_ZOD = {
8
8
  string: 'z.string()',
9
9
  int: 'z.number().int()',
@@ -66,6 +66,7 @@ export const createZodSplitGenerator = () => ({
66
66
  description: 'Zod schemas, one file per table (schemas/<table>.ts + schemas.ts barrel)',
67
67
  generate(ir, context) {
68
68
  const groups = splitGroups(ir);
69
+ const moduleNames = assignModuleNames(groups.tables);
69
70
  const files = [];
70
71
  const fileOf = new Map();
71
72
  for (const name of groups.leftoverEnums)
@@ -73,7 +74,7 @@ export const createZodSplitGenerator = () => ({
73
74
  for (const name of groups.leftoverModels)
74
75
  fileOf.set(name, '_shared');
75
76
  for (const table of groups.tables) {
76
- const moduleName = kebabCase(tableNameOf(table));
77
+ const moduleName = moduleNames.get(table.name) ?? table.name;
77
78
  fileOf.set(table.name, moduleName);
78
79
  for (const [name, home] of groups.enumHome)
79
80
  if (home === table.name)
@@ -117,7 +118,7 @@ export const createZodSplitGenerator = () => ({
117
118
  emit('_shared', enums, models);
118
119
  }
119
120
  for (const table of groups.tables) {
120
- const moduleName = kebabCase(tableNameOf(table));
121
+ const moduleName = moduleNames.get(table.name) ?? table.name;
121
122
  const deps = collectDeps(ir, [table]);
122
123
  const enums = deps.enums
123
124
  .filter(name => groups.enumHome.get(name) === table.name)
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/generate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAenC,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,KAAG,IAqEnD,CAAA"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/generate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA4BnC,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,KAAG,IAgFnD,CAAA"}
@@ -1,8 +1,20 @@
1
- import { generate, generateAll } from '../../_internal/compiler/compiler.js';
1
+ import { diffFiles, generate, generateAll } from '../../_internal/compiler/compiler.js';
2
2
  import { DEFAULT_HEADER, toHeaderComment } from '../../_internal/generators/support/header.js';
3
3
  import { countBySeverity, formatDiagnostics } from '../../_internal/format-diagnostics.js';
4
4
  import { createDefaultRegistry } from '../../_internal/generators/index.js';
5
5
  import { loadConfig } from '../config.js';
6
+ const reportDrift = (changed, missing) => {
7
+ if (changed.length === 0 && missing.length === 0) {
8
+ console.log('✔ no drift — on-disk files match the contract');
9
+ return;
10
+ }
11
+ console.error('✖ drift detected — regenerate and commit the outputs (fbc generate):');
12
+ for (const path of changed)
13
+ console.error(` changed: ${path}`);
14
+ for (const path of missing)
15
+ console.error(` missing: ${path}`);
16
+ process.exit(1);
17
+ };
6
18
  export const registerGenerate = (program) => {
7
19
  program
8
20
  .command('generate')
@@ -11,6 +23,7 @@ export const registerGenerate = (program) => {
11
23
  .option('-o, --out-dir <dir>', 'output directory')
12
24
  .option('-g, --generators <list>', 'comma-separated generator names')
13
25
  .option('--header [text]', 'prepend a comment header to generated files (bare --header = default banner; omitted = contract `header:` or none)')
26
+ .option('--check', 'verify the on-disk files match what would be generated; exits 1 on drift (writes nothing)')
14
27
  .action((entryArg, flags) => {
15
28
  const config = loadConfig();
16
29
  const entry = entryArg ?? config.entry ?? 'contract.yml';
@@ -23,7 +36,7 @@ export const registerGenerate = (program) => {
23
36
  // graph — the one-command flow. Flags switch back to single-target mode.
24
37
  const explicit = flags.outDir !== undefined || flags.generators !== undefined || config.outDir !== undefined || config.generators !== undefined;
25
38
  if (!explicit) {
26
- const all = generateAll(entry, { registry, write: true, ...(context ? { context } : {}) });
39
+ const all = generateAll(entry, { registry, write: !flags.check, ...(context ? { context } : {}) });
27
40
  if (all.targets.length > 0 || !all.ok) {
28
41
  if (all.diagnostics.length > 0) {
29
42
  console.log(formatDiagnostics(all.diagnostics));
@@ -33,6 +46,11 @@ export const registerGenerate = (program) => {
33
46
  console.error(`\n✖ generation aborted: ${errors} error(s)`);
34
47
  process.exit(1);
35
48
  }
49
+ if (flags.check) {
50
+ const drift = diffFiles(all.targets.flatMap(target => target.files));
51
+ reportDrift(drift.changed, drift.missing);
52
+ return;
53
+ }
36
54
  let total = 0;
37
55
  for (const target of all.targets) {
38
56
  total += target.files.length;
@@ -47,7 +65,7 @@ export const registerGenerate = (program) => {
47
65
  outDir,
48
66
  generators,
49
67
  registry,
50
- write: true,
68
+ write: !flags.check,
51
69
  ...(context ? { context } : {}),
52
70
  });
53
71
  if (result.diagnostics.length > 0) {
@@ -58,6 +76,11 @@ export const registerGenerate = (program) => {
58
76
  console.error(`\n✖ generation aborted: ${errors} error(s)`);
59
77
  process.exit(1);
60
78
  }
79
+ if (flags.check) {
80
+ const drift = diffFiles(result.files);
81
+ reportDrift(drift.changed, drift.missing);
82
+ return;
83
+ }
61
84
  for (const file of result.files) {
62
85
  console.log(`generated ${file.path}`);
63
86
  }
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-contract",
3
- "version": "0.1.0-rc.0",
3
+ "version": "0.1.0-rc.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "firebase-contract": "./dist/lib/cli/index.js",