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.
Files changed (189) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +29 -22
  3. package/agent_config/copilot/copilot-instructions.md +2 -0
  4. package/agent_config/cursor/cursorrules +2 -0
  5. package/build/codegen/bin/cli.d.ts +4 -0
  6. package/build/codegen/bin/cli.js +19 -0
  7. package/build/codegen/bin/cli.js.map +1 -1
  8. package/build/codegen/bin/cli.test.js +21 -0
  9. package/build/codegen/bin/cli.test.js.map +1 -1
  10. package/build/codegen/bin/flag-specs.js +3 -0
  11. package/build/codegen/bin/flag-specs.js.map +1 -1
  12. package/build/codegen/check-output.d.ts +28 -0
  13. package/build/codegen/check-output.js +216 -0
  14. package/build/codegen/check-output.js.map +1 -0
  15. package/build/codegen/check-output.test.d.ts +1 -0
  16. package/build/codegen/check-output.test.js +160 -0
  17. package/build/codegen/check-output.test.js.map +1 -0
  18. package/build/codegen/emit/api-route.js +1 -1
  19. package/build/codegen/emit/api-route.js.map +1 -1
  20. package/build/codegen/emit/context.d.ts +9 -0
  21. package/build/codegen/emit/http-stream-route.js +1 -1
  22. package/build/codegen/emit/http-stream-route.js.map +1 -1
  23. package/build/codegen/emit/rpc-route.js +1 -1
  24. package/build/codegen/emit/rpc-route.js.map +1 -1
  25. package/build/codegen/emit/scope-file.js +14 -0
  26. package/build/codegen/emit/scope-file.js.map +1 -1
  27. package/build/codegen/emit/stream-route.js +1 -1
  28. package/build/codegen/emit/stream-route.js.map +1 -1
  29. package/build/codegen/emit-scope.test.js +55 -0
  30. package/build/codegen/emit-scope.test.js.map +1 -1
  31. package/build/codegen/emit-types.js +19 -0
  32. package/build/codegen/emit-types.js.map +1 -1
  33. package/build/codegen/emit-types.test.js +34 -0
  34. package/build/codegen/emit-types.test.js.map +1 -1
  35. package/build/codegen/index.d.ts +14 -0
  36. package/build/codegen/index.js +11 -1
  37. package/build/codegen/index.js.map +1 -1
  38. package/build/codegen/targets/ts/name-collision.test.d.ts +1 -0
  39. package/build/codegen/targets/ts/name-collision.test.js +30 -0
  40. package/build/codegen/targets/ts/name-collision.test.js.map +1 -0
  41. package/build/codegen/targets/ts/run.js +13 -0
  42. package/build/codegen/targets/ts/run.js.map +1 -1
  43. package/build/core/create-http-stream.d.ts +3 -14
  44. package/build/core/create-http-stream.js.map +1 -1
  45. package/build/core/create-http.d.ts +2 -9
  46. package/build/core/create-http.js.map +1 -1
  47. package/build/core/create-http.test-d.d.ts +1 -0
  48. package/build/core/create-http.test-d.js +104 -0
  49. package/build/core/create-http.test-d.js.map +1 -0
  50. package/build/core/procedures.d.ts +6 -16
  51. package/build/core/types.d.ts +49 -4
  52. package/build/errors.d.ts +33 -0
  53. package/build/errors.js +91 -0
  54. package/build/errors.js.map +1 -0
  55. package/build/errors.test.d.ts +1 -0
  56. package/build/errors.test.js +122 -0
  57. package/build/errors.test.js.map +1 -0
  58. package/build/exports.d.ts +1 -1
  59. package/build/exports.js.map +1 -1
  60. package/build/implementations/http/doc-registry.d.ts +28 -0
  61. package/build/implementations/http/doc-registry.js +96 -0
  62. package/build/implementations/http/doc-registry.js.map +1 -0
  63. package/build/implementations/http/doc-registry.test.d.ts +1 -0
  64. package/build/implementations/http/doc-registry.test.js +516 -0
  65. package/build/implementations/http/doc-registry.test.js.map +1 -0
  66. package/build/implementations/http/error-taxonomy.d.ts +249 -0
  67. package/build/implementations/http/error-taxonomy.js +252 -0
  68. package/build/implementations/http/error-taxonomy.js.map +1 -0
  69. package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
  70. package/build/implementations/http/error-taxonomy.test.js +399 -0
  71. package/build/implementations/http/error-taxonomy.test.js.map +1 -0
  72. package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
  73. package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
  74. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
  75. package/build/implementations/http/express-rpc/index.d.ts +125 -0
  76. package/build/implementations/http/express-rpc/index.js +216 -0
  77. package/build/implementations/http/express-rpc/index.js.map +1 -0
  78. package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
  79. package/build/implementations/http/express-rpc/index.test.js +684 -0
  80. package/build/implementations/http/express-rpc/index.test.js.map +1 -0
  81. package/build/implementations/http/express-rpc/types.d.ts +11 -0
  82. package/build/implementations/http/express-rpc/types.js +2 -0
  83. package/build/implementations/http/express-rpc/types.js.map +1 -0
  84. package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
  85. package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
  86. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
  87. package/build/implementations/http/hono-api/index.d.ts +151 -0
  88. package/build/implementations/http/hono-api/index.js +344 -0
  89. package/build/implementations/http/hono-api/index.js.map +1 -0
  90. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  91. package/build/implementations/http/hono-api/index.test.js +992 -0
  92. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  93. package/build/implementations/http/hono-api/types.d.ts +13 -0
  94. package/build/implementations/http/hono-api/types.js +2 -0
  95. package/build/implementations/http/hono-api/types.js.map +1 -0
  96. package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
  97. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
  98. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
  99. package/build/implementations/http/hono-rpc/index.d.ts +130 -0
  100. package/build/implementations/http/hono-rpc/index.js +209 -0
  101. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  102. package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
  103. package/build/implementations/http/hono-rpc/index.test.js +828 -0
  104. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  105. package/build/implementations/http/hono-rpc/types.d.ts +11 -0
  106. package/build/implementations/http/hono-rpc/types.js +2 -0
  107. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  108. package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
  109. package/build/implementations/http/hono-stream/error-taxonomy.test.js +159 -0
  110. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
  111. package/build/implementations/http/hono-stream/index.d.ts +171 -0
  112. package/build/implementations/http/hono-stream/index.js +415 -0
  113. package/build/implementations/http/hono-stream/index.js.map +1 -0
  114. package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
  115. package/build/implementations/http/hono-stream/index.test.js +1383 -0
  116. package/build/implementations/http/hono-stream/index.test.js.map +1 -0
  117. package/build/implementations/http/hono-stream/types.d.ts +15 -0
  118. package/build/implementations/http/hono-stream/types.js +2 -0
  119. package/build/implementations/http/hono-stream/types.js.map +1 -0
  120. package/build/implementations/http/on-request-error.test.d.ts +1 -0
  121. package/build/implementations/http/on-request-error.test.js +173 -0
  122. package/build/implementations/http/on-request-error.test.js.map +1 -0
  123. package/build/implementations/http/route-errors.test.d.ts +1 -0
  124. package/build/implementations/http/route-errors.test.js +139 -0
  125. package/build/implementations/http/route-errors.test.js.map +1 -0
  126. package/build/implementations/types.d.ts +192 -0
  127. package/build/implementations/types.js +2 -0
  128. package/build/implementations/types.js.map +1 -0
  129. package/build/index.d.ts +165 -0
  130. package/build/index.js +258 -0
  131. package/build/index.js.map +1 -0
  132. package/build/index.test.d.ts +1 -0
  133. package/build/index.test.js +920 -0
  134. package/build/index.test.js.map +1 -0
  135. package/build/schema/extract-json-schema.d.ts +2 -0
  136. package/build/schema/extract-json-schema.js +12 -0
  137. package/build/schema/extract-json-schema.js.map +1 -0
  138. package/build/schema/extract-json-schema.test.d.ts +1 -0
  139. package/build/schema/extract-json-schema.test.js +23 -0
  140. package/build/schema/extract-json-schema.test.js.map +1 -0
  141. package/build/schema/parser.d.ts +28 -0
  142. package/build/schema/parser.js +170 -0
  143. package/build/schema/parser.js.map +1 -0
  144. package/build/schema/parser.test.d.ts +1 -0
  145. package/build/schema/parser.test.js +120 -0
  146. package/build/schema/parser.test.js.map +1 -0
  147. package/build/schema/resolve-schema-lib.d.ts +12 -0
  148. package/build/schema/resolve-schema-lib.js +11 -0
  149. package/build/schema/resolve-schema-lib.js.map +1 -0
  150. package/build/schema/resolve-schema-lib.test.d.ts +1 -0
  151. package/build/schema/resolve-schema-lib.test.js +17 -0
  152. package/build/schema/resolve-schema-lib.test.js.map +1 -0
  153. package/build/schema/types.d.ts +8 -0
  154. package/build/schema/types.js +2 -0
  155. package/build/schema/types.js.map +1 -0
  156. package/build/server/types.d.ts +6 -12
  157. package/build/stack-utils.d.ts +25 -0
  158. package/build/stack-utils.js +95 -0
  159. package/build/stack-utils.js.map +1 -0
  160. package/build/stack-utils.test.d.ts +1 -0
  161. package/build/stack-utils.test.js +80 -0
  162. package/build/stack-utils.test.js.map +1 -0
  163. package/docs/client-and-codegen.md +17 -0
  164. package/docs/core.md +1 -0
  165. package/docs/migration-v8-to-v9.md +12 -4
  166. package/package.json +1 -1
  167. package/src/codegen/bin/cli.test.ts +28 -0
  168. package/src/codegen/bin/cli.ts +26 -0
  169. package/src/codegen/bin/flag-specs.ts +3 -0
  170. package/src/codegen/check-output.test.ts +177 -0
  171. package/src/codegen/check-output.ts +262 -0
  172. package/src/codegen/emit/api-route.ts +1 -1
  173. package/src/codegen/emit/context.ts +9 -0
  174. package/src/codegen/emit/http-stream-route.ts +1 -1
  175. package/src/codegen/emit/rpc-route.ts +1 -1
  176. package/src/codegen/emit/scope-file.ts +17 -0
  177. package/src/codegen/emit/stream-route.ts +1 -1
  178. package/src/codegen/emit-scope.test.ts +60 -0
  179. package/src/codegen/emit-types.test.ts +36 -0
  180. package/src/codegen/emit-types.ts +20 -0
  181. package/src/codegen/index.ts +23 -1
  182. package/src/codegen/targets/ts/name-collision.test.ts +34 -0
  183. package/src/codegen/targets/ts/run.ts +16 -0
  184. package/src/core/create-http-stream.ts +11 -12
  185. package/src/core/create-http.test-d.ts +154 -0
  186. package/src/core/create-http.ts +11 -7
  187. package/src/core/types.ts +63 -4
  188. package/src/exports.ts +5 -0
  189. 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":"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"}
@@ -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. First such fix: routes that declare `errors` but no
12
- req/res schema now emit a namespace housing their `Errors` union instead of a
13
- dangling type reference (the old output never typechecked, so no working
14
- consumer's bytes change).
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.1.0",
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
+ })
@@ -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
+ })