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,77 @@
|
|
|
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 { createStubKotlinEmitter, type KotlinEmitResult } 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[] = ['kotlinx.serialization.Serializable']): KotlinEmitResult => ({
|
|
13
|
+
code,
|
|
14
|
+
rootTypeName,
|
|
15
|
+
extractedTypeNames: [],
|
|
16
|
+
imports,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('kotlin 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.kt')
|
|
23
|
+
const envelope = JSON.parse(await readFile(envelopePath, 'utf8'))
|
|
24
|
+
|
|
25
|
+
// Hand-authored slot outputs in the v7.2 nested-class 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 data classes (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 kotlinc E2E (Task 11) exercises real ajsc against the same
|
|
33
|
+
// fixture and would surface any incompatibility.
|
|
34
|
+
const emitter = createStubKotlinEmitter({
|
|
35
|
+
// GetUser
|
|
36
|
+
PathParams: ok('@Serializable\ndata class PathParams(\n val id: String,\n)', 'PathParams'),
|
|
37
|
+
Response: ok(
|
|
38
|
+
'@Serializable\ndata class Response(\n val id: String,\n val name: String,\n @SerialName("created-at") @Contextual val createdAt: java.time.Instant,\n val address: Address,\n) {\n @Serializable\n data class Address(\n val street: String,\n val city: String,\n )\n}',
|
|
39
|
+
'Response',
|
|
40
|
+
['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.Contextual'],
|
|
41
|
+
),
|
|
42
|
+
NotFound: ok(
|
|
43
|
+
'@Serializable\ndata class NotFound(\n val name: String = "NotFound",\n val message: String,\n)',
|
|
44
|
+
'NotFound',
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
// CreateUser
|
|
48
|
+
Body: ok(
|
|
49
|
+
'@Serializable\n@JsonClassDiscriminator("kind")\nsealed interface Body {\n @Serializable\n @SerialName("guest")\n data class GuestBody(\n val displayName: String,\n ) : Body\n\n @Serializable\n @SerialName("registered")\n data class RegisteredBody(\n val email: String,\n val name: String,\n ) : Body\n}',
|
|
50
|
+
'Body',
|
|
51
|
+
['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.json.JsonClassDiscriminator'],
|
|
52
|
+
),
|
|
53
|
+
ValidationError: ok(
|
|
54
|
+
'@Serializable\ndata class ValidationError(\n val name: String = "ValidationError",\n val message: String,\n val field: String? = null,\n)',
|
|
55
|
+
'ValidationError',
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
// ListUsers
|
|
59
|
+
Query: ok(
|
|
60
|
+
'@Serializable\ndata class Query(\n val status: Status? = null,\n val limit: Long? = null,\n) {\n @Serializable\n enum class Status {\n @SerialName("active") ACTIVE,\n @SerialName("inactive") INACTIVE,\n }\n}',
|
|
61
|
+
'Query',
|
|
62
|
+
['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName'],
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const files = await runPipeline({
|
|
67
|
+
envelope, outDir: 'out', dryRun: true,
|
|
68
|
+
target: 'kotlin', kotlinPackage: 'com.example.api',
|
|
69
|
+
kotlinEmitter: emitter,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(files).toHaveLength(1)
|
|
73
|
+
expect(files[0]!.path).toBe(join('out', 'Users.kt'))
|
|
74
|
+
|
|
75
|
+
await assertGoldenOrUpdate(files[0]!.code, goldenPath)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
let ajscResolvable = false
|
|
4
|
+
let emitKotlinFn:
|
|
5
|
+
| ((
|
|
6
|
+
schema: unknown,
|
|
7
|
+
opts: unknown,
|
|
8
|
+
) => { code: string; imports: string[]; rootTypeName: string; extractedTypeNames: string[] })
|
|
9
|
+
| undefined
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const ajsc = await import('ajsc')
|
|
13
|
+
if (typeof (ajsc as { emitKotlin?: unknown }).emitKotlin === 'function') {
|
|
14
|
+
ajscResolvable = true
|
|
15
|
+
emitKotlinFn = (ajsc as { emitKotlin: typeof emitKotlinFn }).emitKotlin!
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// ajsc not installed (e.g. npm install --omit=optional); test skips below.
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('ajsc.emitKotlin — untagged oneOf behavior', () => {
|
|
22
|
+
it.skipIf(!ajscResolvable)(
|
|
23
|
+
'produces a deterministic fallback shape for an untagged oneOf',
|
|
24
|
+
() => {
|
|
25
|
+
const schema = {
|
|
26
|
+
oneOf: [{ type: 'string' }, { type: 'integer' }],
|
|
27
|
+
}
|
|
28
|
+
const result = emitKotlinFn!(schema, {
|
|
29
|
+
rootTypeName: 'Mixed',
|
|
30
|
+
inlineTypes: true,
|
|
31
|
+
unsupportedUnions: 'fallback',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Snapshot pins the current ajsc behavior. If ajsc changes the fallback
|
|
35
|
+
// shape, this diff prompts an intentional review and a corresponding
|
|
36
|
+
// update to docs/codegen-kotlin.md.
|
|
37
|
+
expect({
|
|
38
|
+
code: result.code,
|
|
39
|
+
imports: result.imports.slice().sort(),
|
|
40
|
+
rootTypeName: result.rootTypeName,
|
|
41
|
+
extractedTypeNames: result.extractedTypeNames.slice().sort(),
|
|
42
|
+
}).toMatchSnapshot()
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Companion to test 1: confirms 'fallback' is not load-bearing — ajsc emits the
|
|
47
|
+
// same shape with or without it. If these snapshots ever diverge, that's a
|
|
48
|
+
// meaningful behavior change worth surfacing in docs/codegen-kotlin.md.
|
|
49
|
+
it.skipIf(!ajscResolvable)(
|
|
50
|
+
'silently falls back to empty data class when unsupportedUnions is not specified',
|
|
51
|
+
() => {
|
|
52
|
+
const schema = { oneOf: [{ type: 'string' }, { type: 'integer' }] }
|
|
53
|
+
const result = emitKotlinFn!(schema, { rootTypeName: 'Mixed', inlineTypes: true })
|
|
54
|
+
// When unsupportedUnions defaults (not explicitly set), ajsc does NOT throw.
|
|
55
|
+
// Instead it silently emits an empty data class.
|
|
56
|
+
expect({
|
|
57
|
+
code: result.code,
|
|
58
|
+
imports: result.imports.slice().sort(),
|
|
59
|
+
rootTypeName: result.rootTypeName,
|
|
60
|
+
extractedTypeNames: result.extractedTypeNames.slice().sort(),
|
|
61
|
+
}).toMatchSnapshot()
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kotlin-target pipeline runner. Invoked by the dispatcher in
|
|
3
|
+
* `src/codegen/pipeline.ts` when `target === 'kotlin'`. Emits one `.kt` file
|
|
4
|
+
* per scope via `emitKotlinScope` 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 { KotlinEmitter } from './ajsc-adapter.js'
|
|
14
|
+
import { emitKotlinScope } from './emit-scope-kotlin.js'
|
|
15
|
+
|
|
16
|
+
export interface KotlinRunInput extends TargetRunInput {
|
|
17
|
+
kotlinPackage?: string
|
|
18
|
+
kotlinSerializer?: 'kotlinx' | 'none'
|
|
19
|
+
unsupportedUnions?: 'throw' | 'fallback'
|
|
20
|
+
/** Injected for tests; production wiring resolves a real ajsc emitter. */
|
|
21
|
+
kotlinEmitter?: KotlinEmitter
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Spec §"CLI flags" — `--array-item-naming`, `--depluralize`,
|
|
26
|
+
* `--uncountable-words` also apply to the Kotlin target. The CLI parks them
|
|
27
|
+
* on `options.ajsc` (a TS-target structure historically); copy the
|
|
28
|
+
* Kotlin-relevant subset onto the scope opts here.
|
|
29
|
+
*/
|
|
30
|
+
const AJSC_PASSTHROUGH_KEYS = ['arrayItemNaming', 'depluralize', 'uncountableWords'] as const
|
|
31
|
+
|
|
32
|
+
export async function runKotlinPipeline(input: KotlinRunInput): Promise<GeneratedFile[]> {
|
|
33
|
+
const { envelope, outDir, hash, groups, dryRun = false, cleanOutDir = false } = input
|
|
34
|
+
|
|
35
|
+
// Runtime guards for non-CLI callers (direct API consumers + tests).
|
|
36
|
+
// The CLI validates --kotlin-package in parseArgs and resolves the emitter
|
|
37
|
+
// before calling runPipeline, so end users never hit these throws.
|
|
38
|
+
if (input.kotlinPackage == null) {
|
|
39
|
+
throw new Error('[ts-procedures-codegen] target=kotlin requires kotlinPackage')
|
|
40
|
+
}
|
|
41
|
+
if (input.kotlinEmitter == null) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'[ts-procedures-codegen] target=kotlin requires a kotlinEmitter (CLI resolves via resolveProductionKotlinEmitter)'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const errorSchemas = buildErrorSchemasMap(envelope.errors)
|
|
48
|
+
const ajscPassthrough = input.ajsc ?? {}
|
|
49
|
+
|
|
50
|
+
const files: GeneratedFile[] = []
|
|
51
|
+
const allSkipped: string[] = []
|
|
52
|
+
for (const group of groups) {
|
|
53
|
+
const emitted = emitKotlinScope(
|
|
54
|
+
group,
|
|
55
|
+
{
|
|
56
|
+
kotlinPackage: input.kotlinPackage,
|
|
57
|
+
sourceHash: hash,
|
|
58
|
+
...(input.kotlinSerializer !== undefined ? { serializer: input.kotlinSerializer } : {}),
|
|
59
|
+
...(input.unsupportedUnions !== undefined ? { unsupportedUnions: input.unsupportedUnions } : {}),
|
|
60
|
+
...pickDefined(ajscPassthrough, AJSC_PASSTHROUGH_KEYS),
|
|
61
|
+
},
|
|
62
|
+
input.kotlinEmitter,
|
|
63
|
+
errorSchemas,
|
|
64
|
+
)
|
|
65
|
+
files.push({ path: join(outDir, emitted.filename), code: emitted.code })
|
|
66
|
+
allSkipped.push(...emitted.skippedStreams)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (allSkipped.length > 0) {
|
|
70
|
+
console.log(
|
|
71
|
+
`[ts-procedures-codegen] Skipped ${allSkipped.length} stream route${allSkipped.length === 1 ? '' : 's'} (kotlin target): ${allSkipped.join(', ')}`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await writeGeneratedFiles(files, outDir, { dryRun, cleanOutDir })
|
|
76
|
+
|
|
77
|
+
return files
|
|
78
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Generated by ts-procedures-codegen — do not edit.
|
|
2
|
+
// Source hash: <PLACEHOLDER>
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
public enum Users {
|
|
7
|
+
public enum GetUser {
|
|
8
|
+
public static let method = "GET"
|
|
9
|
+
public static let pathTemplate = "/users/{id}"
|
|
10
|
+
public static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
|
|
11
|
+
|
|
12
|
+
public struct PathParams: Codable {
|
|
13
|
+
public let id: String
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public struct Response: Codable {
|
|
17
|
+
public let id: String
|
|
18
|
+
public let name: String
|
|
19
|
+
public let createdAt: Date
|
|
20
|
+
public let address: Address
|
|
21
|
+
|
|
22
|
+
enum CodingKeys: String, CodingKey {
|
|
23
|
+
case id, name
|
|
24
|
+
case createdAt = "created-at"
|
|
25
|
+
case address
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public struct Address: Codable {
|
|
29
|
+
public let street: String
|
|
30
|
+
public let city: String
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public enum Errors {
|
|
35
|
+
public struct NotFound: Codable {
|
|
36
|
+
public let name: String
|
|
37
|
+
public let message: String
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public enum CreateUser {
|
|
43
|
+
public static let method = "POST"
|
|
44
|
+
public static let pathTemplate = "/users"
|
|
45
|
+
public static let path = "/users"
|
|
46
|
+
|
|
47
|
+
public enum Body: Codable {
|
|
48
|
+
case guest(GuestBody)
|
|
49
|
+
case registered(RegisteredBody)
|
|
50
|
+
|
|
51
|
+
public struct GuestBody: Codable {
|
|
52
|
+
public let kind: String
|
|
53
|
+
public let displayName: String
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public struct RegisteredBody: Codable {
|
|
57
|
+
public let kind: String
|
|
58
|
+
public let email: String
|
|
59
|
+
public let name: String
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public struct Response: Codable {
|
|
64
|
+
public let id: String
|
|
65
|
+
public let name: String
|
|
66
|
+
public let createdAt: Date
|
|
67
|
+
public let address: Address
|
|
68
|
+
|
|
69
|
+
enum CodingKeys: String, CodingKey {
|
|
70
|
+
case id, name
|
|
71
|
+
case createdAt = "created-at"
|
|
72
|
+
case address
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public struct Address: Codable {
|
|
76
|
+
public let street: String
|
|
77
|
+
public let city: String
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public enum Errors {
|
|
82
|
+
public struct ValidationError: Codable {
|
|
83
|
+
public let name: String
|
|
84
|
+
public let message: String
|
|
85
|
+
public let field: String?
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public enum ListUsers {
|
|
91
|
+
public static let method = "GET"
|
|
92
|
+
public static let pathTemplate = "/users"
|
|
93
|
+
public static let path = "/users"
|
|
94
|
+
|
|
95
|
+
public struct Query: Codable {
|
|
96
|
+
public let status: Status?
|
|
97
|
+
public let limit: Int64?
|
|
98
|
+
|
|
99
|
+
public enum Status: String, Codable {
|
|
100
|
+
case active
|
|
101
|
+
case inactive
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public struct Response: Codable {
|
|
106
|
+
public let id: String
|
|
107
|
+
public let name: String
|
|
108
|
+
public let createdAt: Date
|
|
109
|
+
public let address: Address
|
|
110
|
+
|
|
111
|
+
enum CodingKeys: String, CodingKey {
|
|
112
|
+
case id, name
|
|
113
|
+
case createdAt = "created-at"
|
|
114
|
+
case address
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public struct Address: Codable {
|
|
118
|
+
public let street: String
|
|
119
|
+
public let city: String
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { DocEnvelope } from '../../../implementations/types.js'
|
|
3
|
+
import { runPipeline } from '../../pipeline.js'
|
|
4
|
+
import { resolveProductionSwiftEmitter } from './ajsc-adapter.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* End-to-end regression for `--swift-access-level`.
|
|
8
|
+
*
|
|
9
|
+
* Closes a coverage gap: existing unit tests verify our scope/route
|
|
10
|
+
* scaffolding (the outer `enum` keywords) honor the access level, but
|
|
11
|
+
* nothing asserts that real ajsc actually emits `internal struct` /
|
|
12
|
+
* `internal let` in its own output. If ajsc renamed `accessLevel` or
|
|
13
|
+
* changed its semantics, our codegen would silently regress to `public`.
|
|
14
|
+
*
|
|
15
|
+
* This test runs the production pipeline with the real ajsc emitter
|
|
16
|
+
* and pins the keyword presence in the generated source. No toolchain
|
|
17
|
+
* (no swiftc) is required — we only assert string contents.
|
|
18
|
+
*/
|
|
19
|
+
const envelope: DocEnvelope = {
|
|
20
|
+
basePath: '/api',
|
|
21
|
+
headers: [],
|
|
22
|
+
errors: [],
|
|
23
|
+
routes: [
|
|
24
|
+
{
|
|
25
|
+
kind: 'api',
|
|
26
|
+
name: 'GetThing',
|
|
27
|
+
scope: 'things',
|
|
28
|
+
method: 'get',
|
|
29
|
+
path: '/things/:id',
|
|
30
|
+
fullPath: '/things/:id',
|
|
31
|
+
jsonSchema: {},
|
|
32
|
+
// The swift route emitter reads `route.schema.input.*` and
|
|
33
|
+
// `route.schema.returnType` directly; mirror what the codegen
|
|
34
|
+
// pipeline downstream expects rather than the doc-builder
|
|
35
|
+
// `jsonSchema` shape.
|
|
36
|
+
schema: {
|
|
37
|
+
input: {
|
|
38
|
+
pathParams: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: { id: { type: 'string' } },
|
|
41
|
+
required: ['id'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
returnType: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: { id: { type: 'string' } },
|
|
47
|
+
required: ['id'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
errors: [],
|
|
51
|
+
} as unknown as DocEnvelope['routes'][number],
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('swift codegen — --swift-access-level (real ajsc)', () => {
|
|
56
|
+
it('emits `internal` keywords on ajsc-generated structs when accessLevel="internal"', async () => {
|
|
57
|
+
const emitter = await resolveProductionSwiftEmitter()
|
|
58
|
+
const files = await runPipeline({
|
|
59
|
+
envelope,
|
|
60
|
+
outDir: 'out',
|
|
61
|
+
dryRun: true,
|
|
62
|
+
target: 'swift',
|
|
63
|
+
swiftAccessLevel: 'internal',
|
|
64
|
+
swiftEmitter: emitter,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(files).toHaveLength(1)
|
|
68
|
+
const code = files[0]!.code
|
|
69
|
+
|
|
70
|
+
// Real ajsc output for the PathParams + Response slots.
|
|
71
|
+
expect(code).toContain('internal struct PathParams')
|
|
72
|
+
expect(code).toContain('internal struct Response')
|
|
73
|
+
// Property declarations should also flip to internal.
|
|
74
|
+
expect(code).toContain('internal let id: String')
|
|
75
|
+
|
|
76
|
+
// Our scope/route scaffolding flips too — see emit-scope-swift.ts
|
|
77
|
+
// and emit-route-swift.ts, which both consume `accessLevel`.
|
|
78
|
+
expect(code).toContain('internal enum Things')
|
|
79
|
+
expect(code).toContain('internal enum GetThing')
|
|
80
|
+
|
|
81
|
+
// Hard guarantee: nothing in the file leaks `public` declarations.
|
|
82
|
+
// If ajsc silently ignored `accessLevel: 'internal'`, this would fail.
|
|
83
|
+
expect(code).not.toMatch(/\bpublic\s+(struct|enum|let|var|func|static)\b/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('emits `public` keywords on ajsc-generated structs when accessLevel defaults to public', async () => {
|
|
87
|
+
const emitter = await resolveProductionSwiftEmitter()
|
|
88
|
+
const files = await runPipeline({
|
|
89
|
+
envelope,
|
|
90
|
+
outDir: 'out',
|
|
91
|
+
dryRun: true,
|
|
92
|
+
target: 'swift',
|
|
93
|
+
// intentionally omit swiftAccessLevel — default is 'public'
|
|
94
|
+
swiftEmitter: emitter,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(files).toHaveLength(1)
|
|
98
|
+
const code = files[0]!.code
|
|
99
|
+
|
|
100
|
+
expect(code).toContain('public struct PathParams')
|
|
101
|
+
expect(code).toContain('public struct Response')
|
|
102
|
+
expect(code).toContain('public enum Things')
|
|
103
|
+
expect(code).toContain('public enum GetThing')
|
|
104
|
+
|
|
105
|
+
// No `internal` declarations should appear when access level is public.
|
|
106
|
+
expect(code).not.toMatch(/\binternal\s+(struct|enum|let|var|func|static)\b/)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createStubSwiftEmitter, resolveProductionSwiftEmitter, type SwiftEmitResult } from './ajsc-adapter.js'
|
|
3
|
+
|
|
4
|
+
describe('createStubSwiftEmitter', () => {
|
|
5
|
+
it('returns the configured EmitResult for the matching root name', () => {
|
|
6
|
+
const expected: SwiftEmitResult = {
|
|
7
|
+
code: 'public struct User: Codable { public let id: String }',
|
|
8
|
+
rootTypeName: 'User',
|
|
9
|
+
extractedTypeNames: [],
|
|
10
|
+
imports: ['Foundation'],
|
|
11
|
+
}
|
|
12
|
+
const emitter = createStubSwiftEmitter({ User: expected })
|
|
13
|
+
expect(
|
|
14
|
+
emitter.emit(
|
|
15
|
+
{ type: 'object' },
|
|
16
|
+
{
|
|
17
|
+
rootTypeName: 'User',
|
|
18
|
+
inlineTypes: true,
|
|
19
|
+
serializer: 'codable',
|
|
20
|
+
unsupportedUnions: 'throw',
|
|
21
|
+
},
|
|
22
|
+
),
|
|
23
|
+
).toEqual(expected)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('throws when asked to emit a name not in the stub map', () => {
|
|
27
|
+
const emitter = createStubSwiftEmitter({})
|
|
28
|
+
expect(() => emitter.emit({}, { rootTypeName: 'Missing' })).toThrow(/Missing/)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('resolveProductionSwiftEmitter', () => {
|
|
33
|
+
it('returns a working emitter that invokes ajsc.emitSwift when ajsc is installed', async () => {
|
|
34
|
+
const emitter = await resolveProductionSwiftEmitter()
|
|
35
|
+
const result = emitter.emit(
|
|
36
|
+
{ type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
37
|
+
{ rootTypeName: 'Probe' },
|
|
38
|
+
)
|
|
39
|
+
expect(typeof result.code).toBe('string')
|
|
40
|
+
expect(result.code.length).toBeGreaterThan(0)
|
|
41
|
+
expect(result.rootTypeName).toBe('Probe')
|
|
42
|
+
expect(Array.isArray(result.imports)).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
// Note: testing the failure path (ajsc unavailable) requires module mocking;
|
|
45
|
+
// we leave that as a manual-verification path. The error message is pinned
|
|
46
|
+
// by the message text below so any change requires updating both call sites.
|
|
47
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface SwiftEmitResult {
|
|
2
|
+
code: string
|
|
3
|
+
rootTypeName: string
|
|
4
|
+
extractedTypeNames: string[]
|
|
5
|
+
imports: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SwiftEmitOptions {
|
|
9
|
+
rootTypeName: string
|
|
10
|
+
/** Always set true at our call sites; v7.2 default is false. */
|
|
11
|
+
inlineTypes?: boolean
|
|
12
|
+
serializer?: 'codable' | 'none'
|
|
13
|
+
accessLevel?: 'public' | 'internal'
|
|
14
|
+
unsupportedUnions?: 'throw' | 'fallback'
|
|
15
|
+
arrayItemNaming?: string | false
|
|
16
|
+
depluralize?: boolean
|
|
17
|
+
uncountableWords?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SwiftEmitter {
|
|
21
|
+
emit(schema: Record<string, unknown>, opts: SwiftEmitOptions): SwiftEmitResult
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createStubSwiftEmitter(
|
|
25
|
+
results: Record<string, SwiftEmitResult>,
|
|
26
|
+
): SwiftEmitter {
|
|
27
|
+
return {
|
|
28
|
+
emit(_schema, opts) {
|
|
29
|
+
const result = results[opts.rootTypeName]
|
|
30
|
+
if (result == null) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`[stub-swift-emitter] No stubbed result for rootTypeName "${opts.rootTypeName}". ` +
|
|
33
|
+
`Provide one in the results map.`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the production Swift emitter from `ajsc`. Throws a clear error
|
|
43
|
+
* if ajsc is not installed or does not expose `emitSwift` (e.g. consumer
|
|
44
|
+
* ran `npm install --omit=optional` since ajsc is in optionalDependencies).
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveProductionSwiftEmitter(): Promise<SwiftEmitter> {
|
|
47
|
+
let ajsc: { emitSwift?: unknown } | null = null
|
|
48
|
+
let importError: unknown
|
|
49
|
+
try {
|
|
50
|
+
ajsc = (await import('ajsc')) as { emitSwift?: unknown }
|
|
51
|
+
} catch (err) {
|
|
52
|
+
importError = err
|
|
53
|
+
}
|
|
54
|
+
const emitSwift = ajsc?.emitSwift
|
|
55
|
+
if (typeof emitSwift !== 'function') {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'[ts-procedures-codegen] ajsc.emitSwift is not available. ' +
|
|
58
|
+
'Install ajsc (`npm install ajsc`) — it is an optional dependency.',
|
|
59
|
+
importError !== undefined ? { cause: importError } : undefined,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
emit(schema, opts) {
|
|
64
|
+
return (emitSwift as (s: unknown, o: unknown) => SwiftEmitResult)(schema, opts)
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { execFileSync, execSync } from 'node:child_process'
|
|
3
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { runPipeline } from '../../pipeline.js'
|
|
8
|
+
import { resolveProductionSwiftEmitter } from './ajsc-adapter.js'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
|
|
13
|
+
function swiftcAvailable(): boolean {
|
|
14
|
+
try {
|
|
15
|
+
execSync('swiftc --version', { stdio: 'ignore' })
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RUN = process.env.TS_PROCEDURES_SWIFT_E2E === '1'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* E2E: real ajsc → real .swift output → real swiftc parse.
|
|
26
|
+
*
|
|
27
|
+
* Gated on (a) `swiftc` on PATH and (b) opt-in via env var so default
|
|
28
|
+
* `npm test` runs stay green for contributors without the toolchain.
|
|
29
|
+
*
|
|
30
|
+
* Unlike Kotlin, Swift's stdlib + Foundation already ship with `Codable`,
|
|
31
|
+
* so no extra classpath/jars are needed — `swiftc -parse` against a single
|
|
32
|
+
* file is sufficient to validate the output is syntactically well-formed.
|
|
33
|
+
*/
|
|
34
|
+
describe('swift codegen — swiftc compile (gated)', () => {
|
|
35
|
+
it.skipIf(!swiftcAvailable() || !RUN)(
|
|
36
|
+
'parses generated output without errors',
|
|
37
|
+
async () => {
|
|
38
|
+
const emitter = await resolveProductionSwiftEmitter()
|
|
39
|
+
const envelope = JSON.parse(
|
|
40
|
+
readFileSync(join(__dirname, '../../__fixtures__/users-envelope.json'), 'utf8'),
|
|
41
|
+
)
|
|
42
|
+
const files = await runPipeline({
|
|
43
|
+
envelope,
|
|
44
|
+
outDir: 'out',
|
|
45
|
+
dryRun: true,
|
|
46
|
+
target: 'swift',
|
|
47
|
+
swiftEmitter: emitter,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const dir = mkdtempSync(join(tmpdir(), 'tsp-swift-e2e-'))
|
|
51
|
+
const filePaths: string[] = []
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
const dest = join(dir, f.path.split('/').pop()!)
|
|
54
|
+
writeFileSync(dest, f.code)
|
|
55
|
+
filePaths.push(dest)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// -parse only checks syntax; no codegen, no linking, no module deps.
|
|
59
|
+
execFileSync('swiftc', ['-parse', ...filePaths], { stdio: 'inherit' })
|
|
60
|
+
|
|
61
|
+
// Sanity check — at least one .swift file was generated and persisted.
|
|
62
|
+
expect(filePaths.length).toBeGreaterThan(0)
|
|
63
|
+
for (const p of filePaths) expect(existsSync(p)).toBe(true)
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
})
|