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.
- package/README.md +75 -3
- package/dist/lib/_internal/compiler/compiler.d.ts +6 -0
- package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
- package/dist/lib/_internal/compiler/compiler.js +137 -24
- package/dist/lib/_internal/compiler/header-vars.d.ts +41 -0
- package/dist/lib/_internal/compiler/header-vars.d.ts.map +1 -0
- package/dist/lib/_internal/compiler/header-vars.js +117 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +0 -5
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/api/api-dto-generator.js +20 -12
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts +0 -5
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/api/api-types-generator.js +21 -14
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +0 -4
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/api/api-validation-generator.js +11 -5
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +0 -6
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/api/task-payload-generator.js +28 -16
- package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/config/config-generator.js +2 -1
- package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/data-connect/adapter-generator.js +2 -1
- package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/data-connect/graphql-generator.js +2 -1
- package/dist/lib/_internal/generators/data-connect/operations-generator.js +1 -1
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +2 -1
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +2 -1
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +2 -1
- package/dist/lib/_internal/generators/generator.d.ts +20 -0
- package/dist/lib/_internal/generators/generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/id/id-codec-generator.js +2 -1
- package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/sql/sql-migration-generator.js +2 -1
- package/dist/lib/_internal/generators/support/header.d.ts.map +1 -1
- package/dist/lib/_internal/generators/support/header.js +2 -0
- package/dist/lib/_internal/generators/support/templates.d.ts +36 -0
- package/dist/lib/_internal/generators/support/templates.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/templates.js +50 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/typescript/typescript-generator.js +2 -1
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +2 -1
- package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/union/union-generator.js +2 -1
- package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/zod/zod-generator.js +2 -1
- package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
- package/dist/lib/_internal/generators/zod/zod-split-generator.js +2 -1
- package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
- package/dist/lib/_internal/parser/parse.js +53 -9
- package/dist/lib/_internal/parser/raw-document.d.ts +13 -1
- package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
- 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
|
-
(`/...`
|
|
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 |
|
|
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;
|
|
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 {
|
|
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 (!
|
|
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
|
-
|
|
50
|
-
if (out
|
|
51
|
-
|
|
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 =
|
|
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({
|
|
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: {
|
|
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
|
|
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;
|
|
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
|
-
//
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
blocks
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
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"}
|