ts-procedures 7.2.0 → 8.0.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 +139 -53
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +208 -231
- 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 +36 -48
- package/agent_config/cursor/cursorrules +36 -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 +87 -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 +308 -47
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +363 -110
- package/build/codegen/emit-scope.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 +125 -115
- package/build/index.js +10 -222
- package/build/index.js.map +1 -1
- package/build/index.test.js +30 -822
- 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 +34 -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 +98 -24
- package/src/codegen/emit-errors.integration.test.ts +1 -1
- package/src/codegen/emit-scope.test.ts +395 -110
- package/src/codegen/emit-scope.ts +350 -55
- 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 +35 -1091
- package/src/index.ts +50 -474
- 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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { Type } from 'typebox'
|
|
4
|
+
import { Procedures } from '../../../index.js'
|
|
5
|
+
import type { RPCConfig } from '../../types.js'
|
|
6
|
+
import { HonoAppBuilder } from './index.js'
|
|
7
|
+
import { defineErrorTaxonomy } from '../error-taxonomy.js'
|
|
8
|
+
import { DocRegistry } from '../doc-registry.js'
|
|
9
|
+
|
|
10
|
+
describe('HonoAppBuilder — public surface', () => {
|
|
11
|
+
test('constructor with no args', () => {
|
|
12
|
+
const b = new HonoAppBuilder()
|
|
13
|
+
expect(b.app).toBeInstanceOf(Hono)
|
|
14
|
+
expect(b.docs).toEqual([])
|
|
15
|
+
expect(b.skippedProcedures).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('constructor accepts existing Hono app', () => {
|
|
19
|
+
const custom = new Hono()
|
|
20
|
+
const b = new HonoAppBuilder({ app: custom })
|
|
21
|
+
expect(b.app).toBe(custom)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('register is chainable', () => {
|
|
25
|
+
const b = new HonoAppBuilder()
|
|
26
|
+
const P = Procedures<{}, RPCConfig>()
|
|
27
|
+
const result = b.register(P, () => ({}))
|
|
28
|
+
expect(result).toBe(b)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('docs is lazily computed before build()', () => {
|
|
32
|
+
const b = new HonoAppBuilder()
|
|
33
|
+
const P = Procedures<{}, RPCConfig>()
|
|
34
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
35
|
+
b.register(P, () => ({}))
|
|
36
|
+
|
|
37
|
+
const docs1 = b.docs
|
|
38
|
+
expect(docs1).toHaveLength(1)
|
|
39
|
+
expect(docs1[0]!.kind).toBe('rpc')
|
|
40
|
+
// Calling docs again returns the same cached array reference
|
|
41
|
+
expect(b.docs).toBe(docs1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('docs reads after build() return the same content', () => {
|
|
45
|
+
const b = new HonoAppBuilder()
|
|
46
|
+
const P = Procedures<{}, RPCConfig>()
|
|
47
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
48
|
+
b.register(P, () => ({}))
|
|
49
|
+
b.build()
|
|
50
|
+
expect(b.docs).toHaveLength(1)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('HonoAppBuilder — mixed-kind dispatch', () => {
|
|
55
|
+
test('one factory with all four kinds mounts each at the correct path', async () => {
|
|
56
|
+
const P = Procedures<{ uid: string }, RPCConfig>()
|
|
57
|
+
P.Create('Echo', { scope: 'rpc', version: 1 }, async (_ctx, p) => p)
|
|
58
|
+
P.CreateStream('Tail', { scope: 'rpc', version: 1 }, async function* () { yield 1 })
|
|
59
|
+
P.CreateHttp('GetUser', {
|
|
60
|
+
path: '/users/:id',
|
|
61
|
+
method: 'get',
|
|
62
|
+
schema: {
|
|
63
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
64
|
+
res: { body: Type.Object({ id: Type.String() }) },
|
|
65
|
+
},
|
|
66
|
+
}, async () => ({ id: '1' }))
|
|
67
|
+
P.CreateHttpStream('Watch', {
|
|
68
|
+
path: '/watch', method: 'get',
|
|
69
|
+
schema: { yield: Type.String() },
|
|
70
|
+
}, (async function* () { yield 'tick' }) as any)
|
|
71
|
+
|
|
72
|
+
const app = new HonoAppBuilder()
|
|
73
|
+
.register(P, () => ({ uid: 'u1' }))
|
|
74
|
+
.build()
|
|
75
|
+
|
|
76
|
+
const rpc = await app.request('/rpc/echo/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
77
|
+
expect(rpc.status).toBe(200)
|
|
78
|
+
|
|
79
|
+
const stream = await app.request('/rpc/tail/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
|
|
80
|
+
expect(stream.status).toBe(200)
|
|
81
|
+
await stream.text()
|
|
82
|
+
|
|
83
|
+
const http = await app.request('/users/1')
|
|
84
|
+
expect(http.status).toBe(200)
|
|
85
|
+
|
|
86
|
+
const httpStream = await app.request('/watch')
|
|
87
|
+
expect(httpStream.status).toBe(200)
|
|
88
|
+
await httpStream.text()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('toDocEnvelope produces same shape as DocRegistry.toJSON', () => {
|
|
92
|
+
const P = Procedures<{}, RPCConfig>()
|
|
93
|
+
P.Create('Echo', { scope: 'e', version: 1 }, async () => ({}))
|
|
94
|
+
|
|
95
|
+
const errors = defineErrorTaxonomy({})
|
|
96
|
+
const builder = new HonoAppBuilder({ pathPrefix: '/api', errors })
|
|
97
|
+
builder.register(P, () => ({}))
|
|
98
|
+
builder.build()
|
|
99
|
+
|
|
100
|
+
const builderEnvelope = builder.toDocEnvelope({ basePath: '/api', errors })
|
|
101
|
+
const registryEnvelope = new DocRegistry({ basePath: '/api', errors }).from(builder).toJSON()
|
|
102
|
+
|
|
103
|
+
expect(builderEnvelope.basePath).toBe(registryEnvelope.basePath)
|
|
104
|
+
expect(builderEnvelope.routes).toEqual(registryEnvelope.routes)
|
|
105
|
+
expect(builderEnvelope.errors.map(e => e.name).sort()).toEqual(registryEnvelope.errors.map(e => e.name).sort())
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('toDocEnvelope filter and transform options work', () => {
|
|
109
|
+
const P = Procedures<{}, RPCConfig>()
|
|
110
|
+
P.Create('A', { scope: 's', version: 1 }, async () => ({}))
|
|
111
|
+
P.Create('B', { scope: 's', version: 1 }, async () => ({}))
|
|
112
|
+
|
|
113
|
+
const builder = new HonoAppBuilder().register(P, () => ({}))
|
|
114
|
+
const onlyA = builder.toDocEnvelope({ filter: r => r.name === 'A' })
|
|
115
|
+
expect(onlyA.routes).toHaveLength(1)
|
|
116
|
+
expect(onlyA.routes[0]!.name).toBe('A')
|
|
117
|
+
|
|
118
|
+
const tagged = builder.toDocEnvelope({
|
|
119
|
+
transform: env => ({ ...env, tagged: true }) as any,
|
|
120
|
+
}) as any
|
|
121
|
+
expect(tagged.tagged).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('makeRoutePath static is exposed', () => {
|
|
125
|
+
const P = Procedures<{}, RPCConfig>()
|
|
126
|
+
P.Create('GetUser', { scope: 'users', version: 1 }, async () => ({}))
|
|
127
|
+
const proc = P.getProcedure('GetUser')!
|
|
128
|
+
expect(HonoAppBuilder.makeRoutePath({ procedure: proc as any })).toBe('/users/get-user/1')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('HonoAppBuilder — factoryContext variants', () => {
|
|
133
|
+
test('factoryContext as a static object is forwarded into ctx', async () => {
|
|
134
|
+
const P = Procedures<{ requestId: string }, RPCConfig>()
|
|
135
|
+
P.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
|
|
136
|
+
id: ctx.requestId,
|
|
137
|
+
}))
|
|
138
|
+
|
|
139
|
+
const app = new HonoAppBuilder()
|
|
140
|
+
.register(P, { requestId: 'req-123' })
|
|
141
|
+
.build()
|
|
142
|
+
|
|
143
|
+
const res = await app.request('/get-request-id/get-request-id/1', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({}),
|
|
147
|
+
})
|
|
148
|
+
expect(await res.json()).toEqual({ id: 'req-123' })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('factoryContext as an async function is awaited and result reaches handler', async () => {
|
|
152
|
+
const factoryContext = vi.fn(async () => ({ requestId: 'req-456' }))
|
|
153
|
+
const P = Procedures<{ requestId: string }, RPCConfig>()
|
|
154
|
+
P.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
|
|
155
|
+
id: ctx.requestId,
|
|
156
|
+
}))
|
|
157
|
+
|
|
158
|
+
const app = new HonoAppBuilder().register(P, factoryContext).build()
|
|
159
|
+
|
|
160
|
+
const res = await app.request('/get-request-id/get-request-id/1', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({}),
|
|
164
|
+
})
|
|
165
|
+
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
166
|
+
expect(await res.json()).toEqual({ id: 'req-456' })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('factoryContext rejection flows through dispatchPreStreamError → 500', async () => {
|
|
170
|
+
const P = Procedures<{ requestId: string }, RPCConfig>()
|
|
171
|
+
P.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
|
|
172
|
+
id: ctx.requestId,
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
const app = new HonoAppBuilder()
|
|
176
|
+
.register(P, async () => {
|
|
177
|
+
throw new Error('context-load-failed')
|
|
178
|
+
})
|
|
179
|
+
.build()
|
|
180
|
+
|
|
181
|
+
const res = await app.request('/get-request-id/get-request-id/1', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({}),
|
|
185
|
+
})
|
|
186
|
+
expect(res.status).toBe(500)
|
|
187
|
+
const body = await res.json()
|
|
188
|
+
expect(body.error).toContain('context-load-failed')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('factoryContext function receives Hono Context with req.header access', async () => {
|
|
192
|
+
const factoryContext = vi.fn((c: any) => ({
|
|
193
|
+
authHeader: c.req.header('authorization'),
|
|
194
|
+
}))
|
|
195
|
+
const P = Procedures<{ authHeader?: string }, RPCConfig>()
|
|
196
|
+
P.Create('GetAuth', { scope: 'get-auth', version: 1 }, async (ctx) => ({
|
|
197
|
+
auth: ctx.authHeader,
|
|
198
|
+
}))
|
|
199
|
+
|
|
200
|
+
const app = new HonoAppBuilder().register(P, factoryContext).build()
|
|
201
|
+
|
|
202
|
+
const res = await app.request('/get-auth/get-auth/1', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
'Content-Type': 'application/json',
|
|
206
|
+
Authorization: 'Bearer token123',
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify({}),
|
|
209
|
+
})
|
|
210
|
+
expect(factoryContext).toHaveBeenCalledTimes(1)
|
|
211
|
+
expect(factoryContext.mock.calls[0]![0]).toHaveProperty('req')
|
|
212
|
+
expect(await res.json()).toEqual({ auth: 'Bearer token123' })
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('HonoAppBuilder — extendProcedureDoc discriminated union per kind', () => {
|
|
217
|
+
test('rpc: base.kind === "rpc" and returned object is merged into doc', () => {
|
|
218
|
+
const extend = vi.fn(({ base }: { base: { kind: string } }) => ({
|
|
219
|
+
summary: `RPC: ${base.kind}`,
|
|
220
|
+
tags: ['rpc'],
|
|
221
|
+
}))
|
|
222
|
+
const P = Procedures<{}, RPCConfig>()
|
|
223
|
+
P.Create('GetUser', { scope: 'users', version: 1 }, async () => ({}))
|
|
224
|
+
|
|
225
|
+
const builder = new HonoAppBuilder().register(P, () => ({}), { extendProcedureDoc: extend })
|
|
226
|
+
builder.build()
|
|
227
|
+
|
|
228
|
+
expect(extend).toHaveBeenCalledTimes(1)
|
|
229
|
+
expect(extend.mock.calls[0]![0]!.base.kind).toBe('rpc')
|
|
230
|
+
const doc = builder.docs[0] as any
|
|
231
|
+
expect(doc.summary).toBe('RPC: rpc')
|
|
232
|
+
expect(doc.tags).toEqual(['rpc'])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('api (http): base.kind === "api" and returned object is merged into doc', () => {
|
|
236
|
+
const extend = vi.fn(({ base }: { base: { kind: string } }) => ({
|
|
237
|
+
summary: `API: ${base.kind}`,
|
|
238
|
+
tags: ['api'],
|
|
239
|
+
}))
|
|
240
|
+
const P = Procedures<{}>()
|
|
241
|
+
P.CreateHttp(
|
|
242
|
+
'GetUser',
|
|
243
|
+
{
|
|
244
|
+
path: '/users/:id',
|
|
245
|
+
method: 'get',
|
|
246
|
+
schema: {
|
|
247
|
+
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
248
|
+
res: { body: Type.Object({ id: Type.String() }) },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
async () => ({ id: '1' }),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const builder = new HonoAppBuilder().register(P, () => ({}), { extendProcedureDoc: extend })
|
|
255
|
+
builder.build()
|
|
256
|
+
|
|
257
|
+
expect(extend).toHaveBeenCalledTimes(1)
|
|
258
|
+
expect(extend.mock.calls[0]![0]!.base.kind).toBe('api')
|
|
259
|
+
const doc = builder.docs[0] as any
|
|
260
|
+
expect(doc.summary).toBe('API: api')
|
|
261
|
+
expect(doc.tags).toEqual(['api'])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('rpc-stream: base.kind === "stream" and returned object is merged into doc', () => {
|
|
265
|
+
const extend = vi.fn(({ base }: { base: { kind: string } }) => ({
|
|
266
|
+
summary: `Stream: ${base.kind}`,
|
|
267
|
+
tags: ['stream'],
|
|
268
|
+
}))
|
|
269
|
+
const P = Procedures<{}, RPCConfig>()
|
|
270
|
+
P.CreateStream('Tail', { scope: 'rpc', version: 1 }, async function* () {
|
|
271
|
+
yield 1
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const builder = new HonoAppBuilder().register(P, () => ({}), { extendProcedureDoc: extend })
|
|
275
|
+
builder.build()
|
|
276
|
+
|
|
277
|
+
expect(extend).toHaveBeenCalledTimes(1)
|
|
278
|
+
expect(extend.mock.calls[0]![0]!.base.kind).toBe('stream')
|
|
279
|
+
const doc = builder.docs[0] as any
|
|
280
|
+
expect(doc.summary).toBe('Stream: stream')
|
|
281
|
+
expect(doc.tags).toEqual(['stream'])
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('http-stream: base.kind === "http-stream" and returned object is merged into doc', () => {
|
|
285
|
+
const extend = vi.fn(({ base }: { base: { kind: string } }) => ({
|
|
286
|
+
summary: `HTTP-Stream: ${base.kind}`,
|
|
287
|
+
tags: ['http-stream'],
|
|
288
|
+
}))
|
|
289
|
+
const P = Procedures<{}>()
|
|
290
|
+
P.CreateHttpStream(
|
|
291
|
+
'Watch',
|
|
292
|
+
{ path: '/watch', method: 'get', schema: { yield: Type.String() } },
|
|
293
|
+
(async function* () {
|
|
294
|
+
yield 'tick'
|
|
295
|
+
}) as any,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
const builder = new HonoAppBuilder().register(P, () => ({}), { extendProcedureDoc: extend })
|
|
299
|
+
builder.build()
|
|
300
|
+
|
|
301
|
+
expect(extend).toHaveBeenCalledTimes(1)
|
|
302
|
+
expect(extend.mock.calls[0]![0]!.base.kind).toBe('http-stream')
|
|
303
|
+
const doc = builder.docs[0] as any
|
|
304
|
+
expect(doc.summary).toBe('HTTP-Stream: http-stream')
|
|
305
|
+
expect(doc.tags).toEqual(['http-stream'])
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe('HonoAppBuilder — per-factory streamMode override', () => {
|
|
310
|
+
test('register options.streamMode overrides stream.defaultStreamMode', async () => {
|
|
311
|
+
const P = Procedures<{}, RPCConfig>()
|
|
312
|
+
P.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
313
|
+
yield { ok: true }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const app = new HonoAppBuilder({ stream: { defaultStreamMode: 'sse' } })
|
|
317
|
+
.register(P, () => ({}), { streamMode: 'text' })
|
|
318
|
+
.build()
|
|
319
|
+
|
|
320
|
+
const res = await app.request('/test/test/1', {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: { 'Content-Type': 'application/json' },
|
|
323
|
+
body: '{}',
|
|
324
|
+
})
|
|
325
|
+
expect(res.headers.get('content-type')).toContain('text/plain')
|
|
326
|
+
// Drain the response so the stream lifecycle completes cleanly.
|
|
327
|
+
await res.text()
|
|
328
|
+
})
|
|
329
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Context, Hono } from 'hono'
|
|
2
|
+
import type {
|
|
3
|
+
AnyHttpRouteDoc,
|
|
4
|
+
ProceduresFactory,
|
|
5
|
+
ExtractContext,
|
|
6
|
+
StreamMode,
|
|
7
|
+
DocEnvelope,
|
|
8
|
+
ErrorDoc,
|
|
9
|
+
HeaderDoc,
|
|
10
|
+
} from '../../types.js'
|
|
11
|
+
import { DocRegistry } from '../doc-registry.js'
|
|
12
|
+
import type { ErrorTaxonomy } from '../error-taxonomy.js'
|
|
13
|
+
import { buildRpcRouteDoc } from './docs/rpc-doc.js'
|
|
14
|
+
import { buildStreamRouteDoc } from './docs/stream-doc.js'
|
|
15
|
+
import { buildHttpRouteDoc } from './docs/http-doc.js'
|
|
16
|
+
import { buildHttpStreamRouteDoc } from './docs/http-stream-doc.js'
|
|
17
|
+
import { installRpcRoute } from './handlers/rpc.js'
|
|
18
|
+
import { installRpcStreamRoute } from './handlers/stream.js'
|
|
19
|
+
import { installHttpRoute } from './handlers/http.js'
|
|
20
|
+
import { installHttpStreamRoute } from './handlers/http-stream.js'
|
|
21
|
+
import { makeRoutePath as _makeRoutePath } from './path.js'
|
|
22
|
+
import type {
|
|
23
|
+
HonoAppBuilderConfig,
|
|
24
|
+
HonoFactoryItem,
|
|
25
|
+
ExtendProcedureDoc,
|
|
26
|
+
AnyProcedureRegistration,
|
|
27
|
+
OnRequestErrorContext,
|
|
28
|
+
} from './types.js'
|
|
29
|
+
|
|
30
|
+
export type { HonoAppBuilderConfig, OnRequestErrorContext, ExtendProcedureDoc } from './types.js'
|
|
31
|
+
export type { AnyProcedureRegistration } from './types.js'
|
|
32
|
+
export { sse } from './handlers/stream.js'
|
|
33
|
+
export type { SSEOptions, MidStreamErrorResult } from './handlers/stream.js'
|
|
34
|
+
export { defineErrorTaxonomy } from '../error-taxonomy.js'
|
|
35
|
+
export type { ErrorTaxonomy, ErrorTaxonomyEntry, UnknownErrorConfig } from '../error-taxonomy.js'
|
|
36
|
+
export type { QueryParser } from './types.js'
|
|
37
|
+
|
|
38
|
+
export class HonoAppBuilder<TStreamErrorData = unknown> {
|
|
39
|
+
private readonly _app: Hono
|
|
40
|
+
private readonly factories: HonoFactoryItem<any>[] = []
|
|
41
|
+
private _docs: AnyHttpRouteDoc[] | null = null
|
|
42
|
+
private _skipped: { name: string; reason: string }[] = []
|
|
43
|
+
private _built = false
|
|
44
|
+
|
|
45
|
+
constructor(readonly config?: HonoAppBuilderConfig<TStreamErrorData>) {
|
|
46
|
+
this._app = config?.app ?? new Hono()
|
|
47
|
+
|
|
48
|
+
if (config?.onRequestStart) {
|
|
49
|
+
this._app.use('*', async (c, next) => {
|
|
50
|
+
config.onRequestStart!(c)
|
|
51
|
+
await next()
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
if (config?.onRequestEnd) {
|
|
55
|
+
this._app.use('*', async (c, next) => {
|
|
56
|
+
await next()
|
|
57
|
+
config.onRequestEnd!(c)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static makeRoutePath = _makeRoutePath
|
|
63
|
+
|
|
64
|
+
get app(): Hono {
|
|
65
|
+
return this._app
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Lazily computed on first read or `build()`. Computing without `build()`
|
|
70
|
+
* lets tests and tooling introspect routes without spinning up handlers.
|
|
71
|
+
*/
|
|
72
|
+
get docs(): AnyHttpRouteDoc[] {
|
|
73
|
+
if (this._docs === null) this.computeDocs()
|
|
74
|
+
return this._docs!
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get skippedProcedures(): { name: string; reason: string }[] {
|
|
78
|
+
return this._skipped
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
register<TFactory extends ProceduresFactory>(
|
|
82
|
+
factory: TFactory,
|
|
83
|
+
factoryContext:
|
|
84
|
+
| ExtractContext<TFactory>
|
|
85
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
|
|
86
|
+
options?: {
|
|
87
|
+
streamMode?: StreamMode
|
|
88
|
+
extendProcedureDoc?: ExtendProcedureDoc
|
|
89
|
+
},
|
|
90
|
+
): this {
|
|
91
|
+
this.factories.push({
|
|
92
|
+
factory,
|
|
93
|
+
factoryContext,
|
|
94
|
+
streamMode: options?.streamMode,
|
|
95
|
+
extendProcedureDoc: options?.extendProcedureDoc,
|
|
96
|
+
} as HonoFactoryItem<any>)
|
|
97
|
+
// Invalidate docs cache so the next read recomputes.
|
|
98
|
+
this._docs = null
|
|
99
|
+
return this
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private computeDocs(): void {
|
|
103
|
+
const docs: AnyHttpRouteDoc[] = []
|
|
104
|
+
const skipped: { name: string; reason: string }[] = []
|
|
105
|
+
|
|
106
|
+
for (const item of this.factories) {
|
|
107
|
+
for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
|
|
108
|
+
const prefix = this.config?.pathPrefix
|
|
109
|
+
switch (procedure.kind) {
|
|
110
|
+
case 'rpc':
|
|
111
|
+
docs.push(buildRpcRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
112
|
+
break
|
|
113
|
+
case 'rpc-stream':
|
|
114
|
+
docs.push(buildStreamRouteDoc(
|
|
115
|
+
procedure as any,
|
|
116
|
+
item.streamMode ?? this.config?.stream?.defaultStreamMode ?? 'sse',
|
|
117
|
+
prefix,
|
|
118
|
+
item.extendProcedureDoc as any,
|
|
119
|
+
))
|
|
120
|
+
break
|
|
121
|
+
case 'http':
|
|
122
|
+
docs.push(buildHttpRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
123
|
+
break
|
|
124
|
+
case 'http-stream':
|
|
125
|
+
docs.push(buildHttpStreamRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
|
|
126
|
+
break
|
|
127
|
+
default: {
|
|
128
|
+
const reason = `Unknown procedure kind "${(procedure as any).kind}"`
|
|
129
|
+
skipped.push({ name: (procedure as any).name, reason })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this._docs = docs
|
|
136
|
+
this._skipped = skipped
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mounts every registered procedure on the Hono app and returns it.
|
|
141
|
+
* Idempotent: calling twice is a no-op on the second call.
|
|
142
|
+
*/
|
|
143
|
+
build(): Hono {
|
|
144
|
+
if (this._built) return this._app
|
|
145
|
+
this._built = true
|
|
146
|
+
|
|
147
|
+
const docs: AnyHttpRouteDoc[] = []
|
|
148
|
+
const skipped: { name: string; reason: string }[] = []
|
|
149
|
+
const cfg = this.config ?? {}
|
|
150
|
+
|
|
151
|
+
for (const item of this.factories) {
|
|
152
|
+
for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
|
|
153
|
+
switch (procedure.kind) {
|
|
154
|
+
case 'rpc':
|
|
155
|
+
installRpcRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
156
|
+
break
|
|
157
|
+
case 'rpc-stream': {
|
|
158
|
+
const streamMode = item.streamMode ?? cfg.stream?.defaultStreamMode ?? 'sse'
|
|
159
|
+
installRpcStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, streamMode })
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
case 'http':
|
|
163
|
+
installHttpRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
164
|
+
break
|
|
165
|
+
case 'http-stream':
|
|
166
|
+
installHttpStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
|
|
167
|
+
break
|
|
168
|
+
default: {
|
|
169
|
+
const reason = `Unknown procedure kind "${(procedure as any).kind}"`
|
|
170
|
+
skipped.push({ name: (procedure as any).name, reason })
|
|
171
|
+
console.warn(`[ts-procedures hono] Skipping procedure "${(procedure as any).name}": ${reason}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this._docs = docs
|
|
178
|
+
this._skipped = skipped
|
|
179
|
+
return this._app
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Produces a {@link DocEnvelope} for single-app usage. For multi-app
|
|
184
|
+
* aggregation, use {@link DocRegistry} and `from(builder)` instead.
|
|
185
|
+
*
|
|
186
|
+
* Mirrors `DocRegistry.toJSON()` options so the two paths produce
|
|
187
|
+
* interchangeable envelopes for the codegen pipeline.
|
|
188
|
+
*/
|
|
189
|
+
toDocEnvelope<T = DocEnvelope>(options?: {
|
|
190
|
+
basePath?: string
|
|
191
|
+
errors?: ErrorTaxonomy | ErrorDoc[]
|
|
192
|
+
includeDefaults?: boolean
|
|
193
|
+
headers?: HeaderDoc[]
|
|
194
|
+
filter?: (route: AnyHttpRouteDoc) => boolean
|
|
195
|
+
transform?: (envelope: DocEnvelope) => T
|
|
196
|
+
}): T {
|
|
197
|
+
return new DocRegistry({
|
|
198
|
+
basePath: options?.basePath ?? this.config?.pathPrefix,
|
|
199
|
+
errors: options?.errors ?? this.config?.errors,
|
|
200
|
+
includeDefaults: options?.includeDefaults,
|
|
201
|
+
headers: options?.headers,
|
|
202
|
+
}).from(this).toJSON<T>(options)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { makeRoutePath, resolveFullPath } from './path.js'
|
|
3
|
+
import type {
|
|
4
|
+
TProcedureRegistration,
|
|
5
|
+
TStreamProcedureRegistration,
|
|
6
|
+
THttpProcedureRegistration,
|
|
7
|
+
THttpStreamProcedureRegistration,
|
|
8
|
+
} from '../../../types.js'
|
|
9
|
+
|
|
10
|
+
describe('makeRoutePath', () => {
|
|
11
|
+
test('rpc: scope/name/version with kebab-case', () => {
|
|
12
|
+
const proc = {
|
|
13
|
+
name: 'GetUser',
|
|
14
|
+
kind: 'rpc',
|
|
15
|
+
config: { scope: 'users', version: 1 },
|
|
16
|
+
handler: async () => undefined,
|
|
17
|
+
} as unknown as TProcedureRegistration
|
|
18
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/users/get-user/1')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('rpc: array scope joined with /', () => {
|
|
22
|
+
const proc = {
|
|
23
|
+
name: 'List',
|
|
24
|
+
kind: 'rpc',
|
|
25
|
+
config: { scope: ['UserModule', 'admin'], version: 2 },
|
|
26
|
+
handler: async () => undefined,
|
|
27
|
+
} as unknown as TProcedureRegistration
|
|
28
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/user-module/admin/list/2')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('rpc: pathPrefix is normalized (leading slash inserted)', () => {
|
|
32
|
+
const proc = {
|
|
33
|
+
name: 'Echo',
|
|
34
|
+
kind: 'rpc',
|
|
35
|
+
config: { scope: 'echo', version: 1 },
|
|
36
|
+
handler: async () => undefined,
|
|
37
|
+
} as unknown as TProcedureRegistration
|
|
38
|
+
expect(makeRoutePath({ procedure: proc, prefix: 'api/v1' })).toBe('/api/v1/echo/echo/1')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('rpc-stream: same shape as rpc', () => {
|
|
42
|
+
const proc = {
|
|
43
|
+
name: 'Tail',
|
|
44
|
+
kind: 'rpc-stream',
|
|
45
|
+
isStream: true,
|
|
46
|
+
config: { scope: 'logs', version: 1 },
|
|
47
|
+
handler: async function* () {},
|
|
48
|
+
} as unknown as TStreamProcedureRegistration
|
|
49
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/logs/tail/1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('http: prepends prefix to config.path', () => {
|
|
53
|
+
const proc = {
|
|
54
|
+
name: 'GetUser',
|
|
55
|
+
kind: 'http',
|
|
56
|
+
config: { path: '/users/:id', method: 'get' },
|
|
57
|
+
handler: async () => undefined,
|
|
58
|
+
} as unknown as THttpProcedureRegistration
|
|
59
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api/v1' })).toBe('/api/v1/users/:id')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('http: leading slash on path is preserved (not duplicated)', () => {
|
|
63
|
+
const proc = {
|
|
64
|
+
name: 'Get',
|
|
65
|
+
kind: 'http',
|
|
66
|
+
config: { path: 'users', method: 'get' },
|
|
67
|
+
handler: async () => undefined,
|
|
68
|
+
} as unknown as THttpProcedureRegistration
|
|
69
|
+
expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/users')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('http-stream: same shape as http', () => {
|
|
73
|
+
const proc = {
|
|
74
|
+
name: 'TailLogs',
|
|
75
|
+
kind: 'http-stream',
|
|
76
|
+
config: { path: '/logs/tail', method: 'get' },
|
|
77
|
+
handler: async () => ({ stream: (async function* () {})(), initialHeaders: undefined }),
|
|
78
|
+
} as unknown as THttpStreamProcedureRegistration
|
|
79
|
+
expect(makeRoutePath({ procedure: proc })).toBe('/logs/tail')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('resolveFullPath', () => {
|
|
84
|
+
test('combines prefix and path', () => {
|
|
85
|
+
expect(resolveFullPath('/users', '/api/v1')).toBe('/api/v1/users')
|
|
86
|
+
})
|
|
87
|
+
test('normalizes prefix without leading slash', () => {
|
|
88
|
+
expect(resolveFullPath('/users', 'api')).toBe('/api/users')
|
|
89
|
+
})
|
|
90
|
+
test('normalizes path without leading slash', () => {
|
|
91
|
+
expect(resolveFullPath('users', '/api')).toBe('/api/users')
|
|
92
|
+
})
|
|
93
|
+
test('no prefix', () => {
|
|
94
|
+
expect(resolveFullPath('/users')).toBe('/users')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { kebabCase } from 'es-toolkit/string'
|
|
2
|
+
import { castArray } from 'es-toolkit/compat'
|
|
3
|
+
import type {
|
|
4
|
+
TProcedureRegistration,
|
|
5
|
+
TStreamProcedureRegistration,
|
|
6
|
+
THttpProcedureRegistration,
|
|
7
|
+
THttpStreamProcedureRegistration,
|
|
8
|
+
} from '../../../types.js'
|
|
9
|
+
|
|
10
|
+
export type AnyProcedureRegistration =
|
|
11
|
+
| TProcedureRegistration
|
|
12
|
+
| TStreamProcedureRegistration
|
|
13
|
+
| THttpProcedureRegistration<any>
|
|
14
|
+
| THttpStreamProcedureRegistration<any>
|
|
15
|
+
|
|
16
|
+
function normalizePrefix(prefix?: string): string {
|
|
17
|
+
if (!prefix) return ''
|
|
18
|
+
return prefix.startsWith('/') ? prefix : `/${prefix}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the full route path for any procedure kind.
|
|
23
|
+
*
|
|
24
|
+
* - `rpc` / `rpc-stream`: `{prefix}/{kebab(scope)}/{kebab(name)}/{version}`
|
|
25
|
+
* - `http` / `http-stream`: `{prefix}{config.path}` (path-as-given, prefix normalized)
|
|
26
|
+
*/
|
|
27
|
+
export function makeRoutePath({
|
|
28
|
+
procedure,
|
|
29
|
+
prefix,
|
|
30
|
+
}: {
|
|
31
|
+
procedure: AnyProcedureRegistration
|
|
32
|
+
prefix?: string
|
|
33
|
+
}): string {
|
|
34
|
+
const normalizedPrefix = normalizePrefix(prefix)
|
|
35
|
+
|
|
36
|
+
switch (procedure.kind) {
|
|
37
|
+
case 'rpc':
|
|
38
|
+
case 'rpc-stream': {
|
|
39
|
+
const cfg = procedure.config as { scope: string | string[]; version: number }
|
|
40
|
+
const scopeSegments = castArray(cfg.scope).map(kebabCase).join('/')
|
|
41
|
+
return `${normalizedPrefix}/${scopeSegments}/${kebabCase(procedure.name)}/${String(cfg.version).trim()}`
|
|
42
|
+
}
|
|
43
|
+
case 'http':
|
|
44
|
+
case 'http-stream': {
|
|
45
|
+
const cfg = procedure.config as { path: string }
|
|
46
|
+
return resolveFullPath(cfg.path, prefix)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Combines a developer-supplied path with an optional prefix. Both are
|
|
53
|
+
* normalized to begin with a single `/`.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveFullPath(procedurePath: string, prefix?: string): string {
|
|
56
|
+
const normalizedPrefix = normalizePrefix(prefix)
|
|
57
|
+
const normalizedPath = procedurePath.startsWith('/') ? procedurePath : `/${procedurePath}`
|
|
58
|
+
return `${normalizedPrefix}${normalizedPath}`
|
|
59
|
+
}
|