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.
- package/CHANGELOG.md +15 -0
- package/build/adapters/hono/index.d.ts +2 -0
- package/build/adapters/hono/index.js +14 -0
- package/build/adapters/hono/index.js.map +1 -1
- package/build/adapters/hono/index.test.js +30 -0
- package/build/adapters/hono/index.test.js.map +1 -1
- package/build/codegen/emit/route-shared.d.ts +1 -5
- package/build/codegen/emit/route-shared.js +3 -10
- package/build/codegen/emit/route-shared.js.map +1 -1
- package/build/codegen/group-routes.d.ts +2 -7
- package/build/codegen/group-routes.js +4 -13
- package/build/codegen/group-routes.js.map +1 -1
- package/build/codegen/naming.d.ts +2 -2
- package/build/codegen/naming.js +4 -7
- package/build/codegen/naming.js.map +1 -1
- package/build/core/create-http-stream.js +1 -0
- package/build/core/create-http-stream.js.map +1 -1
- package/build/core/create-http.js +1 -0
- package/build/core/create-http.js.map +1 -1
- package/build/core/create-stream.js +1 -0
- package/build/core/create-stream.js.map +1 -1
- package/build/core/create.js +1 -0
- package/build/core/create.js.map +1 -1
- package/build/core/types.d.ts +9 -0
- package/build/naming.d.ts +22 -0
- package/build/naming.js +39 -0
- package/build/naming.js.map +1 -0
- package/build/server/index.d.ts +1 -0
- package/build/server/index.js +1 -0
- package/build/server/index.js.map +1 -1
- package/build/server/scope-name-collision.d.ts +17 -0
- package/build/server/scope-name-collision.js +99 -0
- package/build/server/scope-name-collision.js.map +1 -0
- package/build/server/scope-name-collision.test.d.ts +1 -0
- package/build/server/scope-name-collision.test.js +134 -0
- package/build/server/scope-name-collision.test.js.map +1 -0
- package/docs/client-and-codegen.md +1 -1
- package/package.json +1 -1
- package/src/adapters/hono/index.test.ts +36 -0
- package/src/adapters/hono/index.ts +16 -0
- package/src/codegen/emit/route-shared.ts +3 -9
- package/src/codegen/group-routes.ts +5 -12
- package/src/codegen/naming.ts +4 -7
- package/src/core/create-http-stream.ts +1 -0
- package/src/core/create-http.ts +1 -0
- package/src/core/create-stream.ts +1 -0
- package/src/core/create.ts +1 -0
- package/src/core/types.ts +9 -0
- package/src/naming.ts +38 -0
- package/src/server/index.ts +1 -0
- package/src/server/scope-name-collision.test.ts +151 -0
- 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.
|
|
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
|
-
|
|
11
|
-
|
|
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'
|
package/src/codegen/naming.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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,
|
package/src/core/create-http.ts
CHANGED
|
@@ -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,
|
package/src/core/create.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -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'
|