ts-procedures 6.1.0 → 6.2.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/agent_config/bin/setup.mjs +2 -2
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
- package/agent_config/copilot/copilot-instructions.md +1 -0
- package/agent_config/cursor/cursorrules +1 -0
- package/agent_config/lib/install-claude.mjs +1 -1
- package/build/codegen/bin/cli.d.ts +17 -3
- package/build/codegen/bin/cli.js +79 -3
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/index.d.ts +18 -1
- package/build/codegen/index.js +3 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/pipeline.d.ts +16 -5
- package/build/codegen/pipeline.js +44 -143
- package/build/codegen/pipeline.js.map +1 -1
- package/build/codegen/targets/_shared/error-schemas.d.ts +10 -0
- package/build/codegen/targets/_shared/error-schemas.js +17 -0
- package/build/codegen/targets/_shared/error-schemas.js.map +1 -0
- package/build/codegen/targets/_shared/error-schemas.test.d.ts +1 -0
- package/build/codegen/targets/_shared/error-schemas.test.js +38 -0
- package/build/codegen/targets/_shared/error-schemas.test.js.map +1 -0
- package/build/codegen/targets/_shared/indent.d.ts +6 -0
- package/build/codegen/targets/_shared/indent.js +13 -0
- package/build/codegen/targets/_shared/indent.js.map +1 -0
- package/build/codegen/targets/_shared/indent.test.d.ts +1 -0
- package/build/codegen/targets/_shared/indent.test.js +21 -0
- package/build/codegen/targets/_shared/indent.test.js.map +1 -0
- package/build/codegen/targets/_shared/pascal-case.d.ts +6 -0
- package/build/codegen/targets/_shared/pascal-case.js +13 -0
- package/build/codegen/targets/_shared/pascal-case.js.map +1 -0
- package/build/codegen/targets/_shared/pascal-case.test.d.ts +1 -0
- package/build/codegen/targets/_shared/pascal-case.test.js +25 -0
- package/build/codegen/targets/_shared/pascal-case.test.js.map +1 -0
- package/build/codegen/targets/_shared/path-utils.d.ts +12 -0
- package/build/codegen/targets/_shared/path-utils.js +20 -0
- package/build/codegen/targets/_shared/path-utils.js.map +1 -0
- package/build/codegen/targets/_shared/path-utils.test.d.ts +1 -0
- package/build/codegen/targets/_shared/path-utils.test.js +42 -0
- package/build/codegen/targets/_shared/path-utils.test.js.map +1 -0
- package/build/codegen/targets/_shared/pick-defined.d.ts +11 -0
- package/build/codegen/targets/_shared/pick-defined.js +21 -0
- package/build/codegen/targets/_shared/pick-defined.js.map +1 -0
- package/build/codegen/targets/_shared/pick-defined.test.d.ts +1 -0
- package/build/codegen/targets/_shared/pick-defined.test.js +25 -0
- package/build/codegen/targets/_shared/pick-defined.test.js.map +1 -0
- package/build/codegen/targets/_shared/route-slots.d.ts +17 -0
- package/build/codegen/targets/_shared/route-slots.js +17 -0
- package/build/codegen/targets/_shared/route-slots.js.map +1 -0
- package/build/codegen/targets/_shared/route-slots.test.d.ts +1 -0
- package/build/codegen/targets/_shared/route-slots.test.js +43 -0
- package/build/codegen/targets/_shared/route-slots.test.js.map +1 -0
- package/build/codegen/targets/_shared/target-run.d.ts +27 -0
- package/build/codegen/targets/_shared/target-run.js +2 -0
- package/build/codegen/targets/_shared/target-run.js.map +1 -0
- package/build/codegen/targets/_shared/write-files.d.ts +24 -0
- package/build/codegen/targets/_shared/write-files.js +35 -0
- package/build/codegen/targets/_shared/write-files.js.map +1 -0
- package/build/codegen/targets/_shared/write-files.test.d.ts +1 -0
- package/build/codegen/targets/_shared/write-files.test.js +79 -0
- package/build/codegen/targets/_shared/write-files.test.js.map +1 -0
- package/build/codegen/targets/kotlin/e2e-compile.test.js +1 -1
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +5 -22
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +4 -8
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -12
- package/build/codegen/targets/kotlin/format-kotlin.js +0 -27
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -34
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +1 -1
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/kotlin/run.d.ts +11 -0
- package/build/codegen/targets/kotlin/run.js +51 -0
- package/build/codegen/targets/kotlin/run.js.map +1 -0
- package/build/codegen/targets/swift/access-level.test.d.ts +1 -0
- package/build/codegen/targets/swift/access-level.test.js +98 -0
- package/build/codegen/targets/swift/access-level.test.js.map +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.d.ts +27 -0
- package/build/codegen/targets/swift/ajsc-adapter.js +38 -0
- package/build/codegen/targets/swift/ajsc-adapter.js.map +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.d.ts +1 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.js +37 -0
- package/build/codegen/targets/swift/ajsc-adapter.test.js.map +1 -0
- package/build/codegen/targets/swift/e2e-compile.test.d.ts +1 -0
- package/build/codegen/targets/swift/e2e-compile.test.js +57 -0
- package/build/codegen/targets/swift/e2e-compile.test.js.map +1 -0
- package/build/codegen/targets/swift/emit-route-swift.d.ts +15 -0
- package/build/codegen/targets/swift/emit-route-swift.js +64 -0
- package/build/codegen/targets/swift/emit-route-swift.js.map +1 -0
- package/build/codegen/targets/swift/emit-route-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/emit-route-swift.test.js +258 -0
- package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.d.ts +13 -0
- package/build/codegen/targets/swift/emit-scope-swift.js +36 -0
- package/build/codegen/targets/swift/emit-scope-swift.js.map +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.js +136 -0
- package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/format-swift.d.ts +2 -0
- package/build/codegen/targets/swift/format-swift.js +10 -0
- package/build/codegen/targets/swift/format-swift.js.map +1 -0
- package/build/codegen/targets/swift/format-swift.test.d.ts +1 -0
- package/build/codegen/targets/swift/format-swift.test.js +14 -0
- package/build/codegen/targets/swift/format-swift.test.js.map +1 -0
- package/build/codegen/targets/swift/integration.test.d.ts +1 -0
- package/build/codegen/targets/swift/integration.test.js +53 -0
- package/build/codegen/targets/swift/integration.test.js.map +1 -0
- package/build/codegen/targets/swift/run.d.ts +11 -0
- package/build/codegen/targets/swift/run.js +47 -0
- package/build/codegen/targets/swift/run.js.map +1 -0
- package/build/codegen/targets/ts/run.d.ts +4 -0
- package/build/codegen/targets/ts/run.js +86 -0
- package/build/codegen/targets/ts/run.js.map +1 -0
- package/docs/codegen-kotlin.md +1 -0
- package/docs/codegen-swift.md +314 -0
- package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +1 -1
- package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +1 -1
- package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
- package/package.json +2 -2
- package/src/codegen/bin/cli.ts +91 -7
- package/src/codegen/index.ts +24 -1
- package/src/codegen/pipeline.ts +52 -174
- package/src/codegen/targets/_shared/error-schemas.test.ts +42 -0
- package/src/codegen/targets/_shared/error-schemas.ts +17 -0
- package/src/codegen/targets/_shared/indent.test.ts +25 -0
- package/src/codegen/targets/_shared/indent.ts +12 -0
- package/src/codegen/targets/_shared/pascal-case.test.ts +30 -0
- package/src/codegen/targets/_shared/pascal-case.ts +12 -0
- package/src/codegen/targets/_shared/path-utils.test.ts +51 -0
- package/src/codegen/targets/_shared/path-utils.ts +21 -0
- package/src/codegen/targets/_shared/pick-defined.test.ts +48 -0
- package/src/codegen/targets/_shared/pick-defined.ts +23 -0
- package/src/codegen/targets/_shared/route-slots.test.ts +55 -0
- package/src/codegen/targets/_shared/route-slots.ts +32 -0
- package/src/codegen/targets/_shared/target-run.ts +28 -0
- package/src/codegen/targets/_shared/write-files.test.ts +110 -0
- package/src/codegen/targets/_shared/write-files.ts +53 -0
- package/src/codegen/targets/kotlin/e2e-compile.test.ts +1 -1
- package/src/codegen/targets/kotlin/emit-route-kotlin.ts +5 -25
- package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +4 -9
- package/src/codegen/targets/kotlin/format-kotlin.test.ts +0 -44
- package/src/codegen/targets/kotlin/format-kotlin.ts +0 -32
- package/src/codegen/targets/kotlin/integration.test.ts +1 -1
- package/src/codegen/targets/kotlin/run.ts +78 -0
- package/src/codegen/targets/swift/__fixtures__/users-golden.swift +123 -0
- package/src/codegen/targets/swift/access-level.test.ts +108 -0
- package/src/codegen/targets/swift/ajsc-adapter.test.ts +47 -0
- package/src/codegen/targets/swift/ajsc-adapter.ts +67 -0
- package/src/codegen/targets/swift/e2e-compile.test.ts +66 -0
- package/src/codegen/targets/swift/emit-route-swift.test.ts +300 -0
- package/src/codegen/targets/swift/emit-route-swift.ts +90 -0
- package/src/codegen/targets/swift/emit-scope-swift.test.ts +164 -0
- package/src/codegen/targets/swift/emit-scope-swift.ts +59 -0
- package/src/codegen/targets/swift/format-swift.test.ts +23 -0
- package/src/codegen/targets/swift/format-swift.ts +9 -0
- package/src/codegen/targets/swift/integration.test.ts +80 -0
- package/src/codegen/targets/swift/run.ts +74 -0
- package/src/codegen/targets/ts/run.ts +117 -0
- /package/src/codegen/{targets/kotlin/__fixtures__ → __fixtures__}/users-envelope.json +0 -0
package/src/codegen/bin/cli.ts
CHANGED
|
@@ -22,8 +22,9 @@ export interface CodegenConfig {
|
|
|
22
22
|
selfContained?: boolean
|
|
23
23
|
serviceName?: string
|
|
24
24
|
cleanOutDir?: boolean
|
|
25
|
-
target?: 'ts' | 'kotlin'
|
|
25
|
+
target?: 'ts' | 'kotlin' | 'swift'
|
|
26
26
|
kotlin?: { package: string; serializer?: 'kotlinx' | 'none' }
|
|
27
|
+
swift?: { serializer?: 'codable' | 'none'; accessLevel?: 'public' | 'internal' }
|
|
27
28
|
unsupportedUnions?: 'throw' | 'fallback'
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -40,8 +41,9 @@ export interface ParsedArgs {
|
|
|
40
41
|
selfContained: boolean
|
|
41
42
|
serviceName?: string
|
|
42
43
|
cleanOutDir: boolean
|
|
43
|
-
target?: 'ts' | 'kotlin'
|
|
44
|
+
target?: 'ts' | 'kotlin' | 'swift'
|
|
44
45
|
kotlin?: { package: string; serializer?: 'kotlinx' | 'none' }
|
|
46
|
+
swift?: { serializer?: 'codable' | 'none'; accessLevel?: 'public' | 'internal' }
|
|
45
47
|
unsupportedUnions?: 'throw' | 'fallback'
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -94,9 +96,11 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
94
96
|
let selfContained = config?.selfContained ?? true
|
|
95
97
|
let serviceName: string | undefined = config?.serviceName
|
|
96
98
|
let cleanOutDir = config?.cleanOutDir ?? false
|
|
97
|
-
let target: 'ts' | 'kotlin' | undefined = config?.target
|
|
99
|
+
let target: 'ts' | 'kotlin' | 'swift' | undefined = config?.target
|
|
98
100
|
let kotlinPackage: string | undefined = config?.kotlin?.package
|
|
99
101
|
let kotlinSerializer: 'kotlinx' | 'none' | undefined = config?.kotlin?.serializer
|
|
102
|
+
let swiftSerializer: 'codable' | 'none' | undefined = config?.swift?.serializer
|
|
103
|
+
let swiftAccessLevel: 'public' | 'internal' | undefined = config?.swift?.accessLevel
|
|
100
104
|
let unsupportedUnions: 'throw' | 'fallback' | undefined = config?.unsupportedUnions
|
|
101
105
|
let configPath: string | undefined
|
|
102
106
|
|
|
@@ -149,10 +153,10 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
149
153
|
cleanOutDir = false
|
|
150
154
|
} else if (arg === '--target') {
|
|
151
155
|
const val = argv[++i]
|
|
152
|
-
if (val === 'ts' || val === 'kotlin') {
|
|
156
|
+
if (val === 'ts' || val === 'kotlin' || val === 'swift') {
|
|
153
157
|
target = val
|
|
154
158
|
} else {
|
|
155
|
-
throw new Error(`Invalid --target value: ${val ?? '(missing)'} (expected 'ts' or '
|
|
159
|
+
throw new Error(`Invalid --target value: ${val ?? '(missing)'} (expected 'ts', 'kotlin', or 'swift')`)
|
|
156
160
|
}
|
|
157
161
|
} else if (arg === '--kotlin-package') {
|
|
158
162
|
kotlinPackage = argv[++i]
|
|
@@ -163,6 +167,20 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
163
167
|
} else {
|
|
164
168
|
throw new Error(`Invalid --kotlin-serializer value: ${val ?? '(missing)'} (expected 'kotlinx' or 'none')`)
|
|
165
169
|
}
|
|
170
|
+
} else if (arg === '--swift-serializer') {
|
|
171
|
+
const val = argv[++i]
|
|
172
|
+
if (val === 'codable' || val === 'none') {
|
|
173
|
+
swiftSerializer = val
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error(`Invalid --swift-serializer value: ${val ?? '(missing)'} (expected 'codable' or 'none')`)
|
|
176
|
+
}
|
|
177
|
+
} else if (arg === '--swift-access-level') {
|
|
178
|
+
const val = argv[++i]
|
|
179
|
+
if (val === 'public' || val === 'internal') {
|
|
180
|
+
swiftAccessLevel = val
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error(`Invalid --swift-access-level value: ${val ?? '(missing)'} (expected 'public' or 'internal')`)
|
|
183
|
+
}
|
|
166
184
|
} else if (arg === '--unsupported-unions') {
|
|
167
185
|
const val = argv[++i]
|
|
168
186
|
if (val === 'throw' || val === 'fallback') {
|
|
@@ -178,6 +196,14 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
178
196
|
// configPath is consumed by the caller (main) before parseArgs is called with the loaded config.
|
|
179
197
|
// When called from main, config is already loaded. When called directly (tests), configPath is ignored.
|
|
180
198
|
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Validation — fails fast on user-controllable errors before envelope resolve.
|
|
201
|
+
// Runtime checks (emitter availability, etc.) happen later in pipeline.ts;
|
|
202
|
+
// those guards are aimed at non-CLI callers (direct API consumers, tests).
|
|
203
|
+
// The CLI resolves emitters before invoking `runPipeline`, so users only ever
|
|
204
|
+
// see flag-shape errors from this block, not pipeline-internal throws.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
181
207
|
if (outDir === undefined) {
|
|
182
208
|
throw new Error('Missing required argument: --out <dir>')
|
|
183
209
|
}
|
|
@@ -186,10 +212,15 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
186
212
|
throw new Error('Missing required input source: provide --url <url> or --file <path>')
|
|
187
213
|
}
|
|
188
214
|
|
|
215
|
+
// Kotlin target requires a package; surface this before any I/O happens.
|
|
189
216
|
if (target === 'kotlin' && (kotlinPackage === undefined || kotlinPackage === '')) {
|
|
190
217
|
throw new Error('Missing required argument: --kotlin-package <pkg> (required when --target kotlin)')
|
|
191
218
|
}
|
|
192
219
|
|
|
220
|
+
// Swift target currently has no required flags. If that changes, add the
|
|
221
|
+
// guard here so the failure mode stays consistent (flag-shape errors fire
|
|
222
|
+
// from parseArgs; runtime/emitter wiring errors fire from pipeline.ts).
|
|
223
|
+
|
|
193
224
|
return {
|
|
194
225
|
url,
|
|
195
226
|
file,
|
|
@@ -212,6 +243,14 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
|
|
|
212
243
|
},
|
|
213
244
|
}
|
|
214
245
|
: {}),
|
|
246
|
+
...(swiftSerializer !== undefined || swiftAccessLevel !== undefined
|
|
247
|
+
? {
|
|
248
|
+
swift: {
|
|
249
|
+
...(swiftSerializer !== undefined ? { serializer: swiftSerializer } : {}),
|
|
250
|
+
...(swiftAccessLevel !== undefined ? { accessLevel: swiftAccessLevel } : {}),
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
215
254
|
...(unsupportedUnions !== undefined ? { unsupportedUnions } : {}),
|
|
216
255
|
}
|
|
217
256
|
}
|
|
@@ -260,6 +299,17 @@ async function runWithWatch(parsed: ParsedArgs): Promise<void> {
|
|
|
260
299
|
}
|
|
261
300
|
: {}
|
|
262
301
|
|
|
302
|
+
// Resolve the swift emitter once at watch start; it's stateless and reused per tick.
|
|
303
|
+
const swiftWiring =
|
|
304
|
+
parsed.target === 'swift'
|
|
305
|
+
? {
|
|
306
|
+
target: 'swift' as const,
|
|
307
|
+
swiftEmitter: await (
|
|
308
|
+
await import('../targets/swift/ajsc-adapter.js')
|
|
309
|
+
).resolveProductionSwiftEmitter(),
|
|
310
|
+
}
|
|
311
|
+
: {}
|
|
312
|
+
|
|
263
313
|
let lastHash: string | undefined
|
|
264
314
|
|
|
265
315
|
const run = async (): Promise<void> => {
|
|
@@ -287,7 +337,10 @@ async function runWithWatch(parsed: ParsedArgs): Promise<void> {
|
|
|
287
337
|
cleanOutDir: parsed.cleanOutDir,
|
|
288
338
|
...(parsed.kotlin?.serializer !== undefined ? { kotlinSerializer: parsed.kotlin.serializer } : {}),
|
|
289
339
|
...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
|
|
340
|
+
...(parsed.swift?.serializer !== undefined ? { swiftSerializer: parsed.swift.serializer } : {}),
|
|
341
|
+
...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
|
|
290
342
|
...kotlinWiring,
|
|
343
|
+
...swiftWiring,
|
|
291
344
|
})
|
|
292
345
|
console.log(`[ts-procedures-codegen] Generated client files → ${parsed.outDir}`)
|
|
293
346
|
} catch (err) {
|
|
@@ -308,14 +361,26 @@ async function runWithWatch(parsed: ParsedArgs): Promise<void> {
|
|
|
308
361
|
const KOTLIN_SETUP_GUIDE_URL =
|
|
309
362
|
'https://bitbucket.org/thermsio/ts-procedures/src/master/docs/codegen-kotlin.md'
|
|
310
363
|
|
|
311
|
-
|
|
364
|
+
const SWIFT_SETUP_GUIDE_URL =
|
|
365
|
+
'https://bitbucket.org/thermsio/ts-procedures/src/master/docs/codegen-swift.md'
|
|
366
|
+
|
|
367
|
+
export function printPostRunHints(parsed: { target?: 'ts' | 'kotlin' | 'swift' }): void {
|
|
312
368
|
if (parsed.target === 'kotlin') {
|
|
313
369
|
console.log(`[ts-procedures-codegen] Kotlin setup guide: ${KOTLIN_SETUP_GUIDE_URL}`)
|
|
314
370
|
}
|
|
371
|
+
if (parsed.target === 'swift') {
|
|
372
|
+
console.log(`[ts-procedures-codegen] Swift setup guide: ${SWIFT_SETUP_GUIDE_URL}`)
|
|
373
|
+
}
|
|
315
374
|
}
|
|
316
375
|
|
|
317
376
|
/**
|
|
318
377
|
* Warns about flags that are currently no-ops for the Kotlin target.
|
|
378
|
+
*
|
|
379
|
+
* Scope is intentionally Kotlin-only — the param type omits `'swift'` so a
|
|
380
|
+
* reader can tell at a glance this function will never act on swift. Other
|
|
381
|
+
* targets get their own warner if/when they grow no-op flags. The call site
|
|
382
|
+
* narrows `parsed.target` before invoking.
|
|
383
|
+
*
|
|
319
384
|
* Currently: `--unsupported-unions` is a no-op because ajsc v7.2's Kotlin
|
|
320
385
|
* emitter silently emits an empty data class for untagged oneOf regardless
|
|
321
386
|
* of the flag (see docs/codegen-kotlin.md#untagged-unions).
|
|
@@ -340,7 +405,15 @@ async function main(): Promise<void> {
|
|
|
340
405
|
console.log(`[ts-procedures-codegen] Loaded config from ${configPath ?? DEFAULT_CONFIG_NAME}`)
|
|
341
406
|
}
|
|
342
407
|
const parsed = parseArgs(argv, config)
|
|
343
|
-
|
|
408
|
+
// The warner is intentionally Kotlin-only; pass the relevant fields and
|
|
409
|
+
// narrow `target` away from 'swift' here so the function's param type can
|
|
410
|
+
// stay tight.
|
|
411
|
+
if (parsed.target !== 'swift') {
|
|
412
|
+
warnIfKotlinNoOpFlags({
|
|
413
|
+
target: parsed.target,
|
|
414
|
+
...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
|
|
415
|
+
})
|
|
416
|
+
}
|
|
344
417
|
|
|
345
418
|
const source = parsed.url ?? parsed.file!
|
|
346
419
|
console.log(`[ts-procedures-codegen] Reading docs from ${source}...`)
|
|
@@ -357,6 +430,14 @@ async function main(): Promise<void> {
|
|
|
357
430
|
}
|
|
358
431
|
: {}
|
|
359
432
|
|
|
433
|
+
const swiftWiring =
|
|
434
|
+
parsed.target === 'swift'
|
|
435
|
+
? {
|
|
436
|
+
target: 'swift' as const,
|
|
437
|
+
swiftEmitter: await (await import('../targets/swift/ajsc-adapter.js')).resolveProductionSwiftEmitter(),
|
|
438
|
+
}
|
|
439
|
+
: {}
|
|
440
|
+
|
|
360
441
|
const result = await generateClient({
|
|
361
442
|
url: parsed.url,
|
|
362
443
|
file: parsed.file,
|
|
@@ -370,7 +451,10 @@ async function main(): Promise<void> {
|
|
|
370
451
|
cleanOutDir: parsed.cleanOutDir,
|
|
371
452
|
...(parsed.kotlin?.serializer !== undefined ? { kotlinSerializer: parsed.kotlin.serializer } : {}),
|
|
372
453
|
...(parsed.unsupportedUnions !== undefined ? { unsupportedUnions: parsed.unsupportedUnions } : {}),
|
|
454
|
+
...(parsed.swift?.serializer !== undefined ? { swiftSerializer: parsed.swift.serializer } : {}),
|
|
455
|
+
...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
|
|
373
456
|
...kotlinWiring,
|
|
457
|
+
...swiftWiring,
|
|
374
458
|
})
|
|
375
459
|
if (parsed.dryRun) {
|
|
376
460
|
console.log(`[ts-procedures-codegen] Dry run complete — ${result.length} files would be generated`)
|
package/src/codegen/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveEnvelope, type ResolveInput } from './resolve-envelope.js'
|
|
|
2
2
|
import { runPipeline, type GeneratedFile } from './pipeline.js'
|
|
3
3
|
import type { AjscOptions } from './emit-types.js'
|
|
4
4
|
import type { KotlinEmitter } from './targets/kotlin/ajsc-adapter.js'
|
|
5
|
+
import type { SwiftEmitter } from './targets/swift/ajsc-adapter.js'
|
|
5
6
|
|
|
6
7
|
export interface GenerateClientOptions extends ResolveInput {
|
|
7
8
|
outDir: string
|
|
@@ -12,12 +13,16 @@ export interface GenerateClientOptions extends ResolveInput {
|
|
|
12
13
|
selfContained?: boolean
|
|
13
14
|
serviceName?: string
|
|
14
15
|
cleanOutDir?: boolean
|
|
15
|
-
target?: 'ts' | 'kotlin'
|
|
16
|
+
target?: 'ts' | 'kotlin' | 'swift'
|
|
16
17
|
kotlinPackage?: string
|
|
17
18
|
kotlinSerializer?: 'kotlinx' | 'none'
|
|
18
19
|
unsupportedUnions?: 'throw' | 'fallback'
|
|
19
20
|
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
20
21
|
kotlinEmitter?: KotlinEmitter
|
|
22
|
+
swiftSerializer?: 'codable' | 'none'
|
|
23
|
+
swiftAccessLevel?: 'public' | 'internal'
|
|
24
|
+
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
25
|
+
swiftEmitter?: SwiftEmitter
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export async function generateClient(options: GenerateClientOptions): Promise<GeneratedFile[]> {
|
|
@@ -37,6 +42,9 @@ export async function generateClient(options: GenerateClientOptions): Promise<Ge
|
|
|
37
42
|
kotlinSerializer: options.kotlinSerializer,
|
|
38
43
|
unsupportedUnions: options.unsupportedUnions,
|
|
39
44
|
kotlinEmitter: options.kotlinEmitter,
|
|
45
|
+
swiftSerializer: options.swiftSerializer,
|
|
46
|
+
swiftAccessLevel: options.swiftAccessLevel,
|
|
47
|
+
swiftEmitter: options.swiftEmitter,
|
|
40
48
|
})
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -57,3 +65,18 @@ export type { KotlinEmitOptions } from './targets/kotlin/ajsc-adapter.js'
|
|
|
57
65
|
|
|
58
66
|
/** Result shape produced by `KotlinEmitter.emit`; stable, useful for stub builders in tests. */
|
|
59
67
|
export type { KotlinEmitResult } from './targets/kotlin/ajsc-adapter.js'
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @internal Subject to change with ajsc minor versions. Use for test injection
|
|
71
|
+
* only; consumer code should not depend on this shape.
|
|
72
|
+
*/
|
|
73
|
+
export type { SwiftEmitter } from './targets/swift/ajsc-adapter.js'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @internal Mirrors ajsc's `SwiftConverterOpts` and may shift with ajsc
|
|
77
|
+
* minor versions. Use for test injection only.
|
|
78
|
+
*/
|
|
79
|
+
export type { SwiftEmitOptions } from './targets/swift/ajsc-adapter.js'
|
|
80
|
+
|
|
81
|
+
/** Result shape produced by `SwiftEmitter.emit`; stable, useful for stub builders in tests. */
|
|
82
|
+
export type { SwiftEmitResult } from './targets/swift/ajsc-adapter.js'
|
package/src/codegen/pipeline.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import { mkdir, rm, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
1
|
import { createHash } from 'node:crypto'
|
|
4
2
|
import type { DocEnvelope } from '../implementations/types.js'
|
|
5
3
|
import type { AjscOptions } from './emit-types.js'
|
|
6
4
|
import { groupRoutesByScope } from './group-routes.js'
|
|
7
|
-
import { emitScopeFile } from './emit-scope.js'
|
|
8
|
-
import { emitIndexFile } from './emit-index.js'
|
|
9
|
-
import { emitErrorsFile } from './emit-errors.js'
|
|
10
|
-
import { emitClientTypesFile } from './emit-client-types.js'
|
|
11
|
-
import { emitClientRuntimeFile } from './emit-client-runtime.js'
|
|
12
5
|
import { validateServiceName } from './naming.js'
|
|
6
|
+
import type { GeneratedFile } from './targets/_shared/write-files.js'
|
|
13
7
|
import type { KotlinEmitter } from './targets/kotlin/ajsc-adapter.js'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
8
|
+
import { runKotlinPipeline } from './targets/kotlin/run.js'
|
|
9
|
+
import type { SwiftEmitter } from './targets/swift/ajsc-adapter.js'
|
|
10
|
+
import { runSwiftPipeline } from './targets/swift/run.js'
|
|
11
|
+
import { runTsPipeline } from './targets/ts/run.js'
|
|
16
12
|
|
|
17
13
|
export interface PipelineOptions {
|
|
18
14
|
envelope: DocEnvelope
|
|
@@ -24,193 +20,75 @@ export interface PipelineOptions {
|
|
|
24
20
|
selfContained?: boolean
|
|
25
21
|
serviceName?: string
|
|
26
22
|
cleanOutDir?: boolean
|
|
27
|
-
target?: 'ts' | 'kotlin'
|
|
23
|
+
target?: 'ts' | 'kotlin' | 'swift'
|
|
28
24
|
kotlinPackage?: string
|
|
29
25
|
kotlinSerializer?: 'kotlinx' | 'none'
|
|
30
26
|
unsupportedUnions?: 'throw' | 'fallback'
|
|
31
27
|
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
32
28
|
kotlinEmitter?: KotlinEmitter
|
|
29
|
+
swiftSerializer?: 'codable' | 'none'
|
|
30
|
+
swiftAccessLevel?: 'public' | 'internal'
|
|
31
|
+
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
32
|
+
swiftEmitter?: SwiftEmitter
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export
|
|
36
|
-
path: string
|
|
37
|
-
code: string
|
|
38
|
-
}
|
|
35
|
+
export type { GeneratedFile } from './targets/_shared/write-files.js'
|
|
39
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Top-level codegen entry. Validates the service name, computes the source
|
|
39
|
+
* hash, groups routes by scope, then dispatches to the per-target run module.
|
|
40
|
+
*
|
|
41
|
+
* Per-target modules own their language-specific emission and the file-write
|
|
42
|
+
* tail (dryRun / cleanOutDir / mkdir / writeFile via
|
|
43
|
+
* `_shared/write-files.ts`).
|
|
44
|
+
*/
|
|
40
45
|
export async function runPipeline(options: PipelineOptions): Promise<GeneratedFile[]> {
|
|
41
46
|
const { envelope, outDir, ajsc: ajscOpts, dryRun = false, namespaceTypes = false, selfContained = false, cleanOutDir = false } = options
|
|
42
47
|
const serviceName = options.serviceName ?? 'Api'
|
|
43
48
|
validateServiceName(serviceName)
|
|
49
|
+
|
|
44
50
|
const clientImportPath = selfContained ? './_types' : options.clientImportPath
|
|
45
51
|
if (selfContained && options.clientImportPath != null) {
|
|
46
52
|
console.warn('[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types')
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
const hash = createHash('md5').update(JSON.stringify(envelope)).digest('hex')
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const groups = groupRoutesByScope(envelope.routes)
|
|
53
|
-
const groupArray = Array.from(groups.values())
|
|
54
|
-
|
|
55
|
-
if (options.target === 'kotlin') {
|
|
56
|
-
if (options.kotlinPackage == null) {
|
|
57
|
-
throw new Error('[ts-procedures-codegen] target=kotlin requires kotlinPackage')
|
|
58
|
-
}
|
|
59
|
-
if (options.kotlinEmitter == null) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
'[ts-procedures-codegen] target=kotlin requires a kotlinEmitter (CLI resolves via resolveProductionKotlinEmitter)'
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Route errors are taxonomy keys (string[]); look up actual schemas from the
|
|
66
|
-
// envelope's top-level errors array. Mirrors the existing TS scope emitter.
|
|
67
|
-
const errorSchemas = new Map<string, unknown>()
|
|
68
|
-
for (const e of envelope.errors) {
|
|
69
|
-
if (e.schema != null) errorSchemas.set(e.name, e.schema)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Spec §"CLI flags" — `--array-item-naming`, `--depluralize`, `--uncountable-words`
|
|
73
|
-
// also apply to the Kotlin target. The CLI parks them on `options.ajsc` (a TS-target
|
|
74
|
-
// structure historically); copy the Kotlin-relevant subset onto the scope opts here.
|
|
75
|
-
const ajscPassthrough = options.ajsc ?? {}
|
|
76
|
-
|
|
77
|
-
const AJSC_PASSTHROUGH_KEYS = ['arrayItemNaming', 'depluralize', 'uncountableWords'] as const
|
|
78
|
-
|
|
79
|
-
const kotlinFiles: GeneratedFile[] = []
|
|
80
|
-
const allSkipped: string[] = []
|
|
81
|
-
for (const group of groupArray) {
|
|
82
|
-
const emitted = emitKotlinScope(
|
|
83
|
-
group,
|
|
84
|
-
{
|
|
85
|
-
kotlinPackage: options.kotlinPackage,
|
|
86
|
-
sourceHash: hash,
|
|
87
|
-
...(options.kotlinSerializer !== undefined ? { serializer: options.kotlinSerializer } : {}),
|
|
88
|
-
...(options.unsupportedUnions !== undefined ? { unsupportedUnions: options.unsupportedUnions } : {}),
|
|
89
|
-
...pickDefined(ajscPassthrough, AJSC_PASSTHROUGH_KEYS),
|
|
90
|
-
},
|
|
91
|
-
options.kotlinEmitter,
|
|
92
|
-
errorSchemas,
|
|
93
|
-
)
|
|
94
|
-
kotlinFiles.push({ path: join(outDir, emitted.filename), code: emitted.code })
|
|
95
|
-
allSkipped.push(...emitted.skippedStreams)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (allSkipped.length > 0) {
|
|
99
|
-
console.log(
|
|
100
|
-
`[ts-procedures-codegen] Skipped ${allSkipped.length} stream route${allSkipped.length === 1 ? '' : 's'} (kotlin target): ${allSkipped.join(', ')}`,
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (dryRun) {
|
|
105
|
-
if (cleanOutDir) {
|
|
106
|
-
console.log(`[dry-run] Would clean outDir: ${outDir}`)
|
|
107
|
-
}
|
|
108
|
-
for (const f of kotlinFiles) {
|
|
109
|
-
const bytes = Buffer.byteLength(f.code, 'utf-8')
|
|
110
|
-
console.log(`[dry-run] Would write: ${f.path} (${bytes} bytes)`)
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
if (cleanOutDir) {
|
|
114
|
-
await rm(outDir, { recursive: true, force: true })
|
|
115
|
-
}
|
|
116
|
-
await mkdir(outDir, { recursive: true })
|
|
117
|
-
for (const f of kotlinFiles) {
|
|
118
|
-
await writeFile(f.path, f.code, 'utf-8')
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return kotlinFiles
|
|
123
|
-
}
|
|
56
|
+
const groups = Array.from(groupRoutesByScope(envelope.routes).values())
|
|
124
57
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (selfContained) {
|
|
133
|
-
for (const group of groupArray) {
|
|
134
|
-
if (group.scopeKey === '_types' || group.scopeKey === '_client') {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`[ts-procedures-codegen] Scope "${group.scopeKey}" conflicts with self-contained mode reserved filename "${group.scopeKey}.ts". Rename the scope to avoid collision.`
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const files: GeneratedFile[] = []
|
|
143
|
-
|
|
144
|
-
for (const group of groupArray) {
|
|
145
|
-
const rawCode = await emitScopeFile(group, {
|
|
146
|
-
ajsc: ajscOpts,
|
|
147
|
-
clientImportPath,
|
|
148
|
-
namespaceTypes,
|
|
149
|
-
serviceName,
|
|
150
|
-
errorKeys: errorKeys.size > 0 ? errorKeys : undefined,
|
|
151
|
-
})
|
|
152
|
-
const lines = rawCode.split('\n')
|
|
153
|
-
lines.splice(1, 0, hashComment)
|
|
154
|
-
const code = lines.join('\n')
|
|
155
|
-
files.push({ path: join(outDir, `${group.scopeKey}.ts`), code })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const errorsCode = await emitErrorsFile(envelope.errors, { ajsc: ajscOpts, clientImportPath, namespaceTypes, serviceName })
|
|
159
|
-
const hasErrors = errorsCode != null
|
|
160
|
-
if (errorsCode != null) {
|
|
161
|
-
const errorsLines = errorsCode.split('\n')
|
|
162
|
-
errorsLines.splice(1, 0, hashComment)
|
|
163
|
-
const errorsWithHash = errorsLines.join('\n')
|
|
164
|
-
files.push({ path: join(outDir, '_errors.ts'), code: errorsWithHash })
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// In self-contained mode types come from `./_types` but the runtime
|
|
168
|
-
// (`createClient`) lives in `./_client`. In regular mode both share the
|
|
169
|
-
// single `clientImportPath` (e.g. `ts-procedures/client`).
|
|
170
|
-
const clientRuntimeImportPath = selfContained ? './_client' : clientImportPath
|
|
171
|
-
const rawIndexCode = emitIndexFile(groupArray, {
|
|
58
|
+
const base = {
|
|
59
|
+
envelope,
|
|
60
|
+
outDir,
|
|
61
|
+
hash,
|
|
62
|
+
groups,
|
|
63
|
+
serviceName,
|
|
64
|
+
ajsc: ajscOpts,
|
|
172
65
|
clientImportPath,
|
|
173
|
-
|
|
174
|
-
hasErrors,
|
|
66
|
+
dryRun,
|
|
175
67
|
namespaceTypes,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const indexLines = rawIndexCode.split('\n')
|
|
179
|
-
indexLines.splice(1, 0, hashComment)
|
|
180
|
-
const indexCode = indexLines.join('\n')
|
|
181
|
-
files.push({ path: join(outDir, 'index.ts'), code: indexCode })
|
|
182
|
-
|
|
183
|
-
if (selfContained) {
|
|
184
|
-
const rawTypesCode = await emitClientTypesFile()
|
|
185
|
-
const typesLines = rawTypesCode.split('\n')
|
|
186
|
-
typesLines.splice(1, 0, hashComment)
|
|
187
|
-
const typesCode = typesLines.join('\n')
|
|
188
|
-
files.push({ path: join(outDir, '_types.ts'), code: typesCode })
|
|
189
|
-
|
|
190
|
-
const rawClientCode = await emitClientRuntimeFile()
|
|
191
|
-
const clientLines = rawClientCode.split('\n')
|
|
192
|
-
clientLines.splice(1, 0, hashComment)
|
|
193
|
-
const clientCode = clientLines.join('\n')
|
|
194
|
-
files.push({ path: join(outDir, '_client.ts'), code: clientCode })
|
|
68
|
+
selfContained,
|
|
69
|
+
cleanOutDir,
|
|
195
70
|
}
|
|
196
71
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
72
|
+
switch (options.target) {
|
|
73
|
+
case 'kotlin':
|
|
74
|
+
return runKotlinPipeline({
|
|
75
|
+
...base,
|
|
76
|
+
kotlinPackage: options.kotlinPackage,
|
|
77
|
+
kotlinSerializer: options.kotlinSerializer,
|
|
78
|
+
unsupportedUnions: options.unsupportedUnions,
|
|
79
|
+
kotlinEmitter: options.kotlinEmitter,
|
|
80
|
+
})
|
|
81
|
+
case 'swift':
|
|
82
|
+
return runSwiftPipeline({
|
|
83
|
+
...base,
|
|
84
|
+
swiftSerializer: options.swiftSerializer,
|
|
85
|
+
swiftAccessLevel: options.swiftAccessLevel,
|
|
86
|
+
unsupportedUnions: options.unsupportedUnions,
|
|
87
|
+
swiftEmitter: options.swiftEmitter,
|
|
88
|
+
})
|
|
89
|
+
case 'ts':
|
|
90
|
+
case undefined:
|
|
91
|
+
default:
|
|
92
|
+
return runTsPipeline(base)
|
|
213
93
|
}
|
|
214
|
-
|
|
215
|
-
return files
|
|
216
94
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildErrorSchemasMap } from './error-schemas.js'
|
|
3
|
+
import type { ErrorDoc } from '../../../implementations/types.js'
|
|
4
|
+
|
|
5
|
+
describe('buildErrorSchemasMap', () => {
|
|
6
|
+
it('returns an empty map when there are no errors', () => {
|
|
7
|
+
expect(buildErrorSchemasMap([])).toEqual(new Map())
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('includes errors with schemas', () => {
|
|
11
|
+
const schemaA = { type: 'object', tag: 'a' }
|
|
12
|
+
const schemaB = { type: 'object', tag: 'b' }
|
|
13
|
+
const errors: ErrorDoc[] = [
|
|
14
|
+
{ name: 'A', statusCode: 400, description: '', schema: schemaA },
|
|
15
|
+
{ name: 'B', statusCode: 500, description: '', schema: schemaB },
|
|
16
|
+
]
|
|
17
|
+
const map = buildErrorSchemasMap(errors)
|
|
18
|
+
expect(map.size).toBe(2)
|
|
19
|
+
expect(map.get('A')).toBe(schemaA)
|
|
20
|
+
expect(map.get('B')).toBe(schemaB)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('skips errors without a schema', () => {
|
|
24
|
+
const schemaA = { type: 'object' }
|
|
25
|
+
const errors: ErrorDoc[] = [
|
|
26
|
+
{ name: 'A', statusCode: 400, description: '', schema: schemaA },
|
|
27
|
+
{ name: 'NoSchema', statusCode: 500, description: '' },
|
|
28
|
+
]
|
|
29
|
+
const map = buildErrorSchemasMap(errors)
|
|
30
|
+
expect(map.size).toBe(1)
|
|
31
|
+
expect(map.has('A')).toBe(true)
|
|
32
|
+
expect(map.has('NoSchema')).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keys the map by error name', () => {
|
|
36
|
+
const schema = { type: 'object' }
|
|
37
|
+
const map = buildErrorSchemasMap([
|
|
38
|
+
{ name: 'NotFound', statusCode: 404, description: '', schema },
|
|
39
|
+
])
|
|
40
|
+
expect(Array.from(map.keys())).toEqual(['NotFound'])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ErrorDoc } from '../../../implementations/types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a lookup map from a {@link DocEnvelope}'s top-level `errors` array
|
|
5
|
+
* keyed by `name`. Only entries with a `schema` are included — errors
|
|
6
|
+
* documented without a schema cannot have a corresponding generated type.
|
|
7
|
+
*
|
|
8
|
+
* Targets pass this map to per-route emission so taxonomy keys on
|
|
9
|
+
* `route.errors` (a `string[]`) can be resolved to actual JSON Schemas.
|
|
10
|
+
*/
|
|
11
|
+
export function buildErrorSchemasMap(errors: readonly ErrorDoc[]): Map<string, unknown> {
|
|
12
|
+
const errorSchemas = new Map<string, unknown>()
|
|
13
|
+
for (const e of errors) {
|
|
14
|
+
if (e.schema != null) errorSchemas.set(e.name, e.schema)
|
|
15
|
+
}
|
|
16
|
+
return errorSchemas
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { indent } from './indent.js'
|
|
3
|
+
|
|
4
|
+
describe('indent', () => {
|
|
5
|
+
it('indents every line by 4 spaces per level', () => {
|
|
6
|
+
expect(indent('a\nb', 1)).toBe(' a\n b')
|
|
7
|
+
expect(indent('a', 2)).toBe(' a')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('preserves blank lines without trailing whitespace when indenting', () => {
|
|
11
|
+
expect(indent('a\n\nb', 1)).toBe(' a\n\n b')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns the input unchanged at level 0', () => {
|
|
15
|
+
expect(indent('a\n\nb', 0)).toBe('a\n\nb')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('indents a single line', () => {
|
|
19
|
+
expect(indent('hello', 1)).toBe(' hello')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns empty string for empty input', () => {
|
|
23
|
+
expect(indent('', 1)).toBe('')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indents every non-blank line of `text` by `level * 4` spaces. Blank lines
|
|
3
|
+
* are preserved as empty (no trailing whitespace). Suitable for languages
|
|
4
|
+
* that use 4-space indentation (Kotlin, Swift).
|
|
5
|
+
*/
|
|
6
|
+
export function indent(text: string, level: number): string {
|
|
7
|
+
const prefix = ' '.repeat(level)
|
|
8
|
+
return text
|
|
9
|
+
.split('\n')
|
|
10
|
+
.map((line) => (line.length === 0 ? line : `${prefix}${line}`))
|
|
11
|
+
.join('\n')
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { pascalCase } from './pascal-case.js'
|
|
3
|
+
|
|
4
|
+
describe('pascalCase', () => {
|
|
5
|
+
it('converts a single-word scope', () => {
|
|
6
|
+
expect(pascalCase('users')).toBe('Users')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('converts a kebab-case scope', () => {
|
|
10
|
+
expect(pascalCase('user-management')).toBe('UserManagement')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('handles multi-segment kebab-case', () => {
|
|
14
|
+
expect(pascalCase('a-b-c')).toBe('ABC')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('drops empty segments from leading/trailing/double dashes', () => {
|
|
18
|
+
expect(pascalCase('-users')).toBe('Users')
|
|
19
|
+
expect(pascalCase('users-')).toBe('Users')
|
|
20
|
+
expect(pascalCase('user--mgmt')).toBe('UserMgmt')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns empty string for empty input', () => {
|
|
24
|
+
expect(pascalCase('')).toBe('')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('preserves casing of letters after the leading character', () => {
|
|
28
|
+
expect(pascalCase('user-API')).toBe('UserAPI')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a kebab-case (or single-word) scope key to PascalCase. Used by
|
|
3
|
+
* codegen targets to derive top-level type/namespace identifiers from scope
|
|
4
|
+
* names (e.g. `user-management` → `UserManagement`).
|
|
5
|
+
*/
|
|
6
|
+
export function pascalCase(scope: string): string {
|
|
7
|
+
return scope
|
|
8
|
+
.split('-')
|
|
9
|
+
.filter((p) => p.length > 0)
|
|
10
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
11
|
+
.join('')
|
|
12
|
+
}
|