firebase-contract 0.1.0-rc.6 → 0.1.0-rc.8

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 (58) hide show
  1. package/README.md +75 -3
  2. package/dist/lib/_internal/compiler/compiler.d.ts +6 -0
  3. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
  4. package/dist/lib/_internal/compiler/compiler.js +137 -24
  5. package/dist/lib/_internal/compiler/header-vars.d.ts +41 -0
  6. package/dist/lib/_internal/compiler/header-vars.d.ts.map +1 -0
  7. package/dist/lib/_internal/compiler/header-vars.js +117 -0
  8. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +0 -5
  9. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -1
  10. package/dist/lib/_internal/generators/api/api-dto-generator.js +20 -12
  11. package/dist/lib/_internal/generators/api/api-types-generator.d.ts +0 -5
  12. package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -1
  13. package/dist/lib/_internal/generators/api/api-types-generator.js +21 -14
  14. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +0 -4
  15. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -1
  16. package/dist/lib/_internal/generators/api/api-validation-generator.js +11 -5
  17. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +0 -6
  18. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -1
  19. package/dist/lib/_internal/generators/api/task-payload-generator.js +28 -16
  20. package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -1
  21. package/dist/lib/_internal/generators/config/config-generator.js +2 -1
  22. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -1
  23. package/dist/lib/_internal/generators/data-connect/adapter-generator.js +2 -1
  24. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -1
  25. package/dist/lib/_internal/generators/data-connect/graphql-generator.js +2 -1
  26. package/dist/lib/_internal/generators/data-connect/operations-generator.js +1 -1
  27. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -1
  28. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +2 -1
  29. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -1
  30. package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +2 -1
  31. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -1
  32. package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +2 -1
  33. package/dist/lib/_internal/generators/generator.d.ts +20 -0
  34. package/dist/lib/_internal/generators/generator.d.ts.map +1 -1
  35. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -1
  36. package/dist/lib/_internal/generators/id/id-codec-generator.js +2 -1
  37. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -1
  38. package/dist/lib/_internal/generators/sql/sql-migration-generator.js +2 -1
  39. package/dist/lib/_internal/generators/support/header.d.ts.map +1 -1
  40. package/dist/lib/_internal/generators/support/header.js +2 -0
  41. package/dist/lib/_internal/generators/support/templates.d.ts +36 -0
  42. package/dist/lib/_internal/generators/support/templates.d.ts.map +1 -0
  43. package/dist/lib/_internal/generators/support/templates.js +50 -0
  44. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
  45. package/dist/lib/_internal/generators/typescript/typescript-generator.js +2 -1
  46. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
  47. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +2 -1
  48. package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -1
  49. package/dist/lib/_internal/generators/union/union-generator.js +2 -1
  50. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
  51. package/dist/lib/_internal/generators/zod/zod-generator.js +2 -1
  52. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
  53. package/dist/lib/_internal/generators/zod/zod-split-generator.js +2 -1
  54. package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
  55. package/dist/lib/_internal/parser/parse.js +53 -9
  56. package/dist/lib/_internal/parser/raw-document.d.ts +13 -1
  57. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -69,6 +69,50 @@ generators:
69
69
  (REST path with `{param}` segments dropped).
70
70
  - Entries reference declarations by name — nearest first (same yml → root).
71
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
+
78
+ ```yaml
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
94
+ ```
95
+
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
+
72
116
  `fbc generate contract.yml` materializes **all** declared outputs across the
73
117
  whole import graph in one run. Passing `-o`/`-g` switches to single-target mode
74
118
  (`fbc generate <entry> -o <dir> -g typescript,zod`), and
@@ -88,6 +132,34 @@ fbc generate --header "Managed by fbc" # custom text (multi-line ok)
88
132
  header: default # or any text; the CLI --header flag wins
89
133
  ```
90
134
 
135
+ Header text (contract `header:`, per-generator `header`, CLI `--header`)
136
+ supports `${...}` template variables:
137
+
138
+ | Variable | Value |
139
+ | --- | --- |
140
+ | `${rootContractPath}` | entry (root) yml path, relative to the root yml's directory |
141
+ | `${currentContractPath}` | the yml the output is generated from |
142
+ | `${allContractPath}` | import chain root → current, joined with ` -> ` |
143
+ | `${generatedAt}` | date the file was **first** generated (carried over on regenerate) |
144
+ | `${updatedAt}` | date the content **last changed** |
145
+
146
+ A regenerate that produces identical content keeps both dates and reproduces
147
+ the file byte-for-byte, so `--check` stays drift-free; when content changes,
148
+ `generatedAt` is carried over from the existing file and only `updatedAt`
149
+ moves. Date tokens accept an optional format — `${updatedAt:yyyy-mm-dd HH:mm:ss}`
150
+ — with runs `yyyy`/`yy`, `MM`, `dd`, `HH`, `ss`, and `mm` meaning month before
151
+ any hour token / minute after one (both `yyyy-mm-dd` and `HH:mm` read
152
+ naturally). Default: `yyyy-MM-dd`.
153
+
154
+ The `default` banner includes the source and dates:
155
+
156
+ ```
157
+ // AUTO-GENERATED by firebase-contract. Do not edit by hand.
158
+ // Source: ${currentContractPath}
159
+ // Generated: ${generatedAt} / Updated: ${updatedAt}
160
+ // Regenerate with: fbc generate
161
+ ```
162
+
91
163
  Comment syntax adapts per file type (`//` for TS, `#` for GraphQL/YAML-ish,
92
164
  `--` for SQL).
93
165
 
@@ -360,8 +432,8 @@ apis: # https/callable, keyed by REST path
360
432
  Generator application resolves in two tiers — the section `defaults`
361
433
  (`apis:/tasks:/events:` → `defaults.generators`) and the entry's own
362
434
  `generators:` (which replaces the defaults). `apis:` keys must be REST paths
363
- (`/...`) with an `operationId`; tasks and Pub/Sub events live in their own
364
- sections.
435
+ (`/...` or `METHOD /...` so the same route can appear once per verb) with an
436
+ `operationId`; tasks and Pub/Sub events live in their own sections.
365
437
 
366
438
  For `createCatalog` this yields `CreateCatalogTaskData`, `CreateCatalogTaskPayload =
367
439
  RetryTaskPayload<CreateCatalogTaskData>`, and `CREATE_CATALOG_MAX_ATTEMPTS`, plus the
@@ -516,7 +588,7 @@ project:
516
588
  | `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 |
517
589
  | `api-types` | api | `api-types.ts` | endpoint request/response types |
518
590
  | `api-validation` | api | `api-validation.ts` | endpoint request-validation Zod |
519
- | `api-dto` | api | `api-dtos.ts` | class-validator DTO classes |
591
+ | `api-dto` | api | `<operation>.dto.ts` per api | class-validator DTO classes (NestJS `dto/` convention) |
520
592
  | `task-payloads` | api | `task-payloads.ts` | envelopes + `*TaskData`/`*TaskPayload` + constants |
521
593
  | `sql-migrations` | document | `migrations/constraints.sql` | composite FK / CHECK / index SQL |
522
594
  | `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 |
@@ -4,6 +4,7 @@ import { RawContract } from '../parser/raw-document.js';
4
4
  import { ModuleLoader } from '../resolver/module-loader.js';
5
5
  import { GeneratedFile, GeneratorContext } from '../generators/generator.js';
6
6
  import { GeneratorRegistry } from '../generators/registry.js';
7
+ import { HeaderPathVars } from './header-vars.js';
7
8
  export interface CompileOptions {
8
9
  /** Override module loading (defaults to the file system). */
9
10
  loader?: ModuleLoader;
@@ -29,6 +30,10 @@ export interface GenerateOptions extends CompileOptions {
29
30
  /** When true, write the generated files to disk. */
30
31
  write?: boolean;
31
32
  context?: GeneratorContext;
33
+ /** Header `${...}` path values; defaults to entry-only (root = current = entry). */
34
+ headerVars?: HeaderPathVars;
35
+ /** Clock for `${generatedAt}`/`${updatedAt}` stamping (tests); defaults to now. */
36
+ now?: Date;
32
37
  }
33
38
  export interface GenerateTargetResult {
34
39
  /** The contract file that declared this target. */
@@ -54,6 +59,7 @@ export declare const generateAll: (entryPath: string, options?: {
54
59
  write?: boolean;
55
60
  context?: GeneratorContext;
56
61
  loader?: ModuleLoader;
62
+ now?: Date;
57
63
  }) => GenerateAllResult;
58
64
  export interface FileDrift {
59
65
  /** Files whose on-disk content differs from the generated content. */
@@ -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;AAG5E,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;AAwJD;;;;;;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,iBA0CF,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;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;AAmND;;;;;;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,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
2
+ import { dirname, join, relative } from 'node:path';
3
3
  import { error, hasErrors } from '../diagnostics.js';
4
4
  import { buildIr } from '../ir/build-ir.js';
5
5
  import { resolveImports } from '../resolver/import-resolver.js';
@@ -7,7 +7,8 @@ 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 { kebabCase } from '../generators/support/naming.js';
10
+ import { API_PLACEHOLDER_RE, expandApiPlaceholders } from '../generators/support/templates.js';
11
+ import { expandHeaderPathVars, hasDateTokens, resolveDateTokens, } from './header-vars.js';
11
12
  /**
12
13
  * Run the front end of the pipeline: resolve imports, build the IR, and run
13
14
  * semantic validation. Generators are intentionally not involved here — the CLI
@@ -25,7 +26,6 @@ export const compile = (entryPath, options = {}) => {
25
26
  diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics],
26
27
  };
27
28
  };
28
- const OUT_PLACEHOLDER_RE = /\{(api-name|path)\}/;
29
29
  /** `apis:/tasks:/events:` section a raw api belongs to (drives the section defaults lookup). */
30
30
  const sectionOf = (api) => {
31
31
  if (api.kind === 'task')
@@ -34,25 +34,16 @@ const sectionOf = (api) => {
34
34
  return 'events';
35
35
  return 'apis';
36
36
  };
37
- /** Drop `{param}` segments from a REST path and join the rest (`/ai-models/{id}` → `ai-models`). */
38
- const pathSegments = (apiPath) => apiPath
39
- .split('/')
40
- .filter(segment => segment.length > 0 && !(segment.startsWith('{') && segment.endsWith('}')))
41
- .join('/');
42
37
  const expandOutTemplate = (template, api, diagnostics, source) => {
43
- if (!OUT_PLACEHOLDER_RE.test(template))
38
+ if (!API_PLACEHOLDER_RE.test(template))
44
39
  return template;
45
40
  if (!api) {
46
41
  diagnostics.push(error('INVALID_OUT_TEMPLATE', `"${template}" uses api placeholders but the generator is document-scoped (${source})`));
47
42
  return undefined;
48
43
  }
49
- let out = template.replaceAll('{api-name}', kebabCase(api.name));
50
- if (out.includes('{path}')) {
51
- if (!api.path) {
52
- diagnostics.push(error('INVALID_OUT_TEMPLATE', `"${template}" uses {path} but api "${api.name}" has no REST path (${source})`));
53
- return undefined;
54
- }
55
- out = out.replaceAll('{path}', pathSegments(api.path));
44
+ const out = expandApiPlaceholders(template, api);
45
+ if (out === undefined) {
46
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `"${template}" uses {path} but api "${api.name}" has no REST path (${source})`));
56
47
  }
57
48
  return out;
58
49
  };
@@ -101,6 +92,30 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
101
92
  diagnostics.push(error('UNKNOWN_GENERATOR_DECL', `Api "${apiName}" applies generator "${use.generator}" but no declaration with an "out" was found (${doc.filePath})`));
102
93
  continue;
103
94
  }
95
+ // Output settings resolve nearest-first: entry use → declaration → the
96
+ // generator's built-in default (applied by the generator itself).
97
+ const generator = registry.get(use.generator);
98
+ const output = {};
99
+ const file = use.file ?? decl?.file;
100
+ const split = use.split ?? decl?.split ?? generator?.defaultOutput?.split;
101
+ const options = use.options ?? decl?.options;
102
+ const header = use.header ?? decl?.header;
103
+ if (file !== undefined)
104
+ output.file = file;
105
+ if (split !== undefined)
106
+ output.split = split;
107
+ if (options !== undefined)
108
+ output.options = options;
109
+ // Split mode needs a per-api file name; bundled mode cannot expand one.
110
+ const effectiveFile = file ?? generator?.defaultOutput?.file;
111
+ if (effectiveFile !== undefined && split === false && API_PLACEHOLDER_RE.test(effectiveFile)) {
112
+ 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})`));
113
+ continue;
114
+ }
115
+ if (effectiveFile !== undefined && split === true && !API_PLACEHOLDER_RE.test(effectiveFile)) {
116
+ 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})`));
117
+ continue;
118
+ }
104
119
  const api = { name: apiName, ...(rawApi.path !== undefined ? { path: rawApi.path } : {}) };
105
120
  const expanded = expandOutTemplate(template, api, diagnostics, doc.filePath);
106
121
  if (expanded === undefined)
@@ -108,13 +123,15 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
108
123
  const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
109
124
  if (outDir === undefined)
110
125
  continue;
111
- const key = `${use.generator}\u0000${outDir}`;
126
+ const key = [use.generator, outDir, file ?? '', String(split ?? ''), JSON.stringify(options ?? null), header ?? '\u0001'].join('\u0000');
112
127
  const existing = grouped.get(key);
113
128
  if (existing) {
114
129
  existing.apiNames?.push(apiName);
115
130
  }
116
131
  else {
117
- const job = { source: doc.filePath, generatorName: use.generator, outDir, apiNames: [apiName] };
132
+ const job = { source: doc.filePath, generatorName: use.generator, outDir, apiNames: [apiName], output };
133
+ if (header !== undefined)
134
+ job.header = header;
118
135
  grouped.set(key, job);
119
136
  jobs.push(job);
120
137
  }
@@ -128,13 +145,47 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
128
145
  }
129
146
  if ((generator.scope ?? 'document') === 'api')
130
147
  continue; // applied via entries, not by declaration
148
+ // Document-scoped `split` toggles between a generator and its `-split`
149
+ // variant (e.g. typescript ⇄ typescript-split) instead of reshaping output.
150
+ let generatorName = decl.generator;
151
+ if (decl.split === true && !generatorName.endsWith('-split')) {
152
+ const splitName = `${generatorName}-split`;
153
+ if (registry.get(splitName)) {
154
+ generatorName = splitName;
155
+ }
156
+ else {
157
+ diagnostics.push(error('UNSUPPORTED_SPLIT', `Generator "${decl.generator}" has no split variant (${doc.filePath})`));
158
+ continue;
159
+ }
160
+ }
161
+ else if (decl.split === false && generatorName.endsWith('-split')) {
162
+ const baseName = generatorName.slice(0, -'-split'.length);
163
+ if (registry.get(baseName))
164
+ generatorName = baseName;
165
+ }
166
+ const output = {};
167
+ if (decl.file !== undefined) {
168
+ if (API_PLACEHOLDER_RE.test(decl.file)) {
169
+ diagnostics.push(error('INVALID_OUT_TEMPLATE', `file "${decl.file}" uses api placeholders but generator "${decl.generator}" is document-scoped (${doc.filePath})`));
170
+ continue;
171
+ }
172
+ output.file = decl.file;
173
+ }
174
+ if (decl.options !== undefined)
175
+ output.options = decl.options;
131
176
  const expanded = expandOutTemplate(decl.out, undefined, diagnostics, doc.filePath);
132
177
  if (expanded === undefined)
133
178
  continue;
134
179
  const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
135
180
  if (outDir === undefined)
136
181
  continue;
137
- jobs.push({ source: doc.filePath, generatorName: decl.generator, outDir });
182
+ jobs.push({
183
+ source: doc.filePath,
184
+ generatorName,
185
+ outDir,
186
+ ...(Object.keys(output).length > 0 ? { output } : {}),
187
+ ...(decl.header !== undefined ? { header: decl.header } : {}),
188
+ });
138
189
  }
139
190
  return jobs;
140
191
  };
@@ -156,6 +207,42 @@ export const generateAll = (entryPath, options = {}) => {
156
207
  const registry = options.registry ?? createDefaultRegistry();
157
208
  // Import order puts dependencies first, so the entry (root) document is last.
158
209
  const rootDoc = documents[documents.length - 1];
210
+ const loader = options.loader ?? createFsLoader();
211
+ const rootDir = dirname(rootDoc.filePath);
212
+ const relToRoot = (path) => relative(rootDir, path);
213
+ /** Shortest import chain root → target (BFS over resolved import edges). */
214
+ const importChain = (targetDoc) => {
215
+ if (targetDoc.filePath === rootDoc.filePath)
216
+ return [rootDoc.filePath];
217
+ const byPath = new Map(documents.map(document => [document.filePath, document]));
218
+ const queue = [[rootDoc.filePath]];
219
+ const seen = new Set([rootDoc.filePath]);
220
+ while (queue.length > 0) {
221
+ const chain = queue.shift();
222
+ const doc = byPath.get(chain[chain.length - 1]);
223
+ for (const specifier of doc?.imports ?? []) {
224
+ let importId;
225
+ try {
226
+ importId = loader.resolveId(specifier, doc?.filePath ?? '');
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ if (importId === targetDoc.filePath)
232
+ return [...chain, importId];
233
+ if (!seen.has(importId) && byPath.has(importId)) {
234
+ seen.add(importId);
235
+ queue.push([...chain, importId]);
236
+ }
237
+ }
238
+ }
239
+ return [rootDoc.filePath, targetDoc.filePath];
240
+ };
241
+ const headerVarsFor = (doc) => ({
242
+ rootContractPath: relToRoot(rootDoc.filePath),
243
+ currentContractPath: relToRoot(doc.filePath),
244
+ allContractPath: importChain(doc).map(relToRoot).join(' -> '),
245
+ });
159
246
  const mergeDiagnostics = (incoming) => {
160
247
  // Per-target compiles revisit shared imports; keep only new diagnostics.
161
248
  for (const diagnostic of incoming) {
@@ -164,13 +251,21 @@ export const generateAll = (entryPath, options = {}) => {
164
251
  }
165
252
  }
166
253
  };
167
- const runTarget = (doc, outDir, generators, apiNames) => {
254
+ const runTarget = (doc, outDir, generators, apiNames, output, header) => {
168
255
  const result = generate(doc.filePath, {
169
256
  outDir,
170
257
  generators,
171
258
  registry,
172
259
  write: options.write,
173
- context: { ...options.context, ...(apiNames ? { apiNames } : {}) },
260
+ context: {
261
+ ...options.context,
262
+ ...(apiNames ? { apiNames } : {}),
263
+ ...(output ? { output } : {}),
264
+ // Per-generator header beats the CLI/contract-level one ('' disables it).
265
+ ...(header !== undefined ? { header: toHeaderComment(header) } : {}),
266
+ },
267
+ headerVars: headerVarsFor(doc),
268
+ ...(options.now ? { now: options.now } : {}),
174
269
  ...(options.loader ? { loader: options.loader } : {}),
175
270
  });
176
271
  mergeDiagnostics(result.diagnostics);
@@ -182,7 +277,7 @@ export const generateAll = (entryPath, options = {}) => {
182
277
  // Declaration/application DSL (`generators:` + section defaults / entry `generators:`).
183
278
  const jobDiagnostics = [];
184
279
  for (const job of buildGeneratorJobs(doc, rootDoc, registry, jobDiagnostics)) {
185
- runTarget(doc, job.outDir, [job.generatorName], job.apiNames);
280
+ runTarget(doc, job.outDir, [job.generatorName], job.apiNames, job.output, job.header);
186
281
  }
187
282
  mergeDiagnostics(jobDiagnostics);
188
283
  if (hasErrors(jobDiagnostics))
@@ -225,6 +320,15 @@ export const generate = (entryPath, options) => {
225
320
  if (context.header === undefined && ir.header !== undefined) {
226
321
  context.header = toHeaderComment(ir.header);
227
322
  }
323
+ if (context.header !== undefined) {
324
+ const entryName = relative(dirname(entryPath), entryPath);
325
+ const vars = options.headerVars ?? {
326
+ rootContractPath: entryName,
327
+ currentContractPath: entryName,
328
+ allContractPath: entryName,
329
+ };
330
+ context.header = expandHeaderPathVars(context.header, vars);
331
+ }
228
332
  const files = [];
229
333
  for (const name of names) {
230
334
  const generator = registry.get(name);
@@ -239,11 +343,20 @@ export const generate = (entryPath, options) => {
239
343
  if (hasErrors(collected)) {
240
344
  return { ir, diagnostics: collected, files: [], ok: false };
241
345
  }
346
+ // `${generatedAt}`/`${updatedAt}` resolve against what is on disk so an
347
+ // unchanged regenerate reproduces the file byte-for-byte (keeps --check green).
348
+ const now = options.now ?? new Date();
349
+ const resolved = files.map(file => hasDateTokens(file.content)
350
+ ? {
351
+ ...file,
352
+ content: resolveDateTokens(file.content, existsSync(file.path) ? readFileSync(file.path, 'utf8') : undefined, now),
353
+ }
354
+ : file);
242
355
  if (options.write) {
243
- for (const file of files) {
356
+ for (const file of resolved) {
244
357
  mkdirSync(dirname(file.path), { recursive: true });
245
358
  writeFileSync(file.path, file.content, 'utf8');
246
359
  }
247
360
  }
248
- return { ir, diagnostics: collected, files, ok: true };
361
+ return { ir, diagnostics: collected, files: resolved, ok: true };
249
362
  };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `${...}` template variables available in header text (contract `header:`,
3
+ * per-generator `header`, CLI `--header`):
4
+ *
5
+ * - `${rootContractPath}` — entry (root) yml, relative to the root yml's dir
6
+ * - `${currentContractPath}` — the yml the output is generated from
7
+ * - `${allContractPath}` — import chain root → current, joined with ` -> `
8
+ * - `${generatedAt}` — date the file was first generated (kept stable)
9
+ * - `${updatedAt}` — date the content last changed (a regenerate that
10
+ * produces identical content keeps both dates, so
11
+ * `--check` stays drift-free)
12
+ *
13
+ * Date tokens accept an optional format: `${updatedAt:yyyy-mm-dd HH:mm:ss}`.
14
+ * Format runs: `yyyy`/`yy` year, `MM` month, `dd` day, `HH` hour, `mm` month
15
+ * before any hour token / minute after one (so both `yyyy-mm-dd` and `HH:mm`
16
+ * read naturally), `ss` second. The default format is `yyyy-MM-dd`.
17
+ *
18
+ * Path variables are expanded before generators run; date variables survive
19
+ * into the rendered content and are resolved per file against what is already
20
+ * on disk (see {@link resolveDateTokens}).
21
+ */
22
+ export interface HeaderPathVars {
23
+ rootContractPath: string;
24
+ currentContractPath: string;
25
+ allContractPath: string;
26
+ }
27
+ export declare const expandHeaderPathVars: (header: string, vars: HeaderPathVars) => string;
28
+ export declare const DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
29
+ export declare const hasDateTokens: (content: string) => boolean;
30
+ /** Render `now` with a format string (see the module doc for the token set). */
31
+ export declare const formatDate: (format: string, now: Date) => string;
32
+ /**
33
+ * Resolve `${generatedAt}` / `${updatedAt}` in freshly rendered content against
34
+ * the previously written file:
35
+ *
36
+ * - unchanged content (modulo the dates themselves) → both dates carried over
37
+ * - changed or new content → `updatedAt` = now; `generatedAt` recovered from
38
+ * the existing file's matching header line when possible, else now
39
+ */
40
+ export declare const resolveDateTokens: (content: string, existing: string | undefined, now: Date) => string;
41
+ //# sourceMappingURL=header-vars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-vars.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/header-vars.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAA;IACxB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,MAAM,cAAc,KAAG,MAIjB,CAAA;AAE3D,eAAO,MAAM,mBAAmB,eAAe,CAAA;AAM/C,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,KAAG,OAAsC,CAAA;AAMtF,gFAAgF;AAChF,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,KAAK,IAAI,KAAG,MAwBtD,CAAA;AAyBD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,EAAE,KAAK,IAAI,KAAG,MA4B5F,CAAA"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `${...}` template variables available in header text (contract `header:`,
3
+ * per-generator `header`, CLI `--header`):
4
+ *
5
+ * - `${rootContractPath}` — entry (root) yml, relative to the root yml's dir
6
+ * - `${currentContractPath}` — the yml the output is generated from
7
+ * - `${allContractPath}` — import chain root → current, joined with ` -> `
8
+ * - `${generatedAt}` — date the file was first generated (kept stable)
9
+ * - `${updatedAt}` — date the content last changed (a regenerate that
10
+ * produces identical content keeps both dates, so
11
+ * `--check` stays drift-free)
12
+ *
13
+ * Date tokens accept an optional format: `${updatedAt:yyyy-mm-dd HH:mm:ss}`.
14
+ * Format runs: `yyyy`/`yy` year, `MM` month, `dd` day, `HH` hour, `mm` month
15
+ * before any hour token / minute after one (so both `yyyy-mm-dd` and `HH:mm`
16
+ * read naturally), `ss` second. The default format is `yyyy-MM-dd`.
17
+ *
18
+ * Path variables are expanded before generators run; date variables survive
19
+ * into the rendered content and are resolved per file against what is already
20
+ * on disk (see {@link resolveDateTokens}).
21
+ */
22
+ export const expandHeaderPathVars = (header, vars) => header
23
+ .replaceAll('${rootContractPath}', vars.rootContractPath)
24
+ .replaceAll('${currentContractPath}', vars.currentContractPath)
25
+ .replaceAll('${allContractPath}', vars.allContractPath);
26
+ export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
27
+ const DATE_TOKEN_RE = /\$\{(generatedAt|updatedAt)(?::([^}]+))?\}/;
28
+ const DATE_TOKEN_ALL_RE = /\$\{(generatedAt|updatedAt)(?::([^}]+))?\}/g;
29
+ const FORMAT_RUN_RE = /y+|[Mm]+|[Dd]+|[Hh]+|s+/g;
30
+ export const hasDateTokens = (content) => DATE_TOKEN_RE.test(content);
31
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ const pad = (value, width) => String(value).padStart(width, '0');
33
+ /** Render `now` with a format string (see the module doc for the token set). */
34
+ export const formatDate = (format, now) => {
35
+ let hourSeen = false;
36
+ let monthDone = false;
37
+ return format.replace(FORMAT_RUN_RE, run => {
38
+ const char = run[0];
39
+ const width = run.length;
40
+ if (char === 'y') {
41
+ return width <= 2 ? String(now.getFullYear()).slice(-2) : pad(now.getFullYear(), width);
42
+ }
43
+ if (char === 'M' || char === 'm') {
44
+ const isMonth = char === 'M' || (!monthDone && !hourSeen);
45
+ if (isMonth) {
46
+ monthDone = true;
47
+ return pad(now.getMonth() + 1, width);
48
+ }
49
+ return pad(now.getMinutes(), width);
50
+ }
51
+ if (char === 'D' || char === 'd')
52
+ return pad(now.getDate(), width);
53
+ if (char === 'H' || char === 'h') {
54
+ hourSeen = true;
55
+ return pad(now.getHours(), width);
56
+ }
57
+ return pad(now.getSeconds(), width);
58
+ });
59
+ };
60
+ /** Regex source matching any value rendered with `format` (digits per run, rest literal). */
61
+ const formatToPattern = (format) => {
62
+ let out = '';
63
+ let last = 0;
64
+ for (const match of format.matchAll(FORMAT_RUN_RE)) {
65
+ out += escapeRegExp(format.slice(last, match.index)) + String.raw `\d{${match[0].length}}`;
66
+ last = match.index + match[0].length;
67
+ }
68
+ return out + escapeRegExp(format.slice(last));
69
+ };
70
+ /** Escaped-template → regex source with one capture per date token; records token order. */
71
+ const toDatePattern = (template, tokens) => {
72
+ let out = '';
73
+ let last = 0;
74
+ for (const match of template.matchAll(DATE_TOKEN_ALL_RE)) {
75
+ tokens.push(match[1]);
76
+ out += escapeRegExp(template.slice(last, match.index)) + `(${formatToPattern(match[2] ?? DEFAULT_DATE_FORMAT)})`;
77
+ last = match.index + match[0].length;
78
+ }
79
+ return out + escapeRegExp(template.slice(last));
80
+ };
81
+ /**
82
+ * Resolve `${generatedAt}` / `${updatedAt}` in freshly rendered content against
83
+ * the previously written file:
84
+ *
85
+ * - unchanged content (modulo the dates themselves) → both dates carried over
86
+ * - changed or new content → `updatedAt` = now; `generatedAt` recovered from
87
+ * the existing file's matching header line when possible, else now
88
+ */
89
+ export const resolveDateTokens = (content, existing, now) => {
90
+ if (!hasDateTokens(content))
91
+ return content;
92
+ if (existing !== undefined) {
93
+ const tokens = [];
94
+ const full = new RegExp(`^${toDatePattern(content, tokens)}$`);
95
+ const match = existing.match(full);
96
+ if (match) {
97
+ let index = 0;
98
+ return content.replace(DATE_TOKEN_ALL_RE, () => match[++index]);
99
+ }
100
+ }
101
+ let generatedAt;
102
+ if (existing !== undefined) {
103
+ const generatedLine = content.split('\n').find(line => /\$\{generatedAt(?::[^}]+)?\}/.test(line));
104
+ if (generatedLine !== undefined) {
105
+ const lineTokens = [];
106
+ const lineMatch = existing.match(new RegExp(toDatePattern(generatedLine, lineTokens)));
107
+ if (lineMatch) {
108
+ generatedAt = lineMatch[lineTokens.indexOf('generatedAt') + 1];
109
+ }
110
+ }
111
+ }
112
+ return content.replace(DATE_TOKEN_ALL_RE, (_, name, format) => {
113
+ if (name === 'generatedAt' && generatedAt !== undefined)
114
+ return generatedAt;
115
+ return formatDate(format ?? DEFAULT_DATE_FORMAT, now);
116
+ });
117
+ };
@@ -1,8 +1,3 @@
1
1
  import { Generator } from '../generator.js';
2
- /**
3
- * Generates class-validator DTO classes for API endpoint requests, mirroring the
4
- * hand-written NestJS DTOs (decorators derived from field type +
5
- * constraints). Complements the Zod request schemas for a NestJS ValidationPipe.
6
- */
7
2
  export declare const createApiDtoGenerator: () => Generator;
8
3
  //# sourceMappingURL=api-dto-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-dto-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-dto-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AA0GxF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,SAmBvC,CAAA"}
1
+ {"version":3,"file":"api-dto-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-dto-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AAoHxF,eAAO,MAAM,qBAAqB,QAAO,SAuBvC,CAAA"}
@@ -3,6 +3,7 @@ import { pascalCase, singleQuote } from '../support/naming.js';
3
3
  import { isRelation, relationFkName } from '../support/relations.js';
4
4
  import { selectApis } from '../generator.js';
5
5
  import { headerBlocks } from '../support/header.js';
6
+ import { emitApiFiles, resolveOutput } from '../support/templates.js';
6
7
  const SCALAR_TS = {
7
8
  string: 'string',
8
9
  int: 'number',
@@ -126,23 +127,30 @@ const renderDto = (ir, api, used) => {
126
127
  * hand-written NestJS DTOs (decorators derived from field type +
127
128
  * constraints). Complements the Zod request schemas for a NestJS ValidationPipe.
128
129
  */
130
+ // Default mirrors the NestJS convention: one `dto/create-thing.dto.ts` per
131
+ // endpoint next to its controller (the out template decides the directory).
132
+ const DEFAULT_OUTPUT = { file: '{api-name}.dto.ts', split: true };
129
133
  export const createApiDtoGenerator = () => ({
130
134
  name: 'api-dto',
131
135
  scope: 'api',
132
136
  description: 'class-validator DTO classes for API requests',
137
+ defaultOutput: DEFAULT_OUTPUT,
133
138
  generate(ir, context) {
134
- // DTO は HTTP リクエストボディ用 — task / pubsub のペイロードは task-payloads が担う。
139
+ // DTOs cover HTTP request bodies — task / pubsub payloads belong to task-payloads.
135
140
  const httpApis = selectApis(ir.apis, context).filter(api => api.kind === 'https' || api.kind === 'callable');
136
- if (httpApis.length === 0) {
137
- return [];
138
- }
139
- const used = new Set();
140
- const dtoBlocks = httpApis.map(api => renderDto(ir, api, used));
141
- const blocks = [...headerBlocks(context)];
142
- if (used.size > 0) {
143
- blocks.push(`import { ${[...used].sort().join(', ')} } from 'class-validator'`);
144
- }
145
- blocks.push(...dtoBlocks);
146
- return [{ path: 'api-dtos.ts', content: `${blocks.join('\n\n')}\n` }];
141
+ const output = resolveOutput(context, DEFAULT_OUTPUT);
142
+ return emitApiFiles(httpApis, output, subset => {
143
+ if (subset.length === 0) {
144
+ return undefined;
145
+ }
146
+ const used = new Set();
147
+ const dtos = subset.map(api => renderDto(ir, api, used));
148
+ const blocks = [...headerBlocks(context)];
149
+ if (used.size > 0) {
150
+ blocks.push(`import { ${[...used].sort().join(', ')} } from 'class-validator'`);
151
+ }
152
+ blocks.push(...dtos);
153
+ return `${blocks.join('\n\n')}\n`;
154
+ });
147
155
  },
148
156
  });
@@ -1,8 +1,3 @@
1
1
  import { Generator } from '../generator.js';
2
- /**
3
- * Generates request/response TypeScript types for each API endpoint. A payload
4
- * can reference an existing model or declare inline fields; a void response maps
5
- * to `void`. This mirrors the shared producer/consumer contract pattern.
6
- */
7
2
  export declare const createApiTypesGenerator: () => Generator;
8
3
  //# sourceMappingURL=api-types-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-types-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AA8ExF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAO,SA0BzC,CAAA"}
1
+ {"version":3,"file":"api-types-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AAsFxF,eAAO,MAAM,uBAAuB,QAAO,SA+BzC,CAAA"}