ts-procedures 6.0.2 → 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 +2 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +2 -0
- package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +106 -0
- package/agent_config/claude-code/skills/ts-procedures-swift/SKILL.md +119 -0
- package/agent_config/copilot/copilot-instructions.md +3 -0
- package/agent_config/cursor/cursorrules +3 -0
- package/agent_config/lib/install-claude.mjs +1 -1
- package/build/codegen/bin/cli.d.ts +39 -0
- package/build/codegen/bin/cli.js +164 -0
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +180 -1
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/index.d.ts +36 -0
- package/build/codegen/index.js +8 -0
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/pipeline.d.ts +22 -4
- package/build/codegen/pipeline.js +44 -86
- package/build/codegen/pipeline.js.map +1 -1
- package/build/codegen/pipeline.test.js +162 -0
- package/build/codegen/pipeline.test.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/ajsc-adapter.d.ts +6 -4
- package/build/codegen/targets/kotlin/ajsc-adapter.js +12 -7
- package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -1
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js +20 -2
- package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -1
- package/build/codegen/targets/kotlin/e2e-compile.test.js +41 -9
- package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +6 -2
- package/build/codegen/targets/kotlin/emit-route-kotlin.js +18 -28
- package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +120 -1
- package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +4 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js +12 -11
- package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +39 -0
- package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.d.ts +0 -1
- package/build/codegen/targets/kotlin/format-kotlin.js +0 -7
- package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -1
- package/build/codegen/targets/kotlin/format-kotlin.test.js +1 -8
- package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -1
- package/build/codegen/targets/kotlin/integration.test.js +27 -10
- package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.d.ts +1 -0
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js +50 -0
- package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js.map +1 -0
- 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/build/codegen/test-helpers/golden.d.ts +15 -0
- package/build/codegen/test-helpers/golden.js +30 -0
- package/build/codegen/test-helpers/golden.js.map +1 -0
- package/build/codegen/test-helpers/golden.test.d.ts +1 -0
- package/build/codegen/test-helpers/golden.test.js +76 -0
- package/build/codegen/test-helpers/golden.test.js.map +1 -0
- package/docs/codegen-kotlin.md +176 -0
- package/docs/codegen-swift.md +314 -0
- package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +1993 -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 +314 -0
- package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +264 -0
- package/package.json +2 -2
- package/src/codegen/__fixtures__/users-envelope.json +144 -0
- package/src/codegen/bin/cli.test.ts +200 -1
- package/src/codegen/bin/cli.ts +187 -0
- package/src/codegen/index.ts +50 -0
- package/src/codegen/pipeline.test.ts +175 -0
- package/src/codegen/pipeline.ts +58 -101
- 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/__fixtures__/users-golden.kt +121 -0
- package/src/codegen/targets/kotlin/__snapshots__/probe-unsupported-unions.test.ts.snap +27 -0
- package/src/codegen/targets/kotlin/ajsc-adapter.test.ts +47 -0
- package/src/codegen/targets/kotlin/ajsc-adapter.ts +66 -0
- package/src/codegen/targets/kotlin/e2e-compile.test.ts +86 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +239 -0
- package/src/codegen/targets/kotlin/emit-route-kotlin.ts +89 -0
- package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +112 -0
- package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +60 -0
- package/src/codegen/targets/kotlin/format-kotlin.test.ts +26 -0
- package/src/codegen/targets/kotlin/format-kotlin.ts +13 -0
- package/src/codegen/targets/kotlin/integration.test.ts +77 -0
- package/src/codegen/targets/kotlin/probe-unsupported-unions.test.ts +64 -0
- 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/test-helpers/golden.test.ts +80 -0
- package/src/codegen/test-helpers/golden.ts +34 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { runPipeline } from '../../pipeline.js'
|
|
6
|
+
import { assertGoldenOrUpdate } from '../../test-helpers/golden.js'
|
|
7
|
+
import { createStubSwiftEmitter, type SwiftEmitResult } from './ajsc-adapter.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = dirname(__filename)
|
|
11
|
+
|
|
12
|
+
const ok = (code: string, rootTypeName: string, imports: string[] = []): SwiftEmitResult => ({
|
|
13
|
+
code,
|
|
14
|
+
rootTypeName,
|
|
15
|
+
extractedTypeNames: [],
|
|
16
|
+
imports,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('swift codegen — integration', () => {
|
|
20
|
+
it('produces byte-identical output against the golden fixture', async () => {
|
|
21
|
+
const envelopePath = join(__dirname, '../../__fixtures__/users-envelope.json')
|
|
22
|
+
const goldenPath = join(__dirname, '__fixtures__/users-golden.swift')
|
|
23
|
+
const envelope = JSON.parse(await readFile(envelopePath, 'utf8'))
|
|
24
|
+
|
|
25
|
+
// Hand-authored slot outputs in the v7.2 nested-struct shape (inlineTypes: true).
|
|
26
|
+
//
|
|
27
|
+
// The stub map is keyed on rootTypeName, NOT on (route, slot). Both GetUser
|
|
28
|
+
// and CreateUser have a slot named "Response" — they intentionally share the
|
|
29
|
+
// single stub entry below. The golden file therefore shows three identical
|
|
30
|
+
// Response structs (one per route that has one). This is correct: the
|
|
31
|
+
// integration test pins our file-assembly logic, not real ajsc per-route
|
|
32
|
+
// output. The swiftc E2E test exercises real ajsc against the same fixture
|
|
33
|
+
// and would surface any incompatibility.
|
|
34
|
+
const emitter = createStubSwiftEmitter({
|
|
35
|
+
// GetUser
|
|
36
|
+
PathParams: ok(
|
|
37
|
+
'public struct PathParams: Codable {\n public let id: String\n}',
|
|
38
|
+
'PathParams',
|
|
39
|
+
),
|
|
40
|
+
Response: ok(
|
|
41
|
+
'public struct Response: Codable {\n public let id: String\n public let name: String\n public let createdAt: Date\n public let address: Address\n\n enum CodingKeys: String, CodingKey {\n case id, name\n case createdAt = "created-at"\n case address\n }\n\n public struct Address: Codable {\n public let street: String\n public let city: String\n }\n}',
|
|
42
|
+
'Response',
|
|
43
|
+
['Foundation'],
|
|
44
|
+
),
|
|
45
|
+
NotFound: ok(
|
|
46
|
+
'public struct NotFound: Codable {\n public let name: String\n public let message: String\n}',
|
|
47
|
+
'NotFound',
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
// CreateUser
|
|
51
|
+
Body: ok(
|
|
52
|
+
'public enum Body: Codable {\n case guest(GuestBody)\n case registered(RegisteredBody)\n\n public struct GuestBody: Codable {\n public let kind: String\n public let displayName: String\n }\n\n public struct RegisteredBody: Codable {\n public let kind: String\n public let email: String\n public let name: String\n }\n}',
|
|
53
|
+
'Body',
|
|
54
|
+
),
|
|
55
|
+
ValidationError: ok(
|
|
56
|
+
'public struct ValidationError: Codable {\n public let name: String\n public let message: String\n public let field: String?\n}',
|
|
57
|
+
'ValidationError',
|
|
58
|
+
),
|
|
59
|
+
|
|
60
|
+
// ListUsers
|
|
61
|
+
Query: ok(
|
|
62
|
+
'public struct Query: Codable {\n public let status: Status?\n public let limit: Int64?\n\n public enum Status: String, Codable {\n case active\n case inactive\n }\n}',
|
|
63
|
+
'Query',
|
|
64
|
+
),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const files = await runPipeline({
|
|
68
|
+
envelope,
|
|
69
|
+
outDir: 'out',
|
|
70
|
+
dryRun: true,
|
|
71
|
+
target: 'swift',
|
|
72
|
+
swiftEmitter: emitter,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(files).toHaveLength(1)
|
|
76
|
+
expect(files[0]!.path).toBe(join('out', 'Users.swift'))
|
|
77
|
+
|
|
78
|
+
await assertGoldenOrUpdate(files[0]!.code, goldenPath)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift-target pipeline runner. Invoked by the dispatcher in
|
|
3
|
+
* `src/codegen/pipeline.ts` when `target === 'swift'`. Emits one `.swift`
|
|
4
|
+
* file per scope via `emitSwiftScope` and commits via the shared
|
|
5
|
+
* `writeGeneratedFiles` tail.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import type { GeneratedFile } from '../_shared/write-files.js'
|
|
9
|
+
import { writeGeneratedFiles } from '../_shared/write-files.js'
|
|
10
|
+
import { buildErrorSchemasMap } from '../_shared/error-schemas.js'
|
|
11
|
+
import { pickDefined } from '../_shared/pick-defined.js'
|
|
12
|
+
import type { TargetRunInput } from '../_shared/target-run.js'
|
|
13
|
+
import type { SwiftEmitter } from './ajsc-adapter.js'
|
|
14
|
+
import { emitSwiftScope } from './emit-scope-swift.js'
|
|
15
|
+
|
|
16
|
+
export interface SwiftRunInput extends TargetRunInput {
|
|
17
|
+
swiftSerializer?: 'codable' | 'none'
|
|
18
|
+
swiftAccessLevel?: 'public' | 'internal'
|
|
19
|
+
unsupportedUnions?: 'throw' | 'fallback'
|
|
20
|
+
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
21
|
+
swiftEmitter?: SwiftEmitter
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `--array-item-naming`, `--depluralize`, `--uncountable-words` are reused
|
|
26
|
+
* across targets. The CLI parks them on `options.ajsc`; copy the relevant
|
|
27
|
+
* subset onto the scope opts here.
|
|
28
|
+
*/
|
|
29
|
+
const AJSC_PASSTHROUGH_KEYS = ['arrayItemNaming', 'depluralize', 'uncountableWords'] as const
|
|
30
|
+
|
|
31
|
+
export async function runSwiftPipeline(input: SwiftRunInput): Promise<GeneratedFile[]> {
|
|
32
|
+
const { envelope, outDir, hash, groups, dryRun = false, cleanOutDir = false } = input
|
|
33
|
+
|
|
34
|
+
// Runtime guard for non-CLI callers (direct API consumers + tests).
|
|
35
|
+
// The CLI resolves the emitter before calling runPipeline, so end users
|
|
36
|
+
// never hit this throw. Swift currently has no required CLI flags.
|
|
37
|
+
if (input.swiftEmitter == null) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'[ts-procedures-codegen] target=swift requires a swiftEmitter (CLI resolves via resolveProductionSwiftEmitter)'
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const errorSchemas = buildErrorSchemasMap(envelope.errors)
|
|
44
|
+
const ajscPassthrough = input.ajsc ?? {}
|
|
45
|
+
|
|
46
|
+
const files: GeneratedFile[] = []
|
|
47
|
+
const allSkipped: string[] = []
|
|
48
|
+
for (const group of groups) {
|
|
49
|
+
const emitted = emitSwiftScope(
|
|
50
|
+
group,
|
|
51
|
+
{
|
|
52
|
+
sourceHash: hash,
|
|
53
|
+
...(input.swiftSerializer !== undefined ? { serializer: input.swiftSerializer } : {}),
|
|
54
|
+
...(input.swiftAccessLevel !== undefined ? { accessLevel: input.swiftAccessLevel } : {}),
|
|
55
|
+
...(input.unsupportedUnions !== undefined ? { unsupportedUnions: input.unsupportedUnions } : {}),
|
|
56
|
+
...pickDefined(ajscPassthrough, AJSC_PASSTHROUGH_KEYS),
|
|
57
|
+
},
|
|
58
|
+
input.swiftEmitter,
|
|
59
|
+
errorSchemas,
|
|
60
|
+
)
|
|
61
|
+
files.push({ path: join(outDir, emitted.filename), code: emitted.code })
|
|
62
|
+
allSkipped.push(...emitted.skippedStreams)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (allSkipped.length > 0) {
|
|
66
|
+
console.log(
|
|
67
|
+
`[ts-procedures-codegen] Skipped ${allSkipped.length} stream route${allSkipped.length === 1 ? '' : 's'} (swift target): ${allSkipped.join(', ')}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await writeGeneratedFiles(files, outDir, { dryRun, cleanOutDir })
|
|
72
|
+
|
|
73
|
+
return files
|
|
74
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript-target pipeline runner. Invoked by the dispatcher in
|
|
3
|
+
* `src/codegen/pipeline.ts` when `target` is `'ts'` (or unset). Emits the
|
|
4
|
+
* scope `.ts` files, `_errors.ts`, `index.ts`, and — under self-contained
|
|
5
|
+
* mode — `_types.ts` + `_client.ts`, then commits via the shared
|
|
6
|
+
* `writeGeneratedFiles` tail.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import type { GeneratedFile } from '../_shared/write-files.js'
|
|
10
|
+
import { writeGeneratedFiles } from '../_shared/write-files.js'
|
|
11
|
+
import type { TargetRunInput } from '../_shared/target-run.js'
|
|
12
|
+
import { emitScopeFile } from '../../emit-scope.js'
|
|
13
|
+
import { emitIndexFile } from '../../emit-index.js'
|
|
14
|
+
import { emitErrorsFile } from '../../emit-errors.js'
|
|
15
|
+
import { emitClientTypesFile } from '../../emit-client-types.js'
|
|
16
|
+
import { emitClientRuntimeFile } from '../../emit-client-runtime.js'
|
|
17
|
+
|
|
18
|
+
export type TsRunInput = TargetRunInput
|
|
19
|
+
|
|
20
|
+
export async function runTsPipeline(input: TsRunInput): Promise<GeneratedFile[]> {
|
|
21
|
+
const {
|
|
22
|
+
envelope,
|
|
23
|
+
outDir,
|
|
24
|
+
hash,
|
|
25
|
+
groups,
|
|
26
|
+
serviceName,
|
|
27
|
+
ajsc: ajscOpts,
|
|
28
|
+
clientImportPath,
|
|
29
|
+
dryRun = false,
|
|
30
|
+
namespaceTypes = false,
|
|
31
|
+
selfContained = false,
|
|
32
|
+
cleanOutDir = false,
|
|
33
|
+
} = input
|
|
34
|
+
|
|
35
|
+
const hashComment = `// Source hash: ${hash}`
|
|
36
|
+
|
|
37
|
+
// Error keys that will be emitted in `_errors.ts` — only those with a schema.
|
|
38
|
+
// Scope emit uses this to filter `route.errors` so generated code never
|
|
39
|
+
// references an undefined error type.
|
|
40
|
+
const errorKeys = new Set(
|
|
41
|
+
envelope.errors.filter((e) => e.schema != null).map((e) => e.name),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (selfContained) {
|
|
45
|
+
for (const group of groups) {
|
|
46
|
+
if (group.scopeKey === '_types' || group.scopeKey === '_client') {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[ts-procedures-codegen] Scope "${group.scopeKey}" conflicts with self-contained mode reserved filename "${group.scopeKey}.ts". Rename the scope to avoid collision.`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files: GeneratedFile[] = []
|
|
55
|
+
|
|
56
|
+
for (const group of groups) {
|
|
57
|
+
const rawCode = await emitScopeFile(group, {
|
|
58
|
+
ajsc: ajscOpts,
|
|
59
|
+
clientImportPath,
|
|
60
|
+
namespaceTypes,
|
|
61
|
+
serviceName,
|
|
62
|
+
errorKeys: errorKeys.size > 0 ? errorKeys : undefined,
|
|
63
|
+
})
|
|
64
|
+
const lines = rawCode.split('\n')
|
|
65
|
+
lines.splice(1, 0, hashComment)
|
|
66
|
+
const code = lines.join('\n')
|
|
67
|
+
files.push({ path: join(outDir, `${group.scopeKey}.ts`), code })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const errorsCode = await emitErrorsFile(envelope.errors, {
|
|
71
|
+
ajsc: ajscOpts,
|
|
72
|
+
clientImportPath,
|
|
73
|
+
namespaceTypes,
|
|
74
|
+
serviceName,
|
|
75
|
+
})
|
|
76
|
+
const hasErrors = errorsCode != null
|
|
77
|
+
if (errorsCode != null) {
|
|
78
|
+
const errorsLines = errorsCode.split('\n')
|
|
79
|
+
errorsLines.splice(1, 0, hashComment)
|
|
80
|
+
const errorsWithHash = errorsLines.join('\n')
|
|
81
|
+
files.push({ path: join(outDir, '_errors.ts'), code: errorsWithHash })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// In self-contained mode types come from `./_types` but the runtime
|
|
85
|
+
// (`createClient`) lives in `./_client`. In regular mode both share the
|
|
86
|
+
// single `clientImportPath` (e.g. `ts-procedures/client`).
|
|
87
|
+
const clientRuntimeImportPath = selfContained ? './_client' : clientImportPath
|
|
88
|
+
const rawIndexCode = emitIndexFile(groups, {
|
|
89
|
+
clientImportPath,
|
|
90
|
+
clientRuntimeImportPath,
|
|
91
|
+
hasErrors,
|
|
92
|
+
namespaceTypes,
|
|
93
|
+
serviceName,
|
|
94
|
+
})
|
|
95
|
+
const indexLines = rawIndexCode.split('\n')
|
|
96
|
+
indexLines.splice(1, 0, hashComment)
|
|
97
|
+
const indexCode = indexLines.join('\n')
|
|
98
|
+
files.push({ path: join(outDir, 'index.ts'), code: indexCode })
|
|
99
|
+
|
|
100
|
+
if (selfContained) {
|
|
101
|
+
const rawTypesCode = await emitClientTypesFile()
|
|
102
|
+
const typesLines = rawTypesCode.split('\n')
|
|
103
|
+
typesLines.splice(1, 0, hashComment)
|
|
104
|
+
const typesCode = typesLines.join('\n')
|
|
105
|
+
files.push({ path: join(outDir, '_types.ts'), code: typesCode })
|
|
106
|
+
|
|
107
|
+
const rawClientCode = await emitClientRuntimeFile()
|
|
108
|
+
const clientLines = rawClientCode.split('\n')
|
|
109
|
+
clientLines.splice(1, 0, hashComment)
|
|
110
|
+
const clientCode = clientLines.join('\n')
|
|
111
|
+
files.push({ path: join(outDir, '_client.ts'), code: clientCode })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await writeGeneratedFiles(files, outDir, { dryRun, cleanOutDir })
|
|
115
|
+
|
|
116
|
+
return files
|
|
117
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { assertGoldenOrUpdate } from './golden.js'
|
|
6
|
+
|
|
7
|
+
let tmpDir: string | undefined
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (tmpDir != null) {
|
|
11
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
12
|
+
tmpDir = undefined
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function makeTmp(): string {
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'tsp-golden-'))
|
|
18
|
+
return tmpDir
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('assertGoldenOrUpdate', () => {
|
|
22
|
+
it('asserts byte-equality against golden when produced matches (with hash splice)', async () => {
|
|
23
|
+
const dir = makeTmp()
|
|
24
|
+
const goldenPath = join(dir, 'expected.txt')
|
|
25
|
+
writeFileSync(goldenPath, 'hello\n// Source hash: <PLACEHOLDER>\nworld\n', 'utf-8')
|
|
26
|
+
|
|
27
|
+
const produced = 'hello\n// Source hash: abc123def456\nworld\n'
|
|
28
|
+
await expect(assertGoldenOrUpdate(produced, goldenPath)).resolves.toBeUndefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('throws when produced does not match golden', async () => {
|
|
32
|
+
const dir = makeTmp()
|
|
33
|
+
const goldenPath = join(dir, 'expected.txt')
|
|
34
|
+
writeFileSync(goldenPath, 'expected content\n', 'utf-8')
|
|
35
|
+
|
|
36
|
+
const produced = 'different content\n'
|
|
37
|
+
await expect(assertGoldenOrUpdate(produced, goldenPath)).rejects.toThrow()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('writes a portable golden when UPDATE_GOLDENS=1', async () => {
|
|
41
|
+
const original = process.env.UPDATE_GOLDENS
|
|
42
|
+
process.env.UPDATE_GOLDENS = '1'
|
|
43
|
+
try {
|
|
44
|
+
const dir = makeTmp()
|
|
45
|
+
const goldenPath = join(dir, 'expected.txt')
|
|
46
|
+
const produced = 'hello\n// Source hash: deadbeef1234\nworld\n'
|
|
47
|
+
await assertGoldenOrUpdate(produced, goldenPath)
|
|
48
|
+
const written = readFileSync(goldenPath, 'utf-8')
|
|
49
|
+
expect(written).toBe('hello\n// Source hash: <PLACEHOLDER>\nworld\n')
|
|
50
|
+
} finally {
|
|
51
|
+
if (original === undefined) delete process.env.UPDATE_GOLDENS
|
|
52
|
+
else process.env.UPDATE_GOLDENS = original
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('handles produced output without a source-hash line', async () => {
|
|
57
|
+
const dir = makeTmp()
|
|
58
|
+
const goldenPath = join(dir, 'expected.txt')
|
|
59
|
+
writeFileSync(goldenPath, 'plain content\n', 'utf-8')
|
|
60
|
+
|
|
61
|
+
// No source hash; splice is a no-op on both sides.
|
|
62
|
+
await expect(assertGoldenOrUpdate('plain content\n', goldenPath)).resolves.toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('regenerate mode handles produced output without a source-hash line', async () => {
|
|
66
|
+
const original = process.env.UPDATE_GOLDENS
|
|
67
|
+
process.env.UPDATE_GOLDENS = '1'
|
|
68
|
+
try {
|
|
69
|
+
const dir = makeTmp()
|
|
70
|
+
const goldenPath = join(dir, 'expected.txt')
|
|
71
|
+
const produced = 'no hash line here\n'
|
|
72
|
+
await assertGoldenOrUpdate(produced, goldenPath)
|
|
73
|
+
const written = readFileSync(goldenPath, 'utf-8')
|
|
74
|
+
expect(written).toBe('no hash line here\n')
|
|
75
|
+
} finally {
|
|
76
|
+
if (original === undefined) delete process.env.UPDATE_GOLDENS
|
|
77
|
+
else process.env.UPDATE_GOLDENS = original
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compares produced source against a golden file, with optional regeneration.
|
|
6
|
+
*
|
|
7
|
+
* - Normal mode: reads `goldenPath`, splices the produced source-hash line into
|
|
8
|
+
* the golden's `<PLACEHOLDER>` slot, and asserts byte-equality.
|
|
9
|
+
* - Regen mode (`UPDATE_GOLDENS=1`): replaces the produced source-hash line with
|
|
10
|
+
* `<PLACEHOLDER>` and writes the result to `goldenPath`. The next normal run
|
|
11
|
+
* will assert against the new golden.
|
|
12
|
+
*
|
|
13
|
+
* The source-hash placeholder lets us check generated content into source
|
|
14
|
+
* control without coupling the golden to a specific envelope hash. Codegen
|
|
15
|
+
* outputs that don't include a `// Source hash:` line work too — the
|
|
16
|
+
* splice/replace is a no-op when no match is found.
|
|
17
|
+
*/
|
|
18
|
+
export async function assertGoldenOrUpdate(produced: string, goldenPath: string): Promise<void> {
|
|
19
|
+
if (process.env.UPDATE_GOLDENS === '1') {
|
|
20
|
+
const goldenContent = produced.replace(
|
|
21
|
+
/^\/\/ Source hash: [a-f0-9]+$/m,
|
|
22
|
+
'// Source hash: <PLACEHOLDER>',
|
|
23
|
+
)
|
|
24
|
+
await writeFile(goldenPath, goldenContent, 'utf-8')
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.log(`[golden-test] Wrote golden: ${goldenPath}`)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const goldenTemplate = await readFile(goldenPath, 'utf8')
|
|
31
|
+
const sourceHashLine = produced.split('\n').find((l) => l.startsWith('// Source hash:')) ?? ''
|
|
32
|
+
const goldenWithHash = goldenTemplate.replace('// Source hash: <PLACEHOLDER>', sourceHashLine)
|
|
33
|
+
expect(produced).toBe(goldenWithHash)
|
|
34
|
+
}
|