ts-procedures 5.9.1 → 5.10.2
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/README.md +1 -1
- package/agent_config/bin/postinstall.mjs +3 -3
- package/agent_config/bin/setup.mjs +22 -11
- package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
- package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
- package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
- package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
- package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
- package/agent_config/lib/install-claude.mjs +35 -87
- package/build/src/client/call.d.ts +14 -0
- package/build/src/client/call.js +47 -0
- package/build/src/client/call.js.map +1 -0
- package/build/src/client/call.test.d.ts +1 -0
- package/build/src/client/call.test.js +124 -0
- package/build/src/client/call.test.js.map +1 -0
- package/build/src/client/errors.d.ts +25 -0
- package/build/src/client/errors.js +33 -0
- package/build/src/client/errors.js.map +1 -0
- package/build/src/client/errors.test.d.ts +1 -0
- package/build/src/client/errors.test.js +41 -0
- package/build/src/client/errors.test.js.map +1 -0
- package/build/src/client/fetch-adapter.d.ts +12 -0
- package/build/src/client/fetch-adapter.js +156 -0
- package/build/src/client/fetch-adapter.js.map +1 -0
- package/build/src/client/fetch-adapter.test.d.ts +1 -0
- package/build/src/client/fetch-adapter.test.js +271 -0
- package/build/src/client/fetch-adapter.test.js.map +1 -0
- package/build/src/client/hooks.d.ts +17 -0
- package/build/src/client/hooks.js +40 -0
- package/build/src/client/hooks.js.map +1 -0
- package/build/src/client/hooks.test.d.ts +1 -0
- package/build/src/client/hooks.test.js +163 -0
- package/build/src/client/hooks.test.js.map +1 -0
- package/build/src/client/index.d.ts +22 -0
- package/build/src/client/index.js +67 -0
- package/build/src/client/index.js.map +1 -0
- package/build/src/client/index.test.d.ts +1 -0
- package/build/src/client/index.test.js +231 -0
- package/build/src/client/index.test.js.map +1 -0
- package/build/src/client/request-builder.d.ts +13 -0
- package/build/src/client/request-builder.js +53 -0
- package/build/src/client/request-builder.js.map +1 -0
- package/build/src/client/request-builder.test.d.ts +1 -0
- package/build/src/client/request-builder.test.js +160 -0
- package/build/src/client/request-builder.test.js.map +1 -0
- package/build/src/client/stream.d.ts +27 -0
- package/build/src/client/stream.js +118 -0
- package/build/src/client/stream.js.map +1 -0
- package/build/src/client/stream.test.d.ts +1 -0
- package/build/src/client/stream.test.js +228 -0
- package/build/src/client/stream.test.js.map +1 -0
- package/build/src/client/types.d.ts +78 -0
- package/build/src/client/types.js +3 -0
- package/build/src/client/types.js.map +1 -0
- package/build/src/codegen/bin/cli.d.ts +45 -0
- package/build/src/codegen/bin/cli.js +246 -0
- package/build/src/codegen/bin/cli.js.map +1 -0
- package/build/src/codegen/bin/cli.test.d.ts +1 -0
- package/build/src/codegen/bin/cli.test.js +220 -0
- package/build/src/codegen/bin/cli.test.js.map +1 -0
- package/build/src/codegen/constants.d.ts +1 -0
- package/build/src/codegen/constants.js +2 -0
- package/build/src/codegen/constants.js.map +1 -0
- package/build/src/codegen/e2e.test.d.ts +1 -0
- package/build/src/codegen/e2e.test.js +464 -0
- package/build/src/codegen/e2e.test.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.d.ts +9 -0
- package/build/src/codegen/emit-client-runtime.js +99 -0
- package/build/src/codegen/emit-client-runtime.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
- package/build/src/codegen/emit-client-runtime.test.js +78 -0
- package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
- package/build/src/codegen/emit-client-types.d.ts +8 -0
- package/build/src/codegen/emit-client-types.js +25 -0
- package/build/src/codegen/emit-client-types.js.map +1 -0
- package/build/src/codegen/emit-client-types.test.d.ts +1 -0
- package/build/src/codegen/emit-client-types.test.js +33 -0
- package/build/src/codegen/emit-client-types.test.js.map +1 -0
- package/build/src/codegen/emit-errors.d.ts +19 -0
- package/build/src/codegen/emit-errors.js +59 -0
- package/build/src/codegen/emit-errors.js.map +1 -0
- package/build/src/codegen/emit-errors.test.d.ts +1 -0
- package/build/src/codegen/emit-errors.test.js +175 -0
- package/build/src/codegen/emit-errors.test.js.map +1 -0
- package/build/src/codegen/emit-index.d.ts +12 -0
- package/build/src/codegen/emit-index.js +41 -0
- package/build/src/codegen/emit-index.js.map +1 -0
- package/build/src/codegen/emit-index.test.d.ts +1 -0
- package/build/src/codegen/emit-index.test.js +106 -0
- package/build/src/codegen/emit-index.test.js.map +1 -0
- package/build/src/codegen/emit-scope.d.ts +15 -0
- package/build/src/codegen/emit-scope.js +299 -0
- package/build/src/codegen/emit-scope.js.map +1 -0
- package/build/src/codegen/emit-scope.test.d.ts +1 -0
- package/build/src/codegen/emit-scope.test.js +559 -0
- package/build/src/codegen/emit-scope.test.js.map +1 -0
- package/build/src/codegen/emit-types.d.ts +43 -0
- package/build/src/codegen/emit-types.js +111 -0
- package/build/src/codegen/emit-types.js.map +1 -0
- package/build/src/codegen/emit-types.test.d.ts +1 -0
- package/build/src/codegen/emit-types.test.js +184 -0
- package/build/src/codegen/emit-types.test.js.map +1 -0
- package/build/src/codegen/group-routes.d.ts +23 -0
- package/build/src/codegen/group-routes.js +46 -0
- package/build/src/codegen/group-routes.js.map +1 -0
- package/build/src/codegen/group-routes.test.d.ts +1 -0
- package/build/src/codegen/group-routes.test.js +131 -0
- package/build/src/codegen/group-routes.test.js.map +1 -0
- package/build/src/codegen/index.d.ts +15 -0
- package/build/src/codegen/index.js +16 -0
- package/build/src/codegen/index.js.map +1 -0
- package/build/src/codegen/naming.d.ts +7 -0
- package/build/src/codegen/naming.js +21 -0
- package/build/src/codegen/naming.js.map +1 -0
- package/build/src/codegen/naming.test.d.ts +1 -0
- package/build/src/codegen/naming.test.js +40 -0
- package/build/src/codegen/naming.test.js.map +1 -0
- package/build/src/codegen/pipeline.d.ts +17 -0
- package/build/src/codegen/pipeline.js +78 -0
- package/build/src/codegen/pipeline.js.map +1 -0
- package/build/src/codegen/pipeline.test.d.ts +1 -0
- package/build/src/codegen/pipeline.test.js +269 -0
- package/build/src/codegen/pipeline.test.js.map +1 -0
- package/build/src/codegen/resolve-envelope.d.ts +7 -0
- package/build/src/codegen/resolve-envelope.js +46 -0
- package/build/src/codegen/resolve-envelope.js.map +1 -0
- package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
- package/build/src/codegen/resolve-envelope.test.js +69 -0
- package/build/src/codegen/resolve-envelope.test.js.map +1 -0
- package/build/src/errors.d.ts +33 -0
- package/build/src/errors.js +91 -0
- package/build/src/errors.js.map +1 -0
- package/build/src/errors.test.d.ts +1 -0
- package/build/src/errors.test.js +122 -0
- package/build/src/errors.test.js.map +1 -0
- package/build/src/exports.d.ts +7 -0
- package/build/src/exports.js +8 -0
- package/build/src/exports.js.map +1 -0
- package/build/src/implementations/http/doc-registry.d.ts +12 -0
- package/build/src/implementations/http/doc-registry.js +114 -0
- package/build/src/implementations/http/doc-registry.js.map +1 -0
- package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/src/implementations/http/doc-registry.test.js +347 -0
- package/build/src/implementations/http/doc-registry.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
- package/build/src/implementations/http/express-rpc/index.js +185 -0
- package/build/src/implementations/http/express-rpc/index.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/express-rpc/index.test.js +684 -0
- package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/express-rpc/types.js +2 -0
- package/build/src/implementations/http/express-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.d.ts +102 -0
- package/build/src/implementations/http/hono-api/index.js +341 -0
- package/build/src/implementations/http/hono-api/index.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-api/index.test.js +992 -0
- package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-api/types.d.ts +13 -0
- package/build/src/implementations/http/hono-api/types.js +2 -0
- package/build/src/implementations/http/hono-api/types.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
- package/build/src/implementations/http/hono-rpc/index.js +161 -0
- package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
- package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/hono-rpc/types.js +2 -0
- package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
- package/build/src/implementations/http/hono-stream/index.js +309 -0
- package/build/src/implementations/http/hono-stream/index.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
- package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/src/implementations/http/hono-stream/types.js +2 -0
- package/build/src/implementations/http/hono-stream/types.js.map +1 -0
- package/build/src/implementations/types.d.ts +142 -0
- package/build/src/implementations/types.js +2 -0
- package/build/src/implementations/types.js.map +1 -0
- package/build/src/index.d.ts +165 -0
- package/build/src/index.js +253 -0
- package/build/src/index.js.map +1 -0
- package/build/src/index.test.d.ts +1 -0
- package/build/src/index.test.js +890 -0
- package/build/src/index.test.js.map +1 -0
- package/build/src/schema/compute-schema.d.ts +35 -0
- package/build/src/schema/compute-schema.js +41 -0
- package/build/src/schema/compute-schema.js.map +1 -0
- package/build/src/schema/compute-schema.test.d.ts +1 -0
- package/build/src/schema/compute-schema.test.js +107 -0
- package/build/src/schema/compute-schema.test.js.map +1 -0
- package/build/src/schema/extract-json-schema.d.ts +2 -0
- package/build/src/schema/extract-json-schema.js +12 -0
- package/build/src/schema/extract-json-schema.js.map +1 -0
- package/build/src/schema/extract-json-schema.test.d.ts +1 -0
- package/build/src/schema/extract-json-schema.test.js +23 -0
- package/build/src/schema/extract-json-schema.test.js.map +1 -0
- package/build/src/schema/parser.d.ts +28 -0
- package/build/src/schema/parser.js +170 -0
- package/build/src/schema/parser.js.map +1 -0
- package/build/src/schema/parser.test.d.ts +1 -0
- package/build/src/schema/parser.test.js +120 -0
- package/build/src/schema/parser.test.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.d.ts +12 -0
- package/build/src/schema/resolve-schema-lib.js +11 -0
- package/build/src/schema/resolve-schema-lib.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/src/schema/resolve-schema-lib.test.js +17 -0
- package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/src/schema/types.d.ts +8 -0
- package/build/src/schema/types.js +2 -0
- package/build/src/schema/types.js.map +1 -0
- package/build/src/stack-utils.d.ts +25 -0
- package/build/src/stack-utils.js +95 -0
- package/build/src/stack-utils.js.map +1 -0
- package/build/src/stack-utils.test.d.ts +1 -0
- package/build/src/stack-utils.test.js +80 -0
- package/build/src/stack-utils.test.js.map +1 -0
- package/docs/ai-agent-setup.md +7 -6
- package/docs/core.md +5 -9
- package/docs/streaming.md +9 -9
- package/package.json +2 -13
- package/src/client/call.test.ts +162 -0
- package/src/client/errors.test.ts +43 -0
- package/src/client/fetch-adapter.test.ts +340 -0
- package/src/client/hooks.test.ts +191 -0
- package/src/client/index.test.ts +290 -0
- package/src/client/request-builder.test.ts +184 -0
- package/src/client/stream.test.ts +331 -0
- package/src/codegen/bin/cli.test.ts +260 -0
- package/src/codegen/bin/cli.ts +282 -0
- package/src/codegen/constants.ts +1 -0
- package/src/codegen/e2e.test.ts +565 -0
- package/src/codegen/emit-client-runtime.test.ts +93 -0
- package/src/codegen/emit-client-runtime.ts +114 -0
- package/src/codegen/emit-client-types.test.ts +39 -0
- package/src/codegen/emit-client-types.ts +27 -0
- package/src/codegen/emit-errors.test.ts +202 -0
- package/src/codegen/emit-errors.ts +80 -0
- package/src/codegen/emit-index.test.ts +127 -0
- package/src/codegen/emit-index.ts +58 -0
- package/src/codegen/emit-scope.test.ts +624 -0
- package/src/codegen/emit-scope.ts +389 -0
- package/src/codegen/emit-types.test.ts +205 -0
- package/src/codegen/emit-types.ts +158 -0
- package/src/codegen/group-routes.test.ts +159 -0
- package/src/codegen/group-routes.ts +61 -0
- package/src/codegen/index.ts +30 -0
- package/src/codegen/naming.test.ts +50 -0
- package/src/codegen/naming.ts +25 -0
- package/src/codegen/pipeline.test.ts +316 -0
- package/src/codegen/pipeline.ts +108 -0
- package/src/codegen/resolve-envelope.test.ts +76 -0
- package/src/codegen/resolve-envelope.ts +61 -0
- package/src/errors.test.ts +163 -0
- package/src/errors.ts +107 -0
- package/src/exports.ts +7 -0
- package/src/implementations/http/doc-registry.test.ts +415 -0
- package/src/implementations/http/doc-registry.ts +143 -0
- package/src/implementations/http/express-rpc/README.md +6 -6
- package/src/implementations/http/express-rpc/index.test.ts +957 -0
- package/src/implementations/http/express-rpc/index.ts +266 -0
- package/src/implementations/http/express-rpc/types.ts +16 -0
- package/src/implementations/http/hono-api/index.test.ts +1341 -0
- package/src/implementations/http/hono-api/index.ts +463 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/http/hono-rpc/README.md +6 -6
- package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
- package/src/implementations/http/hono-rpc/index.ts +238 -0
- package/src/implementations/http/hono-rpc/types.ts +16 -0
- package/src/implementations/http/hono-stream/README.md +12 -12
- package/src/implementations/http/hono-stream/index.test.ts +1768 -0
- package/src/implementations/http/hono-stream/index.ts +456 -0
- package/src/implementations/http/hono-stream/types.ts +20 -0
- package/src/implementations/types.ts +174 -0
- package/src/index.test.ts +1185 -0
- package/src/index.ts +522 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +88 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +182 -0
- package/src/schema/parser.ts +215 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +20 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
- package/agent_config/claude-code/skills/review/SKILL.md +0 -53
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
- /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
- /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { generateClient } from './index.js'
|
|
3
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import type { DocEnvelope, RPCHttpRouteDoc, APIHttpRouteDoc, StreamHttpRouteDoc, ErrorDoc } from '../implementations/types.js'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Fixtures
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const rpcRoute: RPCHttpRouteDoc = {
|
|
13
|
+
kind: 'rpc',
|
|
14
|
+
name: 'GetUser',
|
|
15
|
+
path: '/users/1',
|
|
16
|
+
method: 'post',
|
|
17
|
+
scope: 'users',
|
|
18
|
+
version: 1,
|
|
19
|
+
jsonSchema: {
|
|
20
|
+
body: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: { id: { type: 'string' } },
|
|
23
|
+
required: ['id'],
|
|
24
|
+
},
|
|
25
|
+
response: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: { name: { type: 'string' }, email: { type: 'string' } },
|
|
28
|
+
required: ['name', 'email'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const apiRoute: APIHttpRouteDoc = {
|
|
34
|
+
kind: 'api',
|
|
35
|
+
name: 'UpdateUser',
|
|
36
|
+
path: '/users/:id',
|
|
37
|
+
fullPath: '/api/users/:id',
|
|
38
|
+
method: 'put',
|
|
39
|
+
scope: 'users',
|
|
40
|
+
jsonSchema: {
|
|
41
|
+
pathParams: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: { id: { type: 'string' } },
|
|
44
|
+
required: ['id'],
|
|
45
|
+
},
|
|
46
|
+
body: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: { name: { type: 'string' } },
|
|
49
|
+
required: ['name'],
|
|
50
|
+
},
|
|
51
|
+
response: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
54
|
+
required: ['id', 'name'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const streamRoute: StreamHttpRouteDoc = {
|
|
60
|
+
kind: 'stream',
|
|
61
|
+
name: 'WatchEvents',
|
|
62
|
+
path: '/events/stream',
|
|
63
|
+
methods: ['get'],
|
|
64
|
+
streamMode: 'sse',
|
|
65
|
+
scope: 'events',
|
|
66
|
+
version: 1,
|
|
67
|
+
jsonSchema: {
|
|
68
|
+
params: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: { filter: { type: 'string' } },
|
|
71
|
+
required: [],
|
|
72
|
+
},
|
|
73
|
+
// SSE envelope wrapping the actual yield payload
|
|
74
|
+
yieldType: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
data: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: { type: { type: 'string' }, payload: { type: 'string' } },
|
|
80
|
+
required: ['type'],
|
|
81
|
+
},
|
|
82
|
+
event: { type: 'string' },
|
|
83
|
+
id: { type: 'string' },
|
|
84
|
+
retry: { type: 'number' },
|
|
85
|
+
},
|
|
86
|
+
required: ['data'],
|
|
87
|
+
},
|
|
88
|
+
returnType: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: { count: { type: 'number' } },
|
|
91
|
+
required: ['count'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const procedureErrorDoc: ErrorDoc = {
|
|
97
|
+
name: 'ProcedureError',
|
|
98
|
+
statusCode: 500,
|
|
99
|
+
description: 'An error thrown from within a procedure handler via ctx.error().',
|
|
100
|
+
schema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
name: { type: 'string', const: 'ProcedureError' },
|
|
104
|
+
procedureName: { type: 'string' },
|
|
105
|
+
message: { type: 'string' },
|
|
106
|
+
meta: { type: 'object' },
|
|
107
|
+
},
|
|
108
|
+
required: ['name', 'procedureName', 'message'],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const validationErrorDoc: ErrorDoc = {
|
|
113
|
+
name: 'ProcedureValidationError',
|
|
114
|
+
statusCode: 400,
|
|
115
|
+
description: 'Schema validation failed for the procedure input parameters.',
|
|
116
|
+
schema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
name: { type: 'string', const: 'ProcedureValidationError' },
|
|
120
|
+
procedureName: { type: 'string' },
|
|
121
|
+
message: { type: 'string' },
|
|
122
|
+
},
|
|
123
|
+
required: ['name', 'procedureName', 'message'],
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const envelope: DocEnvelope = {
|
|
128
|
+
basePath: '/api',
|
|
129
|
+
headers: [],
|
|
130
|
+
errors: [procedureErrorDoc, validationErrorDoc],
|
|
131
|
+
routes: [rpcRoute, apiRoute, streamRoute],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Helpers
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
function makeTmpDir(): string {
|
|
139
|
+
const dir = join(
|
|
140
|
+
tmpdir(),
|
|
141
|
+
`ts-proc-e2e-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
142
|
+
)
|
|
143
|
+
mkdirSync(dir, { recursive: true })
|
|
144
|
+
return dir
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Tests
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('E2E: generateClient full pipeline', () => {
|
|
152
|
+
let tmpDir: string
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
156
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// ── File existence ─────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
it('creates users.ts, events.ts, and index.ts', async () => {
|
|
163
|
+
tmpDir = makeTmpDir()
|
|
164
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
165
|
+
|
|
166
|
+
expect(existsSync(join(tmpDir, 'users.ts'))).toBe(true)
|
|
167
|
+
expect(existsSync(join(tmpDir, 'events.ts'))).toBe(true)
|
|
168
|
+
expect(existsSync(join(tmpDir, 'index.ts'))).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ── users.ts — RPC route ───────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
it('users.ts contains GetUserParams type', async () => {
|
|
174
|
+
tmpDir = makeTmpDir()
|
|
175
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
176
|
+
|
|
177
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
178
|
+
expect(content).toContain('GetUserParams')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('users.ts contains GetUserResponse type', async () => {
|
|
182
|
+
tmpDir = makeTmpDir()
|
|
183
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
184
|
+
|
|
185
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
186
|
+
expect(content).toContain('GetUserResponse')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ── users.ts — API route ───────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
it('users.ts contains UpdateUserParams type', async () => {
|
|
192
|
+
tmpDir = makeTmpDir()
|
|
193
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
194
|
+
|
|
195
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
196
|
+
expect(content).toContain('UpdateUserParams')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('users.ts contains UpdateUserResponse type', async () => {
|
|
200
|
+
tmpDir = makeTmpDir()
|
|
201
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
202
|
+
|
|
203
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
204
|
+
expect(content).toContain('UpdateUserResponse')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('users.ts API callable uses fullPath in the path field', async () => {
|
|
208
|
+
tmpDir = makeTmpDir()
|
|
209
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
210
|
+
|
|
211
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
212
|
+
// fullPath ('/api/users/:id') should appear in the callable body
|
|
213
|
+
expect(content).toContain("path: '/api/users/:id'")
|
|
214
|
+
// bare path without prefix should NOT appear as path: value for the api route
|
|
215
|
+
expect(content).not.toContain("path: '/users/:id'")
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('users.ts exports bindUsersScope function', async () => {
|
|
219
|
+
tmpDir = makeTmpDir()
|
|
220
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
221
|
+
|
|
222
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
223
|
+
expect(content).toContain('bindUsersScope')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('users.ts uses client.call for RPC route', async () => {
|
|
227
|
+
tmpDir = makeTmpDir()
|
|
228
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
229
|
+
|
|
230
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
231
|
+
expect(content).toContain('client.call')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ── events.ts — Stream route ───────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
it('events.ts contains WatchEventsYield type', async () => {
|
|
237
|
+
tmpDir = makeTmpDir()
|
|
238
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
239
|
+
|
|
240
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
241
|
+
expect(content).toContain('WatchEventsYield')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('events.ts contains WatchEventsReturn type', async () => {
|
|
245
|
+
tmpDir = makeTmpDir()
|
|
246
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
247
|
+
|
|
248
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
249
|
+
expect(content).toContain('WatchEventsReturn')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('events.ts imports TypedStream', async () => {
|
|
253
|
+
tmpDir = makeTmpDir()
|
|
254
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
255
|
+
|
|
256
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
257
|
+
expect(content).toContain('TypedStream')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('events.ts uses client.stream', async () => {
|
|
261
|
+
tmpDir = makeTmpDir()
|
|
262
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
263
|
+
|
|
264
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
265
|
+
expect(content).toContain('client.stream')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("events.ts callable has streamMode: 'sse'", async () => {
|
|
269
|
+
tmpDir = makeTmpDir()
|
|
270
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
271
|
+
|
|
272
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
273
|
+
expect(content).toContain("streamMode: 'sse'")
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('events.ts SSE envelope is unwrapped — yield type does NOT contain event/id/retry fields', async () => {
|
|
277
|
+
tmpDir = makeTmpDir()
|
|
278
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
279
|
+
|
|
280
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
281
|
+
// Extract the WatchEventsYield type declaration line
|
|
282
|
+
const yieldMatch = content.match(/export type WatchEventsYield = (.+)/)
|
|
283
|
+
expect(yieldMatch).not.toBeNull()
|
|
284
|
+
|
|
285
|
+
const yieldTypeBody = yieldMatch![1]
|
|
286
|
+
// The SSE envelope properties should not appear in the resolved yield type
|
|
287
|
+
expect(yieldTypeBody).not.toContain('event')
|
|
288
|
+
expect(yieldTypeBody).not.toContain('retry')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ── index.ts ───────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
it('index.ts contains createScopeBindings', async () => {
|
|
294
|
+
tmpDir = makeTmpDir()
|
|
295
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
296
|
+
|
|
297
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
298
|
+
expect(content).toContain('createScopeBindings')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('index.ts re-exports from users scope', async () => {
|
|
302
|
+
tmpDir = makeTmpDir()
|
|
303
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
304
|
+
|
|
305
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
306
|
+
expect(content).toContain("from './users'")
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('index.ts re-exports from events scope', async () => {
|
|
310
|
+
tmpDir = makeTmpDir()
|
|
311
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
312
|
+
|
|
313
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
314
|
+
expect(content).toContain("from './events'")
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('index.ts imports and uses bindUsersScope', async () => {
|
|
318
|
+
tmpDir = makeTmpDir()
|
|
319
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
320
|
+
|
|
321
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
322
|
+
expect(content).toContain('bindUsersScope')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('index.ts imports and uses bindEventsScope', async () => {
|
|
326
|
+
tmpDir = makeTmpDir()
|
|
327
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
328
|
+
|
|
329
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
330
|
+
expect(content).toContain('bindEventsScope')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// ── _errors.ts ─────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
it('_errors.ts file exists when envelope has errors with schemas', async () => {
|
|
336
|
+
tmpDir = makeTmpDir()
|
|
337
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
338
|
+
|
|
339
|
+
expect(existsSync(join(tmpDir, '_errors.ts'))).toBe(true)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('_errors.ts contains ProcedureError type', async () => {
|
|
343
|
+
tmpDir = makeTmpDir()
|
|
344
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
345
|
+
|
|
346
|
+
const content = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
|
|
347
|
+
expect(content).toContain('export type ProcedureError =')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('_errors.ts contains ProcedureErrorUnion', async () => {
|
|
351
|
+
tmpDir = makeTmpDir()
|
|
352
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
353
|
+
|
|
354
|
+
const content = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
|
|
355
|
+
expect(content).toContain('export type ProcedureErrorUnion =')
|
|
356
|
+
expect(content).toContain('ProcedureError')
|
|
357
|
+
expect(content).toContain('ProcedureValidationError')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('index.ts re-exports from _errors when errors are present', async () => {
|
|
361
|
+
tmpDir = makeTmpDir()
|
|
362
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
363
|
+
|
|
364
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
365
|
+
expect(content).toContain("export * from './_errors'")
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('_errors.ts is not generated when no errors have schemas', async () => {
|
|
369
|
+
tmpDir = makeTmpDir()
|
|
370
|
+
const envelopeNoErrors: DocEnvelope = { ...envelope, errors: [] }
|
|
371
|
+
await generateClient({ envelope: envelopeNoErrors, outDir: tmpDir })
|
|
372
|
+
|
|
373
|
+
expect(existsSync(join(tmpDir, '_errors.ts'))).toBe(false)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('index.ts does not re-export _errors when no errors are present', async () => {
|
|
377
|
+
tmpDir = makeTmpDir()
|
|
378
|
+
const envelopeNoErrors: DocEnvelope = { ...envelope, errors: [] }
|
|
379
|
+
await generateClient({ envelope: envelopeNoErrors, outDir: tmpDir })
|
|
380
|
+
|
|
381
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
382
|
+
expect(content).not.toContain("_errors")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// ── Auto-generated headers ─────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
it('all generated files have the auto-generated header comment', async () => {
|
|
388
|
+
tmpDir = makeTmpDir()
|
|
389
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
390
|
+
|
|
391
|
+
for (const file of ['users.ts', 'events.ts', 'index.ts', '_errors.ts']) {
|
|
392
|
+
const content = readFileSync(join(tmpDir, file), 'utf-8')
|
|
393
|
+
expect(content).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// ── selfContained mode ────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
describe('selfContained: true', () => {
|
|
400
|
+
it('_types.ts is generated and contains ClientInstance, TypedStream, ProcedureCallOptions', async () => {
|
|
401
|
+
tmpDir = makeTmpDir()
|
|
402
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
403
|
+
|
|
404
|
+
expect(existsSync(join(tmpDir, '_types.ts'))).toBe(true)
|
|
405
|
+
const content = readFileSync(join(tmpDir, '_types.ts'), 'utf-8')
|
|
406
|
+
expect(content).toContain('ClientInstance')
|
|
407
|
+
expect(content).toContain('TypedStream')
|
|
408
|
+
expect(content).toContain('ProcedureCallOptions')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('_client.ts is generated and contains createClient, createFetchAdapter, ClientRequestError', async () => {
|
|
412
|
+
tmpDir = makeTmpDir()
|
|
413
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
414
|
+
|
|
415
|
+
expect(existsSync(join(tmpDir, '_client.ts'))).toBe(true)
|
|
416
|
+
const content = readFileSync(join(tmpDir, '_client.ts'), 'utf-8')
|
|
417
|
+
expect(content).toContain('createClient')
|
|
418
|
+
expect(content).toContain('createFetchAdapter')
|
|
419
|
+
expect(content).toContain('ClientRequestError')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('_client.ts imports from ./_types and NOT from ts-procedures/client', async () => {
|
|
423
|
+
tmpDir = makeTmpDir()
|
|
424
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
425
|
+
|
|
426
|
+
const content = readFileSync(join(tmpDir, '_client.ts'), 'utf-8')
|
|
427
|
+
expect(content).toContain("from './_types'")
|
|
428
|
+
expect(content).not.toContain("from 'ts-procedures/client'")
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('scope files import from ./_types and NOT from ts-procedures/client', async () => {
|
|
432
|
+
tmpDir = makeTmpDir()
|
|
433
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
434
|
+
|
|
435
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
436
|
+
expect(content).toContain("from './_types'")
|
|
437
|
+
expect(content).not.toContain("from 'ts-procedures/client'")
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('index.ts imports from ./_types', async () => {
|
|
441
|
+
tmpDir = makeTmpDir()
|
|
442
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
443
|
+
|
|
444
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
445
|
+
expect(content).toContain("from './_types'")
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('_types.ts + _client.ts compile together as valid TypeScript', async () => {
|
|
449
|
+
tmpDir = makeTmpDir()
|
|
450
|
+
await generateClient({ envelope, outDir: tmpDir, selfContained: true })
|
|
451
|
+
|
|
452
|
+
// Write a minimal tsconfig for the generated files
|
|
453
|
+
const tsconfig = {
|
|
454
|
+
compilerOptions: {
|
|
455
|
+
strict: true,
|
|
456
|
+
target: 'ES2022',
|
|
457
|
+
module: 'ES2022',
|
|
458
|
+
moduleResolution: 'bundler',
|
|
459
|
+
noEmit: true,
|
|
460
|
+
skipLibCheck: true,
|
|
461
|
+
},
|
|
462
|
+
include: ['_types.ts', '_client.ts'],
|
|
463
|
+
}
|
|
464
|
+
const { writeFileSync } = await import('node:fs')
|
|
465
|
+
writeFileSync(join(tmpDir, 'tsconfig.json'), JSON.stringify(tsconfig))
|
|
466
|
+
|
|
467
|
+
const { execSync } = await import('node:child_process')
|
|
468
|
+
// Use the project's tsc binary directly (temp dir has no node_modules)
|
|
469
|
+
const tscPath = join(process.cwd(), 'node_modules', '.bin', 'tsc')
|
|
470
|
+
// tsc --noEmit --project tsconfig.json should succeed with exit code 0
|
|
471
|
+
expect(() => {
|
|
472
|
+
execSync(`${tscPath} --noEmit --project ${join(tmpDir, 'tsconfig.json')}`, { stdio: 'pipe' })
|
|
473
|
+
}).not.toThrow()
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// ── namespaceTypes mode ───────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
describe('namespaceTypes: true', () => {
|
|
480
|
+
it('users.ts wraps types in namespace Users with inner route namespaces', async () => {
|
|
481
|
+
tmpDir = makeTmpDir()
|
|
482
|
+
await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
|
|
483
|
+
|
|
484
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
485
|
+
expect(content).toContain('export namespace Users {')
|
|
486
|
+
expect(content).toContain('export namespace GetUser {')
|
|
487
|
+
expect(content).toContain('export namespace UpdateUser {')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('users.ts uses qualified type references in callables', async () => {
|
|
491
|
+
tmpDir = makeTmpDir()
|
|
492
|
+
await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
|
|
493
|
+
|
|
494
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
495
|
+
expect(content).toContain('params: Users.GetUser.Params')
|
|
496
|
+
expect(content).toContain('Promise<Users.GetUser.Response>')
|
|
497
|
+
expect(content).toContain('params: Users.UpdateUser.Params')
|
|
498
|
+
expect(content).toContain('Promise<Users.UpdateUser.Response>')
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('events.ts wraps stream types in namespace', async () => {
|
|
502
|
+
tmpDir = makeTmpDir()
|
|
503
|
+
await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
|
|
504
|
+
|
|
505
|
+
const content = readFileSync(join(tmpDir, 'events.ts'), 'utf-8')
|
|
506
|
+
expect(content).toContain('export namespace Events {')
|
|
507
|
+
expect(content).toContain('export namespace WatchEvents {')
|
|
508
|
+
expect(content).toContain('TypedStream<Events.WatchEvents.Yield, Events.WatchEvents.Return>')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('does not contain flat-style prefixed types', async () => {
|
|
512
|
+
tmpDir = makeTmpDir()
|
|
513
|
+
await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
|
|
514
|
+
|
|
515
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
516
|
+
expect(content).not.toContain('export type GetUserParams')
|
|
517
|
+
expect(content).not.toContain('export type GetUserResponse')
|
|
518
|
+
expect(content).not.toContain('export type UpdateUserPathParams')
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('enumStyle: enum produces named enums inside namespace', async () => {
|
|
522
|
+
// Create envelope with an enum field
|
|
523
|
+
const enumRoute: RPCHttpRouteDoc = {
|
|
524
|
+
kind: 'rpc',
|
|
525
|
+
name: 'SetStatus',
|
|
526
|
+
path: '/status',
|
|
527
|
+
method: 'post',
|
|
528
|
+
scope: 'admin',
|
|
529
|
+
version: 1,
|
|
530
|
+
jsonSchema: {
|
|
531
|
+
body: {
|
|
532
|
+
type: 'object',
|
|
533
|
+
properties: {
|
|
534
|
+
status: { type: 'string', enum: ['active', 'pending', 'disabled'] },
|
|
535
|
+
},
|
|
536
|
+
required: ['status'],
|
|
537
|
+
},
|
|
538
|
+
response: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
properties: { ok: { type: 'boolean' } },
|
|
541
|
+
required: ['ok'],
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
const enumEnvelope: DocEnvelope = {
|
|
546
|
+
basePath: '/api',
|
|
547
|
+
headers: [],
|
|
548
|
+
errors: [],
|
|
549
|
+
routes: [enumRoute],
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
tmpDir = makeTmpDir()
|
|
553
|
+
await generateClient({
|
|
554
|
+
envelope: enumEnvelope,
|
|
555
|
+
outDir: tmpDir,
|
|
556
|
+
namespaceTypes: true,
|
|
557
|
+
ajsc: { enumStyle: 'enum' },
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
const content = readFileSync(join(tmpDir, 'admin.ts'), 'utf-8')
|
|
561
|
+
expect(content).toContain('export namespace Admin {')
|
|
562
|
+
expect(content).toContain('export enum Status')
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { emitClientRuntimeFile } from './emit-client-runtime.js'
|
|
3
|
+
|
|
4
|
+
import { CODEGEN_HEADER } from './constants.js'
|
|
5
|
+
|
|
6
|
+
describe('emitClientRuntimeFile', () => {
|
|
7
|
+
it('returns a string', async () => {
|
|
8
|
+
const result = await emitClientRuntimeFile()
|
|
9
|
+
expect(typeof result).toBe('string')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('contains the auto-generated header comment', async () => {
|
|
13
|
+
const result = await emitClientRuntimeFile()
|
|
14
|
+
expect(result.startsWith(CODEGEN_HEADER)).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("imports from './_types' (not './types.js' or 'ts-procedures/client')", async () => {
|
|
18
|
+
const result = await emitClientRuntimeFile()
|
|
19
|
+
expect(result).toContain("from './_types'")
|
|
20
|
+
expect(result).not.toContain("from './types.js'")
|
|
21
|
+
expect(result).not.toContain("from 'ts-procedures/client'")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('contains NO inter-file imports from sibling modules', async () => {
|
|
25
|
+
const result = await emitClientRuntimeFile()
|
|
26
|
+
const interFileImports = [
|
|
27
|
+
'./errors.js',
|
|
28
|
+
'./hooks.js',
|
|
29
|
+
'./request-builder.js',
|
|
30
|
+
'./call.js',
|
|
31
|
+
'./stream.js',
|
|
32
|
+
'./fetch-adapter.js',
|
|
33
|
+
]
|
|
34
|
+
for (const mod of interFileImports) {
|
|
35
|
+
expect(result).not.toContain(`from '${mod}'`)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('contains export function createClient', async () => {
|
|
40
|
+
const result = await emitClientRuntimeFile()
|
|
41
|
+
expect(result).toMatch(/export function createClient/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('contains export function createFetchAdapter', async () => {
|
|
45
|
+
const result = await emitClientRuntimeFile()
|
|
46
|
+
expect(result).toMatch(/export function createFetchAdapter/)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('contains export class ClientRequestError', async () => {
|
|
50
|
+
const result = await emitClientRuntimeFile()
|
|
51
|
+
expect(result).toMatch(/export class ClientRequestError/)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('contains export class ClientPathParamError', async () => {
|
|
55
|
+
const result = await emitClientRuntimeFile()
|
|
56
|
+
expect(result).toMatch(/export class ClientPathParamError/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('contains export class ClientStreamError', async () => {
|
|
60
|
+
const result = await emitClientRuntimeFile()
|
|
61
|
+
expect(result).toMatch(/export class ClientStreamError/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('contains export async function executeCall', async () => {
|
|
65
|
+
const result = await emitClientRuntimeFile()
|
|
66
|
+
expect(result).toMatch(/export async function executeCall/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('contains export async function executeStream', async () => {
|
|
70
|
+
const result = await emitClientRuntimeFile()
|
|
71
|
+
expect(result).toMatch(/export async function executeStream/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('contains export function createTypedStream', async () => {
|
|
75
|
+
const result = await emitClientRuntimeFile()
|
|
76
|
+
expect(result).toMatch(/export function createTypedStream/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('contains export interface FetchAdapterConfig', async () => {
|
|
80
|
+
const result = await emitClientRuntimeFile()
|
|
81
|
+
expect(result).toMatch(/export interface FetchAdapterConfig/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does NOT contain export type { re-export blocks from index.ts barrel', async () => {
|
|
85
|
+
const result = await emitClientRuntimeFile()
|
|
86
|
+
expect(result).not.toMatch(/export type \{[\s\S]*?\} from/)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does NOT contain orphaned "Barrel exports" section comment', async () => {
|
|
90
|
+
const result = await emitClientRuntimeFile()
|
|
91
|
+
expect(result).not.toContain('// ── Barrel exports')
|
|
92
|
+
})
|
|
93
|
+
})
|