ts-procedures 7.3.0 → 8.1.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/README.md +65 -3
- package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +104 -53
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
- package/agent_config/copilot/copilot-instructions.md +34 -48
- package/agent_config/cursor/cursorrules +34 -48
- package/build/client/call.js +4 -1
- package/build/client/call.js.map +1 -1
- package/build/client/call.test.js +23 -0
- package/build/client/call.test.js.map +1 -1
- package/build/client/fetch-adapter.js +3 -1
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/fetch-adapter.test.js +11 -1
- package/build/client/fetch-adapter.test.js.map +1 -1
- package/build/client/index.test.js +7 -7
- package/build/client/index.test.js.map +1 -1
- package/build/client/request-builder.d.ts +1 -1
- package/build/client/request-builder.js +2 -2
- package/build/client/request-builder.js.map +1 -1
- package/build/client/stream.js +13 -2
- package/build/client/stream.js.map +1 -1
- package/build/client/stream.test.js +32 -7
- package/build/client/stream.test.js.map +1 -1
- package/build/client/typed-error-dispatch.test.js +8 -92
- package/build/client/typed-error-dispatch.test.js.map +1 -1
- package/build/client/types.d.ts +21 -3
- package/build/codegen/bin/cli.js +0 -0
- package/build/codegen/e2e.test.js +418 -23
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +1 -1
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/emit-scope.js +351 -55
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +540 -110
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/emit-types.d.ts +6 -2
- package/build/codegen/emit-types.js +81 -20
- package/build/codegen/emit-types.js.map +1 -1
- package/build/codegen/emit-types.test.js +70 -1
- package/build/codegen/emit-types.test.js.map +1 -1
- package/build/codegen/pipeline.test.js +7 -7
- package/build/codegen/pipeline.test.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/resolve-envelope.test.js +5 -5
- package/build/codegen/resolve-envelope.test.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
- package/build/codegen/targets/_shared/route-slots.js +49 -8
- package/build/codegen/targets/_shared/route-slots.js.map +1 -1
- package/build/codegen/targets/_shared/route-slots.test.js +99 -26
- package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +6 -0
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/swift/access-level.test.js +8 -11
- package/build/codegen/targets/swift/access-level.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
- package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
- package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
- package/build/codegen/targets/swift/integration.test.js +6 -0
- package/build/codegen/targets/swift/integration.test.js.map +1 -1
- package/build/create-http-stream.d.ts +58 -0
- package/build/create-http-stream.js +122 -0
- package/build/create-http-stream.js.map +1 -0
- package/build/create-http-stream.test.js +88 -0
- package/build/create-http-stream.test.js.map +1 -0
- package/build/create-http.d.ts +49 -0
- package/build/create-http.js +108 -0
- package/build/create-http.js.map +1 -0
- package/build/create-http.test.js +137 -0
- package/build/create-http.test.js.map +1 -0
- package/build/create-stream.d.ts +35 -0
- package/build/create-stream.js +123 -0
- package/build/create-stream.js.map +1 -0
- package/build/create-stream.test.js +428 -0
- package/build/create-stream.test.js.map +1 -0
- package/build/create.d.ts +28 -0
- package/build/create.js +82 -0
- package/build/create.js.map +1 -0
- package/build/create.test.js +483 -0
- package/build/create.test.js.map +1 -0
- package/build/exports.d.ts +2 -0
- package/build/implementations/http/astro/index.test.js +20 -12
- package/build/implementations/http/astro/index.test.js.map +1 -1
- package/build/implementations/http/doc-registry.js +1 -1
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +36 -5
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-dispatch.d.ts +76 -0
- package/build/implementations/http/error-dispatch.js +77 -0
- package/build/implementations/http/error-dispatch.js.map +1 -0
- package/build/implementations/http/error-dispatch.test.js +254 -0
- package/build/implementations/http/error-dispatch.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +5 -5
- package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-doc.js +42 -0
- package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
- package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
- package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
- package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
- package/build/implementations/http/hono/docs/stream-doc.js +42 -0
- package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http-stream.js +123 -0
- package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
- package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.d.ts +10 -0
- package/build/implementations/http/hono/handlers/http.js +115 -0
- package/build/implementations/http/hono/handlers/http.js.map +1 -0
- package/build/implementations/http/hono/handlers/http.test.js +118 -0
- package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
- package/build/implementations/http/hono/handlers/rpc.js +32 -0
- package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
- package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
- package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
- package/build/implementations/http/hono/handlers/stream.js +147 -0
- package/build/implementations/http/hono/handlers/stream.js.map +1 -0
- package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
- package/build/implementations/http/hono/handlers/stream.test.js +177 -0
- package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
- package/build/implementations/http/hono/index.d.ts +57 -0
- package/build/implementations/http/hono/index.js +149 -0
- package/build/implementations/http/hono/index.js.map +1 -0
- package/build/implementations/http/hono/index.test.d.ts +1 -0
- package/build/implementations/http/hono/index.test.js +274 -0
- package/build/implementations/http/hono/index.test.js.map +1 -0
- package/build/implementations/http/hono/path.d.ts +17 -0
- package/build/implementations/http/hono/path.js +39 -0
- package/build/implementations/http/hono/path.js.map +1 -0
- package/build/implementations/http/hono/path.test.d.ts +1 -0
- package/build/implementations/http/hono/path.test.js +83 -0
- package/build/implementations/http/hono/path.test.js.map +1 -0
- package/build/implementations/http/hono/types.d.ts +51 -0
- package/build/implementations/http/hono/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.js +6 -96
- package/build/implementations/http/on-request-error.test.js.map +1 -1
- package/build/implementations/http/route-errors.test.js +11 -59
- package/build/implementations/http/route-errors.test.js.map +1 -1
- package/build/implementations/types.d.ts +43 -9
- package/build/index.d.ts +124 -124
- package/build/index.js +10 -221
- package/build/index.js.map +1 -1
- package/build/index.test.js +20 -919
- package/build/index.test.js.map +1 -1
- package/build/migration.test.d.ts +1 -0
- package/build/migration.test.js +34 -0
- package/build/migration.test.js.map +1 -0
- package/build/schema/compute-schema.d.ts +11 -3
- package/build/schema/compute-schema.js +13 -7
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +11 -3
- package/build/schema/parser.js +49 -9
- package/build/schema/parser.js.map +1 -1
- package/build/stack-utils.js +8 -0
- package/build/stack-utils.js.map +1 -1
- package/build/types.d.ts +142 -0
- package/build/types.js.map +1 -0
- package/docs/astro-adapter.md +5 -5
- package/docs/core.md +15 -17
- package/docs/http-integrations.md +83 -170
- package/docs/streaming.md +3 -60
- package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
- package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
- package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
- package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
- package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
- package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
- package/package.json +4 -22
- package/src/client/call.test.ts +26 -0
- package/src/client/call.ts +4 -1
- package/src/client/fetch-adapter.test.ts +14 -1
- package/src/client/fetch-adapter.ts +3 -1
- package/src/client/index.test.ts +7 -7
- package/src/client/request-builder.ts +2 -2
- package/src/client/stream.test.ts +39 -7
- package/src/client/stream.ts +16 -2
- package/src/client/typed-error-dispatch.test.ts +7 -97
- package/src/client/types.ts +21 -3
- package/src/codegen/__fixtures__/users-envelope.json +119 -38
- package/src/codegen/e2e.test.ts +452 -24
- package/src/codegen/emit-errors.integration.test.ts +1 -1
- package/src/codegen/emit-scope.test.ts +581 -110
- package/src/codegen/emit-scope.ts +390 -61
- package/src/codegen/emit-types.test.ts +73 -1
- package/src/codegen/emit-types.ts +82 -21
- package/src/codegen/pipeline.test.ts +7 -7
- package/src/codegen/resolve-envelope.test.ts +5 -5
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
- package/src/codegen/targets/_shared/route-slots.ts +48 -11
- package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
- package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
- package/src/codegen/targets/kotlin/integration.test.ts +19 -0
- package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
- package/src/codegen/targets/swift/access-level.test.ts +8 -11
- package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
- package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
- package/src/codegen/targets/swift/integration.test.ts +17 -0
- package/src/create-http-stream.test.ts +97 -0
- package/src/create-http-stream.ts +191 -0
- package/src/create-http.test.ts +163 -0
- package/src/create-http.ts +211 -0
- package/src/create-stream.test.ts +565 -0
- package/src/create-stream.ts +228 -0
- package/src/create.test.ts +658 -0
- package/src/create.ts +172 -0
- package/src/exports.ts +2 -0
- package/src/implementations/http/README.md +135 -95
- package/src/implementations/http/astro/README.md +4 -5
- package/src/implementations/http/astro/index.test.ts +25 -18
- package/src/implementations/http/doc-registry.test.ts +42 -5
- package/src/implementations/http/doc-registry.ts +1 -1
- package/src/implementations/http/error-dispatch.test.ts +283 -0
- package/src/implementations/http/error-dispatch.ts +176 -0
- package/src/implementations/http/error-taxonomy.ts +5 -5
- package/src/implementations/http/hono/docs/http-doc.ts +43 -0
- package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
- package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
- package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
- package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
- package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
- package/src/implementations/http/hono/handlers/http.test.ts +130 -0
- package/src/implementations/http/hono/handlers/http.ts +147 -0
- package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
- package/src/implementations/http/hono/handlers/rpc.ts +54 -0
- package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
- package/src/implementations/http/hono/handlers/stream.ts +208 -0
- package/src/implementations/http/hono/index.test.ts +329 -0
- package/src/implementations/http/hono/index.ts +204 -0
- package/src/implementations/http/hono/path.test.ts +96 -0
- package/src/implementations/http/hono/path.ts +59 -0
- package/src/implementations/http/hono/types.ts +93 -0
- package/src/implementations/http/on-request-error.test.ts +10 -116
- package/src/implementations/http/route-errors.test.ts +11 -77
- package/src/implementations/types.ts +44 -9
- package/src/index.test.ts +22 -1249
- package/src/index.ts +49 -485
- package/src/migration.test.ts +48 -0
- package/src/schema/compute-schema.ts +26 -12
- package/src/schema/parser.ts +62 -12
- package/src/stack-utils.ts +8 -0
- package/src/types.ts +133 -0
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/express-rpc/index.d.ts +0 -125
- package/build/implementations/http/express-rpc/index.js +0 -216
- package/build/implementations/http/express-rpc/index.js.map +0 -1
- package/build/implementations/http/express-rpc/index.test.js +0 -684
- package/build/implementations/http/express-rpc/index.test.js.map +0 -1
- package/build/implementations/http/express-rpc/types.d.ts +0 -11
- package/build/implementations/http/express-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-api/index.d.ts +0 -151
- package/build/implementations/http/hono-api/index.js +0 -344
- package/build/implementations/http/hono-api/index.js.map +0 -1
- package/build/implementations/http/hono-api/index.test.js +0 -992
- package/build/implementations/http/hono-api/index.test.js.map +0 -1
- package/build/implementations/http/hono-api/types.d.ts +0 -13
- package/build/implementations/http/hono-api/types.js.map +0 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.d.ts +0 -130
- package/build/implementations/http/hono-rpc/index.js +0 -209
- package/build/implementations/http/hono-rpc/index.js.map +0 -1
- package/build/implementations/http/hono-rpc/index.test.js +0 -828
- package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
- package/build/implementations/http/hono-rpc/types.d.ts +0 -11
- package/build/implementations/http/hono-rpc/types.js +0 -2
- package/build/implementations/http/hono-rpc/types.js.map +0 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
- package/build/implementations/http/hono-stream/index.d.ts +0 -171
- package/build/implementations/http/hono-stream/index.js +0 -415
- package/build/implementations/http/hono-stream/index.js.map +0 -1
- package/build/implementations/http/hono-stream/index.test.js +0 -1383
- package/build/implementations/http/hono-stream/index.test.js.map +0 -1
- package/build/implementations/http/hono-stream/types.d.ts +0 -15
- package/build/implementations/http/hono-stream/types.js +0 -2
- package/build/implementations/http/hono-stream/types.js.map +0 -1
- package/src/implementations/http/express-rpc/README.md +0 -280
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
- package/src/implementations/http/express-rpc/index.test.ts +0 -957
- package/src/implementations/http/express-rpc/index.ts +0 -327
- package/src/implementations/http/express-rpc/types.ts +0 -16
- package/src/implementations/http/hono-api/README.md +0 -284
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
- package/src/implementations/http/hono-api/index.test.ts +0 -1341
- package/src/implementations/http/hono-api/index.ts +0 -519
- package/src/implementations/http/hono-api/types.ts +0 -16
- package/src/implementations/http/hono-rpc/README.md +0 -357
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
- package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
- package/src/implementations/http/hono-rpc/index.ts +0 -320
- package/src/implementations/http/hono-rpc/types.ts +0 -16
- package/src/implementations/http/hono-stream/README.md +0 -559
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
- package/src/implementations/http/hono-stream/index.test.ts +0 -1804
- package/src/implementations/http/hono-stream/index.ts +0 -622
- package/src/implementations/http/hono-stream/types.ts +0 -20
- /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
- /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
- /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
- /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
- /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
|
@@ -3,12 +3,14 @@ import type {
|
|
|
3
3
|
RPCHttpRouteDoc,
|
|
4
4
|
APIHttpRouteDoc,
|
|
5
5
|
StreamHttpRouteDoc,
|
|
6
|
+
HttpStreamRouteDoc,
|
|
6
7
|
} from '../implementations/types.js'
|
|
7
8
|
import {
|
|
8
9
|
jsonSchemaToTypeString,
|
|
9
10
|
jsonSchemaToTypeBody,
|
|
10
11
|
jsonSchemaToExtractedTypes,
|
|
11
12
|
renameExtractedTypes,
|
|
13
|
+
extractedDeclName,
|
|
12
14
|
type AjscOptions,
|
|
13
15
|
type ExtractedTypeOutput,
|
|
14
16
|
} from './emit-types.js'
|
|
@@ -58,7 +60,8 @@ interface EmitRouteContext {
|
|
|
58
60
|
* This provides backward compatibility with servers running older ts-procedures
|
|
59
61
|
* versions that don't set `kind` on route docs.
|
|
60
62
|
*/
|
|
61
|
-
function inferRouteKind(route: Record<string, unknown>): 'rpc' | 'api' | 'stream' {
|
|
63
|
+
function inferRouteKind(route: Record<string, unknown>): 'rpc' | 'api' | 'stream' | 'http-stream' {
|
|
64
|
+
if ('streamMode' in route && 'fullPath' in route) return 'http-stream'
|
|
62
65
|
if ('streamMode' in route) return 'stream'
|
|
63
66
|
if ('fullPath' in route) return 'api'
|
|
64
67
|
return 'rpc'
|
|
@@ -107,6 +110,45 @@ function indent(text: string, prefix: string): string {
|
|
|
107
110
|
return text.split('\n').map((line) => (line ? prefix + line : line)).join('\n')
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Tracks extracted declarations emitted into a single namespace, guarding
|
|
115
|
+
* against duplicate identifiers (defense-in-depth on top of the rename pass).
|
|
116
|
+
*
|
|
117
|
+
* - Exact-string duplicates (the same sub-type extracted from two schemas) are
|
|
118
|
+
* silently skipped.
|
|
119
|
+
* - A same-name-but-different-body declaration is a genuine collision the
|
|
120
|
+
* rename pass failed to resolve; emitting it would produce an opaque
|
|
121
|
+
* `TS2300: Duplicate identifier` in the consumer's build. We fail fast at
|
|
122
|
+
* codegen with a message that names the offending identifier instead.
|
|
123
|
+
*
|
|
124
|
+
* Returns the indented declaration line to push, or `null` when it should be
|
|
125
|
+
* skipped (exact duplicate).
|
|
126
|
+
*/
|
|
127
|
+
class DeclarationCollector {
|
|
128
|
+
private readonly seenStrings = new Set<string>()
|
|
129
|
+
private readonly seenNames = new Map<string, string>()
|
|
130
|
+
|
|
131
|
+
constructor(private readonly context: string) {}
|
|
132
|
+
|
|
133
|
+
/** Returns the indented line to emit, or `null` for an exact duplicate. */
|
|
134
|
+
accept(decl: string, indentPrefix: string): string | null {
|
|
135
|
+
if (this.seenStrings.has(decl)) return null
|
|
136
|
+
const name = extractedDeclName(decl)
|
|
137
|
+
if (name != null) {
|
|
138
|
+
if (this.seenNames.has(name)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`[ts-procedures-codegen] duplicate identifier '${name}' while emitting ${this.context}. ` +
|
|
141
|
+
`An extracted sub-type collided with another of the same name and could not be renamed. ` +
|
|
142
|
+
`This is a codegen bug — please report it with the offending schema.`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
this.seenNames.set(name, decl)
|
|
146
|
+
}
|
|
147
|
+
this.seenStrings.add(decl)
|
|
148
|
+
return indent(decl, indentPrefix)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
110
152
|
interface NamedType {
|
|
111
153
|
/** Short name for namespace mode (e.g., 'Params', 'Response'). */
|
|
112
154
|
shortName: string
|
|
@@ -142,7 +184,7 @@ async function formatTypes(
|
|
|
142
184
|
|
|
143
185
|
if (ctx.namespaceTypes) {
|
|
144
186
|
const nsLines: string[] = []
|
|
145
|
-
const
|
|
187
|
+
const collector = new DeclarationCollector(`namespace ${routePascal}`)
|
|
146
188
|
|
|
147
189
|
// Pre-reserve every name the route will declare itself (each shortName +
|
|
148
190
|
// any caller-supplied extras). Extracted sub-types whose names land in
|
|
@@ -163,12 +205,10 @@ async function formatTypes(
|
|
|
163
205
|
|
|
164
206
|
const result = renameExtractedTypes(rawResult, taken)
|
|
165
207
|
|
|
166
|
-
// Collect extracted sub-types (
|
|
208
|
+
// Collect extracted sub-types (dedupe exact dups; throw on real collisions)
|
|
167
209
|
for (const decl of result.declarations) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
nsLines.push(indent(decl, ' '))
|
|
171
|
-
}
|
|
210
|
+
const line = collector.accept(decl, ' ')
|
|
211
|
+
if (line != null) nsLines.push(line)
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
nsLines.push(` export type ${shortName} = ${result.body}`)
|
|
@@ -291,92 +331,379 @@ async function emitRpcRoute(route: RPCHttpRouteDoc, ctx: EmitRouteContext): Prom
|
|
|
291
331
|
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
|
|
292
332
|
}
|
|
293
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Formats a group of named types into a nested sub-namespace block (for namespace mode).
|
|
336
|
+
* Returns an array of lines to be inserted into the parent namespace, and a map of
|
|
337
|
+
* shortName → qualified type reference for use in callables.
|
|
338
|
+
*
|
|
339
|
+
* In flat mode, returns declarations like `export type ${prefix}${shortName} = ...`
|
|
340
|
+
* and refs like `${prefix}${shortName}`.
|
|
341
|
+
*/
|
|
342
|
+
async function formatSubNamespace(
|
|
343
|
+
routePascal: string,
|
|
344
|
+
nsName: string, // e.g. 'Req' or 'Response'
|
|
345
|
+
types: NamedType[],
|
|
346
|
+
ctx: EmitRouteContext,
|
|
347
|
+
taken: Set<string>,
|
|
348
|
+
): Promise<{ nsBlock: string | null; refs: Record<string, string> }> {
|
|
349
|
+
const refs: Record<string, string> = {}
|
|
350
|
+
const nsLines: string[] = []
|
|
351
|
+
const collector = new DeclarationCollector(`namespace ${routePascal}.${nsName}`)
|
|
352
|
+
|
|
353
|
+
// Pre-reserve short names to prevent sub-type extraction collision
|
|
354
|
+
for (const t of types) {
|
|
355
|
+
if (t.schema != null) taken.add(t.shortName)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const { shortName, schema } of types) {
|
|
359
|
+
if (schema == null) continue
|
|
360
|
+
|
|
361
|
+
if (ctx.namespaceTypes) {
|
|
362
|
+
const rawResult = await jsonSchemaToExtractedTypes(schema, ctx.ajsc)
|
|
363
|
+
if (rawResult == null) continue
|
|
364
|
+
|
|
365
|
+
const result = renameExtractedTypes(rawResult, taken)
|
|
366
|
+
|
|
367
|
+
for (const decl of result.declarations) {
|
|
368
|
+
const line = collector.accept(decl, ' ')
|
|
369
|
+
if (line != null) nsLines.push(line)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
nsLines.push(` export type ${shortName} = ${result.body}`)
|
|
373
|
+
refs[shortName] = `${ctx.scopePascal}.${routePascal}.${nsName}.${shortName}`
|
|
374
|
+
} else {
|
|
375
|
+
const flatName = `${routePascal}${nsName}${shortName}`
|
|
376
|
+
const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
|
|
377
|
+
if (body == null) continue
|
|
378
|
+
refs[shortName] = flatName
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (ctx.namespaceTypes) {
|
|
383
|
+
if (nsLines.length === 0) return { nsBlock: null, refs }
|
|
384
|
+
const nsBlock = ` export namespace ${nsName} {\n${nsLines.join('\n')}\n }`
|
|
385
|
+
return { nsBlock, refs }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { nsBlock: null, refs }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Builds the conditional return type string for an API or http-stream callable.
|
|
393
|
+
*
|
|
394
|
+
* - Both body + headers → `{ body: <Body>; headers: <Headers> }`
|
|
395
|
+
* - Only body → `<Body>`
|
|
396
|
+
* - Only headers → `{ headers: <Headers> }`
|
|
397
|
+
* - Neither → `void`
|
|
398
|
+
*/
|
|
399
|
+
function buildApiReturnType(
|
|
400
|
+
bodyRef: string | undefined,
|
|
401
|
+
headersRef: string | undefined,
|
|
402
|
+
): string {
|
|
403
|
+
if (bodyRef && headersRef) {
|
|
404
|
+
return `{ body: ${bodyRef}; headers: ${headersRef} }`
|
|
405
|
+
}
|
|
406
|
+
if (bodyRef) return bodyRef
|
|
407
|
+
if (headersRef) return `{ headers: ${headersRef} }`
|
|
408
|
+
return 'void'
|
|
409
|
+
}
|
|
410
|
+
|
|
294
411
|
async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
295
412
|
const pascal = toPascalCase(route.name)
|
|
296
|
-
const
|
|
413
|
+
const req = route.jsonSchema.req ?? {}
|
|
414
|
+
const res = route.jsonSchema.res ?? {}
|
|
297
415
|
|
|
298
|
-
//
|
|
299
|
-
const
|
|
416
|
+
// Request channels
|
|
417
|
+
const reqChannelKeys = ['pathParams', 'query', 'body', 'headers'] as const
|
|
418
|
+
const reqTypes: NamedType[] = []
|
|
300
419
|
const presentChannels: string[] = []
|
|
301
420
|
|
|
302
|
-
for (const channel of
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
|
|
421
|
+
for (const channel of reqChannelKeys) {
|
|
422
|
+
const schema = req[channel]
|
|
423
|
+
if (schema != null) {
|
|
424
|
+
reqTypes.push({ shortName: toPascalCase(channel), schema })
|
|
306
425
|
presentChannels.push(channel)
|
|
307
426
|
}
|
|
308
427
|
}
|
|
309
428
|
|
|
310
|
-
//
|
|
311
|
-
|
|
429
|
+
// Response slots
|
|
430
|
+
const resTypes: NamedType[] = [
|
|
431
|
+
{ shortName: 'Body', schema: res.body },
|
|
432
|
+
{ shortName: 'Headers', schema: res.headers },
|
|
433
|
+
]
|
|
312
434
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
new Set(['Params']),
|
|
320
|
-
)
|
|
435
|
+
const scopeStr = route.scope ?? 'default'
|
|
436
|
+
const errorUnion = buildErrorUnion(route.errors, ctx)
|
|
437
|
+
const hasErrors = errorUnion !== null
|
|
438
|
+
const errorsRef = ctx.namespaceTypes
|
|
439
|
+
? `${ctx.scopePascal}.${pascal}.Errors`
|
|
440
|
+
: `${pascal}Errors`
|
|
321
441
|
|
|
322
|
-
|
|
442
|
+
const declarations: string[] = []
|
|
323
443
|
let paramsTypeName = 'unknown'
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
444
|
+
let returnTypeName = 'void'
|
|
445
|
+
|
|
446
|
+
// Track reserved names across all sub-namespaces
|
|
447
|
+
const taken = new Set<string>(['Req', 'Response'])
|
|
448
|
+
|
|
449
|
+
if (ctx.namespaceTypes) {
|
|
450
|
+
// Namespace mode: emit nested Req {} and Response {} namespaces inside route namespace.
|
|
451
|
+
// Also emit merged type aliases `export type Req = { ... }` and `export type Response = ...`
|
|
452
|
+
// so they can be used as type arguments to bindCallable (TS requires a TYPE, not a namespace).
|
|
453
|
+
const nsLines: string[] = []
|
|
454
|
+
|
|
455
|
+
const { nsBlock: reqBlock, refs: reqRefs } = await formatSubNamespace(
|
|
456
|
+
pascal, 'Req', reqTypes, ctx, taken
|
|
457
|
+
)
|
|
458
|
+
if (reqBlock) {
|
|
459
|
+
nsLines.push(reqBlock)
|
|
460
|
+
// Merged type alias for Req so it can be used as a generic type arg
|
|
461
|
+
const reqFields = presentChannels
|
|
462
|
+
.map((ch) => `${ch}: Req.${toPascalCase(ch)}`)
|
|
328
463
|
.join('; ')
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
464
|
+
nsLines.push(` export type Req = { ${reqFields} }`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const { nsBlock: resBlock, refs: resRefs } = await formatSubNamespace(
|
|
468
|
+
pascal, 'Response', resTypes, ctx, taken
|
|
469
|
+
)
|
|
470
|
+
if (resBlock) {
|
|
471
|
+
nsLines.push(resBlock)
|
|
472
|
+
// No merged Response type alias needed: we reference Response.Body / Response.Headers
|
|
473
|
+
// directly in the return type string, which are namespace-qualified paths (valid).
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Emit Errors type last (injected by injectRouteErrors below)
|
|
477
|
+
// Build the route namespace block
|
|
478
|
+
if (nsLines.length > 0) {
|
|
479
|
+
declarations.push(` export namespace ${pascal} {\n${nsLines.join('\n\n')}\n }`)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Params type: use the merged Req type alias
|
|
483
|
+
if (presentChannels.length > 0) {
|
|
484
|
+
paramsTypeName = `${ctx.scopePascal}.${pascal}.Req`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Return type
|
|
488
|
+
const bodyRef = resRefs['Body']
|
|
489
|
+
const headersRef = resRefs['Headers']
|
|
490
|
+
returnTypeName = buildApiReturnType(bodyRef, headersRef)
|
|
491
|
+
} else {
|
|
492
|
+
// Flat mode: emit individual types prefixed with route + sub-namespace name
|
|
493
|
+
for (const { shortName, schema } of reqTypes) {
|
|
494
|
+
if (schema == null) continue
|
|
495
|
+
const flatName = `${pascal}Req${shortName}`
|
|
496
|
+
const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
|
|
497
|
+
if (body == null) continue
|
|
498
|
+
declarations.push(`export type ${flatName} = ${body}`)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Flat mode: compose structured Req type
|
|
502
|
+
if (presentChannels.length > 0) {
|
|
343
503
|
const structureFields = presentChannels
|
|
344
|
-
.map((ch) => `${ch}: ${
|
|
504
|
+
.map((ch) => `${ch}: ${pascal}Req${toPascalCase(ch)}`)
|
|
345
505
|
.join('; ')
|
|
346
|
-
declarations.push(`export type ${pascal}
|
|
347
|
-
paramsTypeName = `${pascal}
|
|
506
|
+
declarations.push(`export type ${pascal}Req = { ${structureFields} }`)
|
|
507
|
+
paramsTypeName = `${pascal}Req`
|
|
348
508
|
}
|
|
349
|
-
}
|
|
350
509
|
|
|
351
|
-
|
|
352
|
-
|
|
510
|
+
// Flat mode: emit response types
|
|
511
|
+
let bodyRef: string | undefined
|
|
512
|
+
let headersRef: string | undefined
|
|
353
513
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
514
|
+
for (const { shortName, schema } of resTypes) {
|
|
515
|
+
if (schema == null) continue
|
|
516
|
+
const flatName = `${pascal}Response${shortName}`
|
|
517
|
+
const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
|
|
518
|
+
if (body == null) continue
|
|
519
|
+
declarations.push(`export type ${flatName} = ${body}`)
|
|
520
|
+
if (shortName === 'Body') bodyRef = flatName
|
|
521
|
+
if (shortName === 'Headers') headersRef = flatName
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
returnTypeName = buildApiReturnType(bodyRef, headersRef)
|
|
525
|
+
|
|
526
|
+
// Flat mode errors
|
|
527
|
+
if (errorUnion) {
|
|
528
|
+
declarations.push(`export type ${pascal}Errors = ${errorUnion}`)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const responseHeadersDeclared = res.headers != null
|
|
359
533
|
const helperCall = hasErrors
|
|
360
|
-
? `client.bindCallableTyped<${paramsTypeName}, ${
|
|
361
|
-
: `client.bindCallable<${paramsTypeName}, ${
|
|
534
|
+
? `client.bindCallableTyped<${paramsTypeName}, ${returnTypeName}, ${errorsRef}>`
|
|
535
|
+
: `client.bindCallable<${paramsTypeName}, ${returnTypeName}>`
|
|
362
536
|
|
|
363
|
-
|
|
364
|
-
const callable = [
|
|
365
|
-
` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
|
|
366
|
-
` ${route.name}: ${helperCall}({`,
|
|
537
|
+
const descriptorLines = [
|
|
367
538
|
` name: '${route.name}',`,
|
|
368
539
|
` scope: '${scopeStr}',`,
|
|
369
540
|
` path: '${route.fullPath}',`,
|
|
370
541
|
` method: '${route.method}',`,
|
|
371
542
|
` kind: 'api',`,
|
|
543
|
+
...(responseHeadersDeclared ? [` responseHeadersDeclared: true,`] : []),
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
const callable = [
|
|
547
|
+
` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
|
|
548
|
+
` ${route.name}: ${helperCall}({`,
|
|
549
|
+
...descriptorLines,
|
|
372
550
|
` }),`,
|
|
373
551
|
].join('\n')
|
|
374
552
|
|
|
375
|
-
const hasErrorsInjected =
|
|
553
|
+
const hasErrorsInjected = ctx.namespaceTypes
|
|
554
|
+
? injectRouteErrors(declarations, pascal, errorUnion, ctx.namespaceTypes)
|
|
555
|
+
: errorUnion !== null // flat mode already emitted errors above
|
|
376
556
|
|
|
377
557
|
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
|
|
378
558
|
}
|
|
379
559
|
|
|
560
|
+
async function emitHttpStreamRoute(route: HttpStreamRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
561
|
+
const pascal = toPascalCase(route.name)
|
|
562
|
+
const req = route.jsonSchema.req ?? {}
|
|
563
|
+
const res = route.jsonSchema.res ?? {}
|
|
564
|
+
|
|
565
|
+
// Request channels
|
|
566
|
+
const reqChannelKeys = ['pathParams', 'query', 'body', 'headers'] as const
|
|
567
|
+
const reqTypes: NamedType[] = []
|
|
568
|
+
const presentChannels: string[] = []
|
|
569
|
+
|
|
570
|
+
for (const channel of reqChannelKeys) {
|
|
571
|
+
const schema = req[channel]
|
|
572
|
+
if (schema != null) {
|
|
573
|
+
reqTypes.push({ shortName: toPascalCase(channel), schema })
|
|
574
|
+
presentChannels.push(channel)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Yield + ReturnType
|
|
579
|
+
const yieldSchema = route.jsonSchema.yield
|
|
580
|
+
const returnSchema = route.jsonSchema.returnType
|
|
581
|
+
const resHeadersSchema = res.headers
|
|
582
|
+
|
|
583
|
+
const scopeStr = route.scope ?? 'default'
|
|
584
|
+
const declarations: string[] = []
|
|
585
|
+
let paramsTypeName = 'unknown'
|
|
586
|
+
let yieldTypeName = 'unknown'
|
|
587
|
+
let returnTypeName = 'void'
|
|
588
|
+
|
|
589
|
+
const taken = new Set<string>(['Req', 'Response', 'Yield', 'ReturnType'])
|
|
590
|
+
|
|
591
|
+
if (ctx.namespaceTypes) {
|
|
592
|
+
const nsLines: string[] = []
|
|
593
|
+
|
|
594
|
+
// Req sub-namespace
|
|
595
|
+
const { nsBlock: reqBlock, refs: reqRefs } = await formatSubNamespace(
|
|
596
|
+
pascal, 'Req', reqTypes, ctx, taken
|
|
597
|
+
)
|
|
598
|
+
if (reqBlock) {
|
|
599
|
+
nsLines.push(reqBlock)
|
|
600
|
+
// Merged type alias so Req can be used as a generic type arg (same pattern as emitApiRoute)
|
|
601
|
+
const reqFields = presentChannels
|
|
602
|
+
.map((ch) => `${ch}: Req.${toPascalCase(ch)}`)
|
|
603
|
+
.join('; ')
|
|
604
|
+
nsLines.push(` export type Req = { ${reqFields} }`)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Response sub-namespace (headers only for http-stream)
|
|
608
|
+
const resTypes: NamedType[] = [{ shortName: 'Headers', schema: resHeadersSchema }]
|
|
609
|
+
const { nsBlock: resBlock } = await formatSubNamespace(
|
|
610
|
+
pascal, 'Response', resTypes, ctx, taken
|
|
611
|
+
)
|
|
612
|
+
if (resBlock) nsLines.push(resBlock)
|
|
613
|
+
|
|
614
|
+
// Yield and ReturnType directly in the route namespace
|
|
615
|
+
const directTypes: NamedType[] = [
|
|
616
|
+
{ shortName: 'Yield', schema: yieldSchema },
|
|
617
|
+
{ shortName: 'ReturnType', schema: returnSchema },
|
|
618
|
+
]
|
|
619
|
+
const collector = new DeclarationCollector(`namespace ${pascal}`)
|
|
620
|
+
for (const { shortName, schema } of directTypes) {
|
|
621
|
+
if (schema == null) continue
|
|
622
|
+
const rawResult = await jsonSchemaToExtractedTypes(schema, ctx.ajsc)
|
|
623
|
+
if (rawResult == null) continue
|
|
624
|
+
const result = renameExtractedTypes(rawResult, taken)
|
|
625
|
+
for (const decl of result.declarations) {
|
|
626
|
+
const line = collector.accept(decl, ' ')
|
|
627
|
+
if (line != null) nsLines.push(line)
|
|
628
|
+
}
|
|
629
|
+
nsLines.push(` export type ${shortName} = ${result.body}`)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (nsLines.length > 0) {
|
|
633
|
+
declarations.push(` export namespace ${pascal} {\n${nsLines.join('\n')}\n }`)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (presentChannels.length > 0) {
|
|
637
|
+
paramsTypeName = `${ctx.scopePascal}.${pascal}.Req`
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (yieldSchema != null) yieldTypeName = `${ctx.scopePascal}.${pascal}.Yield`
|
|
641
|
+
if (returnSchema != null) returnTypeName = `${ctx.scopePascal}.${pascal}.ReturnType`
|
|
642
|
+
} else {
|
|
643
|
+
// Flat mode
|
|
644
|
+
for (const { shortName, schema } of reqTypes) {
|
|
645
|
+
if (schema == null) continue
|
|
646
|
+
const flatName = `${pascal}Req${shortName}`
|
|
647
|
+
const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
|
|
648
|
+
if (body == null) continue
|
|
649
|
+
declarations.push(`export type ${flatName} = ${body}`)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (presentChannels.length > 0) {
|
|
653
|
+
const structureFields = presentChannels
|
|
654
|
+
.map((ch) => `${ch}: ${pascal}Req${toPascalCase(ch)}`)
|
|
655
|
+
.join('; ')
|
|
656
|
+
declarations.push(`export type ${pascal}Req = { ${structureFields} }`)
|
|
657
|
+
paramsTypeName = `${pascal}Req`
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (resHeadersSchema != null) {
|
|
661
|
+
const body = await jsonSchemaToTypeBody(resHeadersSchema, ctx.ajsc)
|
|
662
|
+
if (body != null) declarations.push(`export type ${pascal}ResponseHeaders = ${body}`)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (yieldSchema != null) {
|
|
666
|
+
const body = await jsonSchemaToTypeBody(yieldSchema, ctx.ajsc)
|
|
667
|
+
if (body != null) {
|
|
668
|
+
declarations.push(`export type ${pascal}Yield = ${body}`)
|
|
669
|
+
yieldTypeName = `${pascal}Yield`
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (returnSchema != null) {
|
|
674
|
+
const body = await jsonSchemaToTypeBody(returnSchema, ctx.ajsc)
|
|
675
|
+
if (body != null) {
|
|
676
|
+
declarations.push(`export type ${pascal}ReturnType = ${body}`)
|
|
677
|
+
returnTypeName = `${pascal}ReturnType`
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const callable = [
|
|
683
|
+
` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
|
|
684
|
+
` ${route.name}(req: ${paramsTypeName}, options?: ProcedureCallOptions): TypedStream<${yieldTypeName}, ${returnTypeName}> {`,
|
|
685
|
+
` return client.stream<${yieldTypeName}, ${returnTypeName}>({`,
|
|
686
|
+
` name: '${route.name}',`,
|
|
687
|
+
` scope: '${scopeStr}',`,
|
|
688
|
+
` path: '${route.fullPath}',`,
|
|
689
|
+
` method: '${route.method}',`,
|
|
690
|
+
` kind: 'http-stream',`,
|
|
691
|
+
` streamMode: '${route.streamMode}',`,
|
|
692
|
+
` params: req,`,
|
|
693
|
+
` }, options)`,
|
|
694
|
+
` },`,
|
|
695
|
+
].join('\n')
|
|
696
|
+
|
|
697
|
+
const hasErrors = injectRouteErrors(
|
|
698
|
+
declarations,
|
|
699
|
+
pascal,
|
|
700
|
+
buildErrorUnion(route.errors, ctx),
|
|
701
|
+
ctx.namespaceTypes
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
return { typeDeclarations: declarations, callable, hasStream: true, hasErrors }
|
|
705
|
+
}
|
|
706
|
+
|
|
380
707
|
async function emitStreamRoute(route: StreamHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
381
708
|
const pascal = versionedPascal(route.name, route.version)
|
|
382
709
|
|
|
@@ -470,6 +797,8 @@ export async function emitScopeFile(
|
|
|
470
797
|
chunks = await emitApiRoute(route as APIHttpRouteDoc, ctx)
|
|
471
798
|
} else if (kind === 'stream') {
|
|
472
799
|
chunks = await emitStreamRoute(route as StreamHttpRouteDoc, ctx)
|
|
800
|
+
} else if (kind === 'http-stream') {
|
|
801
|
+
chunks = await emitHttpStreamRoute(route as HttpStreamRouteDoc, ctx)
|
|
473
802
|
} else {
|
|
474
803
|
throw new Error(`Unknown route kind "${kind}"`)
|
|
475
804
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { jsonSchemaToTypeString, jsonSchemaToTypeBody, jsonSchemaToExtractedTypes } from './emit-types.js'
|
|
2
|
+
import { jsonSchemaToTypeString, jsonSchemaToTypeBody, jsonSchemaToExtractedTypes, renameExtractedTypes, extractedDeclName } from './emit-types.js'
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Tests
|
|
@@ -212,6 +212,78 @@ describe('jsonSchemaToExtractedTypes', () => {
|
|
|
212
212
|
)
|
|
213
213
|
expect(hasItemsDecl).toBe(true)
|
|
214
214
|
})
|
|
215
|
+
|
|
216
|
+
// Root-cause guard: ajsc glues SIBLING extracted declarations with a single
|
|
217
|
+
// "\n" inside one block. If they aren't split into separate elements, the
|
|
218
|
+
// downstream rename/dedup logic only sees the first one -> duplicate
|
|
219
|
+
// identifiers (TS2300). Each extracted sub-type must occupy its own element.
|
|
220
|
+
it('returns one declaration per element when a schema yields multiple sub-types', async () => {
|
|
221
|
+
const schema = {
|
|
222
|
+
type: 'object',
|
|
223
|
+
required: ['contact'],
|
|
224
|
+
properties: {
|
|
225
|
+
contact: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
required: ['name', 'address'],
|
|
228
|
+
properties: {
|
|
229
|
+
name: { type: 'string' },
|
|
230
|
+
address: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
required: ['city'],
|
|
233
|
+
properties: { city: { type: 'string' } },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
240
|
+
expect(result).not.toBeUndefined()
|
|
241
|
+
// Two sub-types are extracted: Contact and Address.
|
|
242
|
+
const names = result!.declarations.map((d) => extractedDeclName(d))
|
|
243
|
+
expect(names).toContain('Contact')
|
|
244
|
+
expect(names).toContain('Address')
|
|
245
|
+
// Each declaration element holds exactly one `export type|enum|interface`.
|
|
246
|
+
for (const decl of result!.declarations) {
|
|
247
|
+
const count = decl.match(/export\s+(?:type|enum|interface)\s+/g)?.length ?? 0
|
|
248
|
+
expect(count).toBe(1)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('renameExtractedTypes', () => {
|
|
254
|
+
it('renames a colliding declaration and patches its references in the body', () => {
|
|
255
|
+
const result = {
|
|
256
|
+
declarations: ['export type Address = { city: string; }'],
|
|
257
|
+
body: '{ address: Address; }',
|
|
258
|
+
}
|
|
259
|
+
const out = renameExtractedTypes(result, new Set(['Address']))
|
|
260
|
+
expect(out.declarations[0]).toContain('export type AddressInner =')
|
|
261
|
+
expect(out.body).toBe('{ address: AddressInner; }')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Latent correctness guard: extracted sub-types reference each other. When a
|
|
265
|
+
// referenced sub-type is renamed, the reference inside its SIBLING
|
|
266
|
+
// declaration must be patched too — otherwise the renamed type silently
|
|
267
|
+
// resolves to a same-named sub-type from a different schema.
|
|
268
|
+
it('patches cross-references between sibling declarations on rename', () => {
|
|
269
|
+
const result = {
|
|
270
|
+
declarations: [
|
|
271
|
+
'export type Contact = { address: Address; }',
|
|
272
|
+
'export type Address = { city: string; }',
|
|
273
|
+
],
|
|
274
|
+
body: '{ contact: Contact; }',
|
|
275
|
+
}
|
|
276
|
+
// Both names already taken (as if extracted from a second schema sharing them).
|
|
277
|
+
const out = renameExtractedTypes(result, new Set(['Contact', 'Address']))
|
|
278
|
+
// Both declarations renamed.
|
|
279
|
+
expect(out.declarations[0]).toContain('export type ContactInner =')
|
|
280
|
+
expect(out.declarations[1]).toContain('export type AddressInner =')
|
|
281
|
+
// The reference inside ContactInner now points at AddressInner, not the
|
|
282
|
+
// stale Address (which would belong to the other schema).
|
|
283
|
+
expect(out.declarations[0]).toContain('address: AddressInner')
|
|
284
|
+
expect(out.declarations[0]).not.toMatch(/address: Address\b/)
|
|
285
|
+
expect(out.body).toBe('{ contact: ContactInner; }')
|
|
286
|
+
})
|
|
215
287
|
})
|
|
216
288
|
|
|
217
289
|
describe('jsonSchemaToTypeString (prefix stripping)', () => {
|