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,316 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
2
|
+
import { generateClient } from './index.js'
|
|
3
|
+
import { runPipeline } from './pipeline.js'
|
|
4
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import type { DocEnvelope } from '../implementations/types.js'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Fixtures
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const envelope: DocEnvelope = {
|
|
14
|
+
basePath: '/api',
|
|
15
|
+
headers: [],
|
|
16
|
+
errors: [],
|
|
17
|
+
routes: [
|
|
18
|
+
{
|
|
19
|
+
kind: 'rpc',
|
|
20
|
+
name: 'GetUser',
|
|
21
|
+
path: '/users/1',
|
|
22
|
+
method: 'post',
|
|
23
|
+
scope: 'users',
|
|
24
|
+
version: 1,
|
|
25
|
+
jsonSchema: {
|
|
26
|
+
body: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: { id: { type: 'string' } },
|
|
29
|
+
required: ['id'],
|
|
30
|
+
},
|
|
31
|
+
response: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: { name: { type: 'string' } },
|
|
34
|
+
required: ['name'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
kind: 'rpc',
|
|
40
|
+
name: 'CreateInvoice',
|
|
41
|
+
path: '/billing/1',
|
|
42
|
+
method: 'post',
|
|
43
|
+
scope: 'billing',
|
|
44
|
+
version: 1,
|
|
45
|
+
jsonSchema: {
|
|
46
|
+
body: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: { amount: { type: 'number' } },
|
|
49
|
+
required: ['amount'],
|
|
50
|
+
},
|
|
51
|
+
response: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: { invoiceId: { type: 'string' } },
|
|
54
|
+
required: ['invoiceId'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function makeTmpDir(): string {
|
|
66
|
+
const dir = join(tmpdir(), `ts-proc-pipeline-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
67
|
+
mkdirSync(dir, { recursive: true })
|
|
68
|
+
return dir
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Tests
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe('generateClient pipeline', () => {
|
|
76
|
+
let tmpDir: string
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
80
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('creates scope file for each unique scope (users.ts, billing.ts)', async () => {
|
|
85
|
+
tmpDir = makeTmpDir()
|
|
86
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
87
|
+
|
|
88
|
+
expect(existsSync(join(tmpDir, 'users.ts'))).toBe(true)
|
|
89
|
+
expect(existsSync(join(tmpDir, 'billing.ts'))).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('creates index.ts', async () => {
|
|
93
|
+
tmpDir = makeTmpDir()
|
|
94
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
95
|
+
|
|
96
|
+
expect(existsSync(join(tmpDir, 'index.ts'))).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('users.ts contains bindUsersScope function', async () => {
|
|
100
|
+
tmpDir = makeTmpDir()
|
|
101
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
102
|
+
|
|
103
|
+
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
104
|
+
expect(content).toContain('bindUsersScope')
|
|
105
|
+
expect(content).toContain('GetUser')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('billing.ts contains bindBillingScope function', async () => {
|
|
109
|
+
tmpDir = makeTmpDir()
|
|
110
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
111
|
+
|
|
112
|
+
const content = readFileSync(join(tmpDir, 'billing.ts'), 'utf-8')
|
|
113
|
+
expect(content).toContain('bindBillingScope')
|
|
114
|
+
expect(content).toContain('CreateInvoice')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('index.ts exports from both scope files and has createScopeBindings', async () => {
|
|
118
|
+
tmpDir = makeTmpDir()
|
|
119
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
120
|
+
|
|
121
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
122
|
+
expect(content).toContain("from './users'")
|
|
123
|
+
expect(content).toContain("from './billing'")
|
|
124
|
+
expect(content).toContain('createScopeBindings')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('creates outDir if it does not exist', async () => {
|
|
128
|
+
tmpDir = makeTmpDir()
|
|
129
|
+
const nested = join(tmpDir, 'nested', 'output')
|
|
130
|
+
await generateClient({ envelope, outDir: nested })
|
|
131
|
+
|
|
132
|
+
expect(existsSync(join(nested, 'index.ts'))).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('all generated files have the auto-generated header comment', async () => {
|
|
136
|
+
tmpDir = makeTmpDir()
|
|
137
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
138
|
+
|
|
139
|
+
for (const file of ['users.ts', 'billing.ts', 'index.ts']) {
|
|
140
|
+
const content = readFileSync(join(tmpDir, file), 'utf-8')
|
|
141
|
+
expect(content).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('generated files include source hash comment', async () => {
|
|
146
|
+
tmpDir = makeTmpDir()
|
|
147
|
+
await generateClient({ envelope, outDir: tmpDir })
|
|
148
|
+
|
|
149
|
+
for (const file of ['users.ts', 'billing.ts', 'index.ts']) {
|
|
150
|
+
const content = readFileSync(join(tmpDir, file), 'utf-8')
|
|
151
|
+
const lines = content.split('\n')
|
|
152
|
+
expect(lines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('dry-run mode does not create files on disk', async () => {
|
|
157
|
+
tmpDir = makeTmpDir()
|
|
158
|
+
const dryDir = join(tmpDir, 'dry-output')
|
|
159
|
+
await runPipeline({ envelope, outDir: dryDir, dryRun: true })
|
|
160
|
+
expect(existsSync(dryDir)).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('dry-run mode returns generated file entries', async () => {
|
|
164
|
+
tmpDir = makeTmpDir()
|
|
165
|
+
const dryDir = join(tmpDir, 'dry-output')
|
|
166
|
+
const files = await runPipeline({ envelope, outDir: dryDir, dryRun: true })
|
|
167
|
+
expect(files.length).toBe(3) // users.ts, billing.ts, index.ts
|
|
168
|
+
const paths = files.map((f) => f.path)
|
|
169
|
+
expect(paths).toContain(join(dryDir, 'users.ts'))
|
|
170
|
+
expect(paths).toContain(join(dryDir, 'billing.ts'))
|
|
171
|
+
expect(paths).toContain(join(dryDir, 'index.ts'))
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
expect(file.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('selfContained: true produces _types.ts and _client.ts', async () => {
|
|
178
|
+
tmpDir = makeTmpDir()
|
|
179
|
+
const files = await runPipeline({ envelope, outDir: tmpDir, selfContained: true })
|
|
180
|
+
const paths = files.map((f) => f.path)
|
|
181
|
+
expect(paths).toContain(join(tmpDir, '_types.ts'))
|
|
182
|
+
expect(paths).toContain(join(tmpDir, '_client.ts'))
|
|
183
|
+
|
|
184
|
+
// Verify both files have auto-generated header and source hash
|
|
185
|
+
const typesFile = files.find((f) => f.path.endsWith('_types.ts'))!
|
|
186
|
+
const clientFile = files.find((f) => f.path.endsWith('_client.ts'))!
|
|
187
|
+
expect(typesFile.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
|
|
188
|
+
expect(clientFile.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
|
|
189
|
+
const typesLines = typesFile.code.split('\n')
|
|
190
|
+
expect(typesLines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
|
|
191
|
+
const clientLines = clientFile.code.split('\n')
|
|
192
|
+
expect(clientLines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('selfContained: true makes scope files import from ./_types', async () => {
|
|
196
|
+
tmpDir = makeTmpDir()
|
|
197
|
+
const files = await runPipeline({ envelope, outDir: tmpDir, selfContained: true })
|
|
198
|
+
const scopeFiles = files.filter((f) => !f.path.endsWith('index.ts') && !f.path.startsWith(join(tmpDir, '_')))
|
|
199
|
+
expect(scopeFiles.length).toBeGreaterThan(0)
|
|
200
|
+
for (const file of scopeFiles) {
|
|
201
|
+
expect(file.code).toContain("from './_types'")
|
|
202
|
+
expect(file.code).not.toContain("from 'ts-procedures/client'")
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('selfContained + clientImportPath emits a console.warn', async () => {
|
|
207
|
+
tmpDir = makeTmpDir()
|
|
208
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
209
|
+
try {
|
|
210
|
+
await runPipeline({ envelope, outDir: tmpDir, selfContained: true, clientImportPath: './my-client' })
|
|
211
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
212
|
+
'[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types'
|
|
213
|
+
)
|
|
214
|
+
} finally {
|
|
215
|
+
warnSpy.mockRestore()
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('serviceName + namespaceTypes produces prefixed error namespace in _errors.ts', async () => {
|
|
220
|
+
tmpDir = makeTmpDir()
|
|
221
|
+
const envelopeWithErrors: DocEnvelope = {
|
|
222
|
+
...envelope,
|
|
223
|
+
errors: [
|
|
224
|
+
{
|
|
225
|
+
name: 'ProcedureError',
|
|
226
|
+
statusCode: 500,
|
|
227
|
+
description: 'Handler error',
|
|
228
|
+
schema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: { name: { type: 'string' }, message: { type: 'string' } },
|
|
231
|
+
required: ['name', 'message'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
}
|
|
236
|
+
await generateClient({ envelope: envelopeWithErrors, outDir: tmpDir, serviceName: 'Auth', namespaceTypes: true })
|
|
237
|
+
|
|
238
|
+
const errorsContent = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
|
|
239
|
+
expect(errorsContent).toContain('export namespace AuthErrors {')
|
|
240
|
+
expect(errorsContent).not.toContain('export namespace Errors {')
|
|
241
|
+
expect(errorsContent).toContain('AuthProcedureErrorUnion')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('serviceName prefixes ProcedureErrorUnion in flat mode _errors.ts', async () => {
|
|
245
|
+
tmpDir = makeTmpDir()
|
|
246
|
+
const envelopeWithErrors: DocEnvelope = {
|
|
247
|
+
...envelope,
|
|
248
|
+
errors: [
|
|
249
|
+
{
|
|
250
|
+
name: 'ProcedureError',
|
|
251
|
+
statusCode: 500,
|
|
252
|
+
description: 'Handler error',
|
|
253
|
+
schema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: { name: { type: 'string' }, message: { type: 'string' } },
|
|
256
|
+
required: ['name', 'message'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
}
|
|
261
|
+
await generateClient({ envelope: envelopeWithErrors, outDir: tmpDir, serviceName: 'Auth' })
|
|
262
|
+
|
|
263
|
+
const errorsContent = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
|
|
264
|
+
expect(errorsContent).toContain('export type AuthProcedureErrorUnion =')
|
|
265
|
+
expect(errorsContent).not.toContain('export type ProcedureErrorUnion =')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('rejects invalid serviceName', async () => {
|
|
269
|
+
tmpDir = makeTmpDir()
|
|
270
|
+
await expect(
|
|
271
|
+
generateClient({ envelope, outDir: tmpDir, serviceName: '---' })
|
|
272
|
+
).rejects.toThrow('empty')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('rejects serviceName starting with a digit', async () => {
|
|
276
|
+
tmpDir = makeTmpDir()
|
|
277
|
+
await expect(
|
|
278
|
+
generateClient({ envelope, outDir: tmpDir, serviceName: '123svc' })
|
|
279
|
+
).rejects.toThrow('starts with a digit')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('serviceName generates custom factory function name in index.ts', async () => {
|
|
283
|
+
tmpDir = makeTmpDir()
|
|
284
|
+
await generateClient({ envelope, outDir: tmpDir, serviceName: 'Auth' })
|
|
285
|
+
|
|
286
|
+
const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
|
|
287
|
+
expect(content).toContain('createAuthBindings')
|
|
288
|
+
expect(content).not.toContain('createScopeBindings')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('throws when a scope key collides with _types in selfContained mode', async () => {
|
|
292
|
+
tmpDir = makeTmpDir()
|
|
293
|
+
const collisionEnvelope: DocEnvelope = {
|
|
294
|
+
...envelope,
|
|
295
|
+
routes: [
|
|
296
|
+
{
|
|
297
|
+
kind: 'rpc',
|
|
298
|
+
name: 'GetUser',
|
|
299
|
+
path: '/users/1',
|
|
300
|
+
method: 'post',
|
|
301
|
+
scope: '_types',
|
|
302
|
+
version: 1,
|
|
303
|
+
jsonSchema: {
|
|
304
|
+
body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
305
|
+
response: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
await expect(
|
|
311
|
+
runPipeline({ envelope: collisionEnvelope, outDir: tmpDir, selfContained: true })
|
|
312
|
+
).rejects.toThrow(
|
|
313
|
+
'[ts-procedures-codegen] Scope "_types" conflicts with self-contained mode reserved filename "_types.ts". Rename the scope to avoid collision.'
|
|
314
|
+
)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import type { DocEnvelope } from '../implementations/types.js'
|
|
5
|
+
import type { AjscOptions } from './emit-types.js'
|
|
6
|
+
import { groupRoutesByScope } from './group-routes.js'
|
|
7
|
+
import { emitScopeFile } from './emit-scope.js'
|
|
8
|
+
import { emitIndexFile } from './emit-index.js'
|
|
9
|
+
import { emitErrorsFile } from './emit-errors.js'
|
|
10
|
+
import { emitClientTypesFile } from './emit-client-types.js'
|
|
11
|
+
import { emitClientRuntimeFile } from './emit-client-runtime.js'
|
|
12
|
+
import { validateServiceName } from './naming.js'
|
|
13
|
+
|
|
14
|
+
export interface PipelineOptions {
|
|
15
|
+
envelope: DocEnvelope
|
|
16
|
+
outDir: string
|
|
17
|
+
ajsc?: AjscOptions
|
|
18
|
+
clientImportPath?: string
|
|
19
|
+
dryRun?: boolean
|
|
20
|
+
namespaceTypes?: boolean
|
|
21
|
+
selfContained?: boolean
|
|
22
|
+
serviceName?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GeneratedFile {
|
|
26
|
+
path: string
|
|
27
|
+
code: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runPipeline(options: PipelineOptions): Promise<GeneratedFile[]> {
|
|
31
|
+
const { envelope, outDir, ajsc: ajscOpts, dryRun = false, namespaceTypes = false, selfContained = false, serviceName } = options
|
|
32
|
+
if (serviceName != null) {
|
|
33
|
+
validateServiceName(serviceName)
|
|
34
|
+
}
|
|
35
|
+
const clientImportPath = selfContained ? './_types' : options.clientImportPath
|
|
36
|
+
if (selfContained && options.clientImportPath != null) {
|
|
37
|
+
console.warn('[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hash = createHash('md5').update(JSON.stringify(envelope)).digest('hex')
|
|
41
|
+
const hashComment = `// Source hash: ${hash}`
|
|
42
|
+
|
|
43
|
+
const groups = groupRoutesByScope(envelope.routes)
|
|
44
|
+
const groupArray = Array.from(groups.values())
|
|
45
|
+
|
|
46
|
+
if (selfContained) {
|
|
47
|
+
for (const group of groupArray) {
|
|
48
|
+
if (group.scopeKey === '_types' || group.scopeKey === '_client') {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[ts-procedures-codegen] Scope "${group.scopeKey}" conflicts with self-contained mode reserved filename "${group.scopeKey}.ts". Rename the scope to avoid collision.`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const files: GeneratedFile[] = []
|
|
57
|
+
|
|
58
|
+
for (const group of groupArray) {
|
|
59
|
+
const rawCode = await emitScopeFile(group, { ajsc: ajscOpts, clientImportPath, namespaceTypes })
|
|
60
|
+
const lines = rawCode.split('\n')
|
|
61
|
+
lines.splice(1, 0, hashComment)
|
|
62
|
+
const code = lines.join('\n')
|
|
63
|
+
files.push({ path: join(outDir, `${group.scopeKey}.ts`), code })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const errorsCode = await emitErrorsFile(envelope.errors, { ajsc: ajscOpts, clientImportPath, namespaceTypes, serviceName })
|
|
67
|
+
const hasErrors = errorsCode != null
|
|
68
|
+
if (errorsCode != null) {
|
|
69
|
+
const errorsLines = errorsCode.split('\n')
|
|
70
|
+
errorsLines.splice(1, 0, hashComment)
|
|
71
|
+
const errorsWithHash = errorsLines.join('\n')
|
|
72
|
+
files.push({ path: join(outDir, '_errors.ts'), code: errorsWithHash })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rawIndexCode = emitIndexFile(groupArray, { clientImportPath, hasErrors, serviceName })
|
|
76
|
+
const indexLines = rawIndexCode.split('\n')
|
|
77
|
+
indexLines.splice(1, 0, hashComment)
|
|
78
|
+
const indexCode = indexLines.join('\n')
|
|
79
|
+
files.push({ path: join(outDir, 'index.ts'), code: indexCode })
|
|
80
|
+
|
|
81
|
+
if (selfContained) {
|
|
82
|
+
const rawTypesCode = await emitClientTypesFile()
|
|
83
|
+
const typesLines = rawTypesCode.split('\n')
|
|
84
|
+
typesLines.splice(1, 0, hashComment)
|
|
85
|
+
const typesCode = typesLines.join('\n')
|
|
86
|
+
files.push({ path: join(outDir, '_types.ts'), code: typesCode })
|
|
87
|
+
|
|
88
|
+
const rawClientCode = await emitClientRuntimeFile()
|
|
89
|
+
const clientLines = rawClientCode.split('\n')
|
|
90
|
+
clientLines.splice(1, 0, hashComment)
|
|
91
|
+
const clientCode = clientLines.join('\n')
|
|
92
|
+
files.push({ path: join(outDir, '_client.ts'), code: clientCode })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (dryRun) {
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const bytes = Buffer.byteLength(file.code, 'utf-8')
|
|
98
|
+
console.log(`[dry-run] Would write: ${file.path} (${bytes} bytes)`)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
await mkdir(outDir, { recursive: true })
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
await writeFile(file.path, file.code, 'utf-8')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files
|
|
108
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { writeFile, mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { resolveEnvelope } from './resolve-envelope.js'
|
|
6
|
+
import type { DocEnvelope } from '../implementations/types.js'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Fixtures
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const minimalEnvelope: DocEnvelope = {
|
|
13
|
+
basePath: '/api',
|
|
14
|
+
headers: [],
|
|
15
|
+
errors: [],
|
|
16
|
+
routes: [
|
|
17
|
+
{
|
|
18
|
+
kind: 'rpc',
|
|
19
|
+
name: 'Ping',
|
|
20
|
+
path: '/ping/1',
|
|
21
|
+
method: 'post',
|
|
22
|
+
scope: 'ping',
|
|
23
|
+
version: 1,
|
|
24
|
+
jsonSchema: {},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Tests
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
describe('resolveEnvelope', () => {
|
|
34
|
+
it('accepts a DocEnvelope object directly', async () => {
|
|
35
|
+
const result = await resolveEnvelope({ envelope: minimalEnvelope })
|
|
36
|
+
expect(result).toEqual(minimalEnvelope)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reads a DocEnvelope from a JSON file', async () => {
|
|
40
|
+
const dir = await mkdtemp(join(tmpdir(), 'ts-proc-test-'))
|
|
41
|
+
const filePath = join(dir, 'envelope.json')
|
|
42
|
+
try {
|
|
43
|
+
await writeFile(filePath, JSON.stringify(minimalEnvelope), 'utf8')
|
|
44
|
+
const result = await resolveEnvelope({ file: filePath })
|
|
45
|
+
expect(result).toEqual(minimalEnvelope)
|
|
46
|
+
} finally {
|
|
47
|
+
await rm(dir, { recursive: true, force: true })
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('throws when routes array is empty (envelope input)', async () => {
|
|
52
|
+
const empty: DocEnvelope = { basePath: '', headers: [], errors: [], routes: [] }
|
|
53
|
+
await expect(resolveEnvelope({ envelope: empty })).rejects.toThrow(/routes/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws when routes array is empty (file input)', async () => {
|
|
57
|
+
const dir = await mkdtemp(join(tmpdir(), 'ts-proc-test-'))
|
|
58
|
+
const filePath = join(dir, 'empty.json')
|
|
59
|
+
const empty: DocEnvelope = { basePath: '', headers: [], errors: [], routes: [] }
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(filePath, JSON.stringify(empty), 'utf8')
|
|
62
|
+
await expect(resolveEnvelope({ file: filePath })).rejects.toThrow(/routes/)
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(dir, { recursive: true, force: true })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('throws when no input source is provided', async () => {
|
|
69
|
+
await expect(resolveEnvelope({})).rejects.toThrow(/input/)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// URL test skipped — requires network mock
|
|
73
|
+
it.skip('fetches a DocEnvelope from a URL', async () => {
|
|
74
|
+
// Would require mocking fetch
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import type { DocEnvelope } from '../implementations/types.js'
|
|
3
|
+
|
|
4
|
+
export interface ResolveInput {
|
|
5
|
+
url?: string
|
|
6
|
+
file?: string
|
|
7
|
+
envelope?: DocEnvelope
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function resolveEnvelope(input: ResolveInput): Promise<DocEnvelope> {
|
|
11
|
+
let envelope: DocEnvelope
|
|
12
|
+
|
|
13
|
+
if (input.envelope !== undefined) {
|
|
14
|
+
envelope = input.envelope
|
|
15
|
+
} else if (input.file !== undefined) {
|
|
16
|
+
const raw = await readFile(input.file, 'utf8')
|
|
17
|
+
envelope = JSON.parse(raw) as DocEnvelope
|
|
18
|
+
} else if (input.url !== undefined) {
|
|
19
|
+
let response: Response
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(input.url)
|
|
22
|
+
} catch (err) {
|
|
23
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
24
|
+
throw new Error(`[ts-procedures-codegen] Failed to connect to ${input.url}: ${msg}`)
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`[ts-procedures-codegen] ${input.url} returned HTTP ${response.status} ${response.statusText}`)
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
envelope = (await response.json()) as DocEnvelope
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(`[ts-procedures-codegen] ${input.url} returned non-JSON response. Expected a DocEnvelope JSON object.`)
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'resolveEnvelope: no input source provided. Pass one of: envelope, file, or url.'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[ts-procedures-codegen] Invalid DocEnvelope: expected an object with a "routes" array, got ${typeof envelope}`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(envelope.routes)) {
|
|
47
|
+
const keys = Object.keys(envelope).join(', ')
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[ts-procedures-codegen] DocEnvelope is missing "routes" array. Found keys: [${keys}]. ` +
|
|
50
|
+
`Make sure your server uses DocRegistry.toJSON() to produce the envelope.`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (envelope.routes.length === 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'[ts-procedures-codegen] DocEnvelope has an empty "routes" array. Register at least one procedure before generating.'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return envelope
|
|
61
|
+
}
|