firebase-contract 0.1.0-rc.5 → 0.1.0-rc.7

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 (64) hide show
  1. package/README.md +165 -81
  2. package/dist/lib/_internal/compiler/compiler.d.ts +5 -4
  3. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
  4. package/dist/lib/_internal/compiler/compiler.js +207 -23
  5. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +0 -5
  6. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -1
  7. package/dist/lib/_internal/generators/api/api-dto-generator.js +23 -13
  8. package/dist/lib/_internal/generators/api/api-types-generator.d.ts +0 -5
  9. package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -1
  10. package/dist/lib/_internal/generators/api/api-types-generator.js +27 -17
  11. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +0 -4
  12. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -1
  13. package/dist/lib/_internal/generators/api/api-validation-generator.js +15 -6
  14. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +0 -6
  15. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -1
  16. package/dist/lib/_internal/generators/api/task-payload-generator.js +30 -16
  17. package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -1
  18. package/dist/lib/_internal/generators/config/config-generator.js +2 -1
  19. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -1
  20. package/dist/lib/_internal/generators/data-connect/adapter-generator.js +2 -1
  21. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -1
  22. package/dist/lib/_internal/generators/data-connect/graphql-generator.js +2 -1
  23. package/dist/lib/_internal/generators/data-connect/operations-generator.js +1 -1
  24. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -1
  25. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +2 -1
  26. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -1
  27. package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +2 -1
  28. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -1
  29. package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +2 -1
  30. package/dist/lib/_internal/generators/generator.d.ts +36 -0
  31. package/dist/lib/_internal/generators/generator.d.ts.map +1 -1
  32. package/dist/lib/_internal/generators/generator.js +7 -1
  33. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -1
  34. package/dist/lib/_internal/generators/id/id-codec-generator.js +2 -1
  35. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -1
  36. package/dist/lib/_internal/generators/sql/sql-migration-generator.js +2 -1
  37. package/dist/lib/_internal/generators/support/naming.d.ts +1 -0
  38. package/dist/lib/_internal/generators/support/naming.d.ts.map +1 -1
  39. package/dist/lib/_internal/generators/support/naming.js +3 -0
  40. package/dist/lib/_internal/generators/support/templates.d.ts +36 -0
  41. package/dist/lib/_internal/generators/support/templates.d.ts.map +1 -0
  42. package/dist/lib/_internal/generators/support/templates.js +50 -0
  43. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
  44. package/dist/lib/_internal/generators/typescript/typescript-generator.js +2 -1
  45. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
  46. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +2 -1
  47. package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -1
  48. package/dist/lib/_internal/generators/union/union-generator.js +2 -1
  49. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
  50. package/dist/lib/_internal/generators/zod/zod-generator.js +2 -1
  51. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
  52. package/dist/lib/_internal/generators/zod/zod-split-generator.js +2 -1
  53. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -1
  54. package/dist/lib/_internal/ir/build-ir.js +2 -0
  55. package/dist/lib/_internal/ir/ir.d.ts +2 -0
  56. package/dist/lib/_internal/ir/ir.d.ts.map +1 -1
  57. package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
  58. package/dist/lib/_internal/parser/parse.js +194 -17
  59. package/dist/lib/_internal/parser/raw-document.d.ts +46 -7
  60. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
  61. package/dist/lib/cli/commands/generate.js +1 -1
  62. package/dist/lib/cli/commands/init.d.ts.map +1 -1
  63. package/dist/lib/cli/commands/init.js +5 -0
  64. package/package.json +1 -1
package/README.md CHANGED
@@ -32,36 +32,93 @@ Provides the `firebase-contract` (alias `fbc`) CLI and a programmatic API.
32
32
  ```bash
33
33
  fbc init # scaffold contract.yml + firebase-contract.json
34
34
  fbc validate # parse, resolve imports, semantically validate
35
- fbc generate # run every `generate:` target declared in the contract graph
35
+ fbc generate # run every generator declared across the contract graph
36
36
  fbc inspect # print the normalized IR (debugging)
37
37
  ```
38
38
 
39
39
  ### One-command generation
40
40
 
41
- Each contract file can declare its own outputs in a `generate:` section —
42
- out dirs are relative to that yml file:
41
+ Each contract file declares the generators it uses in a top-level
42
+ `generators:` block — a name plus an output template. Declaring a
43
+ **document-scoped** generator (typescript-split, zod-split, …) runs it once for
44
+ that yml; **api-scoped** generators (api-types, api-validation, api-dto,
45
+ task-payloads) run for the entries that opt in (see below):
46
+
47
+ ```yaml
48
+ # contract.yml (root)
49
+ project:
50
+ aliases:
51
+ "#contracts/*": libs/contracts/src/* # out templates may target aliases
52
+ generators:
53
+ - { generator: typescript-split, out: "#contracts" }
54
+ - { generator: api-types, out: "#contracts/api-types/{api-name}" }
55
+ imports:
56
+ - ./apps/shop/data-connect/schema.yml
57
+ ```
43
58
 
44
59
  ```yaml
45
60
  # apps/shop/data-connect/schema.yml
46
- generate:
47
- - { out: src, generators: [data-connect-graphql-split] }
48
- - { out: ., generators: [sql-migrations] }
61
+ generators:
62
+ - { generator: data-connect-graphql-split, out: src }
63
+ - { generator: sql-migrations, out: . }
49
64
  ```
50
65
 
66
+ - `out` resolves relative to the declaring yml; `#alias/...` prefixes resolve
67
+ through the **root** yml's `project.aliases` (relative to the root).
68
+ - Api-scoped templates may use `{api-name}` (kebab-cased name) and `{path}`
69
+ (REST path with `{param}` segments dropped).
70
+ - Entries reference declarations by name — nearest first (same yml → root).
71
+
72
+ #### Output settings: `file`, `split`, `options`, `header`
73
+
74
+ Every declaration (and any entry-level application) can reshape the output —
75
+ the defaults reproduce each generator's built-in layout, so omitting them
76
+ changes nothing:
77
+
51
78
  ```yaml
52
- # contract.yml (root)
53
- generate:
54
- - out: libs/contracts/src
55
- generators: [typescript-split, zod-split, firestore-split, id-codecs]
56
- imports:
57
- - ./apps/shop/data-connect/schema.yml
79
+ generators:
80
+ - generator: api-dto
81
+ out: "src/entries/{path}/dto"
82
+ file: "{api-name}.dto.ts" # file-name template (default shown)
83
+ split: true # one file per api (api-dto's default)
84
+ - generator: api-types
85
+ out: "#contracts/api-types"
86
+ file: "{api-name}.types.ts" # override the bundled default with per-api files
87
+ split: true
88
+ options:
89
+ typesImport: "../types" # where `import type { … } from '…'` points
90
+ - generator: typescript
91
+ out: "#contracts"
92
+ file: models.ts # rename a single-file/barrel output
93
+ split: true # document scope: switches to typescript-split
58
94
  ```
59
95
 
60
- `fbc generate contract.yml` then materializes **all** targets across the whole
61
- import graph in one run. Passing `-o`/`-g` switches to single-target mode
96
+ - **`file`** — output file name. Api scope: a template (`{api-name}`/`{path}`
97
+ allowed when `split: true`). Document scope: renames the single output file,
98
+ or the barrel for `-split` variants.
99
+ - **`split`** — api scope: `true` emits one file per api (the `file` template
100
+ must contain a placeholder), `false` bundles everything into one `file`.
101
+ Document scope: `true` swaps to the generator's `-split` variant
102
+ (`typescript` → `typescript-split`, …) and errors when none exists.
103
+ - **`options`** — free-form string map passed through to the generator.
104
+ Currently: `typesImport` (api-types, task-payloads, data-connect-operations)
105
+ overrides the `./types` import path.
106
+ - **`header`** — per-generator banner override: `default` for the built-in
107
+ AUTO-GENERATED banner, any text for a custom comment, `""` to suppress the
108
+ banner for this generator. Wins over the contract-level `header:` and the
109
+ CLI `--header` flag.
110
+ - Defaults per generator: `api-types` → `api-types.ts` bundled, `api-validation`
111
+ → `api-validation.ts` bundled, `task-payloads` → `task-payloads.ts` bundled,
112
+ `api-dto` → `{api-name}.dto.ts` split.
113
+ - Entry-level settings override the declaration, which overrides the
114
+ generator's default (same nearest-first rule as `out`).
115
+
116
+ `fbc generate contract.yml` materializes **all** declared outputs across the
117
+ whole import graph in one run. Passing `-o`/`-g` switches to single-target mode
62
118
  (`fbc generate <entry> -o <dir> -g typescript,zod`), and
63
119
  `firebase-contract.json` (`entry`, `outDir`, `generators`) can hold defaults.
64
120
 
121
+
65
122
  ### Generated-file headers
66
123
 
67
124
  By default no banner is emitted. Opt in per run or per contract:
@@ -162,24 +219,24 @@ models:
162
219
 
163
220
  | option | meaning |
164
221
  | ----------------------------------- | -------------------------------------------------------- |
165
- | `type` | scalar name, or an enum/model name |
166
- | `optional` | value may be absent (`field?`) |
167
- | `list` | value is an array of `type` |
168
- | `id` | marks the (single) primary identifier |
169
- | `unique` | field-level unique → `@unique` |
170
- | `relation` | model-typed field is a foreign-key relation (see below) |
171
- | `col` | Data Connect column dataType (Int64 PKs default to `bigserial`) |
172
- | `default` | `@default(expr: …)` on the DC column |
173
- | `literal` | pin to one literal value (union discriminant tags) |
174
- | `nullable` | value may be `null` (`.nullable()` / `\| null`) |
175
- | `description` | doc comment carried into generated output |
176
- | `jsdoc` | render the description as a JSDoc block (Firestore fields) |
222
+ | `type` | document | scalar name, or an enum/model name |
223
+ | `optional` | document | value may be absent (`field?`) |
224
+ | `list` | document | value is an array of `type` |
225
+ | `id` | document | marks the (single) primary identifier |
226
+ | `unique` | document | field-level unique → `@unique` |
227
+ | `relation` | document | model-typed field is a foreign-key relation (see below) |
228
+ | `col` | document | Data Connect column dataType (Int64 PKs default to `bigserial`) |
229
+ | `default` | document | `@default(expr: …)` on the DC column |
230
+ | `literal` | document | pin to one literal value (union discriminant tags) |
231
+ | `nullable` | document | value may be `null` (`.nullable()` / `\| null`) |
232
+ | `description` | document | doc comment carried into generated output |
233
+ | `jsdoc` | document | render the description as a JSDoc block (Firestore fields) |
177
234
  | **constraints** → | |
178
- | `min` / `max` | numeric bounds |
179
- | `minLength` / `maxLength` | string/array length bounds |
180
- | `nonempty` | non-empty string/array |
181
- | `pattern` | regex the string must match |
182
- | `email` / `url` | string format |
235
+ | `min` / `max` | document | numeric bounds |
236
+ | `minLength` / `maxLength` | document | string/array length bounds |
237
+ | `nonempty` | document | non-empty string/array |
238
+ | `pattern` | document | regex the string must match |
239
+ | `email` / `url` | document | string format |
183
240
 
184
241
  Constraints flow into the Zod schemas, the API request validation, and the
185
242
  class-validator DTOs.
@@ -188,14 +245,14 @@ class-validator DTOs.
188
245
 
189
246
  | option | meaning |
190
247
  | ------------ | ------------------------------------------------------------------ |
191
- | `key` | composite primary key field names (else the `id: true` field) |
192
- | `table` | table name override (default: snake_case pluralization) |
193
- | `gqlName` | GraphQL type name override |
194
- | `fsName` | Firestore-side rename (avoid collisions with table models) |
195
- | `directives` | `multi` = each type-level directive on its own line |
196
- | `footer` | trailing comment block after the closing `}` in the schema file |
197
- | `indexes` | composite `@index`/`@unique` (`{ fields, name?, unique?, expand? }` — `expand` renders args one per line) |
198
- | `sql` | raw SQL constraints (see [SQL migrations](#sql-migrations)) |
248
+ | `key` | document | composite primary key field names (else the `id: true` field) |
249
+ | `table` | document | table name override (default: snake_case pluralization) |
250
+ | `gqlName` | document | GraphQL type name override |
251
+ | `fsName` | document | Firestore-side rename (avoid collisions with table models) |
252
+ | `directives` | document | `multi` = each type-level directive on its own line |
253
+ | `footer` | document | trailing comment block after the closing `}` in the schema file |
254
+ | `indexes` | document | composite `@index`/`@unique` (`{ fields, name?, unique?, expand? }` — `expand` renders args one per line) |
255
+ | `sql` | document | raw SQL constraints (see [SQL migrations](#sql-migrations)) |
199
256
 
200
257
  ### Embedded vs relation
201
258
 
@@ -273,14 +330,14 @@ Everything below exists to reproduce real hand-written `.gql` files byte-for-byt
273
330
 
274
331
  | option | meaning |
275
332
  | --- | --- |
276
- | `gqlName` | rendered operation name (the yml key stays unique across services) |
277
- | `footer` | trailing `# …` comment block after the operation |
278
- | `raw` | emit the operation body verbatim (multi-model queries, `_or` keyset cursors — field checks are skipped) |
279
- | `single: id \| key` | single-row lookup (`product(id: $id)` / `product(key: { … })`) |
280
- | `keyArg` / `keyVars` | update/delete key argument form and variable renames |
281
- | `whereAnd: true` | render conditions as an `_and: [ … ]` array |
282
- | `entityDir` | override the `operations/<entity>/` output directory |
283
- | `style` | formatting hints: `signature`, `args`, `data`, `orderBy`, `auth`, `key`, `and`, `where` (`inline`/`multi`/`compact`/`bare`) |
333
+ | `gqlName` | document | rendered operation name (the yml key stays unique across services) |
334
+ | `footer` | document | trailing `# …` comment block after the operation |
335
+ | `raw` | document | emit the operation body verbatim (multi-model queries, `_or` keyset cursors — field checks are skipped) |
336
+ | `single: id \| document | key` | single-row lookup (`product(id: $id)` / `product(key: { … })`) |
337
+ | `keyArg` / `keyVars` | document | update/delete key argument form and variable renames |
338
+ | `whereAnd: true` | document | render conditions as an `_and: [ … ]` array |
339
+ | `entityDir` | document | override the `operations/<entity>/` output directory |
340
+ | `style` | document | formatting hints: `signature`, `args`, `data`, `orderBy`, `auth`, `key`, `and`, `where` (`inline`/`multi`/`compact`/`bare`) |
284
341
 
285
342
  Inputs support `var` (variable rename), `required` (override optionality),
286
343
  `literal` (fixed value, no variable), `flat` (write the FK column directly),
@@ -296,11 +353,13 @@ name as long as their connector sets don't overlap.
296
353
 
297
354
  ## API endpoints
298
355
 
299
- Model application endpoints (Cloud Task / callable / https / pubsub). Generates
300
- request/response types (`api-types`), request-validation Zod (`api-validation`),
301
- class-validator DTOs (`api-dto`), and — for `task`/`pubsub` — payload contracts
302
- (`task-payloads`). A payload references a model or declares inline fields; a void
303
- response maps to `void`.
356
+ Model application endpoints in three kind-implied sections — `apis:` (https /
357
+ callable, keyed by REST path), `tasks:` (Cloud Tasks), and `events:` (Pub/Sub).
358
+ The api-scoped generators — request/response types (`api-types`),
359
+ request-validation Zod (`api-validation`), class-validator DTOs (`api-dto`),
360
+ and payload contracts (`task-payloads`) — run for the entries that opt in via
361
+ the section `defaults` or the entry's own `generators:`. A payload references a
362
+ model or declares inline fields; a void response maps to `void`.
304
363
 
305
364
  ```yaml
306
365
  envelopes:
@@ -310,9 +369,10 @@ envelopes:
310
369
  opId: string
311
370
  enqueuedAt: int
312
371
 
313
- apis:
372
+ tasks: # Cloud Tasks (kind implied)
373
+ defaults:
374
+ generators: [task-payloads] # section default: applies to entries below
314
375
  createCatalog:
315
- kind: task # product | callable | https | pubsub
316
376
  envelope: RetryTaskPayload # wraps the product data
317
377
  maxAttempts: 3
318
378
  request:
@@ -322,17 +382,31 @@ apis:
322
382
  response:
323
383
  void: true
324
384
 
385
+ events: # Pub/Sub (kind implied)
325
386
  generateAiResponse:
326
- kind: pubsub
327
387
  topic: ai-review-generate-response
328
388
  timeoutSeconds: 540
329
-
330
- getShopBySlug:
331
- kind: callable
389
+ generators: [task-payloads]
390
+
391
+ apis: # https/callable, keyed by REST path
392
+ defaults:
393
+ generators: [api-types, api-validation]
394
+ /shops/{slug}:
395
+ operationId: getShopBySlug
396
+ kind: callable # https (default) | callable
332
397
  request: { fields: { slug: { type: string, nonempty: true } } }
333
398
  response: { model: Shop }
399
+ generators: # entry-level wins over section defaults;
400
+ - api-types # an inline out overrides the declared template
401
+ - { generator: api-dto, out: "src/entries/{path}" }
334
402
  ```
335
403
 
404
+ Generator application resolves in two tiers — the section `defaults`
405
+ (`apis:/tasks:/events:` → `defaults.generators`) and the entry's own
406
+ `generators:` (which replaces the defaults). `apis:` keys must be REST paths
407
+ (`/...` or `METHOD /...` so the same route can appear once per verb) with an
408
+ `operationId`; tasks and Pub/Sub events live in their own sections.
409
+
336
410
  For `createCatalog` this yields `CreateCatalogTaskData`, `CreateCatalogTaskPayload =
337
411
  RetryTaskPayload<CreateCatalogTaskData>`, and `CREATE_CATALOG_MAX_ATTEMPTS`, plus the
338
412
  request types, Zod schema, and DTO.
@@ -446,6 +520,8 @@ project:
446
520
  idCodec:
447
521
  minLength: 8
448
522
  alphabet: <your-shuffled-sqids-alphabet>
523
+ aliases:
524
+ "#contracts/*": libs/contracts/src/*
449
525
  ```
450
526
 
451
527
  - `services` (`name`/`database`) feed the `FIRESTORE_DATABASES` constants in the
@@ -454,6 +530,9 @@ project:
454
530
  `id-codecs` generator also emits a self-contained `id-core.ts` (the
455
531
  encode/decode primitives), so nothing about id encoding lives outside the
456
532
  contract.
533
+ - `aliases` maps `#alias/...` out-template prefixes to paths relative to the
534
+ **root** yml, so imported contracts can target shared libs without relative
535
+ path gymnastics (longest prefix wins).
457
536
  - `codebases` / service `location` + `connectors` are consumed by the optional
458
537
  `config` generator (preview), which emits per-service `dataconnect.yaml`,
459
538
  per-connector `connector.yaml`, and the `FIRESTORE_DATABASES` /
@@ -465,32 +544,37 @@ project:
465
544
 
466
545
  # Generators
467
546
 
468
- | name | output | notes |
469
- | ------------------------ | ----------------------------- | ------------------------------------------------------ |
470
- | `typescript` | `types.ts` | interfaces + const/union enums |
471
- | `typescript-split` | `types/<table>.ts` + `types.ts` barrel | one file per table; enums/embedded objects co-located |
472
- | `zod` | `schemas.ts` | `z.object` schemas (constraints, `z.lazy` model refs) |
473
- | `zod-split` | `schemas/<table>.ts` + `schemas.ts` barrel | split variant of `zod` |
474
- | `data-connect-graphql` | `schema.gql` | `type … @table(name, key)`; `@col`/`@unique`/`@index`/`@default` |
475
- | `data-connect-graphql-split` | `schema/<table>.gql` | same schema, one file per table (enums co-located) |
476
- | `data-connect-operations`| `<connector>/operations.gql` + `.ts` | queries/mutations (where ops, orderBy, limit, aggregate, exprs, inc), per connector |
477
- | `data-connect-operations-split` | `<connector>/operations/<entity>/{queries,mutations}.gql` | real Data Connect repo layout; shared types stay in `typescript`/`zod` outputs |
478
- | `data-connect-adapter` | `data-connect-adapters.ts` | convert `Any` rows ⇄ logical types |
479
- | `firestore-types` | `firestore-types.ts` | naive TS types with Firestore `Timestamp` (all models) |
480
- | `firestore` | `firestore.ts` | Firestore projection Zod schemas (derived, denormalized) |
481
- | `firestore-split` | `firestore/<collection>.ts` + `firestore.ts` barrel | one file per projection; `_meta_` under `firestore/_/`; DC-schema `.pick()` reuse; `FIRESTORE_DATABASES` constants in the barrel |
482
- | `api-types` | `api-types.ts` | endpoint request/response types |
483
- | `api-validation` | `api-validation.ts` | endpoint request-validation Zod |
484
- | `api-dto` | `api-dtos.ts` | class-validator DTO classes |
485
- | `task-payloads` | `task-payloads.ts` | envelopes + `*TaskData`/`*TaskPayload` + constants |
486
- | `sql-migrations` | `migrations/constraints.sql` | composite FK / CHECK / index SQL |
487
- | `id-codecs` | `id-codecs.ts` (+ `id-core.ts`) | typed per-entity id encode/decode wrappers; emits the Sqids primitives when `project.idCodec` is set |
488
- | `unions` | `unions.ts` | Zod discriminated unions + TS union types |
489
- | `config` | `dataconnect.yaml`, `connector.yaml`, `constants.ts` | DC configs + sync constants (preview — see [Project config](#project-config)) |
547
+ | name | scope | output | notes |
548
+ | ------------------------ | -------- | ----------------------------- | ------------------------------------------------------ |
549
+ | `typescript` | document | `types.ts` | interfaces + const/union enums |
550
+ | `typescript-split` | document | `types/<table>.ts` + `types.ts` barrel | one file per table; enums/embedded objects co-located |
551
+ | `zod` | document | `schemas.ts` | `z.object` schemas (constraints, `z.lazy` model refs) |
552
+ | `zod-split` | document | `schemas/<table>.ts` + `schemas.ts` barrel | split variant of `zod` |
553
+ | `data-connect-graphql` | document | `schema.gql` | `type … @table(name, key)`; `@col`/`@unique`/`@index`/`@default` |
554
+ | `data-connect-graphql-split` | document | `schema/<table>.gql` | same schema, one file per table (enums co-located) |
555
+ | `data-connect-operations`| document | `<connector>/operations.gql` + `.ts` | queries/mutations (where ops, orderBy, limit, aggregate, exprs, inc), per connector |
556
+ | `data-connect-operations-split` | document | `<connector>/operations/<entity>/{queries,mutations}.gql` | real Data Connect repo layout; shared types stay in `typescript`/`zod` outputs |
557
+ | `data-connect-adapter` | document | `data-connect-adapters.ts` | convert `Any` rows ⇄ logical types |
558
+ | `firestore-types` | document | `firestore-types.ts` | naive TS types with Firestore `Timestamp` (all models) |
559
+ | `firestore` | document | `firestore.ts` | Firestore projection Zod schemas (derived, denormalized) |
560
+ | `firestore-split` | document | `firestore/<collection>.ts` + `firestore.ts` barrel | one file per projection; `_meta_` under `firestore/_/`; DC-schema `.pick()` reuse; `FIRESTORE_DATABASES` constants in the barrel |
561
+ | `api-types` | api | `api-types.ts` | endpoint request/response types |
562
+ | `api-validation` | api | `api-validation.ts` | endpoint request-validation Zod |
563
+ | `api-dto` | api | `<operation>.dto.ts` per api | class-validator DTO classes (NestJS `dto/` convention) |
564
+ | `task-payloads` | api | `task-payloads.ts` | envelopes + `*TaskData`/`*TaskPayload` + constants |
565
+ | `sql-migrations` | document | `migrations/constraints.sql` | composite FK / CHECK / index SQL |
566
+ | `id-codecs` | document | `id-codecs.ts` (+ `id-core.ts`) | typed per-entity id encode/decode wrappers; emits the Sqids primitives when `project.idCodec` is set |
567
+ | `unions` | document | `unions.ts` | Zod discriminated unions + TS union types |
568
+ | `config` | document | `dataconnect.yaml`, `connector.yaml`, `constants.ts` | DC configs + sync constants (preview — see [Project config](#project-config)) |
490
569
 
491
570
  Each generator emits nothing when its section is absent, so a small contract
492
571
  produces a small output. Select a subset with `-g`.
493
572
 
573
+ **Scope** decides how a `generators:` declaration takes effect: `document`
574
+ generators run once for the yml that declares them; `api` generators run only
575
+ for the entries that apply them (section `defaults.generators` → entry
576
+ `generators:`, entry wins).
577
+
494
578
  ### The `Any` boundary
495
579
 
496
580
  Data Connect stores JSON and embedded objects as the `Any` scalar, erasing the
@@ -518,7 +602,7 @@ const result = generate('contract.yml', {
518
602
  write: true,
519
603
  })
520
604
 
521
- // Every `generate:` target declared across the import graph:
605
+ // Every generator declared across the import graph:
522
606
  const all = generateAll('contract.yml', { write: true })
523
607
  ```
524
608
 
@@ -43,10 +43,11 @@ export interface GenerateAllResult {
43
43
  ok: boolean;
44
44
  }
45
45
  /**
46
- * Run every `generate:` target declared across the entry contract and its
47
- * (transitive) imports — the one-command flow: each contract file states where
48
- * its own outputs go, and `fbc generate <root>` materializes all of them.
49
- * Output dirs resolve relative to the declaring yml file.
46
+ * Run every generator declared across the entry contract and its (transitive)
47
+ * imports — the one-command flow: each contract file declares its generators
48
+ * (`generators:` + section defaults / entry applications) and
49
+ * `fbc generate <root>` materializes all of them. Output dirs resolve relative
50
+ * to the declaring yml (or the root yml for `#alias/...` templates).
50
51
  */
51
52
  export declare const generateAll: (entryPath: string, options?: {
52
53
  registry?: GeneratorRegistry;
@@ -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,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
+ {"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;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;AAmND;;;;;;GAMG;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,iBAuDF,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"}
@@ -7,6 +7,7 @@ import { createFsLoader } from '../resolver/module-loader.js';
7
7
  import { validateIr } from '../validation/validate.js';
8
8
  import { createDefaultRegistry } from '../generators/index.js';
9
9
  import { toHeaderComment } from '../generators/support/header.js';
10
+ import { API_PLACEHOLDER_RE, expandApiPlaceholders } from '../generators/support/templates.js';
10
11
  /**
11
12
  * Run the front end of the pipeline: resolve imports, build the IR, and run
12
13
  * semantic validation. Generators are intentionally not involved here — the CLI
@@ -24,11 +25,175 @@ export const compile = (entryPath, options = {}) => {
24
25
  diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics],
25
26
  };
26
27
  };
28
+ /** `apis:/tasks:/events:` section a raw api belongs to (drives the section defaults lookup). */
29
+ const sectionOf = (api) => {
30
+ if (api.kind === 'task')
31
+ return 'tasks';
32
+ if (api.kind === 'pubsub')
33
+ return 'events';
34
+ return 'apis';
35
+ };
36
+ const expandOutTemplate = (template, api, diagnostics, source) => {
37
+ if (!API_PLACEHOLDER_RE.test(template))
38
+ return template;
39
+ if (!api) {
40
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `"${template}" uses api placeholders but the generator is document-scoped (${source})`));
41
+ return undefined;
42
+ }
43
+ const out = expandApiPlaceholders(template, api);
44
+ if (out === undefined) {
45
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `"${template}" uses {path} but api "${api.name}" has no REST path (${source})`));
46
+ }
47
+ return out;
48
+ };
49
+ /**
50
+ * Resolve an out template to an absolute directory. `#alias/...` prefixes are
51
+ * matched against the root document's `project.aliases` (longest prefix wins)
52
+ * and resolve relative to the root yml; everything else resolves relative to
53
+ * the declaring yml — same as the legacy `generate:` form.
54
+ */
55
+ const resolveOutDir = (out, declaringDoc, rootDoc, diagnostics) => {
56
+ if (!out.startsWith('#')) {
57
+ return join(dirname(declaringDoc.filePath), out);
58
+ }
59
+ const aliases = rootDoc.project?.aliases ?? {};
60
+ const candidates = Object.entries(aliases)
61
+ .map(([pattern, target]) => ({ prefix: pattern.endsWith('/*') ? pattern.slice(0, -2) : pattern, pattern, target }))
62
+ .filter(({ prefix }) => out === prefix || out.startsWith(`${prefix}/`))
63
+ .sort((a, b) => b.prefix.length - a.prefix.length);
64
+ const match = candidates[0];
65
+ if (!match) {
66
+ diagnostics.push(error('UNKNOWN_ALIAS', `No project.aliases entry matches "${out}" (declare it in the root contract)`));
67
+ return undefined;
68
+ }
69
+ const rest = out.slice(match.prefix.length).replace(/^\//, '');
70
+ const targetBase = match.target.endsWith('/*') ? match.target.slice(0, -2) : match.target;
71
+ return join(dirname(rootDoc.filePath), targetBase, rest);
72
+ };
73
+ /** Find a generator declaration by name — nearest first: the declaring yml, then the root yml. */
74
+ const findGeneratorDecl = (name, doc, rootDoc) => doc.generatorDecls?.find(decl => decl.generator === name) ??
75
+ rootDoc.generatorDecls?.find(decl => decl.generator === name);
27
76
  /**
28
- * Run every `generate:` target declared across the entry contract and its
29
- * (transitive) imports — the one-command flow: each contract file states where
30
- * its own outputs go, and `fbc generate <root>` materializes all of them.
31
- * Output dirs resolve relative to the declaring yml file.
77
+ * Build generator jobs from the declaration/application DSL:
78
+ * - api-scoped generators run for entries that opt in (section `defaults` →
79
+ * entry `generators:`, entry wins; no file-level tier by design)
80
+ * - document-scoped generators run once per yml that declares them
81
+ */
82
+ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
83
+ const jobs = [];
84
+ const grouped = new Map();
85
+ for (const [apiName, rawApi] of Object.entries(doc.apis)) {
86
+ const uses = rawApi.generators ?? doc.sectionDefaults?.[sectionOf(rawApi)];
87
+ for (const use of uses ?? []) {
88
+ const decl = findGeneratorDecl(use.generator, doc, rootDoc);
89
+ const template = use.out ?? decl?.out;
90
+ if (template === undefined) {
91
+ diagnostics.push(error('UNKNOWN_GENERATOR_DECL', `Api "${apiName}" applies generator "${use.generator}" but no declaration with an "out" was found (${doc.filePath})`));
92
+ continue;
93
+ }
94
+ // Output settings resolve nearest-first: entry use → declaration → the
95
+ // generator's built-in default (applied by the generator itself).
96
+ const generator = registry.get(use.generator);
97
+ const output = {};
98
+ const file = use.file ?? decl?.file;
99
+ const split = use.split ?? decl?.split ?? generator?.defaultOutput?.split;
100
+ const options = use.options ?? decl?.options;
101
+ const header = use.header ?? decl?.header;
102
+ if (file !== undefined)
103
+ output.file = file;
104
+ if (split !== undefined)
105
+ output.split = split;
106
+ if (options !== undefined)
107
+ output.options = options;
108
+ // Split mode needs a per-api file name; bundled mode cannot expand one.
109
+ const effectiveFile = file ?? generator?.defaultOutput?.file;
110
+ if (effectiveFile !== undefined && split === false && API_PLACEHOLDER_RE.test(effectiveFile)) {
111
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `file "${effectiveFile}" uses api placeholders but split is false for generator "${use.generator}" — set a plain file name (${doc.filePath})`));
112
+ continue;
113
+ }
114
+ if (effectiveFile !== undefined && split === true && !API_PLACEHOLDER_RE.test(effectiveFile)) {
115
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `split is true for generator "${use.generator}" but file "${effectiveFile}" has no {api-name}/{path} placeholder, so per-api files would overwrite each other (${doc.filePath})`));
116
+ continue;
117
+ }
118
+ const api = { name: apiName, ...(rawApi.path !== undefined ? { path: rawApi.path } : {}) };
119
+ const expanded = expandOutTemplate(template, api, diagnostics, doc.filePath);
120
+ if (expanded === undefined)
121
+ continue;
122
+ const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
123
+ if (outDir === undefined)
124
+ continue;
125
+ const key = [use.generator, outDir, file ?? '', String(split ?? ''), JSON.stringify(options ?? null), header ?? '\u0001'].join('\u0000');
126
+ const existing = grouped.get(key);
127
+ if (existing) {
128
+ existing.apiNames?.push(apiName);
129
+ }
130
+ else {
131
+ const job = { source: doc.filePath, generatorName: use.generator, outDir, apiNames: [apiName], output };
132
+ if (header !== undefined)
133
+ job.header = header;
134
+ grouped.set(key, job);
135
+ jobs.push(job);
136
+ }
137
+ }
138
+ }
139
+ for (const decl of doc.generatorDecls ?? []) {
140
+ const generator = registry.get(decl.generator);
141
+ if (!generator) {
142
+ diagnostics.push(error('UNKNOWN_GENERATOR', `Unknown generator "${decl.generator}" (${doc.filePath})`));
143
+ continue;
144
+ }
145
+ if ((generator.scope ?? 'document') === 'api')
146
+ continue; // applied via entries, not by declaration
147
+ // Document-scoped `split` toggles between a generator and its `-split`
148
+ // variant (e.g. typescript ⇄ typescript-split) instead of reshaping output.
149
+ let generatorName = decl.generator;
150
+ if (decl.split === true && !generatorName.endsWith('-split')) {
151
+ const splitName = `${generatorName}-split`;
152
+ if (registry.get(splitName)) {
153
+ generatorName = splitName;
154
+ }
155
+ else {
156
+ diagnostics.push(error('UNSUPPORTED_SPLIT', `Generator "${decl.generator}" has no split variant (${doc.filePath})`));
157
+ continue;
158
+ }
159
+ }
160
+ else if (decl.split === false && generatorName.endsWith('-split')) {
161
+ const baseName = generatorName.slice(0, -'-split'.length);
162
+ if (registry.get(baseName))
163
+ generatorName = baseName;
164
+ }
165
+ const output = {};
166
+ if (decl.file !== undefined) {
167
+ if (API_PLACEHOLDER_RE.test(decl.file)) {
168
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `file "${decl.file}" uses api placeholders but generator "${decl.generator}" is document-scoped (${doc.filePath})`));
169
+ continue;
170
+ }
171
+ output.file = decl.file;
172
+ }
173
+ if (decl.options !== undefined)
174
+ output.options = decl.options;
175
+ const expanded = expandOutTemplate(decl.out, undefined, diagnostics, doc.filePath);
176
+ if (expanded === undefined)
177
+ continue;
178
+ const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
179
+ if (outDir === undefined)
180
+ continue;
181
+ jobs.push({
182
+ source: doc.filePath,
183
+ generatorName,
184
+ outDir,
185
+ ...(Object.keys(output).length > 0 ? { output } : {}),
186
+ ...(decl.header !== undefined ? { header: decl.header } : {}),
187
+ });
188
+ }
189
+ return jobs;
190
+ };
191
+ /**
192
+ * Run every generator declared across the entry contract and its (transitive)
193
+ * imports — the one-command flow: each contract file declares its generators
194
+ * (`generators:` + section defaults / entry applications) and
195
+ * `fbc generate <root>` materializes all of them. Output dirs resolve relative
196
+ * to the declaring yml (or the root yml for `#alias/...` templates).
32
197
  */
33
198
  export const generateAll = (entryPath, options = {}) => {
34
199
  const { documents, diagnostics } = compile(entryPath, options);
@@ -38,27 +203,46 @@ export const generateAll = (entryPath, options = {}) => {
38
203
  const collected = [...diagnostics];
39
204
  const targets = [];
40
205
  let ok = true;
41
- for (const doc of documents) {
42
- for (const output of doc.outputs ?? []) {
43
- const outDir = join(dirname(doc.filePath), output.out);
44
- const result = generate(doc.filePath, {
45
- outDir,
46
- generators: output.generators,
47
- registry: options.registry,
48
- write: options.write,
49
- ...(options.context ? { context: options.context } : {}),
50
- ...(options.loader ? { loader: options.loader } : {}),
51
- });
52
- // Per-target compiles revisit shared imports; keep only new diagnostics.
53
- for (const diagnostic of result.diagnostics) {
54
- if (!collected.some(d => d.code === diagnostic.code && d.message === diagnostic.message)) {
55
- collected.push(diagnostic);
56
- }
206
+ const registry = options.registry ?? createDefaultRegistry();
207
+ // Import order puts dependencies first, so the entry (root) document is last.
208
+ const rootDoc = documents[documents.length - 1];
209
+ const mergeDiagnostics = (incoming) => {
210
+ // Per-target compiles revisit shared imports; keep only new diagnostics.
211
+ for (const diagnostic of incoming) {
212
+ if (!collected.some(d => d.code === diagnostic.code && d.message === diagnostic.message)) {
213
+ collected.push(diagnostic);
57
214
  }
58
- if (!result.ok)
59
- ok = false;
60
- targets.push({ source: doc.filePath, outDir, generators: output.generators, files: result.files });
61
215
  }
216
+ };
217
+ const runTarget = (doc, outDir, generators, apiNames, output, header) => {
218
+ const result = generate(doc.filePath, {
219
+ outDir,
220
+ generators,
221
+ registry,
222
+ write: options.write,
223
+ context: {
224
+ ...options.context,
225
+ ...(apiNames ? { apiNames } : {}),
226
+ ...(output ? { output } : {}),
227
+ // Per-generator header beats the CLI/contract-level one ('' disables it).
228
+ ...(header !== undefined ? { header: toHeaderComment(header) } : {}),
229
+ },
230
+ ...(options.loader ? { loader: options.loader } : {}),
231
+ });
232
+ mergeDiagnostics(result.diagnostics);
233
+ if (!result.ok)
234
+ ok = false;
235
+ targets.push({ source: doc.filePath, outDir, generators, files: result.files });
236
+ };
237
+ for (const doc of documents) {
238
+ // Declaration/application DSL (`generators:` + section defaults / entry `generators:`).
239
+ const jobDiagnostics = [];
240
+ for (const job of buildGeneratorJobs(doc, rootDoc, registry, jobDiagnostics)) {
241
+ runTarget(doc, job.outDir, [job.generatorName], job.apiNames, job.output, job.header);
242
+ }
243
+ mergeDiagnostics(jobDiagnostics);
244
+ if (hasErrors(jobDiagnostics))
245
+ ok = false;
62
246
  }
63
247
  return { targets, diagnostics: collected, ok };
64
248
  };