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,158 @@
|
|
|
1
|
+
export interface AjscOptions {
|
|
2
|
+
enumStyle?: 'union' | 'enum'
|
|
3
|
+
depluralize?: boolean
|
|
4
|
+
inlineTypes?: boolean
|
|
5
|
+
arrayItemNaming?: string | false
|
|
6
|
+
uncountableWords?: string[]
|
|
7
|
+
jsdoc?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let ajscValidated = false
|
|
11
|
+
async function validateAjscFormat() {
|
|
12
|
+
if (ajscValidated) return
|
|
13
|
+
ajscValidated = true
|
|
14
|
+
const { TypescriptConverter } = await import('ajsc')
|
|
15
|
+
const probe = new TypescriptConverter({ type: 'string' }, { inlineTypes: true })
|
|
16
|
+
const probeCode = (probe.code as string).trim()
|
|
17
|
+
if (probeCode !== 'string') {
|
|
18
|
+
console.warn(`[ts-procedures-codegen] ajsc output format may have changed. Expected "string" for { type: "string" }, got: "${probeCode}"`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function resolveTypeBody(
|
|
23
|
+
schema: Record<string, unknown>,
|
|
24
|
+
options?: AjscOptions,
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
await validateAjscFormat()
|
|
27
|
+
|
|
28
|
+
const { TypescriptConverter } = await import('ajsc')
|
|
29
|
+
|
|
30
|
+
const converter = new TypescriptConverter(schema, {
|
|
31
|
+
...options,
|
|
32
|
+
inlineTypes: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
let code = (converter.code as string).trim()
|
|
36
|
+
if (!code) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[ts-procedures-codegen] ajsc produced empty output. Schema: ${JSON.stringify(schema)}`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
// Strip any 'export type X = ' or 'type X = ' prefix ajsc might add
|
|
42
|
+
code = code.replace(/^(?:export\s+)?type\s+\w+\s*=\s*/, '')
|
|
43
|
+
// Remove trailing semicolons and newlines
|
|
44
|
+
code = code.replace(/;\s*$/, '').trim()
|
|
45
|
+
|
|
46
|
+
return code
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts a JSON Schema to a TypeScript type string using ajsc.
|
|
51
|
+
*
|
|
52
|
+
* Returns `export type ${typeName} = ${code}` where code is the inline type
|
|
53
|
+
* body from TypescriptConverter with inlineTypes: true.
|
|
54
|
+
*
|
|
55
|
+
* Returns undefined for undefined or null schema.
|
|
56
|
+
*/
|
|
57
|
+
export async function jsonSchemaToTypeString(
|
|
58
|
+
typeName: string,
|
|
59
|
+
schema: Record<string, unknown> | undefined,
|
|
60
|
+
options?: AjscOptions,
|
|
61
|
+
): Promise<string | undefined> {
|
|
62
|
+
if (schema == null) return undefined
|
|
63
|
+
const code = await resolveTypeBody(schema, options)
|
|
64
|
+
return `export type ${typeName} = ${code}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Converts a JSON Schema to a bare TypeScript type body using ajsc.
|
|
69
|
+
*
|
|
70
|
+
* Returns just the type literal (e.g., `{ id: string; }`) without any
|
|
71
|
+
* `export type Name =` wrapper. Used by flat mode in emit-scope.
|
|
72
|
+
*
|
|
73
|
+
* Returns undefined for undefined or null schema.
|
|
74
|
+
*/
|
|
75
|
+
export async function jsonSchemaToTypeBody(
|
|
76
|
+
schema: Record<string, unknown> | undefined,
|
|
77
|
+
options?: AjscOptions,
|
|
78
|
+
): Promise<string | undefined> {
|
|
79
|
+
if (schema == null) return undefined
|
|
80
|
+
return resolveTypeBody(schema, options)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Extracted types (namespace mode — uses inlineTypes: false)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export interface ExtractedTypeOutput {
|
|
88
|
+
/** Extracted sub-type/enum declarations (e.g., `export type Address = { ... }`, `export enum Status { ... }`). */
|
|
89
|
+
declarations: string[]
|
|
90
|
+
/** The root type body (bare literal, e.g., `{ user: User; }`) without the `export type Root =` wrapper. */
|
|
91
|
+
body: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Converts a JSON Schema to extracted TypeScript types using ajsc with
|
|
96
|
+
* `inlineTypes: false`. This produces named sub-types (objects, enums) that
|
|
97
|
+
* are separated from the root type body.
|
|
98
|
+
*
|
|
99
|
+
* Used in namespace mode where sub-types live naturally inside the namespace
|
|
100
|
+
* and ajsc formatting options (enumStyle, depluralize, jsdoc, etc.) take effect.
|
|
101
|
+
*
|
|
102
|
+
* Returns undefined for undefined or null schema.
|
|
103
|
+
*/
|
|
104
|
+
export async function jsonSchemaToExtractedTypes(
|
|
105
|
+
schema: Record<string, unknown> | undefined,
|
|
106
|
+
options?: AjscOptions,
|
|
107
|
+
): Promise<ExtractedTypeOutput | undefined> {
|
|
108
|
+
if (schema == null) return undefined
|
|
109
|
+
|
|
110
|
+
await validateAjscFormat()
|
|
111
|
+
|
|
112
|
+
const { TypescriptConverter } = await import('ajsc')
|
|
113
|
+
const converter = new TypescriptConverter(schema, {
|
|
114
|
+
...options,
|
|
115
|
+
inlineTypes: false,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const code = (converter.code as string).trim()
|
|
119
|
+
if (!code) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[ts-procedures-codegen] ajsc produced empty output. Schema: ${JSON.stringify(schema)}`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ajsc with inlineTypes: false produces blocks separated by blank lines:
|
|
126
|
+
// export enum Status { Active = "active" }
|
|
127
|
+
// export type Contact = { name: string; };
|
|
128
|
+
// export type Root = { status: Status; contacts: Array<Contact>; };
|
|
129
|
+
//
|
|
130
|
+
// The Root type is always the last declaration.
|
|
131
|
+
const blocks = code.split(/\n\n+/).map((b) => b.trim()).filter(Boolean)
|
|
132
|
+
|
|
133
|
+
const declarations: string[] = []
|
|
134
|
+
let body = ''
|
|
135
|
+
|
|
136
|
+
for (const block of blocks) {
|
|
137
|
+
if (block.startsWith('export type Root')) {
|
|
138
|
+
// Strip "export type Root = " prefix and trailing ";"
|
|
139
|
+
body = block
|
|
140
|
+
.replace(/^export\s+type\s+Root\s*=\s*/, '')
|
|
141
|
+
.replace(/;\s*$/, '')
|
|
142
|
+
.trim()
|
|
143
|
+
} else {
|
|
144
|
+
// Sub-type or enum declaration — remove trailing ";" for consistency
|
|
145
|
+
declarations.push(block.replace(/;\s*$/, ''))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!body) {
|
|
150
|
+
// Fallback: treat whole output as inline body
|
|
151
|
+
body = code
|
|
152
|
+
.replace(/^(?:export\s+)?type\s+\w+\s*=\s*/, '')
|
|
153
|
+
.replace(/;\s*$/, '')
|
|
154
|
+
.trim()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { declarations, body }
|
|
158
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { normalizeScope, scopeToCamelCase, groupRoutesByScope } from './group-routes.js'
|
|
3
|
+
import type { AnyHttpRouteDoc } from '../implementations/types.js'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Fixtures
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const rpcRouteUsers: AnyHttpRouteDoc = {
|
|
10
|
+
kind: 'rpc',
|
|
11
|
+
name: 'GetUser',
|
|
12
|
+
path: '/users/1',
|
|
13
|
+
method: 'post',
|
|
14
|
+
scope: 'users',
|
|
15
|
+
version: 1,
|
|
16
|
+
jsonSchema: {},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rpcRouteAdmin: AnyHttpRouteDoc = {
|
|
20
|
+
kind: 'rpc',
|
|
21
|
+
name: 'DeleteUser',
|
|
22
|
+
path: '/admin-users/1',
|
|
23
|
+
method: 'post',
|
|
24
|
+
scope: ['admin', 'users'],
|
|
25
|
+
version: 1,
|
|
26
|
+
jsonSchema: {},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rpcRouteUsers2: AnyHttpRouteDoc = {
|
|
30
|
+
kind: 'rpc',
|
|
31
|
+
name: 'ListUsers',
|
|
32
|
+
path: '/users/1',
|
|
33
|
+
method: 'post',
|
|
34
|
+
scope: 'users',
|
|
35
|
+
version: 1,
|
|
36
|
+
jsonSchema: {},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const apiRouteNoScope: AnyHttpRouteDoc = {
|
|
40
|
+
kind: 'api',
|
|
41
|
+
name: 'HealthCheck',
|
|
42
|
+
path: '/health',
|
|
43
|
+
method: 'get',
|
|
44
|
+
fullPath: '/api/health',
|
|
45
|
+
jsonSchema: {},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const apiRouteWithScope: AnyHttpRouteDoc = {
|
|
49
|
+
kind: 'api',
|
|
50
|
+
name: 'ListPosts',
|
|
51
|
+
path: '/posts',
|
|
52
|
+
method: 'get',
|
|
53
|
+
fullPath: '/api/posts',
|
|
54
|
+
scope: 'posts',
|
|
55
|
+
jsonSchema: {},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// normalizeScope
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe('normalizeScope', () => {
|
|
63
|
+
it('returns a string scope as-is', () => {
|
|
64
|
+
expect(normalizeScope('users')).toBe('users')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('joins a string array scope with hyphens', () => {
|
|
68
|
+
expect(normalizeScope(['admin', 'users'])).toBe('admin-users')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns "default" for undefined scope', () => {
|
|
72
|
+
expect(normalizeScope(undefined)).toBe('default')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// scopeToCamelCase
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('scopeToCamelCase', () => {
|
|
81
|
+
it('returns a single word as-is', () => {
|
|
82
|
+
expect(scopeToCamelCase('users')).toBe('users')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('converts a hyphenated scope to camelCase', () => {
|
|
86
|
+
expect(scopeToCamelCase('admin-users')).toBe('adminUsers')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('converts multi-segment hyphenated scope', () => {
|
|
90
|
+
expect(scopeToCamelCase('super-admin-users')).toBe('superAdminUsers')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// groupRoutesByScope
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe('groupRoutesByScope', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.restoreAllMocks()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('groups routes by their scope', () => {
|
|
108
|
+
const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, rpcRouteUsers2]
|
|
109
|
+
const groups = groupRoutesByScope(routes)
|
|
110
|
+
|
|
111
|
+
expect(groups.size).toBe(1)
|
|
112
|
+
expect(groups.has('users')).toBe(true)
|
|
113
|
+
|
|
114
|
+
const usersGroup = groups.get('users')!
|
|
115
|
+
expect(usersGroup.scopeKey).toBe('users')
|
|
116
|
+
expect(usersGroup.camelCase).toBe('users')
|
|
117
|
+
expect(usersGroup.routes).toHaveLength(2)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('handles array scopes by joining with hyphens', () => {
|
|
121
|
+
const routes: AnyHttpRouteDoc[] = [rpcRouteAdmin]
|
|
122
|
+
const groups = groupRoutesByScope(routes)
|
|
123
|
+
|
|
124
|
+
expect(groups.has('admin-users')).toBe(true)
|
|
125
|
+
const group = groups.get('admin-users')!
|
|
126
|
+
expect(group.scopeKey).toBe('admin-users')
|
|
127
|
+
expect(group.camelCase).toBe('adminUsers')
|
|
128
|
+
expect(group.routes).toHaveLength(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('groups routes from multiple scopes', () => {
|
|
132
|
+
const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, rpcRouteAdmin, apiRouteWithScope]
|
|
133
|
+
const groups = groupRoutesByScope(routes)
|
|
134
|
+
|
|
135
|
+
expect(groups.size).toBe(3)
|
|
136
|
+
expect(groups.has('users')).toBe(true)
|
|
137
|
+
expect(groups.has('admin-users')).toBe(true)
|
|
138
|
+
expect(groups.has('posts')).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('warns on missing scope and groups into "default"', () => {
|
|
142
|
+
const routes: AnyHttpRouteDoc[] = [apiRouteNoScope]
|
|
143
|
+
const groups = groupRoutesByScope(routes)
|
|
144
|
+
|
|
145
|
+
expect(console.warn).toHaveBeenCalled()
|
|
146
|
+
expect(groups.has('default')).toBe(true)
|
|
147
|
+
|
|
148
|
+
const defaultGroup = groups.get('default')!
|
|
149
|
+
expect(defaultGroup.scopeKey).toBe('default')
|
|
150
|
+
expect(defaultGroup.camelCase).toBe('default')
|
|
151
|
+
expect(defaultGroup.routes).toHaveLength(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('does not warn when all routes have scopes', () => {
|
|
155
|
+
const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, apiRouteWithScope]
|
|
156
|
+
groupRoutesByScope(routes)
|
|
157
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AnyHttpRouteDoc } from '../implementations/types.js'
|
|
2
|
+
|
|
3
|
+
export interface ScopeGroup {
|
|
4
|
+
scopeKey: string
|
|
5
|
+
camelCase: string
|
|
6
|
+
routes: AnyHttpRouteDoc[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes a scope value to a string key:
|
|
11
|
+
* - string → returned as-is
|
|
12
|
+
* - string[] → joined with '-'
|
|
13
|
+
* - undefined → 'default'
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeScope(scope: string | string[] | undefined): string {
|
|
16
|
+
if (scope === undefined) return 'default'
|
|
17
|
+
if (Array.isArray(scope)) return scope.join('-')
|
|
18
|
+
return scope
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a hyphenated scope key to camelCase.
|
|
23
|
+
* e.g. 'admin-users' → 'adminUsers', 'users' → 'users'
|
|
24
|
+
*/
|
|
25
|
+
export function scopeToCamelCase(scope: string): string {
|
|
26
|
+
const parts = scope.split('-')
|
|
27
|
+
return parts
|
|
28
|
+
.map((part, index) =>
|
|
29
|
+
index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
30
|
+
)
|
|
31
|
+
.join('')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Groups an array of route docs into a Map keyed by normalized scope.
|
|
36
|
+
* Routes without a scope emit a console.warn and are grouped under 'default'.
|
|
37
|
+
*/
|
|
38
|
+
export function groupRoutesByScope(routes: AnyHttpRouteDoc[]): Map<string, ScopeGroup> {
|
|
39
|
+
const groups = new Map<string, ScopeGroup>()
|
|
40
|
+
|
|
41
|
+
for (const route of routes) {
|
|
42
|
+
const rawScope = 'scope' in route ? (route as { scope?: string | string[] }).scope : undefined
|
|
43
|
+
if (rawScope === undefined) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[ts-procedures] Route "${route.name}" has no scope — it will be grouped under "default".`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scopeKey = normalizeScope(rawScope)
|
|
50
|
+
const camelCase = scopeToCamelCase(scopeKey)
|
|
51
|
+
|
|
52
|
+
let group = groups.get(scopeKey)
|
|
53
|
+
if (group === undefined) {
|
|
54
|
+
group = { scopeKey, camelCase, routes: [] }
|
|
55
|
+
groups.set(scopeKey, group)
|
|
56
|
+
}
|
|
57
|
+
group.routes.push(route)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return groups
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolveEnvelope, type ResolveInput } from './resolve-envelope.js'
|
|
2
|
+
import { runPipeline, type GeneratedFile } from './pipeline.js'
|
|
3
|
+
import type { AjscOptions } from './emit-types.js'
|
|
4
|
+
|
|
5
|
+
export interface GenerateClientOptions extends ResolveInput {
|
|
6
|
+
outDir: string
|
|
7
|
+
ajsc?: AjscOptions
|
|
8
|
+
clientImportPath?: string
|
|
9
|
+
dryRun?: boolean
|
|
10
|
+
namespaceTypes?: boolean
|
|
11
|
+
selfContained?: boolean
|
|
12
|
+
serviceName?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function generateClient(options: GenerateClientOptions): Promise<GeneratedFile[]> {
|
|
16
|
+
const envelope = await resolveEnvelope(options)
|
|
17
|
+
return runPipeline({
|
|
18
|
+
envelope,
|
|
19
|
+
outDir: options.outDir,
|
|
20
|
+
ajsc: options.ajsc,
|
|
21
|
+
clientImportPath: options.clientImportPath,
|
|
22
|
+
dryRun: options.dryRun,
|
|
23
|
+
namespaceTypes: options.namespaceTypes,
|
|
24
|
+
selfContained: options.selfContained,
|
|
25
|
+
serviceName: options.serviceName,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type { AjscOptions } from './emit-types.js'
|
|
30
|
+
export type { ResolveInput } from './resolve-envelope.js'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { toPascalCase, validateServiceName } from './naming.js'
|
|
3
|
+
|
|
4
|
+
describe('toPascalCase', () => {
|
|
5
|
+
it('converts a single word', () => {
|
|
6
|
+
expect(toPascalCase('auth')).toBe('Auth')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('converts hyphen-separated words', () => {
|
|
10
|
+
expect(toPascalCase('auth-service')).toBe('AuthService')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('converts underscore-separated words', () => {
|
|
14
|
+
expect(toPascalCase('users_api')).toBe('UsersApi')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('converts space-separated words', () => {
|
|
18
|
+
expect(toPascalCase('my service')).toBe('MyService')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('preserves already-PascalCased input', () => {
|
|
22
|
+
expect(toPascalCase('UsersApi')).toBe('UsersApi')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('validateServiceName', () => {
|
|
27
|
+
it('accepts a valid PascalCase name', () => {
|
|
28
|
+
expect(() => validateServiceName('Auth')).not.toThrow()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('accepts a hyphen-separated name', () => {
|
|
32
|
+
expect(() => validateServiceName('auth-service')).not.toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('accepts an underscore-separated name', () => {
|
|
36
|
+
expect(() => validateServiceName('users_api')).not.toThrow()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('throws for an empty string', () => {
|
|
40
|
+
expect(() => validateServiceName('')).toThrow('empty')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('throws for a string of only separators', () => {
|
|
44
|
+
expect(() => validateServiceName('---')).toThrow('empty')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws when the result starts with a digit', () => {
|
|
48
|
+
expect(() => validateServiceName('123service')).toThrow('starts with a digit')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Converts a string to PascalCase (splits on whitespace, hyphens, underscores). */
|
|
2
|
+
export function toPascalCase(str: string): string {
|
|
3
|
+
return str
|
|
4
|
+
.split(/[\s\-_]+/)
|
|
5
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
6
|
+
.join('')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates that a serviceName option is usable as a TypeScript identifier fragment.
|
|
11
|
+
* Throws a descriptive error if invalid.
|
|
12
|
+
*/
|
|
13
|
+
export function validateServiceName(serviceName: string): void {
|
|
14
|
+
const pascal = toPascalCase(serviceName)
|
|
15
|
+
if (pascal.length === 0) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`[ts-procedures-codegen] --service-name value "${serviceName}" is empty or contains only separator characters.`
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
if (/^\d/.test(pascal)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`[ts-procedures-codegen] --service-name value "${serviceName}" produces "${pascal}" which starts with a digit — provide a name that starts with a letter.`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|