firebase-contract 0.1.0-rc.11 → 0.1.0-rc.12

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 CHANGED
@@ -34,6 +34,7 @@ fbc init # scaffold contract.yml + firebase-contract.json
34
34
  fbc validate # parse, resolve imports, semantically validate
35
35
  fbc generate # run every generator declared across the contract graph
36
36
  fbc inspect # print the normalized IR (debugging)
37
+ fbc where <Name> # locate the yml defining a type (logical / gqlName / fsName / table)
37
38
  ```
38
39
 
39
40
  ### One-command generation
@@ -103,7 +104,13 @@ generators:
103
104
  and errors for generators without one.
104
105
  - **`options`** — free-form string map passed through to the generator.
105
106
  Currently: `typesImport` (api-types, task-payloads, data-connect-operations)
106
- overrides the `./types` import path.
107
+ overrides the types import path. For api-types and task-payloads this is
108
+ normally unnecessary: when the option is absent and a `typescript` generator
109
+ is declared (nearest-first: same yml → root), the import specifier is derived
110
+ automatically as the relative path from the output file to the declared
111
+ barrel — moving or renaming the barrel (`out` / `file`) re-derives it. An
112
+ explicit `typesImport` always wins; without a typescript declaration the
113
+ generator's built-in default (`./types`) applies.
107
114
  - **`header`** — per-generator banner override: `default` for the built-in
108
115
  AUTO-GENERATED banner, any text for a custom comment, `""` to suppress the
109
116
  banner for this generator. Wins over the contract-level `header:` and the
@@ -174,6 +181,18 @@ in other contracts (relative paths or npm packages). Imports are resolved
174
181
  transitively with cycle/duplicate detection and diamond dedup, so you can split
175
182
  a large contract across files that mirror your repo layout.
176
183
 
184
+ **Type names are globally unique.** The namespace is flat across the whole
185
+ import graph: defining the same name twice — same kind (`DUPLICATE_DEFINITION`)
186
+ or across kinds/derived identifiers, e.g. an enum and a model both named
187
+ `Status`, or two enums whose `CONSTANT_CASE` consts collide (`NAME_COLLISION`)
188
+ — aborts generation. Uniqueness is enforced per output namespace with its
189
+ *effective* names, so `fsName`/`gqlName` renames legitimately reuse a name in a
190
+ different output: GraphQL names are checked per `data-connect-graphql` scope
191
+ (services may map distinct logical names onto the same gqlName), and the
192
+ firestore module is checked under `fsName ?? name` alongside doc names. A bare
193
+ type reference therefore always resolves to exactly one definition — use
194
+ `fbc where <Name>` to find it.
195
+
177
196
  ```yaml
178
197
  version: 1
179
198
  imports:
@@ -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,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAG3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAEL,cAAc,EAGf,MAAM,kBAAkB,CAAA;AAEzB,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;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,mFAAmF;IACnF,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;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;AA6MD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IACP,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,IAAI,CAAA;CACN,KACL,iBA0FF,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,cAgEtE,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,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAI3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAEL,cAAc,EAGf,MAAM,kBAAkB,CAAA;AAEzB,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,aAkBzE,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;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,mFAAmF;IACnF,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;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;AAyOD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IACP,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,IAAI,CAAA;CACN,KACL,iBA0FF,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,cAgEtE,CAAA"}
@@ -4,6 +4,7 @@ import { error, hasErrors } from '../diagnostics.js';
4
4
  import { buildIr } from '../ir/build-ir.js';
5
5
  import { resolveImports } from '../resolver/import-resolver.js';
6
6
  import { createFsLoader } from '../resolver/module-loader.js';
7
+ import { graphqlNameCollisions } from '../validation/rules.js';
7
8
  import { validateIr } from '../validation/validate.js';
8
9
  import { createDefaultRegistry } from '../generators/index.js';
9
10
  import { toHeaderComment } from '../generators/support/header.js';
@@ -20,10 +21,18 @@ export const compile = (entryPath, options = {}) => {
20
21
  const { documents, diagnostics: importDiagnostics } = resolveImports(entryPath, loader);
21
22
  const { ir, diagnostics: buildDiagnostics } = buildIr(documents);
22
23
  const validationDiagnostics = validateIr(ir);
24
+ // A GraphQL schema is generated per data-connect-graphql declaration from the
25
+ // declaring yml's subtree, so its name uniqueness only holds at that scope —
26
+ // distinct services may reuse a gqlName. generateAll re-compiles each target
27
+ // with the declaring yml as entry, which is exactly when this check applies.
28
+ const entryDoc = documents[documents.length - 1];
29
+ const graphqlDiagnostics = entryDoc?.generatorDecls?.some(decl => decl.generator === 'data-connect-graphql')
30
+ ? graphqlNameCollisions(ir)
31
+ : [];
23
32
  return {
24
33
  ir,
25
34
  documents,
26
- diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics],
35
+ diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics, ...graphqlDiagnostics],
27
36
  };
28
37
  };
29
38
  /** `apis:/tasks:/events:` section a raw api belongs to (drives the section defaults lookup). */
@@ -74,6 +83,30 @@ const resolveOutDir = (out, declaringDoc, rootDoc, diagnostics) => {
74
83
  /** Find a generator declaration by name — nearest first: the declaring yml, then the root yml. */
75
84
  const findGeneratorDecl = (name, doc, rootDoc) => doc.generatorDecls?.find(decl => decl.generator === name) ??
76
85
  rootDoc.generatorDecls?.find(decl => decl.generator === name);
86
+ /** Generators whose output imports the shared types barrel via `options.typesImport`. */
87
+ const TYPES_IMPORT_GENERATORS = new Set(['api-types', 'task-payloads']);
88
+ /**
89
+ * Derive the default `typesImport` specifier from the typescript declaration
90
+ * itself: the relative path from the job's output directory to the declared
91
+ * barrel (`out` + `file`, default `types.ts`). The declaration resolves
92
+ * nearest-first (same yml → root), mirroring how entries resolve declarations,
93
+ * so both outputs stay in sync when the barrel is moved or renamed. An explicit
94
+ * `options.typesImport` always wins; without a typescript declaration in scope
95
+ * the generator's built-in default applies.
96
+ */
97
+ const deriveTypesImport = (outDir, doc, rootDoc) => {
98
+ const decl = findGeneratorDecl('typescript', doc, rootDoc);
99
+ if (!decl)
100
+ return undefined;
101
+ const declaringDoc = doc.generatorDecls?.includes(decl) ? doc : rootDoc;
102
+ // Alias problems are reported when the typescript job itself is built.
103
+ const barrelDir = resolveOutDir(decl.out, declaringDoc, rootDoc, []);
104
+ if (barrelDir === undefined)
105
+ return undefined;
106
+ const barrel = join(barrelDir, (decl.file ?? 'types.ts').replace(/\.ts$/, ''));
107
+ const specifier = relative(outDir, barrel);
108
+ return specifier.startsWith('.') ? specifier : `./${specifier}`;
109
+ };
77
110
  /**
78
111
  * Build generator jobs from the declaration/application DSL:
79
112
  * - api-scoped generators run for entries that opt in (section `defaults` →
@@ -98,14 +131,12 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
98
131
  const output = {};
99
132
  const file = use.file ?? decl?.file;
100
133
  const split = use.split ?? decl?.split ?? generator?.defaultOutput?.split;
101
- const options = use.options ?? decl?.options;
134
+ let options = use.options ?? decl?.options;
102
135
  const header = use.header ?? decl?.header;
103
136
  if (file !== undefined)
104
137
  output.file = file;
105
138
  if (split !== undefined)
106
139
  output.split = split;
107
- if (options !== undefined)
108
- output.options = options;
109
140
  // Split mode needs a per-api file name; bundled mode cannot expand one.
110
141
  const effectiveFile = file ?? generator?.defaultOutput?.file;
111
142
  if (effectiveFile !== undefined && split === false && API_PLACEHOLDER_RE.test(effectiveFile)) {
@@ -123,6 +154,13 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
123
154
  const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
124
155
  if (outDir === undefined)
125
156
  continue;
157
+ if (TYPES_IMPORT_GENERATORS.has(use.generator) && options?.['typesImport'] === undefined) {
158
+ const derived = deriveTypesImport(outDir, doc, rootDoc);
159
+ if (derived !== undefined)
160
+ options = { ...(options ?? {}), typesImport: derived };
161
+ }
162
+ if (options !== undefined)
163
+ output.options = options;
126
164
  const key = [use.generator, outDir, file ?? '', String(split ?? ''), JSON.stringify(options ?? null), header ?? '\u0001'].join('\u0000');
127
165
  const existing = grouped.get(key);
128
166
  if (existing) {
@@ -28,5 +28,37 @@ export declare const apis: ValidationRule;
28
28
  export declare const firestore: ValidationRule;
29
29
  /** Union variants must be known models, each carrying the discriminant field. */
30
30
  export declare const unions: ValidationRule;
31
+ /**
32
+ * Type names must be unique within every output namespace — the export surface
33
+ * where generated identifiers converge (a split layout still merges into one
34
+ * barrel, so physical files are irrelevant). Renames (`gqlName` / `fsName`)
35
+ * deliberately move a type between namespaces, so each namespace is checked
36
+ * independently against its *effective* names:
37
+ *
38
+ * - TypeScript types module: model/enum/union logical names, enum const
39
+ * (`CONSTANT_CASE`) and `…Key` companions, plus the reserved `Json` type
40
+ * - zod schemas module: logical names and their `…Schema` companions
41
+ * - unions module: union names/schemas plus the variant schemas it imports
42
+ * - firestore module: doc names, co-located enums/embedded models under
43
+ * `fsName ?? name`, plus the `_Meta_…` / `FIRESTORE_DATABASES` runtime
44
+ *
45
+ * GraphQL names are checked separately ({@link graphqlNameCollisions}) because
46
+ * that namespace is scoped per data-connect-graphql declaration, not global.
47
+ *
48
+ * Same-kind duplicates are already DUPLICATE_DEFINITION at merge time; this
49
+ * rule closes every cross-kind and derived-identifier hole. Each colliding
50
+ * definition pair is reported once (the first namespace that trips).
51
+ */
52
+ export declare const nameCollisions: ValidationRule;
53
+ /**
54
+ * GraphQL name uniqueness under `gqlName ?? name`. Unlike {@link nameCollisions}
55
+ * this is NOT a default rule: a GraphQL schema exists per `data-connect-graphql`
56
+ * declaration, scoped to the declaring yml's import subtree — distinct services
57
+ * may intentionally reuse a gqlName (e.g. a `T2…` logical name mapped back to
58
+ * the plain name inside its own service). The compiler runs this check only
59
+ * when the entry document itself declares a GraphQL-emitting generator, i.e.
60
+ * exactly at the scope the schema is generated from.
61
+ */
62
+ export declare const graphqlNameCollisions: ValidationRule;
31
63
  export declare const DEFAULT_RULES: ValidationRule[];
32
64
  //# sourceMappingURL=rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/validation/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAa,EAAE,EAAgB,MAAM,aAAa,CAAA;AAEzD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,CAAA;AAIrD,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,cAU3B,CAAA;AAEH,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,cAQpB,CAAA;AAEL,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,EAAE,cAgB9B,CAAA;AAEJ,kFAAkF;AAClF,eAAO,MAAM,UAAU,EAAE,cAiCxB,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,cA+BnB,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,cAkC9B,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,cAAc,EAAE,cAU1B,CAAA;AAEH,mGAAmG;AACnG,eAAO,MAAM,UAAU,EAAE,cA+ErB,CAAA;AAEJ,uFAAuF;AACvF,eAAO,MAAM,IAAI,EAAE,cAYf,CAAA;AAEJ,wFAAwF;AACxF,eAAO,MAAM,SAAS,EAAE,cA+BpB,CAAA;AAEJ,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,cAoBjB,CAAA;AAEJ,eAAO,MAAM,aAAa,EAAE,cAAc,EAYzC,CAAA"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/validation/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;AAG9D,OAAO,EAAa,EAAE,EAAgB,MAAM,aAAa,CAAA;AAEzD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,CAAA;AAIrD,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,cAU3B,CAAA;AAEH,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,cAQpB,CAAA;AAEL,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,EAAE,cAgB9B,CAAA;AAEJ,kFAAkF;AAClF,eAAO,MAAM,UAAU,EAAE,cAiCxB,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,cA+BnB,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,cAkC9B,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,cAAc,EAAE,cAU1B,CAAA;AAEH,mGAAmG;AACnG,eAAO,MAAM,UAAU,EAAE,cA+ErB,CAAA;AAEJ,uFAAuF;AACvF,eAAO,MAAM,IAAI,EAAE,cAYf,CAAA;AAEJ,wFAAwF;AACxF,eAAO,MAAM,SAAS,EAAE,cA+BpB,CAAA;AAEJ,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,cAoBjB,CAAA;AAaJ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,EAAE,cA+E5B,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAmBnC,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,cAAc,EAazC,CAAA"}
@@ -1,4 +1,6 @@
1
1
  import { error, warning } from '../diagnostics.js';
2
+ import { constantCase } from '../generators/support/naming.js';
3
+ import { isTable } from '../generators/support/split.js';
2
4
  import { findModel } from '../ir/ir.js';
3
5
  const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
6
  /** Every field type must resolve to a known scalar, enum, or model. */
@@ -267,6 +269,122 @@ export const unions = ir => ir.unions.flatMap(union => {
267
269
  }
268
270
  return diagnostics;
269
271
  });
272
+ const RESERVED_OWNER = 'the generated runtime';
273
+ /**
274
+ * Type names must be unique within every output namespace — the export surface
275
+ * where generated identifiers converge (a split layout still merges into one
276
+ * barrel, so physical files are irrelevant). Renames (`gqlName` / `fsName`)
277
+ * deliberately move a type between namespaces, so each namespace is checked
278
+ * independently against its *effective* names:
279
+ *
280
+ * - TypeScript types module: model/enum/union logical names, enum const
281
+ * (`CONSTANT_CASE`) and `…Key` companions, plus the reserved `Json` type
282
+ * - zod schemas module: logical names and their `…Schema` companions
283
+ * - unions module: union names/schemas plus the variant schemas it imports
284
+ * - firestore module: doc names, co-located enums/embedded models under
285
+ * `fsName ?? name`, plus the `_Meta_…` / `FIRESTORE_DATABASES` runtime
286
+ *
287
+ * GraphQL names are checked separately ({@link graphqlNameCollisions}) because
288
+ * that namespace is scoped per data-connect-graphql declaration, not global.
289
+ *
290
+ * Same-kind duplicates are already DUPLICATE_DEFINITION at merge time; this
291
+ * rule closes every cross-kind and derived-identifier hole. Each colliding
292
+ * definition pair is reported once (the first namespace that trips).
293
+ */
294
+ export const nameCollisions = ir => {
295
+ const diagnostics = [];
296
+ const reportedPairs = new Set();
297
+ const check = (space, claims) => {
298
+ const first = new Map();
299
+ for (const claim of claims) {
300
+ const prev = first.get(claim.identifier);
301
+ if (!prev) {
302
+ first.set(claim.identifier, claim);
303
+ continue;
304
+ }
305
+ if (prev.owner === claim.owner)
306
+ continue;
307
+ const pair = [prev.owner, claim.owner].sort().join(' vs ');
308
+ if (reportedPairs.has(pair))
309
+ continue;
310
+ reportedPairs.add(pair);
311
+ diagnostics.push(error('NAME_COLLISION', `${prev.owner} and ${claim.owner} both emit "${claim.identifier}" in the ${space} output`, {
312
+ file: claim.file ?? prev.file,
313
+ path: claim.path,
314
+ }));
315
+ }
316
+ };
317
+ const claim = (owner, file, path, identifiers) => identifiers.map(identifier => ({ owner, identifier, file, path }));
318
+ // TypeScript types module: the logical namespace every other output derives from.
319
+ check('TypeScript types', [
320
+ { owner: RESERVED_OWNER, identifier: 'Json', path: '(generated)' },
321
+ ...ir.models.flatMap(m => claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [m.name])),
322
+ ...ir.enums.flatMap(e => claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [e.name, `${e.name}Key`, constantCase(e.name)])),
323
+ ...ir.unions.flatMap(u => claim(`union "${u.name}"`, u.sourceFile, `unions.${u.name}`, [u.name])),
324
+ ]);
325
+ // zod schemas module: logical names plus their `…Schema` consts.
326
+ check('zod schemas', [
327
+ ...ir.models.flatMap(m => claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [m.name, `${m.name}Schema`])),
328
+ ...ir.enums.flatMap(e => claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [e.name, `${e.name}Schema`])),
329
+ ]);
330
+ // unions module: union declarations plus the variant schemas imported into it.
331
+ const variantOwners = new Map();
332
+ for (const union of ir.unions)
333
+ for (const variant of union.variants)
334
+ variantOwners.set(variant, `model "${variant}"`);
335
+ check('unions', [
336
+ ...ir.unions.flatMap(u => claim(`union "${u.name}"`, u.sourceFile, `unions.${u.name}`, [u.name, `${u.name}Schema`])),
337
+ ...[...variantOwners.entries()].map(([variant, owner]) => {
338
+ const model = findModel(ir, variant);
339
+ return { owner, identifier: `${variant}Schema`, file: model?.sourceFile, path: `models.${variant}` };
340
+ }),
341
+ ]);
342
+ // firestore module: doc schemas + co-located enums/embedded models under fsName-effective names.
343
+ // Tables are imported as `Dc…Schema` aliases, so they don't claim names here.
344
+ check('firestore', [
345
+ ...['_Meta_', '_Meta_Schema', '_Meta_Operation', '_Meta_OperationSchema', 'FIRESTORE_DATABASES', 'FirestoreDatabaseKey', 'FirestoreDatabaseId'].map(identifier => ({ owner: RESERVED_OWNER, identifier, path: '(generated)' })),
346
+ ...ir.firestore.flatMap(d => claim(`firestore doc "${d.name}"`, d.sourceFile, `firestore.${d.name}`, [d.name, `${d.name}Schema`])),
347
+ ...ir.enums.flatMap(e => {
348
+ const fs = e.fsName ?? e.name;
349
+ return claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [fs, `${fs}Key`, constantCase(fs), `${fs}Schema`]);
350
+ }),
351
+ ...ir.models
352
+ .filter(m => !isTable(m))
353
+ .flatMap(m => {
354
+ const fs = m.fsName ?? m.name;
355
+ return claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [fs, `${fs}Schema`]);
356
+ }),
357
+ ]);
358
+ return diagnostics;
359
+ };
360
+ /**
361
+ * GraphQL name uniqueness under `gqlName ?? name`. Unlike {@link nameCollisions}
362
+ * this is NOT a default rule: a GraphQL schema exists per `data-connect-graphql`
363
+ * declaration, scoped to the declaring yml's import subtree — distinct services
364
+ * may intentionally reuse a gqlName (e.g. a `T2…` logical name mapped back to
365
+ * the plain name inside its own service). The compiler runs this check only
366
+ * when the entry document itself declares a GraphQL-emitting generator, i.e.
367
+ * exactly at the scope the schema is generated from.
368
+ */
369
+ export const graphqlNameCollisions = ir => {
370
+ const diagnostics = [];
371
+ const first = new Map();
372
+ const claims = [
373
+ ...ir.models.map(m => ({ owner: `model "${m.name}"`, name: m.gqlName ?? m.name, file: m.sourceFile, path: `models.${m.name}` })),
374
+ ...ir.enums.map(e => ({ owner: `enum "${e.name}"`, name: e.gqlName ?? e.name, file: e.sourceFile, path: `enums.${e.name}` })),
375
+ ];
376
+ for (const { owner, name, file, path } of claims) {
377
+ const prev = first.get(name);
378
+ if (!prev) {
379
+ first.set(name, { owner, file, path });
380
+ continue;
381
+ }
382
+ if (prev.owner === owner)
383
+ continue;
384
+ diagnostics.push(error('NAME_COLLISION', `${prev.owner} and ${owner} both emit "${name}" in the GraphQL schema output`, { file: file ?? prev.file, path }));
385
+ }
386
+ return diagnostics;
387
+ };
270
388
  export const DEFAULT_RULES = [
271
389
  unresolvedTypes,
272
390
  emptyEnums,
@@ -279,4 +397,5 @@ export const DEFAULT_RULES = [
279
397
  apis,
280
398
  firestore,
281
399
  unions,
400
+ nameCollisions,
282
401
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAOnC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,KAAG,IAKnD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,KAAG,IAMnD,CAAA"}
@@ -2,10 +2,12 @@ import { registerGenerate } from './generate.js';
2
2
  import { registerInit } from './init.js';
3
3
  import { registerInspect } from './inspect.js';
4
4
  import { registerValidate } from './validate.js';
5
+ import { registerWhere } from './where.js';
5
6
  /** Register every CLI command on the program. */
6
7
  export const registerCommands = (program) => {
7
8
  registerInit(program);
8
9
  registerValidate(program);
9
10
  registerGenerate(program);
10
11
  registerInspect(program);
12
+ registerWhere(program);
11
13
  };
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const registerWhere: (program: Command) => void;
3
+ //# sourceMappingURL=where.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/where.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA+CnC,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,KAAG,IA2BhD,CAAA"}
@@ -0,0 +1,57 @@
1
+ import { compile } from '../../_internal/compiler/compiler.js';
2
+ import { hasErrors } from '../../_internal/diagnostics.js';
3
+ import { formatDiagnostics } from '../../_internal/format-diagnostics.js';
4
+ import { loadConfig } from '../config.js';
5
+ const definitionsOf = (ir) => [
6
+ ...ir.models.map(m => ({
7
+ kind: 'model',
8
+ name: m.name,
9
+ sourceFile: m.sourceFile,
10
+ yamlPath: `models.${m.name}`,
11
+ renames: [
12
+ ...(m.gqlName ? [`gqlName: ${m.gqlName}`] : []),
13
+ ...(m.fsName ? [`fsName: ${m.fsName}`] : []),
14
+ ...(m.table ? [`table: ${m.table}`] : []),
15
+ ],
16
+ })),
17
+ ...ir.enums.map(e => ({
18
+ kind: 'enum',
19
+ name: e.name,
20
+ sourceFile: e.sourceFile,
21
+ yamlPath: `enums.${e.name}`,
22
+ renames: [...(e.gqlName ? [`gqlName: ${e.gqlName}`] : []), ...(e.fsName ? [`fsName: ${e.fsName}`] : [])],
23
+ })),
24
+ ...ir.unions.map(u => ({ kind: 'union', name: u.name, sourceFile: u.sourceFile, yamlPath: `unions.${u.name}`, renames: [] })),
25
+ ...ir.firestore.map(d => ({ kind: 'firestore doc', name: d.name, sourceFile: d.sourceFile, yamlPath: `firestore.${d.name}`, renames: [] })),
26
+ ...ir.envelopes.map(e => ({ kind: 'envelope', name: e.name, sourceFile: e.sourceFile, yamlPath: `envelopes.${e.name}`, renames: [] })),
27
+ ...ir.apis.map(a => ({ kind: 'api', name: a.name, sourceFile: a.sourceFile, yamlPath: `apis.${a.name}`, renames: [] })),
28
+ ...ir.operations.map(o => ({ kind: 'operation', name: o.name, sourceFile: o.sourceFile, yamlPath: `operations.${o.name}`, renames: [] })),
29
+ ];
30
+ /** True when the definition is known by `query` under any of its names. */
31
+ const matches = (definition, query) => definition.name === query || definition.renames.some(rename => rename.endsWith(`: ${query}`));
32
+ export const registerWhere = (program) => {
33
+ program
34
+ .command('where')
35
+ .description('Locate the yml that defines a type (matches logical, gqlName, fsName, and table names)')
36
+ .argument('<name>', 'type name to look up')
37
+ .argument('[entry]', 'entry contract file')
38
+ .action((name, entryArg) => {
39
+ const config = loadConfig();
40
+ const entry = entryArg ?? config.entry ?? 'contract.yml';
41
+ const { ir, diagnostics } = compile(entry);
42
+ if (hasErrors(diagnostics)) {
43
+ console.error(formatDiagnostics(diagnostics));
44
+ process.exit(1);
45
+ }
46
+ const found = definitionsOf(ir).filter(definition => matches(definition, name));
47
+ if (found.length === 0) {
48
+ console.error(`"${name}" is not defined in ${entry} or its imports`);
49
+ process.exit(1);
50
+ }
51
+ for (const definition of found) {
52
+ const location = definition.sourceFile ?? '(unknown file)';
53
+ const renames = definition.renames.length > 0 ? ` [${definition.renames.join(', ')}]` : '';
54
+ console.log(`${definition.kind} ${definition.name} ${location} (${definition.yamlPath})${renames}`);
55
+ }
56
+ });
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-contract",
3
- "version": "0.1.0-rc.11",
3
+ "version": "0.1.0-rc.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "firebase-contract": "./dist/lib/cli/index.js",