ts-procedures 9.1.0 → 9.3.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 +55 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +29 -22
- 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 +19 -0
- 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 +1 -1
- 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 +14 -0
- 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-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/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/core/create-http-stream.d.ts +3 -14
- package/build/core/create-http-stream.js.map +1 -1
- package/build/core/create-http.d.ts +2 -9
- package/build/core/create-http.js.map +1 -1
- package/build/core/create-http.test-d.d.ts +1 -0
- package/build/core/create-http.test-d.js +104 -0
- package/build/core/create-http.test-d.js.map +1 -0
- package/build/core/procedures.d.ts +6 -16
- package/build/core/types.d.ts +49 -4
- package/build/errors.d.ts +33 -0
- package/build/errors.js +91 -0
- package/build/errors.js.map +1 -0
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +122 -0
- package/build/errors.test.js.map +1 -0
- package/build/exports.d.ts +1 -1
- package/build/exports.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +28 -0
- package/build/implementations/http/doc-registry.js +96 -0
- package/build/implementations/http/doc-registry.js.map +1 -0
- package/build/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/implementations/http/doc-registry.test.js +516 -0
- package/build/implementations/http/doc-registry.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +249 -0
- package/build/implementations/http/error-taxonomy.js +252 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +125 -0
- package/build/implementations/http/express-rpc/index.js +216 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -0
- package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/index.test.js +684 -0
- package/build/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/implementations/http/express-rpc/types.js +2 -0
- package/build/implementations/http/express-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +151 -0
- package/build/implementations/http/hono-api/index.js +344 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +992 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +130 -0
- package/build/implementations/http/hono-rpc/index.js +209 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +828 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +159 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +171 -0
- package/build/implementations/http/hono-stream/index.js +415 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +1383 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +139 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +192 -0
- package/build/implementations/types.js +2 -0
- package/build/implementations/types.js.map +1 -0
- package/build/index.d.ts +165 -0
- package/build/index.js +258 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +1 -0
- package/build/index.test.js +920 -0
- package/build/index.test.js.map +1 -0
- package/build/schema/extract-json-schema.d.ts +2 -0
- package/build/schema/extract-json-schema.js +12 -0
- package/build/schema/extract-json-schema.js.map +1 -0
- package/build/schema/extract-json-schema.test.d.ts +1 -0
- package/build/schema/extract-json-schema.test.js +23 -0
- package/build/schema/extract-json-schema.test.js.map +1 -0
- package/build/schema/parser.d.ts +28 -0
- package/build/schema/parser.js +170 -0
- package/build/schema/parser.js.map +1 -0
- package/build/schema/parser.test.d.ts +1 -0
- package/build/schema/parser.test.js +120 -0
- package/build/schema/parser.test.js.map +1 -0
- package/build/schema/resolve-schema-lib.d.ts +12 -0
- package/build/schema/resolve-schema-lib.js +11 -0
- package/build/schema/resolve-schema-lib.js.map +1 -0
- package/build/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/schema/resolve-schema-lib.test.js +17 -0
- package/build/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/schema/types.d.ts +8 -0
- package/build/schema/types.js +2 -0
- package/build/schema/types.js.map +1 -0
- package/build/server/types.d.ts +6 -12
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/docs/client-and-codegen.md +17 -0
- package/docs/core.md +1 -0
- package/docs/migration-v8-to-v9.md +12 -4
- package/package.json +1 -1
- package/src/codegen/bin/cli.test.ts +28 -0
- package/src/codegen/bin/cli.ts +26 -0
- 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 +1 -1
- 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 +17 -0
- package/src/codegen/emit/stream-route.ts +1 -1
- 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/targets/ts/name-collision.test.ts +34 -0
- package/src/codegen/targets/ts/run.ts +16 -0
- package/src/core/create-http-stream.ts +11 -12
- package/src/core/create-http.test-d.ts +154 -0
- package/src/core/create-http.ts +11 -7
- package/src/core/types.ts +63 -4
- package/src/exports.ts +5 -0
- package/src/server/types.ts +8 -12
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal ts-procedures files that should be skipped when finding user code.
|
|
3
|
+
* Only skip the core library files, not test files or user code.
|
|
4
|
+
*/
|
|
5
|
+
const INTERNAL_FILES = [
|
|
6
|
+
'/index.ts',
|
|
7
|
+
'/index.js',
|
|
8
|
+
'/errors.ts',
|
|
9
|
+
'/errors.js',
|
|
10
|
+
'/stack-utils.ts',
|
|
11
|
+
'/stack-utils.js',
|
|
12
|
+
'/compute-schema.ts',
|
|
13
|
+
'/compute-schema.js',
|
|
14
|
+
'/parser.ts',
|
|
15
|
+
'/parser.js',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Captures the stack trace at the call site and extracts the definition location.
|
|
19
|
+
* Finds the first stack frame outside of ts-procedures internal files.
|
|
20
|
+
*/
|
|
21
|
+
export function captureDefinitionInfo() {
|
|
22
|
+
const err = new Error();
|
|
23
|
+
const stack = err.stack;
|
|
24
|
+
if (!stack) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const lines = stack.split('\n');
|
|
28
|
+
// Find the first frame that's not from ts-procedures internals
|
|
29
|
+
// Skip the first line (Error message) and frames from this module
|
|
30
|
+
let userFrame;
|
|
31
|
+
for (let i = 1; i < lines.length; i++) {
|
|
32
|
+
const rawLine = lines[i];
|
|
33
|
+
if (!rawLine)
|
|
34
|
+
continue;
|
|
35
|
+
const line = rawLine.trim();
|
|
36
|
+
// Skip empty or invalid frames
|
|
37
|
+
if (!line.startsWith('at ')) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Skip frames from ts-procedures internal source files
|
|
41
|
+
const isInternalFile = INTERNAL_FILES.some(file => line.includes(file));
|
|
42
|
+
if (isInternalFile) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Skip frames from ts-procedures in node_modules (when used as a dependency)
|
|
46
|
+
if (line.includes('/node_modules/ts-procedures/') || line.includes('\\node_modules\\ts-procedures\\')) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Skip internal node frames
|
|
50
|
+
if (line.includes('node:') || line.startsWith('at Module.') || line.startsWith('at Object.<anonymous> (node:')) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
userFrame = line;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (!userFrame) {
|
|
57
|
+
return { definitionStack: stack };
|
|
58
|
+
}
|
|
59
|
+
const definedAt = parseStackFrame(userFrame);
|
|
60
|
+
return {
|
|
61
|
+
definedAt,
|
|
62
|
+
definitionStack: stack,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parses a V8 stack frame line to extract file, line, and column info.
|
|
67
|
+
* Handles formats like:
|
|
68
|
+
* - "at Object.<anonymous> (/path/to/file.ts:10:5)"
|
|
69
|
+
* - "at functionName (/path/to/file.ts:10:5)"
|
|
70
|
+
* - "at /path/to/file.ts:10:5"
|
|
71
|
+
*/
|
|
72
|
+
function parseStackFrame(frame) {
|
|
73
|
+
// Match patterns like "(path:line:column)" or just "path:line:column"
|
|
74
|
+
const match = frame.match(/\(([^)]+):(\d+):(\d+)\)$/) || frame.match(/at ([^:]+):(\d+):(\d+)$/);
|
|
75
|
+
if (match && match[1] && match[2] && match[3]) {
|
|
76
|
+
return {
|
|
77
|
+
file: match[1],
|
|
78
|
+
line: parseInt(match[2], 10),
|
|
79
|
+
column: parseInt(match[3], 10),
|
|
80
|
+
raw: frame,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Formats definition info for appending to error stacks.
|
|
87
|
+
*/
|
|
88
|
+
export function formatDefinitionInfo(info, procedureName) {
|
|
89
|
+
if (!info.definedAt) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const { file, line, column } = info.definedAt;
|
|
93
|
+
return `\n--- Procedure "${procedureName}" defined at ---\n at ${file}:${line}:${column}`;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=stack-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-utils.js","sourceRoot":"","sources":["../src/stack-utils.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AACH,MAAM,cAAc,GAAG;IACrB,WAAW;IACX,WAAW;IACX,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,iBAAiB;IACjB,oBAAoB;IACpB,oBAAoB;IACpB,YAAY;IACZ,YAAY;CACb,CAAA;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAA;IACvB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;IAEvB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAE/B,+DAA+D;IAC/D,kEAAkE;IAClE,IAAI,SAA6B,CAAA;IAEjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QACxB,IAAI,CAAC,OAAO;YAAE,SAAQ;QACtB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;QAE3B,+BAA+B;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,SAAQ;QACV,CAAC;QAED,uDAAuD;QACvD,MAAM,cAAc,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;QACvE,IAAI,cAAc,EAAE,CAAC;YACnB,SAAQ;QACV,CAAC;QAED,6EAA6E;QAC7E,IAAI,IAAI,CAAC,QAAQ,CAAC,8BAA8B,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iCAAiC,CAAC,EAAE,CAAC;YACtG,SAAQ;QACV,CAAC;QAED,4BAA4B;QAC5B,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,8BAA8B,CAAC,EAAE,CAAC;YAC/G,SAAQ;QACV,CAAC;QAED,SAAS,GAAG,IAAI,CAAA;QAChB,MAAK;IACP,CAAC;IAED,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,CAAA;IACnC,CAAC;IAED,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,CAAA;IAE5C,OAAO;QACL,SAAS;QACT,eAAe,EAAE,KAAK;KACvB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,sEAAsE;IACtE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAA;IAE/F,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACd,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC5B,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC9B,GAAG,EAAE,KAAK;SACX,CAAA;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAoB,EAAE,aAAqB;IAC9E,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAA;IAC7C,OAAO,oBAAoB,aAAa,4BAA4B,IAAI,IAAI,IAAI,IAAI,MAAM,EAAE,CAAA;AAC9F,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { captureDefinitionInfo, formatDefinitionInfo } from './stack-utils.js';
|
|
3
|
+
describe('Stack Utils', () => {
|
|
4
|
+
describe('captureDefinitionInfo', () => {
|
|
5
|
+
test('returns definition info with definedAt', () => {
|
|
6
|
+
const info = captureDefinitionInfo();
|
|
7
|
+
// Should capture the call site in this test file
|
|
8
|
+
expect(info).toBeDefined();
|
|
9
|
+
expect(info.definitionStack).toBeDefined();
|
|
10
|
+
expect(typeof info.definitionStack).toBe('string');
|
|
11
|
+
});
|
|
12
|
+
test('definedAt contains file, line, column when available', () => {
|
|
13
|
+
const info = captureDefinitionInfo();
|
|
14
|
+
// The definedAt should be present since we're calling from user code (test file)
|
|
15
|
+
if (info.definedAt) {
|
|
16
|
+
expect(info.definedAt.file).toBeDefined();
|
|
17
|
+
expect(typeof info.definedAt.file).toBe('string');
|
|
18
|
+
expect(info.definedAt.line).toBeDefined();
|
|
19
|
+
expect(typeof info.definedAt.line).toBe('number');
|
|
20
|
+
expect(info.definedAt.line).toBeGreaterThan(0);
|
|
21
|
+
expect(info.definedAt.column).toBeDefined();
|
|
22
|
+
expect(typeof info.definedAt.column).toBe('number');
|
|
23
|
+
expect(info.definedAt.column).toBeGreaterThan(0);
|
|
24
|
+
expect(info.definedAt.raw).toBeDefined();
|
|
25
|
+
expect(typeof info.definedAt.raw).toBe('string');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
test('definitionStack contains Error stack trace', () => {
|
|
29
|
+
const info = captureDefinitionInfo();
|
|
30
|
+
expect(info.definitionStack).toContain('Error');
|
|
31
|
+
expect(info.definitionStack).toContain('at ');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('formatDefinitionInfo', () => {
|
|
35
|
+
test('returns undefined when definedAt is not present', () => {
|
|
36
|
+
const info = {};
|
|
37
|
+
const result = formatDefinitionInfo(info, 'TestProcedure');
|
|
38
|
+
expect(result).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
test('returns formatted string when definedAt is present', () => {
|
|
41
|
+
const info = {
|
|
42
|
+
definedAt: {
|
|
43
|
+
file: '/app/procedures/test.ts',
|
|
44
|
+
line: 42,
|
|
45
|
+
column: 5,
|
|
46
|
+
raw: 'at Object.<anonymous> (/app/procedures/test.ts:42:5)',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const result = formatDefinitionInfo(info, 'TestProcedure');
|
|
50
|
+
expect(result).toBeDefined();
|
|
51
|
+
expect(result).toContain('--- Procedure "TestProcedure" defined at ---');
|
|
52
|
+
expect(result).toContain('/app/procedures/test.ts:42:5');
|
|
53
|
+
});
|
|
54
|
+
test('includes procedure name in formatted output', () => {
|
|
55
|
+
const info = {
|
|
56
|
+
definedAt: {
|
|
57
|
+
file: '/path/to/file.ts',
|
|
58
|
+
line: 10,
|
|
59
|
+
column: 3,
|
|
60
|
+
raw: 'at /path/to/file.ts:10:3',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const result = formatDefinitionInfo(info, 'MyCustomProcedure');
|
|
64
|
+
expect(result).toContain('"MyCustomProcedure"');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('integration with procedure creation', () => {
|
|
68
|
+
test('captures location from calling code', () => {
|
|
69
|
+
// Helper to simulate what happens in Create()
|
|
70
|
+
function simulateCreate() {
|
|
71
|
+
return captureDefinitionInfo();
|
|
72
|
+
}
|
|
73
|
+
const info = simulateCreate();
|
|
74
|
+
// Should have captured the location of the simulateCreate() call
|
|
75
|
+
expect(info).toBeDefined();
|
|
76
|
+
expect(info.definitionStack).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=stack-utils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-utils.test.js","sourceRoot":"","sources":["../src/stack-utils.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
package/docs/core.md
CHANGED
|
@@ -74,6 +74,7 @@ const { UpdateUser } = CreateHttp(
|
|
|
74
74
|
**Rules:**
|
|
75
75
|
- `schema.req` and `schema.params` are **mutually exclusive** — `schema.params` belongs to RPC procedures (`Create`/`CreateStream`), `schema.req` to HTTP procedures. Using `schema.params` on `CreateHttp` throws `ProcedureRegistrationError`.
|
|
76
76
|
- Valid channels are `pathParams`, `query`, `body`, and `headers`; each is validated independently with per-channel error messages.
|
|
77
|
+
- The channel names are provided by `THttpReqInput` (and its alias `APIInput` from `ts-procedures/http`) so that IDEs offer autocomplete when writing `schema: { req: { ... } }`. Use `satisfies APIInput` for additional typo protection on the keys.
|
|
77
78
|
- `schema.res.body` documents the response body (drives codegen); omit `schema.res` entirely for 204-style no-content routes.
|
|
78
79
|
- Factory-level `http.pathPrefix` / `http.scope` defaults apply; per-route config wins.
|
|
79
80
|
- For HTTP streaming routes, use `CreateHttpStream` — same `schema.req` channels plus `schema.yield`.
|
|
@@ -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": "9.
|
|
3
|
+
"version": "9.3.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",
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
})
|