ts-procedures 9.1.0 → 10.0.0
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/CHANGELOG.md +73 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +4 -0
- package/agent_config/copilot/copilot-instructions.md +2 -0
- package/agent_config/cursor/cursorrules +2 -0
- package/build/codegen/bin/cli.d.ts +4 -0
- package/build/codegen/bin/cli.js +20 -1
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +21 -0
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/bin/flag-specs.js +3 -0
- package/build/codegen/bin/flag-specs.js.map +1 -1
- package/build/codegen/check-output.d.ts +28 -0
- package/build/codegen/check-output.js +216 -0
- package/build/codegen/check-output.js.map +1 -0
- package/build/codegen/check-output.test.d.ts +1 -0
- package/build/codegen/check-output.test.js +160 -0
- package/build/codegen/check-output.test.js.map +1 -0
- package/build/codegen/emit/api-route.js +2 -2
- package/build/codegen/emit/api-route.js.map +1 -1
- package/build/codegen/emit/context.d.ts +9 -0
- package/build/codegen/emit/http-stream-route.js +1 -1
- package/build/codegen/emit/http-stream-route.js.map +1 -1
- package/build/codegen/emit/rpc-route.js +1 -1
- package/build/codegen/emit/rpc-route.js.map +1 -1
- package/build/codegen/emit/scope-file.js +15 -1
- package/build/codegen/emit/scope-file.js.map +1 -1
- package/build/codegen/emit/stream-route.js +1 -1
- package/build/codegen/emit/stream-route.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +2 -2
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/emit-scope.test.js +55 -0
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/emit-types.js +19 -0
- package/build/codegen/emit-types.js.map +1 -1
- package/build/codegen/emit-types.test.js +34 -0
- package/build/codegen/emit-types.test.js.map +1 -1
- package/build/codegen/index.d.ts +14 -0
- package/build/codegen/index.js +11 -1
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/targets/ts/name-collision.test.d.ts +1 -0
- package/build/codegen/targets/ts/name-collision.test.js +30 -0
- package/build/codegen/targets/ts/name-collision.test.js.map +1 -0
- package/build/codegen/targets/ts/run.js +13 -0
- package/build/codegen/targets/ts/run.js.map +1 -1
- package/build/codegen/test-helpers/run-tsc.js +1 -1
- package/build/codegen/test-helpers/run-tsc.js.map +1 -1
- package/docs/client-and-codegen.md +17 -0
- package/docs/migration-v8-to-v9.md +12 -4
- package/package.json +8 -8
- package/src/codegen/bin/cli.test.ts +28 -0
- package/src/codegen/bin/cli.ts +27 -1
- package/src/codegen/bin/flag-specs.ts +3 -0
- package/src/codegen/check-output.test.ts +177 -0
- package/src/codegen/check-output.ts +262 -0
- package/src/codegen/emit/api-route.ts +2 -2
- package/src/codegen/emit/context.ts +9 -0
- package/src/codegen/emit/http-stream-route.ts +1 -1
- package/src/codegen/emit/rpc-route.ts +1 -1
- package/src/codegen/emit/scope-file.ts +19 -1
- package/src/codegen/emit/stream-route.ts +1 -1
- package/src/codegen/emit-errors.integration.test.ts +2 -2
- package/src/codegen/emit-scope.test.ts +60 -0
- package/src/codegen/emit-types.test.ts +36 -0
- package/src/codegen/emit-types.ts +20 -0
- package/src/codegen/index.ts +23 -1
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/targets/ts/name-collision.test.ts +34 -0
- package/src/codegen/targets/ts/run.ts +16 -0
- package/src/codegen/test-helpers/run-tsc.ts +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { runPipeline } from '../../pipeline.js';
|
|
3
|
+
import { makeRpcRoute, makeEnvelope } from '../../__fixtures__/make-envelope.js';
|
|
4
|
+
// Bug repro (downstream): `--service-name Godmode` together with a scope named
|
|
5
|
+
// `godmode` emitted two `export type GodmodeClient = …` declarations (the
|
|
6
|
+
// aggregate client type AND the scope port type) → duplicate identifier. We now
|
|
7
|
+
// fail fast at generation time with both knobs the user can turn.
|
|
8
|
+
describe('ts target: --service-name vs scope-name collision', () => {
|
|
9
|
+
const envelope = makeEnvelope([
|
|
10
|
+
makeRpcRoute({
|
|
11
|
+
name: 'DoThing',
|
|
12
|
+
scope: 'godmode',
|
|
13
|
+
jsonSchema: { body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
14
|
+
}),
|
|
15
|
+
]);
|
|
16
|
+
it('throws when a scope pascalizes to the same name as --service-name', async () => {
|
|
17
|
+
await expect(runPipeline({ envelope, outDir: 'out', dryRun: true, serviceName: 'Godmode', namespaceTypes: true })).rejects.toThrow(/Scope "godmode" produces client port type "GodmodeClient" which collides/);
|
|
18
|
+
});
|
|
19
|
+
it('does not throw when --service-name differs from every scope', async () => {
|
|
20
|
+
const files = await runPipeline({
|
|
21
|
+
envelope,
|
|
22
|
+
outDir: 'out',
|
|
23
|
+
dryRun: true,
|
|
24
|
+
serviceName: 'GodmodeApi',
|
|
25
|
+
namespaceTypes: true,
|
|
26
|
+
});
|
|
27
|
+
expect(files.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=name-collision.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"name-collision.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/ts/name-collision.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAEhF,+EAA+E;AAC/E,0EAA0E;AAC1E,gFAAgF;AAChF,kEAAkE;AAClE,QAAQ,CAAC,mDAAmD,EAAE,GAAG,EAAE;IACjE,MAAM,QAAQ,GAAG,YAAY,CAAC;QAC5B,YAAY,CAAC;YACX,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,SAAS;YAChB,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE;SACnG,CAAC;KACH,CAAC,CAAA;IAEF,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,MAAM,MAAM,CACV,WAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CACrG,CAAC,OAAO,CAAC,OAAO,CAAC,0EAA0E,CAAC,CAAA;IAC/F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ;YACR,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI;YACZ,WAAW,EAAE,YAAY;YACzB,cAAc,EAAE,IAAI;SACrB,CAAC,CAAA;QACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -14,6 +14,7 @@ import { emitClientTypesFile } from '../../emit-client-types.js';
|
|
|
14
14
|
import { emitClientRuntimeFile } from '../../emit-client-runtime.js';
|
|
15
15
|
import { collectModels, resolveModelImports } from '../../collect-models.js';
|
|
16
16
|
import { emitModelsFile } from '../../emit-models.js';
|
|
17
|
+
import { toPascalCase } from '../../naming.js';
|
|
17
18
|
export async function runTsPipeline(input) {
|
|
18
19
|
const { envelope, outDir, hash, groups, serviceName, ajsc: ajscOpts, clientImportPath, dryRun = false, namespaceTypes = false, selfContained = false, cleanOutDir = false, sharedTypesImport, sharedModelsModule, strictSharedModels = false, logger, } = input;
|
|
19
20
|
const shareModels = input.shareModels ?? false;
|
|
@@ -22,6 +23,18 @@ export async function runTsPipeline(input) {
|
|
|
22
23
|
// Scope emit uses this to filter `route.errors` so generated code never
|
|
23
24
|
// references an undefined error type.
|
|
24
25
|
const errorKeys = new Set(envelope.errors.filter((e) => e.schema != null).map((e) => e.name));
|
|
26
|
+
// The index emits one aggregate `${ServiceName}Client` type AND one
|
|
27
|
+
// `${ScopePascal}Client` port type per scope. When a scope's PascalCase name
|
|
28
|
+
// equals the service name's, the two declarations collide (duplicate
|
|
29
|
+
// identifier). Fail fast with both knobs the user can turn, rather than emit
|
|
30
|
+
// a non-compiling index.
|
|
31
|
+
const servicePascal = toPascalCase(serviceName);
|
|
32
|
+
for (const group of groups) {
|
|
33
|
+
if (toPascalCase(group.camelCase) === servicePascal) {
|
|
34
|
+
throw new Error(`[ts-procedures-codegen] Scope "${group.scopeKey}" produces client port type "${servicePascal}Client" which collides with the aggregate client type "${servicePascal}Client" derived from --service-name "${serviceName}". ` +
|
|
35
|
+
`Rename the scope or change --service-name so they differ.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
if (selfContained) {
|
|
26
39
|
for (const group of groups) {
|
|
27
40
|
if (group.scopeKey === '_types' || group.scopeKey === '_client') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../../src/codegen/targets/ts/run.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAE/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../../src/codegen/targets/ts/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -43,7 +43,7 @@ export function runTsc(args) {
|
|
|
43
43
|
const stdout = e.stdout?.toString() ?? '';
|
|
44
44
|
const stderr = e.stderr?.toString() ?? '';
|
|
45
45
|
const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
46
|
-
throw new Error(`[runTsc] tsc failed for ${tsconfigPath}:\n${combined || (e.message ?? '(no output)')}
|
|
46
|
+
throw new Error(`[runTsc] tsc failed for ${tsconfigPath}:\n${combined || (e.message ?? '(no output)')}`, { cause: err });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
//# sourceMappingURL=run-tsc.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-tsc.js","sourceRoot":"","sources":["../../../src/codegen/test-helpers/run-tsc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,eAAe,EAAE;QACf,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,QAAQ;QAChB,gBAAgB,EAAE,SAAS;QAC3B,MAAM,EAAE,IAAI;QACZ,YAAY,EAAE,IAAI;KACnB;IACD,OAAO,EAAE,CAAC,SAAS,CAAC;CACZ,CAAA;AAEV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,MAAM,CAAC,IAItB;IACC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC5E,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;QAChC,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAA;IAClE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IAClE,IAAI,CAAC;QACH,QAAQ,CAAC,GAAG,OAAO,uBAAuB,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAC9E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAA6D,CAAA;QACvE,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QACzC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QACzC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QACnE,MAAM,IAAI,KAAK,CACb,2BAA2B,YAAY,MAAM,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,aAAa,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"run-tsc.js","sourceRoot":"","sources":["../../../src/codegen/test-helpers/run-tsc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,eAAe,EAAE;QACf,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,QAAQ;QAChB,MAAM,EAAE,QAAQ;QAChB,gBAAgB,EAAE,SAAS;QAC3B,MAAM,EAAE,IAAI;QACZ,YAAY,EAAE,IAAI;KACnB;IACD,OAAO,EAAE,CAAC,SAAS,CAAC;CACZ,CAAA;AAEV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,MAAM,CAAC,IAItB;IACC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC5E,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;QAChC,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAA;IAClE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IAClE,IAAI,CAAC;QACH,QAAQ,CAAC,GAAG,OAAO,uBAAuB,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAC9E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAA6D,CAAA;QACvE,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QACzC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QACzC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QACnE,MAAM,IAAI,KAAK,CACb,2BAA2B,YAAY,MAAM,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,aAAa,CAAC,EAAE,EACvF,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -141,11 +141,21 @@ generated/
|
|
|
141
141
|
| `--share-models` / `--no-share-models` | Collect `$id`-bearing subschemas into a shared `_models.ts` hub; scopes import from there instead of inlining types. | **On** |
|
|
142
142
|
| `--shared-models-module <module>` | Convention form: re-export every `$id` model (with no explicit `sharedTypesImport` entry) from this single module. E.g. `--shared-models-module @app/schemas`. | Off |
|
|
143
143
|
| `--strict-shared-models` | Fail the build if any `$id` model would be generated locally as a structural twin (i.e., not covered by `sharedTypesImport` or `sharedModelsModule`). CI guard. | Off |
|
|
144
|
+
| `--check` | Self-validate the emitted output and **exit non-zero** if it won't compile — turns a silently-broken client into a loud generation failure. CI guard. | Off |
|
|
145
|
+
| `--check-mode <scan\|tsc>` | How `--check` validates. `scan`: a dependency-free, in-memory scan for duplicate identifiers and dangling type references. `tsc`: a full `tsc --noEmit` type-check via the optional `typescript` peer (falls back to `scan` with a warning if `typescript` isn't installed). Implies `--check`. | `scan` |
|
|
144
146
|
|
|
145
147
|
> **Note:** ajsc formatting options (`--enum-style`, `--depluralize`, `--array-item-naming`, `--uncountable-words`) only take effect in namespace mode (the default). With `--no-namespace-types`, all types are inlined and these options have no effect.
|
|
146
148
|
>
|
|
147
149
|
> You can also use a `ts-procedures-codegen.config.json` file in your project root instead of CLI flags. CLI flags override config values.
|
|
148
150
|
|
|
151
|
+
### Generation-time safety
|
|
152
|
+
|
|
153
|
+
Codegen fails fast (non-zero exit) rather than emitting a client that won't compile:
|
|
154
|
+
|
|
155
|
+
- **Route-name collisions.** Two routes in the same scope whose names PascalCase to the same identifier (e.g. `get-task` and `getTask`, or `Foo` at version 2 and a literal `FooV2`) would emit a merged namespace with duplicate types and a duplicated callable key. Codegen throws naming both routes — rename one.
|
|
156
|
+
- **`--service-name` vs scope collisions.** A scope whose PascalCase name equals `--service-name` (e.g. scope `godmode` with `--service-name Godmode`) would emit two `GodmodeClient` declarations. Codegen throws — rename the scope or change `--service-name`.
|
|
157
|
+
- **`--check` self-validation.** Opt in to validate the *whole* emitted output before trusting it (see the flag table above). Use it in CI so a generator regression can never silently overwrite a working client with a broken one.
|
|
158
|
+
|
|
149
159
|
## Adapter Interface
|
|
150
160
|
|
|
151
161
|
The client requires an adapter that handles the actual HTTP transport. A built-in fetch adapter is included, and you can implement your own for any HTTP library:
|
|
@@ -531,9 +541,16 @@ await generateClient({
|
|
|
531
541
|
arrayItemNaming: 'Item',
|
|
532
542
|
uncountableWords: ['criteria'],
|
|
533
543
|
},
|
|
544
|
+
validate: true, // optional — self-check the output and THROW if it won't compile
|
|
545
|
+
// (the peer of the CLI's --check). Pass { mode: 'tsc' } for a full
|
|
546
|
+
// type-check via the optional `typescript` peer; defaults to the
|
|
547
|
+
// dependency-free dangling-reference scan. Runs on the in-memory
|
|
548
|
+
// output, so it works under dryRun too.
|
|
534
549
|
})
|
|
535
550
|
```
|
|
536
551
|
|
|
552
|
+
When `validate` fails, `generateClient` throws an `Error` whose message lists every problem (`file:line — message`) — the same report the CLI prints to stderr. Use it in a build script to fail the build the moment the generator would emit a client that doesn't compile.
|
|
553
|
+
|
|
537
554
|
## Shared Model Types (`_models.ts`)
|
|
538
555
|
|
|
539
556
|
When `shareModels` is on (the default), the codegen pre-pass collects every subschema with a `$id` field, deduplicates them, and emits a single `_models.ts` hub. Every scope that references one of those types imports from `./_models` instead of inlining it — so the type is defined once and shared across scopes.
|
|
@@ -8,10 +8,18 @@ with v9 produces the same files — nothing to migrate on the consumer side.
|
|
|
8
8
|
The flip side of that guarantee: known v8 codegen issues carried over into
|
|
9
9
|
9.0.0 unchanged — output couldn't change, so neither could the bugs. Fixes
|
|
10
10
|
resume in 9.x releases when they can land without breaking parity for output
|
|
11
|
-
that compiles today.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
that compiles today. Each is parity-safe because the affected output never
|
|
12
|
+
typechecked, so no working consumer's bytes change:
|
|
13
|
+
|
|
14
|
+
- **9.1.0** — routes that declare `errors` but no req/res schema now emit a
|
|
15
|
+
namespace housing their `Errors` union instead of a dangling type reference.
|
|
16
|
+
- **9.2.0** — namespace mode no longer emits a dangling `export type … = Root`
|
|
17
|
+
for a root-level `anyOf`/`oneOf` body (it inlines the union, as flat mode
|
|
18
|
+
already did). Plus two new fail-fast guards: a scope with two routes whose
|
|
19
|
+
names PascalCase to the same identifier, and a scope name colliding with
|
|
20
|
+
`--service-name`, now error at generation time instead of emitting a
|
|
21
|
+
non-compiling client. Codegen also gains an opt-in `--check` self-validation
|
|
22
|
+
(see the [client/codegen guide](client-and-codegen.md#cli-reference)).
|
|
15
23
|
|
|
16
24
|
Server-side and tooling APIs have breaking changes, listed exhaustively below.
|
|
17
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
|
|
5
5
|
"main": "build/exports.js",
|
|
6
6
|
"types": "build/exports.d.ts",
|
|
@@ -76,15 +76,15 @@
|
|
|
76
76
|
"typebox": "^1.0.30"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@eslint/js": "^
|
|
80
|
-
"astro": "^6.3.1",
|
|
81
|
-
"eslint": "^9.17.0",
|
|
82
|
-
"globals": "^15.0.0",
|
|
83
|
-
"hono": "^4.7.4",
|
|
84
|
-
"typescript": "^5.7.0",
|
|
85
|
-
"typescript-eslint": "^8.53.0",
|
|
79
|
+
"@eslint/js": "^10.0.1",
|
|
86
80
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
87
81
|
"@typescript-eslint/parser": "^8.53.0",
|
|
82
|
+
"astro": "^7.0.2",
|
|
83
|
+
"eslint": "^10.5.0",
|
|
84
|
+
"globals": "^17.7.0",
|
|
85
|
+
"hono": "^4.7.4",
|
|
86
|
+
"typescript": "^6.0.3",
|
|
87
|
+
"typescript-eslint": "^8.53.0",
|
|
88
88
|
"vitest": "^4.0.18"
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -593,3 +593,31 @@ describe('--shared-models-module and --strict-shared-models', () => {
|
|
|
593
593
|
expect(parsed.strictSharedModels).toBe(true)
|
|
594
594
|
})
|
|
595
595
|
})
|
|
596
|
+
|
|
597
|
+
describe('--check / --check-mode', () => {
|
|
598
|
+
it('defaults check to false', () => {
|
|
599
|
+
expect(parseArgs(['--out', 'gen', '--file', 'e.json']).check).toBe(false)
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('parses --check as a boolean', () => {
|
|
603
|
+
expect(parseArgs(['--out', 'gen', '--file', 'e.json', '--check']).check).toBe(true)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('--check-mode sets the mode and implies --check', () => {
|
|
607
|
+
const parsed = parseArgs(['--out', 'gen', '--file', 'e.json', '--check-mode', 'tsc'])
|
|
608
|
+
expect(parsed.check).toBe(true)
|
|
609
|
+
expect(parsed.checkMode).toBe('tsc')
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('rejects an invalid --check-mode value', () => {
|
|
613
|
+
expect(() => parseArgs(['--out', 'gen', '--file', 'e.json', '--check-mode', 'nope'])).toThrow(
|
|
614
|
+
/Invalid --check-mode/,
|
|
615
|
+
)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('seeds check/checkMode from config when the flags are absent', () => {
|
|
619
|
+
const parsed = parseArgs(['--out', 'gen', '--file', 'e.json'], { check: true, checkMode: 'tsc' })
|
|
620
|
+
expect(parsed.check).toBe(true)
|
|
621
|
+
expect(parsed.checkMode).toBe('tsc')
|
|
622
|
+
})
|
|
623
|
+
})
|
package/src/codegen/bin/cli.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface CodegenConfig {
|
|
|
32
32
|
sharedTypesImport?: SharedTypesImportMap
|
|
33
33
|
sharedModelsModule?: string
|
|
34
34
|
strictSharedModels?: boolean
|
|
35
|
+
check?: boolean
|
|
36
|
+
checkMode?: 'scan' | 'tsc'
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface ParsedArgs {
|
|
@@ -55,6 +57,8 @@ export interface ParsedArgs {
|
|
|
55
57
|
sharedTypesImport?: SharedTypesImportMap
|
|
56
58
|
sharedModelsModule?: string
|
|
57
59
|
strictSharedModels: boolean
|
|
60
|
+
check: boolean
|
|
61
|
+
checkMode?: 'scan' | 'tsc'
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
// ---------------------------------------------------------------------------
|
|
@@ -75,7 +79,7 @@ export async function loadConfigFile(configPath?: string): Promise<CodegenConfig
|
|
|
75
79
|
} catch (err) {
|
|
76
80
|
if (configPath !== undefined) {
|
|
77
81
|
// Explicit path — always throw
|
|
78
|
-
throw new Error(`[ts-procedures-codegen] Failed to load config from ${filePath}: ${err instanceof Error ? err.message : err}
|
|
82
|
+
throw new Error(`[ts-procedures-codegen] Failed to load config from ${filePath}: ${err instanceof Error ? err.message : err}`, { cause: err })
|
|
79
83
|
}
|
|
80
84
|
// Default path — silently ignore if not found
|
|
81
85
|
return undefined
|
|
@@ -172,6 +176,8 @@ interface ParseState {
|
|
|
172
176
|
shareModels: boolean
|
|
173
177
|
sharedModelsModule: string | undefined
|
|
174
178
|
strictSharedModels: boolean
|
|
179
|
+
check: boolean
|
|
180
|
+
checkMode: 'scan' | 'tsc' | undefined
|
|
175
181
|
configPath: string | undefined
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -249,6 +255,9 @@ const FLAG_HANDLERS: Record<string, FlagHandler> = {
|
|
|
249
255
|
'--no-share-models': (s) => { s.shareModels = false },
|
|
250
256
|
'--shared-models-module': (s, v) => { s.sharedModelsModule = v },
|
|
251
257
|
'--strict-shared-models': (s) => { s.strictSharedModels = true },
|
|
258
|
+
'--check': (s) => { s.check = true },
|
|
259
|
+
// Selecting a mode implies --check, so `--check-mode tsc` alone enables it.
|
|
260
|
+
'--check-mode': enumValueFlag('--check-mode', ['scan', 'tsc'], (s, v) => { s.checkMode = v; s.check = true }),
|
|
252
261
|
// configPath is consumed by the caller (main) before parseArgs is called with the loaded config.
|
|
253
262
|
// When called from main, config is already loaded. When called directly (tests), configPath is ignored.
|
|
254
263
|
'--config': (s, v) => { s.configPath = v },
|
|
@@ -290,6 +299,8 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
290
299
|
shareModels: config?.shareModels ?? true,
|
|
291
300
|
sharedModelsModule: config?.sharedModelsModule,
|
|
292
301
|
strictSharedModels: config?.strictSharedModels ?? false,
|
|
302
|
+
check: config?.check ?? false,
|
|
303
|
+
checkMode: config?.checkMode,
|
|
293
304
|
configPath: undefined,
|
|
294
305
|
}
|
|
295
306
|
const sharedTypesImport = config?.sharedTypesImport
|
|
@@ -385,6 +396,8 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
385
396
|
...(sharedTypesImport !== undefined ? { sharedTypesImport } : {}),
|
|
386
397
|
...(state.sharedModelsModule !== undefined ? { sharedModelsModule: state.sharedModelsModule } : {}),
|
|
387
398
|
strictSharedModels: state.strictSharedModels,
|
|
399
|
+
check: state.check,
|
|
400
|
+
...(state.checkMode !== undefined ? { checkMode: state.checkMode } : {}),
|
|
388
401
|
}
|
|
389
402
|
}
|
|
390
403
|
|
|
@@ -619,6 +632,19 @@ async function main(): Promise<void> {
|
|
|
619
632
|
console.log(`[ts-procedures-codegen] Generated ${result.length} files → ${parsed.outDir}`)
|
|
620
633
|
printPostRunHints(parsed)
|
|
621
634
|
}
|
|
635
|
+
|
|
636
|
+
if (parsed.check) {
|
|
637
|
+
const { checkGeneratedFiles, formatCheckResult } = await import('../check-output.js')
|
|
638
|
+
const check = await checkGeneratedFiles(result, {
|
|
639
|
+
mode: parsed.checkMode ?? 'scan',
|
|
640
|
+
logger: (message: string) => { console.warn(message) },
|
|
641
|
+
})
|
|
642
|
+
if (!check.ok) {
|
|
643
|
+
console.error(formatCheckResult(check))
|
|
644
|
+
throw new Error(`Generated output failed the ${check.mode} self-check. The client may not compile.`)
|
|
645
|
+
}
|
|
646
|
+
console.log(`[ts-procedures-codegen] Self-check (${check.mode}) passed — no problems found.`)
|
|
647
|
+
}
|
|
622
648
|
}
|
|
623
649
|
}
|
|
624
650
|
|
|
@@ -49,6 +49,9 @@ export const FLAG_SPECS: readonly FlagSpec[] = [
|
|
|
49
49
|
{ name: '--swift-serializer', arg: '<codable|none>', takesValue: true, description: 'Swift Codable conformance', group: 'Targets', default: 'codable' },
|
|
50
50
|
{ name: '--swift-access-level', arg: '<public|internal>', takesValue: true, description: 'Swift access level', group: 'Targets', default: 'public' },
|
|
51
51
|
{ name: '--unsupported-unions', arg: '<throw|fallback>', takesValue: true, description: 'Behaviour for untagged oneOf schemas', group: 'Targets', default: 'throw' },
|
|
52
|
+
// Misc
|
|
53
|
+
{ name: '--check', takesValue: false, description: 'Self-validate the generated output; exit non-zero on problems', group: 'Misc' },
|
|
54
|
+
{ name: '--check-mode', arg: '<scan|tsc>', takesValue: true, description: 'How --check validates: scan (dependency-free) or tsc (type-check; needs typescript, falls back to scan). Implies --check', group: 'Misc', default: 'scan' },
|
|
52
55
|
]
|
|
53
56
|
|
|
54
57
|
export const KNOWN_FLAGS: readonly string[] = FLAG_SPECS.map((s) => s.name)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { scanGeneratedFiles, checkGeneratedFiles, formatCheckResult } from './check-output.js'
|
|
3
|
+
import { runPipeline } from './pipeline.js'
|
|
4
|
+
import { generateClient } from './index.js'
|
|
5
|
+
import { makeRpcRoute, makeEnvelope } from './__fixtures__/make-envelope.js'
|
|
6
|
+
import type { GeneratedFile } from './targets/_shared/write-files.js'
|
|
7
|
+
|
|
8
|
+
const file = (path: string, code: string): GeneratedFile => ({ path, code })
|
|
9
|
+
|
|
10
|
+
const validEnvelope = makeEnvelope([
|
|
11
|
+
makeRpcRoute({
|
|
12
|
+
name: 'GetUser',
|
|
13
|
+
scope: 'users',
|
|
14
|
+
jsonSchema: {
|
|
15
|
+
body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
16
|
+
response: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
describe('check-output scan', () => {
|
|
22
|
+
it('does not flag same-named types in different namespaces (no false positive)', () => {
|
|
23
|
+
const code = [
|
|
24
|
+
'export namespace Tasks {',
|
|
25
|
+
' export namespace A {',
|
|
26
|
+
' export type Ref = { a: string }',
|
|
27
|
+
' }',
|
|
28
|
+
' export namespace B {',
|
|
29
|
+
' export type Ref = { b: number }',
|
|
30
|
+
' }',
|
|
31
|
+
'}',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n')
|
|
34
|
+
expect(scanGeneratedFiles([file('/out/tasks.ts', code)])).toEqual([])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('flags a dangling bare type-alias reference (the namespace-mode Root bug)', () => {
|
|
38
|
+
const code = [
|
|
39
|
+
'export namespace Tasks {',
|
|
40
|
+
' export namespace SetLink {',
|
|
41
|
+
' export type Params = Root',
|
|
42
|
+
' }',
|
|
43
|
+
'}',
|
|
44
|
+
'',
|
|
45
|
+
].join('\n')
|
|
46
|
+
const issues = scanGeneratedFiles([file('/out/tasks.ts', code)])
|
|
47
|
+
expect(issues).toHaveLength(1)
|
|
48
|
+
expect(issues[0]!.message).toContain('dangling reference')
|
|
49
|
+
expect(issues[0]!.message).toContain('Root')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not flag a bare alias that resolves to an import', () => {
|
|
53
|
+
const code = ["import type { Message } from './_models'", 'export type Params = Message', ''].join('\n')
|
|
54
|
+
expect(scanGeneratedFiles([file('/out/a.ts', code)])).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not flag a bare alias that resolves to a built-in', () => {
|
|
58
|
+
expect(scanGeneratedFiles([file('/out/a.ts', 'export type Params = string\n')])).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('does not flag a bare alias that resolves to a type declared in the same file', () => {
|
|
62
|
+
const code = ['export type Inner = { a: string }', 'export type Params = Inner', ''].join('\n')
|
|
63
|
+
expect(scanGeneratedFiles([file('/out/a.ts', code)])).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('checkGeneratedFiles orchestration', () => {
|
|
68
|
+
it('returns ok for clean scan', async () => {
|
|
69
|
+
const r = await checkGeneratedFiles([file('/out/a.ts', 'export type X = string\n')], { mode: 'scan' })
|
|
70
|
+
expect(r).toEqual({ ok: true, mode: 'scan', issues: [] })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns not-ok with issues for broken scan', async () => {
|
|
74
|
+
const r = await checkGeneratedFiles([file('/out/a.ts', 'export type X = Nope\n')], { mode: 'scan' })
|
|
75
|
+
expect(r.ok).toBe(false)
|
|
76
|
+
expect(r.mode).toBe('scan')
|
|
77
|
+
expect(r.issues).toHaveLength(1)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('tsc mode type-checks in-memory and passes clean self-resolving output', async () => {
|
|
81
|
+
const r = await checkGeneratedFiles([file('/out/a.ts', 'export type X = string\nexport const y: X = "z"\n')], {
|
|
82
|
+
mode: 'tsc',
|
|
83
|
+
})
|
|
84
|
+
// typescript is a devDependency here, so tsc mode actually runs.
|
|
85
|
+
expect(r.mode).toBe('tsc')
|
|
86
|
+
expect(r.ok).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('tsc mode flags a real type error', async () => {
|
|
90
|
+
const r = await checkGeneratedFiles([file('/out/a.ts', 'export const n: number = "not a number"\n')], {
|
|
91
|
+
mode: 'tsc',
|
|
92
|
+
})
|
|
93
|
+
expect(r.mode).toBe('tsc')
|
|
94
|
+
expect(r.ok).toBe(false)
|
|
95
|
+
expect(r.issues.length).toBeGreaterThan(0)
|
|
96
|
+
expect(r.issues[0]!.message).toMatch(/^TS\d+:/)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// End-to-end: a real self-contained client (vendored ./_client + ./_types)
|
|
100
|
+
// must type-check via the in-memory host, proving relative imports resolve
|
|
101
|
+
// within the emitted set without anything written to disk.
|
|
102
|
+
it('tsc mode passes real self-contained generated output (dry-run, in-memory)', async () => {
|
|
103
|
+
const envelope = makeEnvelope([
|
|
104
|
+
makeRpcRoute({
|
|
105
|
+
name: 'GetUser',
|
|
106
|
+
scope: 'users',
|
|
107
|
+
jsonSchema: {
|
|
108
|
+
body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
109
|
+
response: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
])
|
|
113
|
+
const files = await runPipeline({
|
|
114
|
+
envelope,
|
|
115
|
+
outDir: '/unused-out',
|
|
116
|
+
dryRun: true,
|
|
117
|
+
selfContained: true,
|
|
118
|
+
namespaceTypes: true,
|
|
119
|
+
})
|
|
120
|
+
const r = await checkGeneratedFiles(files, { mode: 'tsc' })
|
|
121
|
+
expect(r.mode).toBe('tsc')
|
|
122
|
+
expect(r.issues).toEqual([])
|
|
123
|
+
expect(r.ok).toBe(true)
|
|
124
|
+
|
|
125
|
+
// The dependency-free scan must also pass the same real output.
|
|
126
|
+
expect(scanGeneratedFiles(files)).toEqual([])
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('formatCheckResult', () => {
|
|
131
|
+
it('renders a header and one line per issue', () => {
|
|
132
|
+
const text = formatCheckResult({
|
|
133
|
+
ok: false,
|
|
134
|
+
mode: 'scan',
|
|
135
|
+
issues: [{ file: '/out/tasks.ts', line: 4, message: 'Type alias "Params" references "Root" …' }],
|
|
136
|
+
})
|
|
137
|
+
expect(text).toContain('Self-check (scan) found 1 problem(s)')
|
|
138
|
+
expect(text).toContain('/out/tasks.ts:4 — Type alias "Params" references "Root" …')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('generateClient({ validate })', () => {
|
|
143
|
+
it('returns files when valid output passes the scan', async () => {
|
|
144
|
+
const files = await generateClient({
|
|
145
|
+
envelope: validEnvelope,
|
|
146
|
+
outDir: '/unused-out',
|
|
147
|
+
dryRun: true,
|
|
148
|
+
selfContained: true,
|
|
149
|
+
namespaceTypes: true,
|
|
150
|
+
validate: true,
|
|
151
|
+
})
|
|
152
|
+
expect(files.length).toBeGreaterThan(0)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('returns files when valid output passes the tsc check', async () => {
|
|
156
|
+
const files = await generateClient({
|
|
157
|
+
envelope: validEnvelope,
|
|
158
|
+
outDir: '/unused-out',
|
|
159
|
+
dryRun: true,
|
|
160
|
+
selfContained: true,
|
|
161
|
+
namespaceTypes: true,
|
|
162
|
+
validate: { mode: 'tsc' },
|
|
163
|
+
})
|
|
164
|
+
expect(files.length).toBeGreaterThan(0)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('does not validate when the option is omitted', async () => {
|
|
168
|
+
const files = await generateClient({
|
|
169
|
+
envelope: validEnvelope,
|
|
170
|
+
outDir: '/unused-out',
|
|
171
|
+
dryRun: true,
|
|
172
|
+
selfContained: true,
|
|
173
|
+
namespaceTypes: true,
|
|
174
|
+
})
|
|
175
|
+
expect(files.length).toBeGreaterThan(0)
|
|
176
|
+
})
|
|
177
|
+
})
|