vxrn 2.0.0-beta.98.1 → 2.6.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/dist/backgroundComputation.mjs +145 -0
- package/dist/backgroundComputation.mjs.map +1 -0
- package/dist/backgroundComputation.native.js +163 -0
- package/dist/backgroundComputation.native.js.map +1 -0
- package/dist/backgroundComputationNative.mjs +16 -0
- package/dist/backgroundComputationNative.mjs.map +1 -0
- package/dist/backgroundComputationNative.native.js +18 -0
- package/dist/backgroundComputationNative.native.js.map +1 -0
- package/dist/config/getBaseViteConfigOnly.mjs +6 -10
- package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
- package/dist/config/getBaseViteConfigOnly.native.js +6 -10
- package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
- package/dist/config/getOptimizeDeps.mjs +2 -1
- package/dist/config/getOptimizeDeps.mjs.map +1 -1
- package/dist/config/getOptimizeDeps.native.js +2 -1
- package/dist/config/getOptimizeDeps.native.js.map +1 -1
- package/dist/config/getViteServerConfig.mjs +13 -0
- package/dist/config/getViteServerConfig.mjs.map +1 -1
- package/dist/config/getViteServerConfig.native.js +15 -1
- package/dist/config/getViteServerConfig.native.js.map +1 -1
- package/dist/constants/defaults.mjs +4 -0
- package/dist/constants/defaults.mjs.map +1 -1
- package/dist/constants/defaults.native.js +4 -0
- package/dist/constants/defaults.native.js.map +1 -1
- package/dist/exports/build.native.js +1 -1
- package/dist/exports/build.native.js.map +1 -1
- package/dist/exports/clean.native.js +1 -1
- package/dist/exports/clean.native.js.map +1 -1
- package/dist/exports/createServer.native.js +2 -2
- package/dist/exports/createServer.native.js.map +1 -1
- package/dist/exports/dev.mjs +3 -0
- package/dist/exports/dev.mjs.map +1 -1
- package/dist/exports/dev.native.js +6 -3
- package/dist/exports/dev.native.js.map +1 -1
- package/dist/exports/loadEnv.native.js +1 -1
- package/dist/exports/loadEnv.native.js.map +1 -1
- package/dist/exports/patch.native.js +1 -1
- package/dist/exports/patch.native.js.map +1 -1
- package/dist/exports/prebuild.mjs +0 -22
- package/dist/exports/prebuild.mjs.map +1 -1
- package/dist/exports/prebuild.native.js +1 -23
- package/dist/exports/prebuild.native.js.map +1 -1
- package/dist/exports/prebuildWithoutExpo.mjs +1133 -182
- package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
- package/dist/exports/prebuildWithoutExpo.native.js +1606 -244
- package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
- package/dist/exports/runAndroid.native.js +1 -1
- package/dist/exports/runAndroid.native.js.map +1 -1
- package/dist/exports/runIos.native.js +1 -1
- package/dist/exports/runIos.native.js.map +1 -1
- package/dist/exports/serve.native.js +1 -1
- package/dist/exports/serve.native.js.map +1 -1
- package/dist/exports/serveStaticAssets.native.js +1 -1
- package/dist/exports/serveStaticAssets.native.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.mjs +4 -1
- package/dist/index.native.js +4 -1
- package/dist/native-source-contract.mjs +3 -0
- package/dist/native-source-contract.native.js +3 -0
- package/dist/patches/builtInDepPatches.mjs +0 -13
- package/dist/patches/builtInDepPatches.mjs.map +1 -1
- package/dist/patches/builtInDepPatches.native.js +0 -13
- package/dist/patches/builtInDepPatches.native.js.map +1 -1
- package/dist/plugins/autoDepOptimizePlugin.mjs +3 -2
- package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
- package/dist/plugins/autoDepOptimizePlugin.native.js +6 -3
- package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
- package/dist/plugins/backgroundComputationPlugin.mjs +27 -0
- package/dist/plugins/backgroundComputationPlugin.mjs.map +1 -0
- package/dist/plugins/backgroundComputationPlugin.native.js +32 -0
- package/dist/plugins/backgroundComputationPlugin.native.js.map +1 -0
- package/dist/plugins/reactNativeCommonJsPlugin.native.js +3 -3
- package/dist/plugins/reactNativeCommonJsPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.mjs +2 -1
- package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.native.js +3 -2
- package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +87 -24
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +162 -45
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/plugins/workletImportsPlugin.mjs +9 -30
- package/dist/plugins/workletImportsPlugin.mjs.map +1 -1
- package/dist/plugins/workletImportsPlugin.native.js +10 -35
- package/dist/plugins/workletImportsPlugin.native.js.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.mjs +3 -3
- package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.native.js +3 -3
- package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
- package/dist/runtime/hmr-client.native.js +3 -7
- package/dist/runtime/hmr-client.native.js.map +1 -1
- package/dist/runtime/hmr-runtime.native.js +32 -40
- package/dist/runtime/hmr-runtime.native.js.map +1 -1
- package/dist/serve/node.native.js +2 -2
- package/dist/serve/node.native.js.map +1 -1
- package/dist/swift-package-id.mjs +3 -0
- package/dist/swift-package-id.native.js +3 -0
- package/dist/user-interface/index.native.js +6 -6
- package/dist/user-interface/index.native.js.map +1 -1
- package/dist/utils/compileNativeDevBytecode.mjs +93 -0
- package/dist/utils/compileNativeDevBytecode.mjs.map +1 -0
- package/dist/utils/compileNativeDevBytecode.native.js +98 -0
- package/dist/utils/compileNativeDevBytecode.native.js.map +1 -0
- package/dist/utils/createNativeDevEngine.mjs +227 -20
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +249 -37
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/execSync.native.js +1 -1
- package/dist/utils/execSync.native.js.map +1 -1
- package/dist/utils/getServerEntry.native.js +2 -2
- package/dist/utils/getServerEntry.native.js.map +1 -1
- package/dist/utils/nativeRun.mjs +32 -16
- package/dist/utils/nativeRun.mjs.map +1 -1
- package/dist/utils/nativeRun.native.js +43 -18
- package/dist/utils/nativeRun.native.js.map +1 -1
- package/dist/utils/nativeSourceContract.mjs +1823 -0
- package/dist/utils/nativeSourceContract.mjs.map +1 -0
- package/dist/utils/nativeSourceContract.native.js +2425 -0
- package/dist/utils/nativeSourceContract.native.js.map +1 -0
- package/dist/utils/patches.native.js +15 -24
- package/dist/utils/patches.native.js.map +1 -1
- package/dist/utils/printServerUrls.native.js +1 -1
- package/dist/utils/printServerUrls.native.js.map +1 -1
- package/dist/utils/scanDepsToOptimize.mjs +7 -1
- package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
- package/dist/utils/scanDepsToOptimize.native.js +27 -2
- package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
- package/dist/utils/swiftPackageId.mjs +31 -2
- package/dist/utils/swiftPackageId.mjs.map +1 -1
- package/dist/utils/swiftPackageId.native.js +50 -2
- package/dist/utils/swiftPackageId.native.js.map +1 -1
- package/dist/vxrn-vite-plugin.native.js +1 -1
- package/dist/vxrn-vite-plugin.native.js.map +1 -1
- package/expo-plugin.cjs +792 -2
- package/native-project-patches.cjs +201 -10
- package/package.json +34 -15
- package/src/backgroundComputation.ts +220 -0
- package/src/backgroundComputationNative.ts +23 -0
- package/src/config/getBaseViteConfigOnly.ts +8 -9
- package/src/config/getOptimizeDeps.ts +3 -1
- package/src/config/getViteServerConfig.ts +16 -0
- package/src/config/glbAssets.test.ts +228 -0
- package/src/constants/defaults.ts +6 -0
- package/src/exports/dev.ts +5 -1
- package/src/exports/expoPlugin.test.mjs +337 -15
- package/src/exports/prebuild.ts +0 -31
- package/src/exports/prebuildWithoutExpo.test.ts +1623 -46
- package/src/exports/prebuildWithoutExpo.ts +1606 -216
- package/src/index.ts +8 -0
- package/src/native-source-contract.ts +16 -0
- package/src/patches/builtInDepPatches.ts +0 -21
- package/src/plugins/autoDepOptimizePlugin.ts +7 -2
- package/src/plugins/backgroundComputationPlugin.test.ts +77 -0
- package/src/plugins/backgroundComputationPlugin.ts +29 -0
- package/src/plugins/reactNativeDevAssetPlugin.ts +5 -1
- package/src/plugins/reactNativeDevServer.test.ts +74 -0
- package/src/plugins/reactNativeDevServer.ts +145 -28
- package/src/plugins/rnCodegenPlugin.test.ts +30 -0
- package/src/plugins/workletImportsPlugin.ts +19 -37
- package/src/rn-commands/bundle/buildBundle.ts +5 -3
- package/src/swift-package-id.ts +1 -0
- package/src/utils/compileNativeDevBytecode.test.ts +168 -0
- package/src/utils/compileNativeDevBytecode.ts +117 -0
- package/src/utils/createNativeDevEngine.test.ts +643 -18
- package/src/utils/createNativeDevEngine.ts +278 -48
- package/src/utils/nativeRun.test.ts +125 -23
- package/src/utils/nativeRun.ts +51 -23
- package/src/utils/nativeSourceArtifacts.test.ts +39 -0
- package/src/utils/nativeSourceContract.test.ts +600 -0
- package/src/utils/nativeSourceContract.ts +1897 -0
- package/src/utils/scanDepsToOptimize.test.ts +80 -0
- package/src/utils/scanDepsToOptimize.ts +18 -7
- package/src/utils/swiftPackageId.ts +25 -1
- package/types/backgroundComputation.d.ts +28 -0
- package/types/backgroundComputation.d.ts.map +1 -0
- package/types/backgroundComputationNative.d.ts +2 -0
- package/types/backgroundComputationNative.d.ts.map +1 -0
- package/types/config/getBaseViteConfigOnly.d.ts.map +1 -1
- package/types/config/getOptimizeDeps.d.ts.map +1 -1
- package/types/config/getViteServerConfig.d.ts.map +1 -1
- package/types/constants/defaults.d.ts.map +1 -1
- package/types/exports/dev.d.ts.map +1 -1
- package/types/exports/prebuild.d.ts.map +1 -1
- package/types/exports/prebuildWithoutExpo.d.ts +6 -0
- package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/native-source-contract.d.ts +2 -0
- package/types/native-source-contract.d.ts.map +1 -0
- package/types/patches/builtInDepPatches.d.ts.map +1 -1
- package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
- package/types/plugins/backgroundComputationPlugin.d.ts +3 -0
- package/types/plugins/backgroundComputationPlugin.d.ts.map +1 -0
- package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/plugins/workletImportsPlugin.d.ts.map +1 -1
- package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
- package/types/swift-package-id.d.ts +2 -0
- package/types/swift-package-id.d.ts.map +1 -0
- package/types/utils/compileNativeDevBytecode.d.ts +3 -0
- package/types/utils/compileNativeDevBytecode.d.ts.map +1 -0
- package/types/utils/createNativeDevEngine.d.ts +7 -22
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/nativeRun.d.ts.map +1 -1
- package/types/utils/nativeSourceContract.d.ts +121 -0
- package/types/utils/nativeSourceContract.d.ts.map +1 -0
- package/types/utils/scanDepsToOptimize.d.ts +1 -0
- package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
- package/types/utils/swiftPackageId.d.ts +1 -0
- package/types/utils/swiftPackageId.d.ts.map +1 -1
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { nativeSourceContract, renderKotlinSourceGlue, renderSwiftSourceGlue, swiftPodManifest, writeNativeSourceDeclarations } from './nativeSourceContract'
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const tsc = join(require.resolve('typescript/package.json'), '..', 'bin', 'tsc')
|
|
11
|
+
|
|
12
|
+
describe('native source contract', () => {
|
|
13
|
+
it('types optional collection elements as nullable array members', () => {
|
|
14
|
+
const swift = nativeSourceContract(
|
|
15
|
+
'/app/native/Echo.swift',
|
|
16
|
+
`@MainActor final class Echo: PeachModule {
|
|
17
|
+
init() {}
|
|
18
|
+
func echo(_ values: [String?]) -> [String?] { values }
|
|
19
|
+
}`
|
|
20
|
+
)
|
|
21
|
+
const kotlin = nativeSourceContract(
|
|
22
|
+
'/app/native/Echo.kt',
|
|
23
|
+
`object Echo : OneModule {
|
|
24
|
+
fun echo(values: List<String?>): List<String?> = values
|
|
25
|
+
}`
|
|
26
|
+
)
|
|
27
|
+
for (const contract of [swift, kotlin]) {
|
|
28
|
+
const [method] = contract.modules[0].methods
|
|
29
|
+
expect(method.parameters[0].type).toBe('(string | null)[]')
|
|
30
|
+
expect(method.result).toBe('(string | null)[]')
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('derives matching typed exports from Swift and Kotlin module sources', () => {
|
|
35
|
+
const swift = nativeSourceContract(
|
|
36
|
+
'/app/native/Audio.swift',
|
|
37
|
+
`@MainActor final class AudioMath: PeachModule {
|
|
38
|
+
init() {}
|
|
39
|
+
func rms(_ samples: [Double]) -> Double { 1 }
|
|
40
|
+
func label(name: String) throws -> String { name }
|
|
41
|
+
}`
|
|
42
|
+
)
|
|
43
|
+
const kotlin = nativeSourceContract(
|
|
44
|
+
'/app/native/Audio.kt',
|
|
45
|
+
`object AudioMath : OneModule {
|
|
46
|
+
fun rms(samples: List<Double>): Double = 1.0
|
|
47
|
+
fun label(name: String): String = name
|
|
48
|
+
}`
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(swift.modules).toEqual([
|
|
52
|
+
{
|
|
53
|
+
name: 'AudioMath',
|
|
54
|
+
methods: [
|
|
55
|
+
{ name: 'rms', parameters: [{ name: 'samples', label: '_', type: 'number[]', nativeType: '[Double]' }], result: 'number', nativeResult: 'Double', throws: false, async: false },
|
|
56
|
+
{ name: 'label', parameters: [{ name: 'name', label: 'name', type: 'string', nativeType: 'String' }], result: 'string', nativeResult: 'String', throws: true, async: false },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
])
|
|
60
|
+
expect(kotlin.modules).toEqual([
|
|
61
|
+
{
|
|
62
|
+
name: 'AudioMath',
|
|
63
|
+
methods: [
|
|
64
|
+
{ name: 'rms', parameters: [{ name: 'samples', label: null, type: 'number[]', nativeType: 'List<Double>' }], result: 'number', nativeResult: 'Double', throws: true, async: false },
|
|
65
|
+
{ name: 'label', parameters: [{ name: 'name', label: null, type: 'string', nativeType: 'String' }], result: 'string', nativeResult: 'String', throws: true, async: false },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
])
|
|
69
|
+
expect(swift.hash).toMatch(/^[a-f0-9]{64}$/)
|
|
70
|
+
|
|
71
|
+
const temp = mkdtempSync(join(tmpdir(), 'one-native-source-types-'))
|
|
72
|
+
writeFileSync(join(temp, 'Audio.d.swift.ts'), swift.declaration)
|
|
73
|
+
writeFileSync(join(temp, 'Audio.d.kt.ts'), kotlin.declaration)
|
|
74
|
+
writeFileSync(join(temp, 'tsconfig.json'), JSON.stringify({
|
|
75
|
+
compilerOptions: {
|
|
76
|
+
strict: true,
|
|
77
|
+
noEmit: true,
|
|
78
|
+
allowArbitraryExtensions: true,
|
|
79
|
+
module: 'esnext',
|
|
80
|
+
moduleResolution: 'bundler',
|
|
81
|
+
types: [],
|
|
82
|
+
},
|
|
83
|
+
include: ['call.ts'],
|
|
84
|
+
}))
|
|
85
|
+
writeFileSync(join(temp, 'call.ts'), `import { AudioMath as Swift } from './Audio.swift'
|
|
86
|
+
import { AudioMath as Kotlin } from './Audio.kt'
|
|
87
|
+
const a: Promise<number> = Swift.rms([1, 2])
|
|
88
|
+
const b: Promise<string> = Kotlin.label('good')
|
|
89
|
+
void [a, b]
|
|
90
|
+
Swift.rms('wrong')
|
|
91
|
+
`)
|
|
92
|
+
// one compilation must accept the valid calls and reject only the wrong argument.
|
|
93
|
+
const badCall = spawnSync(process.execPath, [tsc, '-p', temp], { encoding: 'utf8' })
|
|
94
|
+
expect(badCall.status).toBe(1)
|
|
95
|
+
expect(badCall.stdout.match(/error TS\d+:/g)).toEqual(['error TS2345:'])
|
|
96
|
+
expect(badCall.stdout).toMatch(/TS2345:.*string.*number\[\]/)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('declares a Swift default view only from the file with @main', () => {
|
|
100
|
+
const view = nativeSourceContract(
|
|
101
|
+
'/app/native/Badge.swift',
|
|
102
|
+
`@main struct Badge: PeachPackage {
|
|
103
|
+
init() {}
|
|
104
|
+
func view(props: JSON) -> some View { Text("badge") }
|
|
105
|
+
}`
|
|
106
|
+
)
|
|
107
|
+
expect(view.defaultView).toBe(true)
|
|
108
|
+
expect(nativeSourceContract('/app/native/Helper.swift', 'struct Helper {}').defaultView).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('imports public Kotlin composables as typed components with callbacks', () => {
|
|
112
|
+
const contract = nativeSourceContract(
|
|
113
|
+
'/app/native/Counter.kt',
|
|
114
|
+
`package app.counter
|
|
115
|
+
@Composable
|
|
116
|
+
fun Counter(title: String, subtitle: String? = null, onTap: () -> Unit, onPick: ((Int) -> Unit)? = null, modifier: Modifier = Modifier) {
|
|
117
|
+
Text(title)
|
|
118
|
+
}
|
|
119
|
+
@Composable private fun Hidden() {}
|
|
120
|
+
fun helper(x: Int) = x`
|
|
121
|
+
)
|
|
122
|
+
expect(contract.views).toEqual([
|
|
123
|
+
{
|
|
124
|
+
name: 'Counter',
|
|
125
|
+
props: [
|
|
126
|
+
{ name: 'title', type: 'string', nativeType: 'String', optional: false, callback: null },
|
|
127
|
+
{ name: 'subtitle', type: 'string | null', nativeType: 'String?', optional: true, callback: null },
|
|
128
|
+
{ name: 'onTap', type: '() => void', nativeType: '()->Unit', optional: false, callback: [] },
|
|
129
|
+
{ name: 'onPick', type: '((value0: number) => void) | null', nativeType: '((Int)->Unit)?', optional: true, callback: [{ type: 'number', nativeType: 'Int' }] },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
])
|
|
133
|
+
const glue = renderKotlinSourceGlue('K_counter', contract).source
|
|
134
|
+
expect(glue).toContain('class OneNativeSourceViews_K_counter: OneNativeSourceViewDispatch')
|
|
135
|
+
expect(glue).toContain('onPick = if (props.optBoolean("onPick")) ({ a0 -> emit("onPick"')
|
|
136
|
+
expect(() =>
|
|
137
|
+
nativeSourceContract('/app/native/Bad.kt', '@Composable\nfun Bad(count: Int = 1) {}')
|
|
138
|
+
).toThrow(/Bad\.kt:2:.*may only default to null/)
|
|
139
|
+
expect(() =>
|
|
140
|
+
nativeSourceContract('/app/native/Bad.kt', '@Composable\nfun Bad(onTap: () -> Int) {}')
|
|
141
|
+
).toThrow(/must return Unit/)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('omits the events import and date/data helpers a contract never uses', () => {
|
|
145
|
+
// kotlin/wasm cannot resolve the jvm/android/event-host symbols, so a
|
|
146
|
+
// resources-shaped contract must not reference them.
|
|
147
|
+
const contract = nativeSourceContract(
|
|
148
|
+
'/app/native/Resources.kt',
|
|
149
|
+
`@Composable
|
|
150
|
+
fun Resources(title: String, onStep: ((Int) -> Unit)? = null) {
|
|
151
|
+
Text(title)
|
|
152
|
+
}`
|
|
153
|
+
)
|
|
154
|
+
expect(contract.modules).toEqual([])
|
|
155
|
+
expect(contract.structs).toEqual([])
|
|
156
|
+
expect(contract.enums).toEqual([])
|
|
157
|
+
const glue = renderKotlinSourceGlue('K_resources', contract).source
|
|
158
|
+
for (const symbol of [
|
|
159
|
+
'OneNativeSourceEvents',
|
|
160
|
+
'readDate',
|
|
161
|
+
'java.util.Date',
|
|
162
|
+
'java.text.SimpleDateFormat',
|
|
163
|
+
'readData',
|
|
164
|
+
'android.util.Base64',
|
|
165
|
+
'dateToString',
|
|
166
|
+
]) {
|
|
167
|
+
expect(glue).not.toContain(symbol)
|
|
168
|
+
}
|
|
169
|
+
expect(glue).toContain('import androidx.compose.runtime.Composable')
|
|
170
|
+
expect(glue).toContain('import dev.onejs.one.OneNativeSourceDispatch')
|
|
171
|
+
expect(glue).toContain('title = readString(')
|
|
172
|
+
const dated = nativeSourceContract(
|
|
173
|
+
'/app/native/Atlas.kt',
|
|
174
|
+
'object Atlas : OneModule {\n fun at(when: Date): Date = when\n fun blob(data: ByteArray): ByteArray = data\n fun onTick(callback: (Double) -> Unit) {}\n}'
|
|
175
|
+
)
|
|
176
|
+
const datedGlue = renderKotlinSourceGlue('K_atlas', dated).source
|
|
177
|
+
expect(datedGlue).toContain('import dev.onejs.one.source.OneNativeSourceEvents')
|
|
178
|
+
expect(datedGlue).toContain('private fun readDate(')
|
|
179
|
+
expect(datedGlue).toContain('private fun readData(')
|
|
180
|
+
expect(datedGlue).toContain('private fun dateToString(')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('imports public Swift views as typed components with callbacks', () => {
|
|
184
|
+
const contract = nativeSourceContract(
|
|
185
|
+
'/app/native/Level.swift',
|
|
186
|
+
`import SwiftUI
|
|
187
|
+
public struct Level: View {
|
|
188
|
+
let value: Double
|
|
189
|
+
var caption: String? = nil
|
|
190
|
+
let onChange: (Double) -> Void
|
|
191
|
+
let onReset: (() -> Void)?
|
|
192
|
+
@State private var dragging = false
|
|
193
|
+
@Environment(\\.colorScheme) var scheme
|
|
194
|
+
private let step = 0.1
|
|
195
|
+
static let shared = 1
|
|
196
|
+
var body: some View { Text("level") }
|
|
197
|
+
}
|
|
198
|
+
struct Helper: View {
|
|
199
|
+
let anything: Int
|
|
200
|
+
var body: some View { EmptyView() }
|
|
201
|
+
}`
|
|
202
|
+
)
|
|
203
|
+
expect(contract.views).toEqual([
|
|
204
|
+
{
|
|
205
|
+
name: 'Level',
|
|
206
|
+
props: [
|
|
207
|
+
{ name: 'value', type: 'number', nativeType: 'Double', optional: false, callback: null },
|
|
208
|
+
{ name: 'caption', type: 'string | null', nativeType: 'String?', optional: true, callback: null },
|
|
209
|
+
{ name: 'onChange', type: '(value0: number) => void', nativeType: '(Double)->Void', optional: false, callback: [{ type: 'number', nativeType: 'Double' }] },
|
|
210
|
+
{ name: 'onReset', type: '(() => void) | null', nativeType: '(()->Void)?', optional: true, callback: [] },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
])
|
|
214
|
+
const glue = renderSwiftSourceGlue('rn_host_app', [contract]).source
|
|
215
|
+
expect(glue).toContain('@objc(OneNativeSourceViews_rn_host_app) @MainActor public final class OneNativeSourceViews_rn_host_app: NSObject, OneNativeSourceViewDispatch')
|
|
216
|
+
expect(glue).toContain('value: try decode(props["value"] ?? missing("props.value"), as: Double.self),')
|
|
217
|
+
expect(glue).toContain('onReset: (props["onReset"] as? Bool) == true ? { () -> Void in emit("onReset", args([])) } : nil')
|
|
218
|
+
|
|
219
|
+
const temp = mkdtempSync(join(tmpdir(), 'one-native-source-views-'))
|
|
220
|
+
writeFileSync(join(temp, 'Level.d.swift.ts'), contract.declaration)
|
|
221
|
+
writeFileSync(join(temp, 'tsconfig.json'), JSON.stringify({
|
|
222
|
+
compilerOptions: { strict: true, noEmit: true, allowArbitraryExtensions: true, module: 'esnext', moduleResolution: 'bundler', types: [] },
|
|
223
|
+
include: ['use.ts'],
|
|
224
|
+
}))
|
|
225
|
+
writeFileSync(join(temp, 'react.d.ts'), "declare module 'react' { export interface ReactElement {} }")
|
|
226
|
+
writeFileSync(join(temp, 'use.ts'), `/// <reference path="./react.d.ts" />
|
|
227
|
+
import { Level } from './Level.swift'
|
|
228
|
+
Level({ value: 1, onChange: (next: number) => void next, onReset: null })
|
|
229
|
+
Level({ value: 'wrong', onChange: () => {} })
|
|
230
|
+
`)
|
|
231
|
+
// one compilation must accept the valid element and reject only the wrong prop.
|
|
232
|
+
const badProp = spawnSync(process.execPath, [tsc, '-p', temp], { encoding: 'utf8' })
|
|
233
|
+
expect(badProp.stdout.match(/error TS\d+:/g)).toEqual(['error TS2322:'])
|
|
234
|
+
|
|
235
|
+
for (const [source, message] of [
|
|
236
|
+
['public struct Bad: View {\n @Binding var value: Double\n var body: some View { EmptyView() }\n}', /Bad\.swift:2:.*@Binding value in view Bad needs a value/],
|
|
237
|
+
['public struct Bad: View {\n private let value: Double\n var body: some View { EmptyView() }\n}', /Bad\.swift:2:.*private value in view Bad needs a value/],
|
|
238
|
+
['public struct Bad: View {\n var count: Int = 1\n var body: some View { EmptyView() }\n}', /Bad\.swift:2:.*may only default to nil/],
|
|
239
|
+
['public struct Bad: View {\n let value: Int\n init(value: Int) { self.value = value }\n var body: some View { EmptyView() }\n}', /Bad\.swift:3:.*memberwise initializer/],
|
|
240
|
+
['public struct Bad<T>: View {\n var body: some View { EmptyView() }\n}', /Bad\.swift:1:.*generic view Bad/],
|
|
241
|
+
['public struct Bad: View {\n let onTap: () -> Int\n var body: some View { EmptyView() }\n}', /must return Void/],
|
|
242
|
+
] as const) {
|
|
243
|
+
expect(() => nativeSourceContract('/app/native/Bad.swift', source)).toThrow(message)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('keeps the objc dispatch class off wasi, whose sdk mirrors objectivec', () => {
|
|
248
|
+
const contract = nativeSourceContract(
|
|
249
|
+
'/app/native/Audio.swift',
|
|
250
|
+
`@MainActor final class AudioMath: PeachModule {
|
|
251
|
+
init() {}
|
|
252
|
+
func rms(_ samples: [Double]) -> Double { 1 }
|
|
253
|
+
}`
|
|
254
|
+
)
|
|
255
|
+
const glue = renderSwiftSourceGlue('rn_host_app', [contract]).source
|
|
256
|
+
expect(glue).toContain('#if canImport(ObjectiveC) && !os(WASI)')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('rejects unsupported signatures at their source location', () => {
|
|
260
|
+
expect(() =>
|
|
261
|
+
nativeSourceContract(
|
|
262
|
+
'/app/native/Audio.swift',
|
|
263
|
+
`final class AudioMath: PeachModule {
|
|
264
|
+
func unsupported(_ id: String, _ callback: () -> Void) -> Void {}
|
|
265
|
+
}`
|
|
266
|
+
)
|
|
267
|
+
).toThrow(/Audio\.swift:2:.*unsupported/)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('does not export a private method or hide the method after it', () => {
|
|
271
|
+
const contract = nativeSourceContract('/app/native/Audio.swift', `final class AudioMath: PeachModule {
|
|
272
|
+
private func key() -> String { "secret" }
|
|
273
|
+
func label() -> String { "public" }
|
|
274
|
+
}`)
|
|
275
|
+
expect(contract.modules[0].methods.map((method) => method.name)).toEqual(['label'])
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('reads modifiers from the method alone, not from a property declared above it', () => {
|
|
279
|
+
const swift = nativeSourceContract('/app/native/Audio.swift', `final class AudioMath: PeachModule {
|
|
280
|
+
private var hits = 0
|
|
281
|
+
static let shared = AudioMath()
|
|
282
|
+
@available(iOS 17, *) func rms(_ samples: [Double]) -> Double { 0 }
|
|
283
|
+
}`)
|
|
284
|
+
expect(swift.modules[0].methods.map((method) => method.name)).toEqual(['rms'])
|
|
285
|
+
const kotlin = nativeSourceContract('/app/native/Audio.kt', `package app.audio
|
|
286
|
+
import dev.onejs.one.source.OneModule
|
|
287
|
+
object AudioMath : OneModule {
|
|
288
|
+
private var hits = 0
|
|
289
|
+
fun rms(samples: List<Double>): Double = samples.sum()
|
|
290
|
+
private val scale = 2
|
|
291
|
+
suspend fun later(ms: Int): String = "slept"
|
|
292
|
+
}`)
|
|
293
|
+
expect(kotlin.modules[0].methods.map((method) => [method.name, method.async])).toEqual([['rms', false], ['later', true]])
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('generates adjacent declarations and removes only obsolete generated ones', () => {
|
|
297
|
+
const root = mkdtempSync(join(tmpdir(), 'one-native-source-project-'))
|
|
298
|
+
const source = join(root, 'Audio.swift')
|
|
299
|
+
const declaration = join(root, 'Audio.d.swift.ts')
|
|
300
|
+
writeFileSync(source, 'class Audio: PeachModule { func level() -> Int { 1 } }')
|
|
301
|
+
writeNativeSourceDeclarations(root)
|
|
302
|
+
expect(readFileSync(declaration, 'utf8')).toContain('level(): Promise<number>')
|
|
303
|
+
writeFileSync(source, 'class Audio {}')
|
|
304
|
+
writeNativeSourceDeclarations(root)
|
|
305
|
+
expect(existsSync(declaration)).toBe(false)
|
|
306
|
+
writeFileSync(declaration, 'declare const handWritten: true\n')
|
|
307
|
+
writeNativeSourceDeclarations(root)
|
|
308
|
+
expect(readFileSync(declaration, 'utf8')).toBe('declare const handWritten: true\n')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('uses the declared Swift language mode and rejects dependencies a pod cannot link', () => {
|
|
312
|
+
expect(swiftPodManifest('Package.swift', `let package = Package(
|
|
313
|
+
targets: [.executableTarget(name: "App", swiftSettings: [.swiftLanguageMode(.v6), .defaultIsolation(MainActor.self)])]
|
|
314
|
+
)`)).toEqual({ languageMode: 6, mainActorIsolation: true })
|
|
315
|
+
expect(swiftPodManifest('Package.swift', 'let package = Package(name: "App")').languageMode).toBe(5)
|
|
316
|
+
expect(() => swiftPodManifest('Package.swift', `let package = Package(dependencies: [.package(url: "https://example.com/third-party.git", from: "1.0.0")])`)).toThrow(/SwiftPM package dependencies/)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('native source events', () => {
|
|
321
|
+
const swiftEvents = `@MainActor final class Clock: PeachModule {
|
|
322
|
+
init() {}
|
|
323
|
+
|
|
324
|
+
func onTick(_ callback: @escaping (Double) -> Void) {}
|
|
325
|
+
func onReady(_ callback: () -> Void) {}
|
|
326
|
+
func now() -> Double { 1 }
|
|
327
|
+
}
|
|
328
|
+
`
|
|
329
|
+
const kotlinEvents = `object Clock : OneModule {
|
|
330
|
+
fun onTick(callback: (Double) -> Unit) {}
|
|
331
|
+
fun onReady(callback: () -> Unit) {}
|
|
332
|
+
fun now(): Double = 1.0
|
|
333
|
+
}
|
|
334
|
+
`
|
|
335
|
+
|
|
336
|
+
it('marks single-callback void methods as subscriptions on both platforms', () => {
|
|
337
|
+
const swift = nativeSourceContract('/app/native/Clock.swift', swiftEvents)
|
|
338
|
+
const kotlin = nativeSourceContract('/app/native/Clock.kt', kotlinEvents)
|
|
339
|
+
for (const contract of [swift, kotlin]) {
|
|
340
|
+
const [onTick, onReady, now] = contract.modules[0].methods
|
|
341
|
+
expect(onTick.parameters).toEqual([])
|
|
342
|
+
expect(onTick.result).toBe('void')
|
|
343
|
+
expect(onTick.subscription).toEqual({
|
|
344
|
+
payload: [{ type: 'number', nativeType: 'Double', wire: undefined }],
|
|
345
|
+
})
|
|
346
|
+
expect(onReady.subscription).toEqual({ payload: [] })
|
|
347
|
+
expect(now.subscription).toBeUndefined()
|
|
348
|
+
}
|
|
349
|
+
expect(swift.declaration).toContain('onTick(listener: (value0: number) => void): () => void')
|
|
350
|
+
expect(swift.declaration).toContain('onReady(listener: () => void): () => void')
|
|
351
|
+
expect(swift.declaration).toContain('now(): Promise<number>')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('renders subscribe cases that hand the native closure to the author', () => {
|
|
355
|
+
const swift = nativeSourceContract('/app/native/Clock.swift', swiftEvents)
|
|
356
|
+
const glue = renderSwiftSourceGlue('clock_pkg', [swift]).source
|
|
357
|
+
expect(glue).toContain('let channel = "OneNativeSource.clock_pkg.Clock.onTick"')
|
|
358
|
+
expect(glue).toContain(
|
|
359
|
+
'ClockInstance.onTick({ (a0: Double) in postEvent(channel, args([a0])) })'
|
|
360
|
+
)
|
|
361
|
+
expect(glue).toContain('ClockInstance.onReady({ postEvent(channel, args([])) })')
|
|
362
|
+
expect(glue).toContain('OneNativeSourceEmitter.send(channel, args)')
|
|
363
|
+
// under wasi there is no javascript runtime, so posted events queue for
|
|
364
|
+
// the peach host instead of trapping.
|
|
365
|
+
expect(glue).not.toContain('fatalError("native events need a device build")')
|
|
366
|
+
expect(glue).toContain('pendingEvents.append(Event(channel: channel, args: args))')
|
|
367
|
+
expect(glue).toContain('static func pollEvents()')
|
|
368
|
+
expect(glue).toContain('peach_poll_events')
|
|
369
|
+
// the clock package has no views, so the subscribe cases must still see
|
|
370
|
+
// the args helper: it cannot live only in the view glue.
|
|
371
|
+
expect(glue).toContain('private static func args(_ values: [any Encodable]) -> String')
|
|
372
|
+
|
|
373
|
+
const kotlin = nativeSourceContract('/app/native/Clock.kt', kotlinEvents)
|
|
374
|
+
const kotlinGlue = renderKotlinSourceGlue('K_clock', kotlin).source
|
|
375
|
+
expect(kotlinGlue).toContain('import dev.onejs.one.source.OneNativeSourceEvents')
|
|
376
|
+
expect(kotlinGlue).toContain(
|
|
377
|
+
'Clock.onTick({ a0 -> OneNativeSourceEvents.send("OneNativeSource.K_clock.Clock.onTick", JSONArray().put(JSONObject.wrap(a0)).toString()) })'
|
|
378
|
+
)
|
|
379
|
+
expect(kotlinGlue).toContain(
|
|
380
|
+
'Clock.onReady({ OneNativeSourceEvents.send("OneNativeSource.K_clock.Clock.onReady", JSONArray().toString()) })'
|
|
381
|
+
)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('rejects callbacks that cannot subscribe', () => {
|
|
385
|
+
// a nullable callback is not a subscription; the call path rejects it
|
|
386
|
+
expect(() =>
|
|
387
|
+
nativeSourceContract(
|
|
388
|
+
'/app/native/Clock.swift',
|
|
389
|
+
'final class Clock: PeachModule {\n func onTick(_ callback: ((Double) -> Void)?) {}\n}'
|
|
390
|
+
)
|
|
391
|
+
).toThrow(/unsupported native export type/)
|
|
392
|
+
// a callback beside other parameters is not a subscription either
|
|
393
|
+
expect(() =>
|
|
394
|
+
nativeSourceContract(
|
|
395
|
+
'/app/native/Clock.kt',
|
|
396
|
+
'object Clock : OneModule {\n fun watch(id: String, callback: (Double) -> Unit) {}\n}'
|
|
397
|
+
)
|
|
398
|
+
).toThrow(/unsupported native export type/)
|
|
399
|
+
// a subscription must return nothing to unsubscribe into
|
|
400
|
+
expect(() =>
|
|
401
|
+
nativeSourceContract(
|
|
402
|
+
'/app/native/Clock.swift',
|
|
403
|
+
'final class Clock: PeachModule {\n func onTick(_ callback: (Double) -> Void) -> Bool { true }\n}'
|
|
404
|
+
)
|
|
405
|
+
).toThrow(/unsupported native export type/)
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
describe('native source structs, enums, dates and data', () => {
|
|
410
|
+
const swiftSource = `struct Point: Codable {
|
|
411
|
+
var x: Double
|
|
412
|
+
var y: Double
|
|
413
|
+
var label: String? = nil
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
enum Direction: String, Codable {
|
|
417
|
+
case north
|
|
418
|
+
case south
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@MainActor final class Atlas: PeachModule {
|
|
422
|
+
init() {}
|
|
423
|
+
|
|
424
|
+
func pin(_ point: Point) -> Point { point }
|
|
425
|
+
func heading(_ direction: Direction) -> Direction { direction }
|
|
426
|
+
func at(_ when: Date) -> Date { when }
|
|
427
|
+
func blob(_ data: Data) -> Data { data }
|
|
428
|
+
func pins(_ points: [Point]) -> [String: Date] { [:] }
|
|
429
|
+
}
|
|
430
|
+
`
|
|
431
|
+
const kotlinSource = `package app.atlas
|
|
432
|
+
|
|
433
|
+
data class Point(val x: Double, val y: Double, val label: String? = null)
|
|
434
|
+
|
|
435
|
+
enum class Direction {
|
|
436
|
+
NORTH,
|
|
437
|
+
SOUTH
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
object Atlas : OneModule {
|
|
441
|
+
fun pin(point: Point): Point = point
|
|
442
|
+
fun heading(direction: Direction): Direction = direction
|
|
443
|
+
fun at(when: Date): Date = when
|
|
444
|
+
fun blob(data: ByteArray): ByteArray = data
|
|
445
|
+
fun pins(points: List<Point>): Map<String, Date> = mapOf()
|
|
446
|
+
}
|
|
447
|
+
`
|
|
448
|
+
|
|
449
|
+
it('types struct, enum, date and data payloads on both platforms', () => {
|
|
450
|
+
const swift = nativeSourceContract('/app/native/Atlas.swift', swiftSource)
|
|
451
|
+
const kotlinContract = nativeSourceContract('/app/native/Atlas.kt', kotlinSource)
|
|
452
|
+
for (const contract of [swift, kotlinContract]) {
|
|
453
|
+
const [pin, heading, at, blob, pins] = contract.modules[0].methods
|
|
454
|
+
expect(pin.parameters[0].type).toBe('Point')
|
|
455
|
+
expect(pin.parameters[0].wire).toEqual({ kind: 'struct', name: 'Point' })
|
|
456
|
+
expect(pin.result).toBe('Point')
|
|
457
|
+
expect(heading.parameters[0].type).toBe('Direction')
|
|
458
|
+
expect(at.parameters[0].type).toBe('Date')
|
|
459
|
+
expect(at.parameters[0].wire).toEqual({ kind: 'date' })
|
|
460
|
+
expect(blob.parameters[0].type).toBe('Uint8Array')
|
|
461
|
+
expect(blob.parameters[0].wire).toEqual({ kind: 'data' })
|
|
462
|
+
expect(pins.parameters[0].wire).toEqual({ kind: 'array', element: { kind: 'struct', name: 'Point' } })
|
|
463
|
+
expect(pins.resultWire).toEqual({ kind: 'map', value: { kind: 'date' } })
|
|
464
|
+
}
|
|
465
|
+
expect(swift.structs).toEqual([
|
|
466
|
+
{
|
|
467
|
+
name: 'Point',
|
|
468
|
+
fields: [
|
|
469
|
+
{ name: 'x', type: 'number', nativeType: 'Double', optional: false, wire: undefined, fallback: undefined },
|
|
470
|
+
{ name: 'y', type: 'number', nativeType: 'Double', optional: false, wire: undefined, fallback: undefined },
|
|
471
|
+
{ name: 'label', type: 'string | null', nativeType: 'String?', optional: true, wire: undefined, fallback: undefined },
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
])
|
|
475
|
+
expect(swift.enums).toEqual([
|
|
476
|
+
{ name: 'Direction', backing: 'string', cases: [{ name: 'north', value: 'north' }, { name: 'south', value: 'south' }] },
|
|
477
|
+
])
|
|
478
|
+
expect(kotlinContract.structs[0].fields.map((field) => [field.name, field.type, field.optional])).toEqual([
|
|
479
|
+
['x', 'number', false],
|
|
480
|
+
['y', 'number', false],
|
|
481
|
+
['label', 'string | null', true],
|
|
482
|
+
])
|
|
483
|
+
expect(kotlinContract.enums).toEqual([
|
|
484
|
+
{ name: 'Direction', backing: 'string', cases: [{ name: 'NORTH', value: 'NORTH' }, { name: 'SOUTH', value: 'SOUTH' }] },
|
|
485
|
+
])
|
|
486
|
+
expect(swift.declaration).toContain('export interface Point { x: number; y: number; label?: string | null; }')
|
|
487
|
+
expect(swift.declaration).toContain('export type Direction = "north" | "south"')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('reads qualified stdlib spellings as the same types', () => {
|
|
491
|
+
const swift = nativeSourceContract(
|
|
492
|
+
'/app/native/Atlas.swift',
|
|
493
|
+
'final class Atlas: PeachModule {\n func at(_ when: Foundation.Date) -> Foundation.Date { when }\n func blob(_ data: Foundation.Data) -> Foundation.Data { data }\n}'
|
|
494
|
+
)
|
|
495
|
+
expect(swift.modules[0].methods[0].parameters[0].type).toBe('Date')
|
|
496
|
+
expect(swift.modules[0].methods[0].parameters[0].wire).toEqual({ kind: 'date' })
|
|
497
|
+
expect(swift.modules[0].methods[1].parameters[0].type).toBe('Uint8Array')
|
|
498
|
+
const kotlin = nativeSourceContract(
|
|
499
|
+
'/app/native/Atlas.kt',
|
|
500
|
+
'object Atlas : OneModule {\n fun at(moment: java.util.Date): java.util.Date = moment\n}'
|
|
501
|
+
)
|
|
502
|
+
expect(kotlin.modules[0].methods[0].parameters[0].type).toBe('Date')
|
|
503
|
+
expect(renderKotlinSourceGlue('K_atlas', kotlin).source).toContain('readDate(args.get(0), "args[0]")')
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('rejects unusable named types at their use site', () => {
|
|
507
|
+
const cases = [
|
|
508
|
+
['struct Plain {\n var x: Double\n}\nfinal class Atlas: PeachModule {\n func pin(_ point: Plain) -> Double { 1 }\n}', /must conform to Codable/],
|
|
509
|
+
['struct Box<T: Codable> {\n var boxed: T\n}\nfinal class Atlas: PeachModule {\n func pin(_ point: Box<Double>) -> Double { 1 }\n}', /unsupported native export type/],
|
|
510
|
+
['enum Shape {\n case circle(Double)\n}\nfinal class Atlas: PeachModule {\n func draw(_ shape: Shape) -> Double { 1 }\n}', /needs a String or Int backing/],
|
|
511
|
+
['enum Direction: String {\n case north\n}\nfinal class Atlas: PeachModule {\n func heading(_ direction: Direction) -> Double { 1 }\n}', /must conform to Codable/],
|
|
512
|
+
['enum Level: Int, Codable {\n case low\n case high\n}\nfinal class Atlas: PeachModule {\n func rank(_ level: Level) -> Double { 1 }\n}', null],
|
|
513
|
+
['final class Atlas: PeachModule {\n func pin(_ point: Point) -> Double { 1 }\n}', /unsupported native export type Point/],
|
|
514
|
+
['private struct Point: Codable {\n var x: Double\n}\nfinal class Atlas: PeachModule {\n func pin(_ point: Point) -> Double { 1 }\n}', /private to its file/],
|
|
515
|
+
] as const
|
|
516
|
+
for (const [source, message] of cases) {
|
|
517
|
+
if (message === null) {
|
|
518
|
+
const contract = nativeSourceContract('/app/native/Atlas.swift', source)
|
|
519
|
+
expect(contract.enums).toEqual([
|
|
520
|
+
{ name: 'Level', backing: 'number', cases: [{ name: 'low', value: 0 }, { name: 'high', value: 1 }] },
|
|
521
|
+
])
|
|
522
|
+
expect(contract.declaration).toContain('export type Level = 0 | 1')
|
|
523
|
+
} else {
|
|
524
|
+
expect(() => nativeSourceContract('/app/native/Atlas.swift', source)).toThrow(message)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
expect(() =>
|
|
528
|
+
nativeSourceContract('/app/native/Atlas.kt', 'private data class Point(val x: Double)\nobject Atlas : OneModule {\n fun pin(point: Point): Double = 1.0\n}')
|
|
529
|
+
).toThrow(/must be public/)
|
|
530
|
+
expect(() =>
|
|
531
|
+
nativeSourceContract('/app/native/Atlas.kt', 'data class Point(val tap: (Double) -> Unit)\nobject Atlas : OneModule {\n fun pin(point: Point): Double = 1.0\n}')
|
|
532
|
+
).toThrow(/unsupported native export type/)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('leaves view-only files untouched when no method uses a named type', () => {
|
|
536
|
+
const contract = nativeSourceContract(
|
|
537
|
+
'/app/native/Level.swift',
|
|
538
|
+
`import SwiftUI
|
|
539
|
+
public struct Level: View {
|
|
540
|
+
let value: Double
|
|
541
|
+
let onChange: (Double) -> Void
|
|
542
|
+
public var body: some View { Text("") }
|
|
543
|
+
}
|
|
544
|
+
@MainActor final class AudioMath: PeachModule {
|
|
545
|
+
init() {}
|
|
546
|
+
func rms(_ samples: [Double]) -> Double { 1 }
|
|
547
|
+
}`
|
|
548
|
+
)
|
|
549
|
+
expect(contract.structs).toEqual([])
|
|
550
|
+
expect(contract.enums).toEqual([])
|
|
551
|
+
expect(contract.modules[0].methods[0].parameters[0].wire).toBeUndefined()
|
|
552
|
+
expect(contract.modules[0].methods[0].resultWire).toBeUndefined()
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('renders iso dates and struct helpers into both glues', () => {
|
|
556
|
+
const swift = nativeSourceContract('/app/native/Atlas.swift', swiftSource)
|
|
557
|
+
const glue = renderSwiftSourceGlue('atlas_pkg', [swift]).source
|
|
558
|
+
expect(glue).toContain('decoder.dateDecodingStrategy = .custom { try decodeDate($0) }')
|
|
559
|
+
expect(glue).toContain('encoder.dateEncodingStrategy = .iso8601')
|
|
560
|
+
expect(glue).toContain('decode(values[0], as: Point.self)')
|
|
561
|
+
expect(glue).toContain('return try encode(result)')
|
|
562
|
+
|
|
563
|
+
const kotlin = nativeSourceContract('/app/native/Atlas.kt', kotlinSource)
|
|
564
|
+
const kotlinGlue = renderKotlinSourceGlue('K_atlas', kotlin).source
|
|
565
|
+
expect(kotlinGlue).toContain('private fun readPoint(value: Any, path: String): app.atlas.Point {')
|
|
566
|
+
expect(kotlinGlue).toContain('private fun encodePoint(value: app.atlas.Point): JSONObject {')
|
|
567
|
+
expect(kotlinGlue).toContain('private fun readDate(value: Any, path: String): java.util.Date {')
|
|
568
|
+
expect(kotlinGlue).toContain('readPoint(args.get(0), "args[0]")')
|
|
569
|
+
expect(kotlinGlue).toContain('JSONArray().put(dateToString(result))')
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('typechecks date, data, struct and enum calls', () => {
|
|
573
|
+
const swift = nativeSourceContract('/app/native/Atlas.swift', swiftSource)
|
|
574
|
+
const temp = mkdtempSync(join(tmpdir(), 'one-native-source-payloads-'))
|
|
575
|
+
writeFileSync(join(temp, 'Atlas.d.swift.ts'), swift.declaration)
|
|
576
|
+
writeFileSync(join(temp, 'tsconfig.json'), JSON.stringify({
|
|
577
|
+
compilerOptions: {
|
|
578
|
+
strict: true,
|
|
579
|
+
noEmit: true,
|
|
580
|
+
allowArbitraryExtensions: true,
|
|
581
|
+
module: 'esnext',
|
|
582
|
+
moduleResolution: 'bundler',
|
|
583
|
+
types: [],
|
|
584
|
+
},
|
|
585
|
+
include: ['call.ts'],
|
|
586
|
+
}))
|
|
587
|
+
writeFileSync(join(temp, 'call.ts'), `import { Atlas, type Point } from './Atlas.swift'
|
|
588
|
+
const point: Point = { x: 1, y: 2 }
|
|
589
|
+
const a: Promise<Date> = Atlas.at(new Date())
|
|
590
|
+
const b: Promise<Uint8Array> = Atlas.blob(new Uint8Array([1]))
|
|
591
|
+
const c: Promise<Point> = Atlas.pin(point)
|
|
592
|
+
void [a, b, c]
|
|
593
|
+
Atlas.at('yesterday')
|
|
594
|
+
`)
|
|
595
|
+
// one compilation must accept the valid calls and reject only the string date.
|
|
596
|
+
const badCall = spawnSync(process.execPath, [tsc, '-p', temp], { encoding: 'utf8' })
|
|
597
|
+
expect(badCall.status).toBe(1)
|
|
598
|
+
expect(badCall.stdout.match(/error TS\d+:/g)).toEqual(['error TS2345:'])
|
|
599
|
+
})
|
|
600
|
+
})
|