ts-procedures 5.9.1 → 5.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent_config/bin/postinstall.mjs +3 -3
- package/agent_config/bin/setup.mjs +22 -11
- package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
- package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
- package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
- package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
- package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
- package/agent_config/lib/install-claude.mjs +35 -87
- package/build/src/client/call.d.ts +14 -0
- package/build/src/client/call.js +47 -0
- package/build/src/client/call.js.map +1 -0
- package/build/src/client/call.test.d.ts +1 -0
- package/build/src/client/call.test.js +124 -0
- package/build/src/client/call.test.js.map +1 -0
- package/build/src/client/errors.d.ts +25 -0
- package/build/src/client/errors.js +33 -0
- package/build/src/client/errors.js.map +1 -0
- package/build/src/client/errors.test.d.ts +1 -0
- package/build/src/client/errors.test.js +41 -0
- package/build/src/client/errors.test.js.map +1 -0
- package/build/src/client/fetch-adapter.d.ts +12 -0
- package/build/src/client/fetch-adapter.js +156 -0
- package/build/src/client/fetch-adapter.js.map +1 -0
- package/build/src/client/fetch-adapter.test.d.ts +1 -0
- package/build/src/client/fetch-adapter.test.js +271 -0
- package/build/src/client/fetch-adapter.test.js.map +1 -0
- package/build/src/client/hooks.d.ts +17 -0
- package/build/src/client/hooks.js +40 -0
- package/build/src/client/hooks.js.map +1 -0
- package/build/src/client/hooks.test.d.ts +1 -0
- package/build/src/client/hooks.test.js +163 -0
- package/build/src/client/hooks.test.js.map +1 -0
- package/build/src/client/index.d.ts +22 -0
- package/build/src/client/index.js +67 -0
- package/build/src/client/index.js.map +1 -0
- package/build/src/client/index.test.d.ts +1 -0
- package/build/src/client/index.test.js +231 -0
- package/build/src/client/index.test.js.map +1 -0
- package/build/src/client/request-builder.d.ts +13 -0
- package/build/src/client/request-builder.js +53 -0
- package/build/src/client/request-builder.js.map +1 -0
- package/build/src/client/request-builder.test.d.ts +1 -0
- package/build/src/client/request-builder.test.js +160 -0
- package/build/src/client/request-builder.test.js.map +1 -0
- package/build/src/client/stream.d.ts +27 -0
- package/build/src/client/stream.js +118 -0
- package/build/src/client/stream.js.map +1 -0
- package/build/src/client/stream.test.d.ts +1 -0
- package/build/src/client/stream.test.js +228 -0
- package/build/src/client/stream.test.js.map +1 -0
- package/build/src/client/types.d.ts +78 -0
- package/build/src/client/types.js +3 -0
- package/build/src/client/types.js.map +1 -0
- package/build/src/codegen/bin/cli.d.ts +45 -0
- package/build/src/codegen/bin/cli.js +246 -0
- package/build/src/codegen/bin/cli.js.map +1 -0
- package/build/src/codegen/bin/cli.test.d.ts +1 -0
- package/build/src/codegen/bin/cli.test.js +220 -0
- package/build/src/codegen/bin/cli.test.js.map +1 -0
- package/build/src/codegen/constants.d.ts +1 -0
- package/build/src/codegen/constants.js +2 -0
- package/build/src/codegen/constants.js.map +1 -0
- package/build/src/codegen/e2e.test.d.ts +1 -0
- package/build/src/codegen/e2e.test.js +464 -0
- package/build/src/codegen/e2e.test.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.d.ts +9 -0
- package/build/src/codegen/emit-client-runtime.js +99 -0
- package/build/src/codegen/emit-client-runtime.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
- package/build/src/codegen/emit-client-runtime.test.js +78 -0
- package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
- package/build/src/codegen/emit-client-types.d.ts +8 -0
- package/build/src/codegen/emit-client-types.js +25 -0
- package/build/src/codegen/emit-client-types.js.map +1 -0
- package/build/src/codegen/emit-client-types.test.d.ts +1 -0
- package/build/src/codegen/emit-client-types.test.js +33 -0
- package/build/src/codegen/emit-client-types.test.js.map +1 -0
- package/build/src/codegen/emit-errors.d.ts +19 -0
- package/build/src/codegen/emit-errors.js +59 -0
- package/build/src/codegen/emit-errors.js.map +1 -0
- package/build/src/codegen/emit-errors.test.d.ts +1 -0
- package/build/src/codegen/emit-errors.test.js +175 -0
- package/build/src/codegen/emit-errors.test.js.map +1 -0
- package/build/src/codegen/emit-index.d.ts +12 -0
- package/build/src/codegen/emit-index.js +41 -0
- package/build/src/codegen/emit-index.js.map +1 -0
- package/build/src/codegen/emit-index.test.d.ts +1 -0
- package/build/src/codegen/emit-index.test.js +106 -0
- package/build/src/codegen/emit-index.test.js.map +1 -0
- package/build/src/codegen/emit-scope.d.ts +15 -0
- package/build/src/codegen/emit-scope.js +299 -0
- package/build/src/codegen/emit-scope.js.map +1 -0
- package/build/src/codegen/emit-scope.test.d.ts +1 -0
- package/build/src/codegen/emit-scope.test.js +559 -0
- package/build/src/codegen/emit-scope.test.js.map +1 -0
- package/build/src/codegen/emit-types.d.ts +43 -0
- package/build/src/codegen/emit-types.js +111 -0
- package/build/src/codegen/emit-types.js.map +1 -0
- package/build/src/codegen/emit-types.test.d.ts +1 -0
- package/build/src/codegen/emit-types.test.js +184 -0
- package/build/src/codegen/emit-types.test.js.map +1 -0
- package/build/src/codegen/group-routes.d.ts +23 -0
- package/build/src/codegen/group-routes.js +46 -0
- package/build/src/codegen/group-routes.js.map +1 -0
- package/build/src/codegen/group-routes.test.d.ts +1 -0
- package/build/src/codegen/group-routes.test.js +131 -0
- package/build/src/codegen/group-routes.test.js.map +1 -0
- package/build/src/codegen/index.d.ts +15 -0
- package/build/src/codegen/index.js +16 -0
- package/build/src/codegen/index.js.map +1 -0
- package/build/src/codegen/naming.d.ts +7 -0
- package/build/src/codegen/naming.js +21 -0
- package/build/src/codegen/naming.js.map +1 -0
- package/build/src/codegen/naming.test.d.ts +1 -0
- package/build/src/codegen/naming.test.js +40 -0
- package/build/src/codegen/naming.test.js.map +1 -0
- package/build/src/codegen/pipeline.d.ts +17 -0
- package/build/src/codegen/pipeline.js +78 -0
- package/build/src/codegen/pipeline.js.map +1 -0
- package/build/src/codegen/pipeline.test.d.ts +1 -0
- package/build/src/codegen/pipeline.test.js +269 -0
- package/build/src/codegen/pipeline.test.js.map +1 -0
- package/build/src/codegen/resolve-envelope.d.ts +7 -0
- package/build/src/codegen/resolve-envelope.js +46 -0
- package/build/src/codegen/resolve-envelope.js.map +1 -0
- package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
- package/build/src/codegen/resolve-envelope.test.js +69 -0
- package/build/src/codegen/resolve-envelope.test.js.map +1 -0
- package/build/src/errors.d.ts +33 -0
- package/build/src/errors.js +91 -0
- package/build/src/errors.js.map +1 -0
- package/build/src/errors.test.d.ts +1 -0
- package/build/src/errors.test.js +122 -0
- package/build/src/errors.test.js.map +1 -0
- package/build/src/exports.d.ts +7 -0
- package/build/src/exports.js +8 -0
- package/build/src/exports.js.map +1 -0
- package/build/src/implementations/http/doc-registry.d.ts +12 -0
- package/build/src/implementations/http/doc-registry.js +114 -0
- package/build/src/implementations/http/doc-registry.js.map +1 -0
- package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/src/implementations/http/doc-registry.test.js +347 -0
- package/build/src/implementations/http/doc-registry.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
- package/build/src/implementations/http/express-rpc/index.js +185 -0
- package/build/src/implementations/http/express-rpc/index.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/express-rpc/index.test.js +684 -0
- package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/express-rpc/types.js +2 -0
- package/build/src/implementations/http/express-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.d.ts +102 -0
- package/build/src/implementations/http/hono-api/index.js +341 -0
- package/build/src/implementations/http/hono-api/index.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-api/index.test.js +992 -0
- package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-api/types.d.ts +13 -0
- package/build/src/implementations/http/hono-api/types.js +2 -0
- package/build/src/implementations/http/hono-api/types.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
- package/build/src/implementations/http/hono-rpc/index.js +161 -0
- package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
- package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/hono-rpc/types.js +2 -0
- package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
- package/build/src/implementations/http/hono-stream/index.js +309 -0
- package/build/src/implementations/http/hono-stream/index.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
- package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/src/implementations/http/hono-stream/types.js +2 -0
- package/build/src/implementations/http/hono-stream/types.js.map +1 -0
- package/build/src/implementations/types.d.ts +142 -0
- package/build/src/implementations/types.js +2 -0
- package/build/src/implementations/types.js.map +1 -0
- package/build/src/index.d.ts +165 -0
- package/build/src/index.js +253 -0
- package/build/src/index.js.map +1 -0
- package/build/src/index.test.d.ts +1 -0
- package/build/src/index.test.js +890 -0
- package/build/src/index.test.js.map +1 -0
- package/build/src/schema/compute-schema.d.ts +35 -0
- package/build/src/schema/compute-schema.js +41 -0
- package/build/src/schema/compute-schema.js.map +1 -0
- package/build/src/schema/compute-schema.test.d.ts +1 -0
- package/build/src/schema/compute-schema.test.js +107 -0
- package/build/src/schema/compute-schema.test.js.map +1 -0
- package/build/src/schema/extract-json-schema.d.ts +2 -0
- package/build/src/schema/extract-json-schema.js +12 -0
- package/build/src/schema/extract-json-schema.js.map +1 -0
- package/build/src/schema/extract-json-schema.test.d.ts +1 -0
- package/build/src/schema/extract-json-schema.test.js +23 -0
- package/build/src/schema/extract-json-schema.test.js.map +1 -0
- package/build/src/schema/parser.d.ts +28 -0
- package/build/src/schema/parser.js +170 -0
- package/build/src/schema/parser.js.map +1 -0
- package/build/src/schema/parser.test.d.ts +1 -0
- package/build/src/schema/parser.test.js +120 -0
- package/build/src/schema/parser.test.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.d.ts +12 -0
- package/build/src/schema/resolve-schema-lib.js +11 -0
- package/build/src/schema/resolve-schema-lib.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/src/schema/resolve-schema-lib.test.js +17 -0
- package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/src/schema/types.d.ts +8 -0
- package/build/src/schema/types.js +2 -0
- package/build/src/schema/types.js.map +1 -0
- package/build/src/stack-utils.d.ts +25 -0
- package/build/src/stack-utils.js +95 -0
- package/build/src/stack-utils.js.map +1 -0
- package/build/src/stack-utils.test.d.ts +1 -0
- package/build/src/stack-utils.test.js +80 -0
- package/build/src/stack-utils.test.js.map +1 -0
- package/docs/ai-agent-setup.md +7 -6
- package/docs/core.md +5 -9
- package/docs/streaming.md +9 -9
- package/package.json +2 -13
- package/src/client/call.test.ts +162 -0
- package/src/client/errors.test.ts +43 -0
- package/src/client/fetch-adapter.test.ts +340 -0
- package/src/client/hooks.test.ts +191 -0
- package/src/client/index.test.ts +290 -0
- package/src/client/request-builder.test.ts +184 -0
- package/src/client/stream.test.ts +331 -0
- package/src/codegen/bin/cli.test.ts +260 -0
- package/src/codegen/bin/cli.ts +282 -0
- package/src/codegen/constants.ts +1 -0
- package/src/codegen/e2e.test.ts +565 -0
- package/src/codegen/emit-client-runtime.test.ts +93 -0
- package/src/codegen/emit-client-runtime.ts +114 -0
- package/src/codegen/emit-client-types.test.ts +39 -0
- package/src/codegen/emit-client-types.ts +27 -0
- package/src/codegen/emit-errors.test.ts +202 -0
- package/src/codegen/emit-errors.ts +80 -0
- package/src/codegen/emit-index.test.ts +127 -0
- package/src/codegen/emit-index.ts +58 -0
- package/src/codegen/emit-scope.test.ts +624 -0
- package/src/codegen/emit-scope.ts +389 -0
- package/src/codegen/emit-types.test.ts +205 -0
- package/src/codegen/emit-types.ts +158 -0
- package/src/codegen/group-routes.test.ts +159 -0
- package/src/codegen/group-routes.ts +61 -0
- package/src/codegen/index.ts +30 -0
- package/src/codegen/naming.test.ts +50 -0
- package/src/codegen/naming.ts +25 -0
- package/src/codegen/pipeline.test.ts +316 -0
- package/src/codegen/pipeline.ts +108 -0
- package/src/codegen/resolve-envelope.test.ts +76 -0
- package/src/codegen/resolve-envelope.ts +61 -0
- package/src/errors.test.ts +163 -0
- package/src/errors.ts +107 -0
- package/src/exports.ts +7 -0
- package/src/implementations/http/doc-registry.test.ts +415 -0
- package/src/implementations/http/doc-registry.ts +143 -0
- package/src/implementations/http/express-rpc/README.md +6 -6
- package/src/implementations/http/express-rpc/index.test.ts +957 -0
- package/src/implementations/http/express-rpc/index.ts +266 -0
- package/src/implementations/http/express-rpc/types.ts +16 -0
- package/src/implementations/http/hono-api/index.test.ts +1341 -0
- package/src/implementations/http/hono-api/index.ts +463 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/http/hono-rpc/README.md +6 -6
- package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
- package/src/implementations/http/hono-rpc/index.ts +238 -0
- package/src/implementations/http/hono-rpc/types.ts +16 -0
- package/src/implementations/http/hono-stream/README.md +12 -12
- package/src/implementations/http/hono-stream/index.test.ts +1768 -0
- package/src/implementations/http/hono-stream/index.ts +456 -0
- package/src/implementations/http/hono-stream/types.ts +20 -0
- package/src/implementations/types.ts +174 -0
- package/src/index.test.ts +1185 -0
- package/src/index.ts +522 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +88 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +182 -0
- package/src/schema/parser.ts +215 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +20 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
- package/agent_config/claude-code/skills/review/SKILL.md +0 -53
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
- /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
- /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import type { ScopeGroup } from './group-routes.js'
|
|
2
|
+
import type {
|
|
3
|
+
RPCHttpRouteDoc,
|
|
4
|
+
APIHttpRouteDoc,
|
|
5
|
+
StreamHttpRouteDoc,
|
|
6
|
+
} from '../implementations/types.js'
|
|
7
|
+
import {
|
|
8
|
+
jsonSchemaToTypeString,
|
|
9
|
+
jsonSchemaToTypeBody,
|
|
10
|
+
jsonSchemaToExtractedTypes,
|
|
11
|
+
type AjscOptions,
|
|
12
|
+
type ExtractedTypeOutput,
|
|
13
|
+
} from './emit-types.js'
|
|
14
|
+
import { CODEGEN_HEADER } from './constants.js'
|
|
15
|
+
import { toPascalCase } from './naming.js'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface EmitScopeOptions {
|
|
22
|
+
ajsc?: AjscOptions
|
|
23
|
+
clientImportPath?: string
|
|
24
|
+
namespaceTypes?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RouteChunks {
|
|
28
|
+
typeDeclarations: string[]
|
|
29
|
+
callable: string
|
|
30
|
+
hasStream: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface EmitRouteContext {
|
|
34
|
+
ajsc?: AjscOptions
|
|
35
|
+
namespaceTypes: boolean
|
|
36
|
+
scopePascal: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Infers the route kind when the `kind` discriminant is missing.
|
|
45
|
+
* This provides backward compatibility with servers running older ts-procedures
|
|
46
|
+
* versions that don't set `kind` on route docs.
|
|
47
|
+
*/
|
|
48
|
+
function inferRouteKind(route: Record<string, unknown>): 'rpc' | 'api' | 'stream' {
|
|
49
|
+
if ('streamMode' in route) return 'stream'
|
|
50
|
+
if ('fullPath' in route) return 'api'
|
|
51
|
+
return 'rpc'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether a JSON schema looks like an SSE envelope.
|
|
56
|
+
* SSE envelopes have properties: data, event, id (and optionally retry).
|
|
57
|
+
*/
|
|
58
|
+
function isSseEnvelope(schema: Record<string, unknown>): boolean {
|
|
59
|
+
const props = schema.properties
|
|
60
|
+
if (props == null || typeof props !== 'object') return false
|
|
61
|
+
const keys = Object.keys(props as Record<string, unknown>)
|
|
62
|
+
return keys.includes('data') && keys.includes('event') && keys.includes('id')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Unwraps an SSE envelope schema to return the inner `data` property schema.
|
|
67
|
+
* If not an SSE envelope, returns the schema unchanged.
|
|
68
|
+
*/
|
|
69
|
+
function unwrapSseEnvelope(
|
|
70
|
+
schema: Record<string, unknown>
|
|
71
|
+
): Record<string, unknown> {
|
|
72
|
+
if (!isSseEnvelope(schema)) return schema
|
|
73
|
+
const props = schema.properties as Record<string, Record<string, unknown>>
|
|
74
|
+
const dataSchema = props['data']
|
|
75
|
+
return dataSchema ?? schema
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the PascalCase display name for a route, appending `V{version}`
|
|
80
|
+
* when version > 1. Version 1 produces no suffix for backward compatibility.
|
|
81
|
+
*/
|
|
82
|
+
function versionedPascal(name: string, version: number | undefined): string {
|
|
83
|
+
const pascal = toPascalCase(name)
|
|
84
|
+
if (version != null && version > 1) return `${pascal}V${version}`
|
|
85
|
+
return pascal
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Shared type formatting helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/** Indent each line of a multi-line string by a given prefix. */
|
|
93
|
+
function indent(text: string, prefix: string): string {
|
|
94
|
+
return text.split('\n').map((line) => (line ? prefix + line : line)).join('\n')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface NamedType {
|
|
98
|
+
/** Short name for namespace mode (e.g., 'Params', 'Response'). */
|
|
99
|
+
shortName: string
|
|
100
|
+
/** Schema to convert, or undefined if not present. */
|
|
101
|
+
schema: Record<string, unknown> | undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface FormattedTypes {
|
|
105
|
+
/** Type declarations to add to the file. */
|
|
106
|
+
declarations: string[]
|
|
107
|
+
/** Map of shortName → qualified type reference (for callables). */
|
|
108
|
+
refs: Record<string, string>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Converts multiple schemas into type declarations and type references.
|
|
113
|
+
* In flat mode: `export type ${routePascal}${shortName} = <body>`
|
|
114
|
+
* In namespace mode: extracted sub-types + named types inside `export namespace ${routePascal} { ... }`
|
|
115
|
+
*/
|
|
116
|
+
async function formatTypes(
|
|
117
|
+
routePascal: string,
|
|
118
|
+
types: NamedType[],
|
|
119
|
+
ctx: EmitRouteContext,
|
|
120
|
+
): Promise<FormattedTypes> {
|
|
121
|
+
const declarations: string[] = []
|
|
122
|
+
const refs: Record<string, string> = {}
|
|
123
|
+
|
|
124
|
+
if (ctx.namespaceTypes) {
|
|
125
|
+
const nsLines: string[] = []
|
|
126
|
+
const seenDeclarations = new Set<string>()
|
|
127
|
+
|
|
128
|
+
for (const { shortName, schema } of types) {
|
|
129
|
+
if (schema == null) continue
|
|
130
|
+
|
|
131
|
+
const result = await jsonSchemaToExtractedTypes(schema, ctx.ajsc)
|
|
132
|
+
if (result == null) continue
|
|
133
|
+
|
|
134
|
+
// Collect extracted sub-types (deduplicate across schemas)
|
|
135
|
+
for (const decl of result.declarations) {
|
|
136
|
+
if (!seenDeclarations.has(decl)) {
|
|
137
|
+
seenDeclarations.add(decl)
|
|
138
|
+
nsLines.push(indent(decl, ' '))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
nsLines.push(` export type ${shortName} = ${result.body}`)
|
|
143
|
+
refs[shortName] = `${ctx.scopePascal}.${routePascal}.${shortName}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (nsLines.length > 0) {
|
|
147
|
+
declarations.push(` export namespace ${routePascal} {\n${nsLines.join('\n')}\n }`)
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
for (const { shortName, schema } of types) {
|
|
151
|
+
if (schema == null) continue
|
|
152
|
+
|
|
153
|
+
const flatName = `${routePascal}${shortName}`
|
|
154
|
+
const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
|
|
155
|
+
if (body == null) continue
|
|
156
|
+
|
|
157
|
+
declarations.push(`export type ${flatName} = ${body}`)
|
|
158
|
+
refs[shortName] = flatName
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { declarations, refs }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Route emitters
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
async function emitRpcRoute(route: RPCHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
170
|
+
const pascal = versionedPascal(route.name, route.version)
|
|
171
|
+
|
|
172
|
+
const { declarations, refs } = await formatTypes(pascal, [
|
|
173
|
+
{ shortName: 'Params', schema: route.jsonSchema.body },
|
|
174
|
+
{ shortName: 'Response', schema: route.jsonSchema.response },
|
|
175
|
+
], ctx)
|
|
176
|
+
|
|
177
|
+
const paramsTypeName = refs['Params'] ?? 'unknown'
|
|
178
|
+
const responseTypeName = refs['Response'] ?? 'unknown'
|
|
179
|
+
const scopeStr = Array.isArray(route.scope) ? route.scope.join('-') : route.scope
|
|
180
|
+
|
|
181
|
+
const callable = [
|
|
182
|
+
` /** ${route.method.toUpperCase()} ${route.path} */`,
|
|
183
|
+
` ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
|
|
184
|
+
` return client.call<${responseTypeName}>({`,
|
|
185
|
+
` name: '${pascal}',`,
|
|
186
|
+
` scope: '${scopeStr}',`,
|
|
187
|
+
` path: '${route.path}',`,
|
|
188
|
+
` method: '${route.method}',`,
|
|
189
|
+
` kind: 'rpc',`,
|
|
190
|
+
` params,`,
|
|
191
|
+
` }, options)`,
|
|
192
|
+
` },`,
|
|
193
|
+
].join('\n')
|
|
194
|
+
|
|
195
|
+
return { typeDeclarations: declarations, callable, hasStream: false }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
199
|
+
const pascal = toPascalCase(route.name)
|
|
200
|
+
const channelKeys = ['pathParams', 'query', 'body', 'headers'] as const
|
|
201
|
+
|
|
202
|
+
// Build channel types + structured Params type
|
|
203
|
+
const channelTypes: NamedType[] = []
|
|
204
|
+
const presentChannels: string[] = []
|
|
205
|
+
|
|
206
|
+
for (const channel of channelKeys) {
|
|
207
|
+
const channelSchema = route.jsonSchema[channel]
|
|
208
|
+
if (channelSchema != null) {
|
|
209
|
+
channelTypes.push({ shortName: toPascalCase(channel), schema: channelSchema })
|
|
210
|
+
presentChannels.push(channel)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add response
|
|
215
|
+
channelTypes.push({ shortName: 'Response', schema: route.jsonSchema.response })
|
|
216
|
+
|
|
217
|
+
const { declarations, refs } = await formatTypes(pascal, channelTypes, ctx)
|
|
218
|
+
|
|
219
|
+
// Compose structured Params type from channels
|
|
220
|
+
let paramsTypeName = 'unknown'
|
|
221
|
+
if (presentChannels.length > 0) {
|
|
222
|
+
if (ctx.namespaceTypes) {
|
|
223
|
+
const structureFields = presentChannels
|
|
224
|
+
.map((ch) => `${ch}: ${toPascalCase(ch)}`)
|
|
225
|
+
.join('; ')
|
|
226
|
+
// Insert Params type into the namespace block (before closing brace)
|
|
227
|
+
const lastIdx = declarations.length - 1
|
|
228
|
+
if (lastIdx >= 0) {
|
|
229
|
+
const lastDecl = declarations[lastIdx]!
|
|
230
|
+
const closingIdx = lastDecl.lastIndexOf(' }')
|
|
231
|
+
if (closingIdx !== -1) {
|
|
232
|
+
declarations[lastIdx] =
|
|
233
|
+
lastDecl.slice(0, closingIdx) +
|
|
234
|
+
` export type Params = { ${structureFields} }\n` +
|
|
235
|
+
lastDecl.slice(closingIdx)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
paramsTypeName = `${ctx.scopePascal}.${pascal}.Params`
|
|
239
|
+
} else {
|
|
240
|
+
const structureFields = presentChannels
|
|
241
|
+
.map((ch) => `${ch}: ${refs[toPascalCase(ch)]}`)
|
|
242
|
+
.join('; ')
|
|
243
|
+
declarations.push(`export type ${pascal}Params = { ${structureFields} }`)
|
|
244
|
+
paramsTypeName = `${pascal}Params`
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const responseTypeName = refs['Response'] ?? 'unknown'
|
|
249
|
+
const scopeStr = route.scope ?? 'default'
|
|
250
|
+
|
|
251
|
+
const callable = [
|
|
252
|
+
` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
|
|
253
|
+
` ${route.name}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
|
|
254
|
+
` return client.call<${responseTypeName}>({`,
|
|
255
|
+
` name: '${route.name}',`,
|
|
256
|
+
` scope: '${scopeStr}',`,
|
|
257
|
+
` path: '${route.fullPath}',`,
|
|
258
|
+
` method: '${route.method}',`,
|
|
259
|
+
` kind: 'api',`,
|
|
260
|
+
` params,`,
|
|
261
|
+
` }, options)`,
|
|
262
|
+
` },`,
|
|
263
|
+
].join('\n')
|
|
264
|
+
|
|
265
|
+
return { typeDeclarations: declarations, callable, hasStream: false }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function emitStreamRoute(route: StreamHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
|
|
269
|
+
const pascal = versionedPascal(route.name, route.version)
|
|
270
|
+
|
|
271
|
+
// Unwrap SSE envelope from yieldType
|
|
272
|
+
let yieldSchema = route.jsonSchema.yieldType
|
|
273
|
+
if (yieldSchema != null && route.streamMode === 'sse' && isSseEnvelope(yieldSchema)) {
|
|
274
|
+
yieldSchema = unwrapSseEnvelope(yieldSchema)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { declarations, refs } = await formatTypes(pascal, [
|
|
278
|
+
{ shortName: 'Params', schema: route.jsonSchema.params },
|
|
279
|
+
{ shortName: 'Yield', schema: yieldSchema },
|
|
280
|
+
{ shortName: 'Return', schema: route.jsonSchema.returnType },
|
|
281
|
+
], ctx)
|
|
282
|
+
|
|
283
|
+
const paramsTypeName = refs['Params'] ?? 'unknown'
|
|
284
|
+
const yieldTypeName = refs['Yield'] ?? 'unknown'
|
|
285
|
+
const returnTypeName = refs['Return'] ?? 'void'
|
|
286
|
+
const scopeStr = Array.isArray(route.scope) ? route.scope.join('-') : route.scope
|
|
287
|
+
|
|
288
|
+
const callable = [
|
|
289
|
+
` /** ${route.methods.map((m) => m.toUpperCase()).join('|')} ${route.path} */`,
|
|
290
|
+
` ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): TypedStream<${yieldTypeName}, ${returnTypeName}> {`,
|
|
291
|
+
` return client.stream<${yieldTypeName}, ${returnTypeName}>({`,
|
|
292
|
+
` name: '${pascal}',`,
|
|
293
|
+
` scope: '${scopeStr}',`,
|
|
294
|
+
` path: '${route.path}',`,
|
|
295
|
+
` method: '${route.methods[0] ?? 'get'}',`,
|
|
296
|
+
` kind: 'stream',`,
|
|
297
|
+
` streamMode: '${route.streamMode}',`,
|
|
298
|
+
` params,`,
|
|
299
|
+
` }, options)`,
|
|
300
|
+
` },`,
|
|
301
|
+
].join('\n')
|
|
302
|
+
|
|
303
|
+
return { typeDeclarations: declarations, callable, hasStream: true }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// emitScopeFile
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generates a complete TypeScript scope file for a ScopeGroup.
|
|
312
|
+
*
|
|
313
|
+
* When `namespaceTypes` is true, types are wrapped in nested TypeScript namespaces
|
|
314
|
+
* and ajsc runs with `inlineTypes: false` so formatting options (enumStyle, depluralize,
|
|
315
|
+
* jsdoc, etc.) produce extracted sub-types inside each namespace.
|
|
316
|
+
*/
|
|
317
|
+
export async function emitScopeFile(
|
|
318
|
+
group: ScopeGroup,
|
|
319
|
+
options?: EmitScopeOptions,
|
|
320
|
+
): Promise<string> {
|
|
321
|
+
const { ajsc: ajscOpts, clientImportPath = 'ts-procedures/client', namespaceTypes = false } = options ?? {}
|
|
322
|
+
|
|
323
|
+
const pascal = toPascalCase(group.camelCase)
|
|
324
|
+
const ctx: EmitRouteContext = { ajsc: ajscOpts, namespaceTypes, scopePascal: pascal }
|
|
325
|
+
|
|
326
|
+
const allTypeDeclarations: string[] = []
|
|
327
|
+
const callables: string[] = []
|
|
328
|
+
let hasStream = false
|
|
329
|
+
|
|
330
|
+
for (const route of group.routes) {
|
|
331
|
+
let chunks: RouteChunks
|
|
332
|
+
const kind = route.kind ?? inferRouteKind(route as Record<string, unknown>)
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
if (kind === 'rpc') {
|
|
336
|
+
chunks = await emitRpcRoute(route as RPCHttpRouteDoc, ctx)
|
|
337
|
+
} else if (kind === 'api') {
|
|
338
|
+
chunks = await emitApiRoute(route as APIHttpRouteDoc, ctx)
|
|
339
|
+
} else if (kind === 'stream') {
|
|
340
|
+
chunks = await emitStreamRoute(route as StreamHttpRouteDoc, ctx)
|
|
341
|
+
} else {
|
|
342
|
+
throw new Error(`Unknown route kind "${kind}"`)
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
346
|
+
throw new Error(
|
|
347
|
+
`[ts-procedures-codegen] Failed to emit route "${route.name}" (kind: ${kind}, scope: ${group.scopeKey}): ${msg}`
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
allTypeDeclarations.push(...chunks.typeDeclarations)
|
|
352
|
+
callables.push(chunks.callable)
|
|
353
|
+
if (chunks.hasStream) hasStream = true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Build import line
|
|
357
|
+
const clientImports = hasStream
|
|
358
|
+
? `import type { ClientInstance, ProcedureCallOptions, TypedStream } from '${clientImportPath}'`
|
|
359
|
+
: `import type { ClientInstance, ProcedureCallOptions } from '${clientImportPath}'`
|
|
360
|
+
|
|
361
|
+
let typesBlock: string
|
|
362
|
+
if (namespaceTypes && allTypeDeclarations.length > 0) {
|
|
363
|
+
typesBlock = `export namespace ${pascal} {\n${allTypeDeclarations.join('\n\n')}\n}\n`
|
|
364
|
+
} else {
|
|
365
|
+
typesBlock =
|
|
366
|
+
allTypeDeclarations.length > 0
|
|
367
|
+
? allTypeDeclarations.join('\n') + '\n'
|
|
368
|
+
: ''
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const callablesBlock = callables.join('\n\n')
|
|
372
|
+
|
|
373
|
+
return [
|
|
374
|
+
CODEGEN_HEADER,
|
|
375
|
+
clientImports,
|
|
376
|
+
'',
|
|
377
|
+
'// ── Types ────────────────────────────────────────',
|
|
378
|
+
'',
|
|
379
|
+
typesBlock,
|
|
380
|
+
'// ── Callables ────────────────────────────────────',
|
|
381
|
+
'',
|
|
382
|
+
`export function bind${pascal}Scope(client: ClientInstance) {`,
|
|
383
|
+
' return {',
|
|
384
|
+
callablesBlock,
|
|
385
|
+
' }',
|
|
386
|
+
'}',
|
|
387
|
+
'',
|
|
388
|
+
].join('\n')
|
|
389
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { jsonSchemaToTypeString, jsonSchemaToTypeBody, jsonSchemaToExtractedTypes } from './emit-types.js'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Tests
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
describe('jsonSchemaToTypeString', () => {
|
|
9
|
+
it('converts a simple object schema with required fields', async () => {
|
|
10
|
+
const schema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
id: { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
required: ['id'],
|
|
16
|
+
}
|
|
17
|
+
const result = await jsonSchemaToTypeString('MyType', schema)
|
|
18
|
+
expect(result).toBe('export type MyType = { id: string; }')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('includes optional marker for non-required fields', async () => {
|
|
22
|
+
const schema = {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
id: { type: 'string' },
|
|
26
|
+
name: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
required: ['id'],
|
|
29
|
+
}
|
|
30
|
+
const result = await jsonSchemaToTypeString('User', schema)
|
|
31
|
+
expect(result).toBe('export type User = { id: string; name?: string; }')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('converts a string enum schema', async () => {
|
|
35
|
+
const schema = {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['a', 'b'],
|
|
38
|
+
}
|
|
39
|
+
const result = await jsonSchemaToTypeString('Status', schema)
|
|
40
|
+
expect(result).toBe('export type Status = "a" | "b"')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('converts a plain string schema', async () => {
|
|
44
|
+
const schema = { type: 'string' }
|
|
45
|
+
const result = await jsonSchemaToTypeString('Id', schema)
|
|
46
|
+
expect(result).toBe('export type Id = string')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns undefined for undefined schema', async () => {
|
|
50
|
+
const result = await jsonSchemaToTypeString('Missing', undefined)
|
|
51
|
+
expect(result).toBeUndefined()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('uses the typeName in the export statement', async () => {
|
|
55
|
+
const schema = { type: 'number' }
|
|
56
|
+
const result = await jsonSchemaToTypeString('Count', schema)
|
|
57
|
+
expect(result).toMatch(/^export type Count = /)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('handles already-clean inline output', async () => {
|
|
61
|
+
// Existing schemas still work — no regression
|
|
62
|
+
const schema = {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: { id: { type: 'string' } },
|
|
65
|
+
required: ['id'],
|
|
66
|
+
}
|
|
67
|
+
const result = await jsonSchemaToTypeString('MyType', schema)
|
|
68
|
+
expect(result).toBe('export type MyType = { id: string; }')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns undefined for null schema', async () => {
|
|
72
|
+
const result = await jsonSchemaToTypeString('Missing', null as unknown as undefined)
|
|
73
|
+
expect(result).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('jsonSchemaToTypeBody', () => {
|
|
78
|
+
it('returns bare type literal without export wrapper', async () => {
|
|
79
|
+
const schema = {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: { id: { type: 'string' } },
|
|
82
|
+
required: ['id'],
|
|
83
|
+
}
|
|
84
|
+
const result = await jsonSchemaToTypeBody(schema)
|
|
85
|
+
expect(result).toBe('{ id: string; }')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns undefined for undefined schema', async () => {
|
|
89
|
+
const result = await jsonSchemaToTypeBody(undefined)
|
|
90
|
+
expect(result).toBeUndefined()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns plain type for scalar schema', async () => {
|
|
94
|
+
const result = await jsonSchemaToTypeBody({ type: 'string' })
|
|
95
|
+
expect(result).toBe('string')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns union for enum schema', async () => {
|
|
99
|
+
const result = await jsonSchemaToTypeBody({ type: 'string', enum: ['a', 'b'] })
|
|
100
|
+
expect(result).toBe('"a" | "b"')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('jsonSchemaToExtractedTypes', () => {
|
|
105
|
+
it('returns undefined for undefined schema', async () => {
|
|
106
|
+
const result = await jsonSchemaToExtractedTypes(undefined)
|
|
107
|
+
expect(result).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('extracts nested object types as declarations', async () => {
|
|
111
|
+
const schema = {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
user: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: { name: { type: 'string' } },
|
|
117
|
+
required: ['name'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
required: ['user'],
|
|
121
|
+
}
|
|
122
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
123
|
+
expect(result).not.toBeUndefined()
|
|
124
|
+
expect(result!.declarations.length).toBeGreaterThan(0)
|
|
125
|
+
expect(result!.declarations.some((d) => d.includes('export type User'))).toBe(true)
|
|
126
|
+
expect(result!.body).toContain('user: User')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns empty declarations for flat schemas', async () => {
|
|
130
|
+
const schema = { type: 'string' }
|
|
131
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
132
|
+
expect(result).not.toBeUndefined()
|
|
133
|
+
expect(result!.declarations).toEqual([])
|
|
134
|
+
expect(result!.body).toBe('string')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('extracts enums with enumStyle: enum', async () => {
|
|
138
|
+
const schema = {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
status: { type: 'string', enum: ['active', 'pending'] },
|
|
142
|
+
},
|
|
143
|
+
required: ['status'],
|
|
144
|
+
}
|
|
145
|
+
const result = await jsonSchemaToExtractedTypes(schema, { enumStyle: 'enum' })
|
|
146
|
+
expect(result).not.toBeUndefined()
|
|
147
|
+
expect(result!.declarations.some((d) => d.includes('export enum Status'))).toBe(true)
|
|
148
|
+
expect(result!.body).toContain('status: Status')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('strips trailing semicolons from declarations', async () => {
|
|
152
|
+
const schema = {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
item: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: { id: { type: 'string' } },
|
|
158
|
+
required: ['id'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
required: ['item'],
|
|
162
|
+
}
|
|
163
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
164
|
+
expect(result).not.toBeUndefined()
|
|
165
|
+
for (const decl of result!.declarations) {
|
|
166
|
+
expect(decl.trimEnd()).not.toMatch(/;\s*$/)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('body does not contain export type Root prefix', async () => {
|
|
171
|
+
const schema = {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: { id: { type: 'string' } },
|
|
174
|
+
required: ['id'],
|
|
175
|
+
}
|
|
176
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
177
|
+
expect(result).not.toBeUndefined()
|
|
178
|
+
expect(result!.body).not.toContain('export type Root')
|
|
179
|
+
expect(result!.body).toContain('id: string')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('jsonSchemaToTypeString (prefix stripping)', () => {
|
|
184
|
+
it('strips "export type Root = " prefix and does not double-wrap', async () => {
|
|
185
|
+
vi.resetModules()
|
|
186
|
+
vi.doMock('ajsc', () => ({
|
|
187
|
+
TypescriptConverter: class {
|
|
188
|
+
code: string
|
|
189
|
+
constructor(_schema: unknown, _opts: unknown) {
|
|
190
|
+
this.code = '\n\nexport type Root = { id: string; };\n'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
}))
|
|
194
|
+
|
|
195
|
+
const { jsonSchemaToTypeString: fn } = await import('./emit-types.js')
|
|
196
|
+
const result = await fn('MyType', { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] })
|
|
197
|
+
// Should be the clean body, not double-wrapped
|
|
198
|
+
expect(result).toBe('export type MyType = { id: string; }')
|
|
199
|
+
expect(result).not.toContain('export type Root')
|
|
200
|
+
expect(result).not.toContain('export type MyType = export type')
|
|
201
|
+
|
|
202
|
+
vi.doUnmock('ajsc')
|
|
203
|
+
vi.resetModules()
|
|
204
|
+
})
|
|
205
|
+
})
|