ts-procedures 10.2.2 → 10.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 (52) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/build/adapters/hono/index.d.ts +2 -0
  3. package/build/adapters/hono/index.js +14 -0
  4. package/build/adapters/hono/index.js.map +1 -1
  5. package/build/adapters/hono/index.test.js +30 -0
  6. package/build/adapters/hono/index.test.js.map +1 -1
  7. package/build/codegen/emit/route-shared.d.ts +1 -5
  8. package/build/codegen/emit/route-shared.js +3 -10
  9. package/build/codegen/emit/route-shared.js.map +1 -1
  10. package/build/codegen/group-routes.d.ts +2 -7
  11. package/build/codegen/group-routes.js +4 -13
  12. package/build/codegen/group-routes.js.map +1 -1
  13. package/build/codegen/naming.d.ts +2 -2
  14. package/build/codegen/naming.js +4 -7
  15. package/build/codegen/naming.js.map +1 -1
  16. package/build/core/create-http-stream.js +1 -0
  17. package/build/core/create-http-stream.js.map +1 -1
  18. package/build/core/create-http.js +1 -0
  19. package/build/core/create-http.js.map +1 -1
  20. package/build/core/create-stream.js +1 -0
  21. package/build/core/create-stream.js.map +1 -1
  22. package/build/core/create.js +1 -0
  23. package/build/core/create.js.map +1 -1
  24. package/build/core/types.d.ts +9 -0
  25. package/build/naming.d.ts +22 -0
  26. package/build/naming.js +39 -0
  27. package/build/naming.js.map +1 -0
  28. package/build/server/index.d.ts +1 -0
  29. package/build/server/index.js +1 -0
  30. package/build/server/index.js.map +1 -1
  31. package/build/server/scope-name-collision.d.ts +17 -0
  32. package/build/server/scope-name-collision.js +99 -0
  33. package/build/server/scope-name-collision.js.map +1 -0
  34. package/build/server/scope-name-collision.test.d.ts +1 -0
  35. package/build/server/scope-name-collision.test.js +134 -0
  36. package/build/server/scope-name-collision.test.js.map +1 -0
  37. package/docs/client-and-codegen.md +1 -1
  38. package/package.json +1 -1
  39. package/src/adapters/hono/index.test.ts +36 -0
  40. package/src/adapters/hono/index.ts +16 -0
  41. package/src/codegen/emit/route-shared.ts +3 -9
  42. package/src/codegen/group-routes.ts +5 -12
  43. package/src/codegen/naming.ts +4 -7
  44. package/src/core/create-http-stream.ts +1 -0
  45. package/src/core/create-http.ts +1 -0
  46. package/src/core/create-stream.ts +1 -0
  47. package/src/core/create.ts +1 -0
  48. package/src/core/types.ts +9 -0
  49. package/src/naming.ts +38 -0
  50. package/src/server/index.ts +1 -0
  51. package/src/server/scope-name-collision.test.ts +151 -0
  52. package/src/server/scope-name-collision.ts +112 -0
@@ -0,0 +1,17 @@
1
+ import type { AnyProcedureRegistration } from '../core/types.js';
2
+ /**
3
+ * The identifier a procedure generates in the client. Built from the same
4
+ * shared naming primitives the codegen emitters use, so the two cannot disagree:
5
+ * - `rpc` / `rpc-stream` → `versionedPascal(name, version)` (V{n} when version > 1)
6
+ * - `http` / `http-stream` → `toPascalCase(name)` (no version suffix)
7
+ */
8
+ export declare function generatedRouteName(procedure: AnyProcedureRegistration<any, any>): string;
9
+ /**
10
+ * Throws a {@link ProcedureRegistrationError} if any two procedures collapse to
11
+ * the same `(scope, generatedName)`. No-op when every procedure is distinct.
12
+ *
13
+ * Call this once with every procedure that will be served on a single app —
14
+ * including procedures contributed by separate factories — since cross-factory
15
+ * collisions are the case the per-factory registry can't see.
16
+ */
17
+ export declare function assertNoScopeNameCollisions(procedures: Iterable<AnyProcedureRegistration<any, any>>): void;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Server-start guard that mirrors the codegen TS emitter's per-scope name
3
+ * collision check (`src/codegen/emit/scope-file.ts`).
4
+ *
5
+ * The core registry only enforces that raw procedure NAMES are unique within a
6
+ * single factory. That misses two cases the client codegen rejects — and which
7
+ * therefore produce a server that boots happily but can never have a working
8
+ * generated client:
9
+ *
10
+ * 1. Two names in the same scope that PascalCase to the same identifier
11
+ * (e.g. `get-task` and `getTask` → `GetTask`), and
12
+ * 2. The same `(scope, name)` registered across two different factories that
13
+ * are mounted on one app (each factory has its own registry, so neither
14
+ * duplicate check fires).
15
+ *
16
+ * Both collapse to a single generated type + callable key in the emitted client
17
+ * (duplicate identifier / duplicate object key), which is exactly what
18
+ * `emitScopeFile` throws on. We surface the same failure at server start, with a
19
+ * trace pointing at every colliding definition site, so it's caught in the
20
+ * server developer's own codebase instead of downstream at codegen time.
21
+ */
22
+ import { ProcedureRegistrationError } from '../core/errors.js';
23
+ import { normalizeScope, toPascalCase, versionedPascal } from '../naming.js';
24
+ /**
25
+ * The identifier a procedure generates in the client. Built from the same
26
+ * shared naming primitives the codegen emitters use, so the two cannot disagree:
27
+ * - `rpc` / `rpc-stream` → `versionedPascal(name, version)` (V{n} when version > 1)
28
+ * - `http` / `http-stream` → `toPascalCase(name)` (no version suffix)
29
+ */
30
+ export function generatedRouteName(procedure) {
31
+ if (procedure.kind === 'rpc' || procedure.kind === 'rpc-stream') {
32
+ return versionedPascal(procedure.name, procedure.config.version);
33
+ }
34
+ return toPascalCase(procedure.name);
35
+ }
36
+ /** The scope grouping key codegen would file this procedure under. */
37
+ function scopeKeyOf(procedure) {
38
+ return normalizeScope(procedure.config.scope);
39
+ }
40
+ function definedAtString(info) {
41
+ const at = info?.definedAt;
42
+ return at ? ` (defined at ${at.file}:${at.line}:${at.column})` : '';
43
+ }
44
+ /**
45
+ * Throws a {@link ProcedureRegistrationError} if any two procedures collapse to
46
+ * the same `(scope, generatedName)`. No-op when every procedure is distinct.
47
+ *
48
+ * Call this once with every procedure that will be served on a single app —
49
+ * including procedures contributed by separate factories — since cross-factory
50
+ * collisions are the case the per-factory registry can't see.
51
+ */
52
+ export function assertNoScopeNameCollisions(procedures) {
53
+ // scopeKey → generatedName → procedures that produced it
54
+ const byScope = new Map();
55
+ for (const procedure of procedures) {
56
+ const scopeKey = scopeKeyOf(procedure);
57
+ const generated = generatedRouteName(procedure);
58
+ let byName = byScope.get(scopeKey);
59
+ if (byName === undefined) {
60
+ byName = new Map();
61
+ byScope.set(scopeKey, byName);
62
+ }
63
+ const existing = byName.get(generated);
64
+ if (existing === undefined)
65
+ byName.set(generated, [procedure]);
66
+ else
67
+ existing.push(procedure);
68
+ }
69
+ const collisions = [];
70
+ for (const [scopeKey, byName] of byScope) {
71
+ for (const [generated, procs] of byName) {
72
+ if (procs.length > 1)
73
+ collisions.push({ scopeKey, generated, procs });
74
+ }
75
+ }
76
+ if (collisions.length === 0)
77
+ return;
78
+ const details = collisions
79
+ .map(({ scopeKey, generated, procs }) => {
80
+ const lines = procs
81
+ .map((p) => ` - "${p.name}" (${p.kind})${definedAtString(p.definitionInfo)}`)
82
+ .join('\n');
83
+ return ` Scope "${scopeKey}" → generated name "${generated}":\n${lines}`;
84
+ })
85
+ .join('\n');
86
+ // Anchor the error (and its appended definition-site frame) on the first
87
+ // colliding procedure; the message enumerates every collision.
88
+ const first = collisions[0].procs[0];
89
+ throw new ProcedureRegistrationError(first.name, `Procedure name collision — the server cannot start.\n\n` +
90
+ `These procedures collapse to the same generated client identifier within the same scope. ` +
91
+ `The generated client would emit duplicate types and a duplicate callable key for each pair ` +
92
+ `(this is the same failure the codegen rejects), so the server refuses to start rather than ` +
93
+ `boot a config no client can be generated for:\n\n` +
94
+ `${details}\n\n` +
95
+ `Names are PascalCased per scope, so different raw names can still collide ` +
96
+ `(e.g. "get-task" and "getTask" both become "GetTask"). Rename one procedure in each pair, ` +
97
+ `or move it to a different scope, so every procedure in a scope generates a distinct name.`, first.definitionInfo);
98
+ }
99
+ //# sourceMappingURL=scope-name-collision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-name-collision.js","sourceRoot":"","sources":["../../src/server/scope-name-collision.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,134 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { Procedures } from '../core/procedures.js';
3
+ import { ProcedureRegistrationError } from '../core/errors.js';
4
+ import { toPascalCase, versionedPascal } from '../naming.js';
5
+ import { assertNoScopeNameCollisions, generatedRouteName } from './scope-name-collision.js';
6
+ describe('generatedRouteName — per-kind dispatch', () => {
7
+ // generatedRouteName routes each kind to the right shared naming primitive
8
+ // (versionedPascal for rpc/rpc-stream, toPascalCase for http kinds) — the same
9
+ // primitives the codegen emitters use, so the guard and codegen can't disagree.
10
+ test('rpc / rpc-stream match versionedPascal(name, version)', () => {
11
+ const cases = [
12
+ { name: 'get-task', version: 1 },
13
+ { name: 'getTask', version: 2 },
14
+ { name: 'log_out', version: 3 },
15
+ { name: 'Foo', version: 1 },
16
+ ];
17
+ for (const { name, version } of cases) {
18
+ const rpc = { name, kind: 'rpc', config: { version } };
19
+ const stream = { name, kind: 'rpc-stream', config: { version } };
20
+ expect(generatedRouteName(rpc)).toBe(versionedPascal(name, version));
21
+ expect(generatedRouteName(stream)).toBe(versionedPascal(name, version));
22
+ }
23
+ });
24
+ test('http / http-stream match toPascalCase(name), ignoring any version', () => {
25
+ for (const name of ['get-user', 'getUser', 'log out', 'list_items']) {
26
+ const http = { name, kind: 'http', config: { method: 'get', path: '/x', version: 9 } };
27
+ const httpStream = { name, kind: 'http-stream', config: { method: 'get', path: '/x' } };
28
+ expect(generatedRouteName(http)).toBe(toPascalCase(name));
29
+ expect(generatedRouteName(httpStream)).toBe(toPascalCase(name));
30
+ }
31
+ });
32
+ test('version 1 produces no V suffix; version > 1 does', () => {
33
+ const v1 = { name: 'Thing', kind: 'rpc', config: { version: 1 } };
34
+ const v2 = { name: 'Thing', kind: 'rpc', config: { version: 2 } };
35
+ expect(generatedRouteName(v1)).toBe('Thing');
36
+ expect(generatedRouteName(v2)).toBe('ThingV2');
37
+ });
38
+ });
39
+ describe('assertNoScopeNameCollisions', () => {
40
+ test('no-op when every procedure generates a distinct scoped name', () => {
41
+ const P = Procedures();
42
+ P.Create('GetTask', { scope: 'tasks', version: 1 }, async () => ({}));
43
+ P.Create('ListTasks', { scope: 'tasks', version: 1 }, async () => ({}));
44
+ expect(() => assertNoScopeNameCollisions(P.getProcedures())).not.toThrow();
45
+ });
46
+ test('same name in DIFFERENT scopes is allowed (separate scope files)', () => {
47
+ const A = Procedures();
48
+ A.Create('Get', { scope: 'users', version: 1 }, async () => ({}));
49
+ const B = Procedures();
50
+ B.Create('Get', { scope: 'orders', version: 1 }, async () => ({}));
51
+ expect(() => assertNoScopeNameCollisions([...A.getProcedures(), ...B.getProcedures()])).not.toThrow();
52
+ });
53
+ test('detects names that pascalize to the same identifier in one scope', () => {
54
+ const P = Procedures();
55
+ // distinct raw names → both register on the core registry...
56
+ P.Create('get-task', { scope: 'tasks', version: 1 }, async () => ({}));
57
+ P.Create('getTask', { scope: 'tasks', version: 1 }, async () => ({}));
58
+ // ...but both pascalize to "GetTask".
59
+ expect(() => assertNoScopeNameCollisions(P.getProcedures())).toThrow(ProcedureRegistrationError);
60
+ expect(() => assertNoScopeNameCollisions(P.getProcedures())).toThrow(/GetTask/);
61
+ });
62
+ test('detects the same (scope, name) contributed by two separate factories', () => {
63
+ const A = Procedures();
64
+ A.Create('LogOut', { scope: 'auth', version: 1 }, async () => ({}));
65
+ const B = Procedures();
66
+ B.Create('LogOut', { scope: 'auth', version: 1 }, async () => ({}));
67
+ const all = [...A.getProcedures(), ...B.getProcedures()];
68
+ expect(() => assertNoScopeNameCollisions(all)).toThrow(/Scope "auth".*LogOut/s);
69
+ });
70
+ test('version disambiguates: Foo@v1 and Foo@v2 (across factories) do not collide', () => {
71
+ // Same raw name can't register twice in one factory, but two factories can
72
+ // each contribute a "Foo" — distinct versions keep their generated names apart.
73
+ const A = Procedures();
74
+ A.Create('Foo', { scope: 's', version: 1 }, async () => ({}));
75
+ const B = Procedures();
76
+ B.Create('Foo', { scope: 's', version: 2 }, async () => ({}));
77
+ expect(() => assertNoScopeNameCollisions([...A.getProcedures(), ...B.getProcedures()])).not.toThrow();
78
+ });
79
+ test('version collision: Foo@v2 and FooV2@v1 both generate "FooV2"', () => {
80
+ const P = Procedures();
81
+ P.Create('Foo', { scope: 's', version: 2 }, async () => ({}));
82
+ P.Create('FooV2', { scope: 's', version: 1 }, async () => ({}));
83
+ expect(() => assertNoScopeNameCollisions(P.getProcedures())).toThrow(/FooV2/);
84
+ });
85
+ test('rpc and http procedures collide when they share scope + generated name', () => {
86
+ const A = Procedures();
87
+ A.Create('Thing', { scope: 'mix', version: 1 }, async () => ({}));
88
+ const B = Procedures();
89
+ B.CreateHttp('Thing', { scope: 'mix', path: '/thing', method: 'get' }, async () => undefined);
90
+ expect(() => assertNoScopeNameCollisions([...A.getProcedures(), ...B.getProcedures()])).toThrow(/Thing/);
91
+ });
92
+ test('procedures with no scope are grouped under "default" and can collide', () => {
93
+ const P = Procedures();
94
+ P.CreateHttp('do-thing', { path: '/a', method: 'get' }, async () => undefined);
95
+ P.CreateHttp('doThing', { path: '/b', method: 'get' }, async () => undefined);
96
+ expect(() => assertNoScopeNameCollisions(P.getProcedures())).toThrow(/Scope "default".*DoThing/s);
97
+ });
98
+ test('error message names both procedures and includes their definition sites', () => {
99
+ const P = Procedures();
100
+ P.Create('get-task', { scope: 'tasks', version: 1 }, async () => ({}));
101
+ P.Create('getTask', { scope: 'tasks', version: 1 }, async () => ({}));
102
+ let caught;
103
+ try {
104
+ assertNoScopeNameCollisions(P.getProcedures());
105
+ }
106
+ catch (err) {
107
+ caught = err;
108
+ }
109
+ expect(caught).toBeInstanceOf(ProcedureRegistrationError);
110
+ expect(caught.message).toContain('"get-task"');
111
+ expect(caught.message).toContain('"getTask"');
112
+ expect(caught.message).toContain('GetTask');
113
+ // definition site captured at registration flows into the trace
114
+ expect(caught.message).toContain('scope-name-collision.test.ts');
115
+ expect(caught.getDefinitionLocation()).toContain('scope-name-collision.test.ts');
116
+ });
117
+ test('reports every colliding group in one error', () => {
118
+ const P = Procedures();
119
+ P.Create('a-one', { scope: 's', version: 1 }, async () => ({}));
120
+ P.Create('aOne', { scope: 's', version: 1 }, async () => ({}));
121
+ P.Create('b-two', { scope: 's', version: 1 }, async () => ({}));
122
+ P.Create('bTwo', { scope: 's', version: 1 }, async () => ({}));
123
+ let message = '';
124
+ try {
125
+ assertNoScopeNameCollisions(P.getProcedures());
126
+ }
127
+ catch (err) {
128
+ message = err.message;
129
+ }
130
+ expect(message).toContain('AOne');
131
+ expect(message).toContain('BTwo');
132
+ });
133
+ });
134
+ //# sourceMappingURL=scope-name-collision.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-name-collision.test.js","sourceRoot":"","sources":["../../src/server/scope-name-collision.test.ts"],"names":[],"mappings":"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"}
@@ -152,7 +152,7 @@ generated/
152
152
 
153
153
  Codegen fails fast (non-zero exit) rather than emitting a client that won't compile:
154
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.
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. The same collision is now also rejected at **server start** (`HonoAppBuilder.build()` throws a `ProcedureRegistrationError` pointing at every colliding definition), so it surfaces in your server before it ever reaches codegen — including the cross-factory case where two factories each contribute the same `(scope, name)` and neither factory's registry can see the other.
156
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
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
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "10.2.2",
3
+ "version": "10.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",
@@ -395,3 +395,39 @@ describe('HonoAppBuilder — middleware (new)', () => {
395
395
  expect(calls).toContain('used')
396
396
  })
397
397
  })
398
+
399
+ describe('HonoAppBuilder — scope/name collision guard', () => {
400
+ test('build() crashes when two routes pascalize to the same name in a scope', () => {
401
+ const P = Procedures<object, RPCConfig>()
402
+ // distinct raw names register fine on the core registry...
403
+ P.Create('get-task', { scope: 'tasks', version: 1 }, async () => ({}))
404
+ P.Create('getTask', { scope: 'tasks', version: 1 }, async () => ({}))
405
+
406
+ const b = new HonoAppBuilder()
407
+ b.register(P, () => ({}))
408
+ // ...but both generate "GetTask", which the client codegen rejects.
409
+ expect(() => b.build()).toThrow(/Scope "tasks".*GetTask/s)
410
+ })
411
+
412
+ test('build() crashes on the same (scope, name) contributed by two factories', () => {
413
+ const A = Procedures<object, RPCConfig>()
414
+ A.Create('LogOut', { scope: 'auth', version: 1 }, async () => ({}))
415
+ const B = Procedures<object, RPCConfig>()
416
+ B.Create('LogOut', { scope: 'auth', version: 1 }, async () => ({}))
417
+
418
+ const b = new HonoAppBuilder()
419
+ b.register(A, () => ({}))
420
+ b.register(B, () => ({}))
421
+ expect(() => b.build()).toThrow(/LogOut/)
422
+ })
423
+
424
+ test('build() succeeds when scoped names are all distinct', () => {
425
+ const P = Procedures<object, RPCConfig>()
426
+ P.Create('GetTask', { scope: 'tasks', version: 1 }, async () => ({}))
427
+ P.Create('ListTasks', { scope: 'tasks', version: 1 }, async () => ({}))
428
+
429
+ const b = new HonoAppBuilder()
430
+ b.register(P, () => ({}))
431
+ expect(() => b.build()).not.toThrow()
432
+ })
433
+ })
@@ -10,6 +10,7 @@ import type {
10
10
  } from '../../server/types.js'
11
11
  import type { AnyProcedureRegistration } from '../../core/types.js'
12
12
  import { DocRegistry } from '../../server/doc-registry.js'
13
+ import { assertNoScopeNameCollisions } from '../../server/scope-name-collision.js'
13
14
  import type { ErrorTaxonomy } from '../../server/errors/taxonomy.js'
14
15
  import type { FactoryContextInit } from '../../server/context.js'
15
16
  import { buildRpcRouteDoc } from '../../server/docs/rpc-doc.js'
@@ -145,6 +146,13 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
145
146
  return this
146
147
  }
147
148
 
149
+ /** Every procedure across every registered factory, in registration order. */
150
+ private *allProcedures(): Iterable<AnyProcedureRegistration<any, any>> {
151
+ for (const item of this.factories) {
152
+ yield* item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration<any, any>>
153
+ }
154
+ }
155
+
148
156
  private computeDocs(): void {
149
157
  const docs: AnyHttpRouteDoc[] = []
150
158
  const skipped: { name: string; reason: string }[] = []
@@ -190,6 +198,14 @@ export class HonoAppBuilder<TStreamErrorData = unknown> {
190
198
  if (this._built) return this._app
191
199
  this._built = true
192
200
 
201
+ // Fail fast on procedures that would collapse to the same generated client
202
+ // identifier within a scope (e.g. `get-task`/`getTask`, or the same
203
+ // `(scope, name)` contributed by two factories). The per-factory registry
204
+ // can't catch these — but the client codegen rejects them — so crash here
205
+ // with a trace to every colliding definition instead of booting a server no
206
+ // client can be generated for.
207
+ assertNoScopeNameCollisions(this.allProcedures())
208
+
193
209
  const docs: AnyHttpRouteDoc[] = []
194
210
  const skipped: { name: string; reason: string }[] = []
195
211
  const cfg = this.config ?? {}
@@ -6,15 +6,9 @@
6
6
  import { toPascalCase } from '../naming.js'
7
7
  import type { EmitRouteContext } from './context.js'
8
8
 
9
- /**
10
- * Returns the PascalCase display name for a route, appending `V{version}`
11
- * when version > 1. Version 1 produces no suffix for backward compatibility.
12
- */
13
- export function versionedPascal(name: string, version: number | undefined): string {
14
- const pascal = toPascalCase(name)
15
- if (version != null && version > 1) return `${pascal}V${version}`
16
- return pascal
17
- }
9
+ // `versionedPascal` now lives in the neutral naming module (shared with the
10
+ // server-start collision guard); re-exported here to keep its import path stable.
11
+ export { versionedPascal } from '../../naming.js'
18
12
 
19
13
  // ---------------------------------------------------------------------------
20
14
  // Route-level Errors union injection
@@ -1,4 +1,9 @@
1
1
  import type { AnyHttpRouteDoc } from '../server/types.js'
2
+ import { normalizeScope } from '../naming.js'
3
+
4
+ // Re-exported so existing `./group-routes.js` importers keep working; the
5
+ // definition is shared with the server-start collision guard (see src/naming.ts).
6
+ export { normalizeScope }
2
7
 
3
8
  export interface ScopeGroup {
4
9
  scopeKey: string
@@ -6,18 +11,6 @@ export interface ScopeGroup {
6
11
  routes: AnyHttpRouteDoc[]
7
12
  }
8
13
 
9
- /**
10
- * Normalizes a scope value to a string key:
11
- * - string → returned as-is
12
- * - string[] → joined with '-'
13
- * - undefined → 'default'
14
- */
15
- export function normalizeScope(scope: string | string[] | undefined): string {
16
- if (scope === undefined) return 'default'
17
- if (Array.isArray(scope)) return scope.join('-')
18
- return scope
19
- }
20
-
21
14
  /**
22
15
  * Converts a hyphenated scope key to camelCase.
23
16
  * e.g. 'admin-users' → 'adminUsers', 'users' → 'users'
@@ -1,10 +1,7 @@
1
- /** Converts a string to PascalCase (splits on whitespace, hyphens, underscores). */
2
- export function toPascalCase(str: string): string {
3
- return str
4
- .split(/[\s\-_]+/)
5
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
6
- .join('')
7
- }
1
+ // Re-exported from the neutral naming module so codegen and the server-start
2
+ // collision guard share one definition (see src/naming.ts).
3
+ import { toPascalCase } from '../naming.js'
4
+ export { toPascalCase }
8
5
 
9
6
  /**
10
7
  * Validates that a serviceName option is usable as a TypeScript identifier fragment.
@@ -176,6 +176,7 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
176
176
  const registeredProcedure: THttpStreamProcedureRegistration<TContext> = {
177
177
  name,
178
178
  kind: 'http-stream',
179
+ definitionInfo,
179
180
  config: {
180
181
  path,
181
182
  method: config.method,
@@ -91,6 +91,7 @@ export function makeCreateHttp<TContext>(runtime: FactoryRuntime<TContext, any>)
91
91
  const registeredProcedure: THttpProcedureRegistration<TContext> = {
92
92
  name,
93
93
  kind: 'http',
94
+ definitionInfo,
94
95
  config: {
95
96
  path,
96
97
  method: config.method,
@@ -63,6 +63,7 @@ export function makeCreateStream<TContext, TExtendedConfig>(runtime: FactoryRunt
63
63
  const registeredProcedure: TStreamProcedureRegistration<TContext, TExtendedConfig> = {
64
64
  name,
65
65
  kind: 'rpc-stream',
66
+ definitionInfo,
66
67
  config: {
67
68
  ...config,
68
69
  description: config.description,
@@ -58,6 +58,7 @@ export function makeCreate<TContext, TExtendedConfig>(runtime: FactoryRuntime<TC
58
58
  const registeredProcedure: TProcedureRegistration<TContext, TExtendedConfig> = {
59
59
  name,
60
60
  kind: 'rpc',
61
+ definitionInfo,
61
62
  config: {
62
63
  ...config,
63
64
  description: config.description,
package/src/core/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type * as AJV from 'ajv'
2
+ import type { DefinitionInfo } from './definition-site.js'
2
3
  import type { ProcedureError } from './errors.js'
3
4
  import type { SchemaAdapter } from '../schema/adapter.js'
4
5
  import type { Infer, Prettify, TJSONSchema } from '../schema/json-schema.js'
@@ -35,6 +36,8 @@ export type ProcedureKind = 'rpc' | 'rpc-stream' | 'http' | 'http-stream'
35
36
  export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
36
37
  name: string
37
38
  kind: 'rpc'
39
+ /** Source location where the procedure was defined; powers actionable error traces. */
40
+ definitionInfo?: DefinitionInfo
38
41
  config: {
39
42
  description?: string
40
43
  schema?: {
@@ -51,6 +54,8 @@ export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown
51
54
  export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
52
55
  name: string
53
56
  kind: 'rpc-stream'
57
+ /** Source location where the procedure was defined; powers actionable error traces. */
58
+ definitionInfo?: DefinitionInfo
54
59
  config: {
55
60
  description?: string
56
61
  schema?: {
@@ -145,6 +150,8 @@ export type TCreateHttpConfig<
145
150
  export type THttpProcedureRegistration<TContext = unknown> = {
146
151
  name: string
147
152
  kind: 'http'
153
+ /** Source location where the procedure was defined; powers actionable error traces. */
154
+ definitionInfo?: DefinitionInfo
148
155
  config: {
149
156
  path: string
150
157
  method: HttpMethod
@@ -166,6 +173,8 @@ export type THttpProcedureRegistration<TContext = unknown> = {
166
173
  export type THttpStreamProcedureRegistration<TContext = unknown> = {
167
174
  name: string
168
175
  kind: 'http-stream'
176
+ /** Source location where the procedure was defined; powers actionable error traces. */
177
+ definitionInfo?: DefinitionInfo
169
178
  config: {
170
179
  path: string
171
180
  method: HttpMethod
package/src/naming.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Identifier-naming primitives shared by the codegen emitters and the
3
+ * server-start collision guard. These MUST agree byte-for-byte across both —
4
+ * the server refuses to start a config the codegen would reject, and that only
5
+ * holds if both compute generated names the same way. Keeping them in one
6
+ * neutral module (imported by `src/codegen/*` and `src/server/*` alike) makes
7
+ * that a structural guarantee instead of a parity test.
8
+ */
9
+
10
+ /** Converts a string to PascalCase (splits on whitespace, hyphens, underscores). */
11
+ export function toPascalCase(str: string): string {
12
+ return str
13
+ .split(/[\s\-_]+/)
14
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
15
+ .join('')
16
+ }
17
+
18
+ /**
19
+ * Returns the PascalCase display name for a route, appending `V{version}`
20
+ * when version > 1. Version 1 produces no suffix for backward compatibility.
21
+ */
22
+ export function versionedPascal(name: string, version: number | undefined): string {
23
+ const pascal = toPascalCase(name)
24
+ if (version != null && version > 1) return `${pascal}V${version}`
25
+ return pascal
26
+ }
27
+
28
+ /**
29
+ * Normalizes a scope value to a string grouping key:
30
+ * - string → returned as-is
31
+ * - string[] → joined with '-'
32
+ * - undefined → 'default'
33
+ */
34
+ export function normalizeScope(scope: string | string[] | undefined): string {
35
+ if (scope === undefined) return 'default'
36
+ if (Array.isArray(scope)) return scope.join('-')
37
+ return scope
38
+ }
@@ -27,3 +27,4 @@ export { buildHttpStreamRouteDoc } from './docs/http-stream-doc.js'
27
27
  export { DocRegistry } from './doc-registry.js'
28
28
  export { writeDocEnvelope } from './doc-envelope.js'
29
29
  export type { DocEnvelopeSource } from './doc-envelope.js'
30
+ export { assertNoScopeNameCollisions, generatedRouteName } from './scope-name-collision.js'