vxrn 2.0.0-beta.99.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.
Files changed (210) hide show
  1. package/dist/backgroundComputation.mjs +145 -0
  2. package/dist/backgroundComputation.mjs.map +1 -0
  3. package/dist/backgroundComputation.native.js +163 -0
  4. package/dist/backgroundComputation.native.js.map +1 -0
  5. package/dist/backgroundComputationNative.mjs +16 -0
  6. package/dist/backgroundComputationNative.mjs.map +1 -0
  7. package/dist/backgroundComputationNative.native.js +18 -0
  8. package/dist/backgroundComputationNative.native.js.map +1 -0
  9. package/dist/config/getBaseViteConfigOnly.mjs +6 -10
  10. package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
  11. package/dist/config/getBaseViteConfigOnly.native.js +6 -10
  12. package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
  13. package/dist/config/getOptimizeDeps.mjs +2 -1
  14. package/dist/config/getOptimizeDeps.mjs.map +1 -1
  15. package/dist/config/getOptimizeDeps.native.js +2 -1
  16. package/dist/config/getOptimizeDeps.native.js.map +1 -1
  17. package/dist/config/getViteServerConfig.mjs +13 -0
  18. package/dist/config/getViteServerConfig.mjs.map +1 -1
  19. package/dist/config/getViteServerConfig.native.js +15 -1
  20. package/dist/config/getViteServerConfig.native.js.map +1 -1
  21. package/dist/constants/defaults.mjs +4 -0
  22. package/dist/constants/defaults.mjs.map +1 -1
  23. package/dist/constants/defaults.native.js +4 -0
  24. package/dist/constants/defaults.native.js.map +1 -1
  25. package/dist/exports/build.native.js +1 -1
  26. package/dist/exports/build.native.js.map +1 -1
  27. package/dist/exports/clean.native.js +1 -1
  28. package/dist/exports/clean.native.js.map +1 -1
  29. package/dist/exports/createServer.native.js +2 -2
  30. package/dist/exports/createServer.native.js.map +1 -1
  31. package/dist/exports/dev.mjs +3 -0
  32. package/dist/exports/dev.mjs.map +1 -1
  33. package/dist/exports/dev.native.js +6 -3
  34. package/dist/exports/dev.native.js.map +1 -1
  35. package/dist/exports/loadEnv.native.js +1 -1
  36. package/dist/exports/loadEnv.native.js.map +1 -1
  37. package/dist/exports/patch.native.js +1 -1
  38. package/dist/exports/patch.native.js.map +1 -1
  39. package/dist/exports/prebuild.mjs +0 -22
  40. package/dist/exports/prebuild.mjs.map +1 -1
  41. package/dist/exports/prebuild.native.js +1 -23
  42. package/dist/exports/prebuild.native.js.map +1 -1
  43. package/dist/exports/prebuildWithoutExpo.mjs +1116 -178
  44. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  45. package/dist/exports/prebuildWithoutExpo.native.js +1592 -243
  46. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  47. package/dist/exports/runAndroid.native.js +1 -1
  48. package/dist/exports/runAndroid.native.js.map +1 -1
  49. package/dist/exports/runIos.native.js +1 -1
  50. package/dist/exports/runIos.native.js.map +1 -1
  51. package/dist/exports/serve.native.js +1 -1
  52. package/dist/exports/serve.native.js.map +1 -1
  53. package/dist/exports/serveStaticAssets.native.js +1 -1
  54. package/dist/exports/serveStaticAssets.native.js.map +1 -1
  55. package/dist/index.js +4 -1
  56. package/dist/index.mjs +4 -1
  57. package/dist/index.native.js +4 -1
  58. package/dist/native-source-contract.mjs +3 -0
  59. package/dist/native-source-contract.native.js +3 -0
  60. package/dist/patches/builtInDepPatches.mjs +0 -13
  61. package/dist/patches/builtInDepPatches.mjs.map +1 -1
  62. package/dist/patches/builtInDepPatches.native.js +0 -13
  63. package/dist/patches/builtInDepPatches.native.js.map +1 -1
  64. package/dist/plugins/autoDepOptimizePlugin.mjs +3 -2
  65. package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
  66. package/dist/plugins/autoDepOptimizePlugin.native.js +6 -3
  67. package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
  68. package/dist/plugins/backgroundComputationPlugin.mjs +27 -0
  69. package/dist/plugins/backgroundComputationPlugin.mjs.map +1 -0
  70. package/dist/plugins/backgroundComputationPlugin.native.js +32 -0
  71. package/dist/plugins/backgroundComputationPlugin.native.js.map +1 -0
  72. package/dist/plugins/reactNativeCommonJsPlugin.native.js +3 -3
  73. package/dist/plugins/reactNativeCommonJsPlugin.native.js.map +1 -1
  74. package/dist/plugins/reactNativeDevAssetPlugin.mjs +2 -1
  75. package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
  76. package/dist/plugins/reactNativeDevAssetPlugin.native.js +3 -2
  77. package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
  78. package/dist/plugins/reactNativeDevServer.mjs +87 -24
  79. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  80. package/dist/plugins/reactNativeDevServer.native.js +162 -45
  81. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  82. package/dist/plugins/workletImportsPlugin.mjs +9 -30
  83. package/dist/plugins/workletImportsPlugin.mjs.map +1 -1
  84. package/dist/plugins/workletImportsPlugin.native.js +10 -35
  85. package/dist/plugins/workletImportsPlugin.native.js.map +1 -1
  86. package/dist/rn-commands/bundle/buildBundle.mjs +3 -3
  87. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  88. package/dist/rn-commands/bundle/buildBundle.native.js +3 -3
  89. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  90. package/dist/runtime/hmr-client.native.js +3 -7
  91. package/dist/runtime/hmr-client.native.js.map +1 -1
  92. package/dist/runtime/hmr-runtime.native.js +32 -40
  93. package/dist/runtime/hmr-runtime.native.js.map +1 -1
  94. package/dist/serve/node.native.js +2 -2
  95. package/dist/serve/node.native.js.map +1 -1
  96. package/dist/swift-package-id.mjs +3 -0
  97. package/dist/swift-package-id.native.js +3 -0
  98. package/dist/user-interface/index.native.js +6 -6
  99. package/dist/user-interface/index.native.js.map +1 -1
  100. package/dist/utils/compileNativeDevBytecode.mjs +93 -0
  101. package/dist/utils/compileNativeDevBytecode.mjs.map +1 -0
  102. package/dist/utils/compileNativeDevBytecode.native.js +98 -0
  103. package/dist/utils/compileNativeDevBytecode.native.js.map +1 -0
  104. package/dist/utils/createNativeDevEngine.mjs +227 -20
  105. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  106. package/dist/utils/createNativeDevEngine.native.js +249 -37
  107. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  108. package/dist/utils/execSync.native.js +1 -1
  109. package/dist/utils/execSync.native.js.map +1 -1
  110. package/dist/utils/getServerEntry.native.js +2 -2
  111. package/dist/utils/getServerEntry.native.js.map +1 -1
  112. package/dist/utils/nativeRun.mjs +32 -16
  113. package/dist/utils/nativeRun.mjs.map +1 -1
  114. package/dist/utils/nativeRun.native.js +43 -18
  115. package/dist/utils/nativeRun.native.js.map +1 -1
  116. package/dist/utils/nativeSourceContract.mjs +1823 -0
  117. package/dist/utils/nativeSourceContract.mjs.map +1 -0
  118. package/dist/utils/nativeSourceContract.native.js +2425 -0
  119. package/dist/utils/nativeSourceContract.native.js.map +1 -0
  120. package/dist/utils/patches.native.js +15 -24
  121. package/dist/utils/patches.native.js.map +1 -1
  122. package/dist/utils/printServerUrls.native.js +1 -1
  123. package/dist/utils/printServerUrls.native.js.map +1 -1
  124. package/dist/utils/scanDepsToOptimize.mjs +7 -1
  125. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  126. package/dist/utils/scanDepsToOptimize.native.js +27 -2
  127. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  128. package/dist/utils/swiftPackageId.mjs +31 -2
  129. package/dist/utils/swiftPackageId.mjs.map +1 -1
  130. package/dist/utils/swiftPackageId.native.js +50 -2
  131. package/dist/utils/swiftPackageId.native.js.map +1 -1
  132. package/dist/vxrn-vite-plugin.native.js +1 -1
  133. package/dist/vxrn-vite-plugin.native.js.map +1 -1
  134. package/expo-plugin.cjs +792 -2
  135. package/native-project-patches.cjs +201 -10
  136. package/package.json +34 -15
  137. package/src/backgroundComputation.ts +220 -0
  138. package/src/backgroundComputationNative.ts +23 -0
  139. package/src/config/getBaseViteConfigOnly.ts +8 -9
  140. package/src/config/getOptimizeDeps.ts +3 -1
  141. package/src/config/getViteServerConfig.ts +16 -0
  142. package/src/config/glbAssets.test.ts +228 -0
  143. package/src/constants/defaults.ts +6 -0
  144. package/src/exports/dev.ts +5 -1
  145. package/src/exports/expoPlugin.test.mjs +337 -15
  146. package/src/exports/prebuild.ts +0 -31
  147. package/src/exports/prebuildWithoutExpo.test.ts +1588 -48
  148. package/src/exports/prebuildWithoutExpo.ts +1573 -213
  149. package/src/index.ts +8 -0
  150. package/src/native-source-contract.ts +16 -0
  151. package/src/patches/builtInDepPatches.ts +0 -21
  152. package/src/plugins/autoDepOptimizePlugin.ts +7 -2
  153. package/src/plugins/backgroundComputationPlugin.test.ts +77 -0
  154. package/src/plugins/backgroundComputationPlugin.ts +29 -0
  155. package/src/plugins/reactNativeDevAssetPlugin.ts +5 -1
  156. package/src/plugins/reactNativeDevServer.test.ts +74 -0
  157. package/src/plugins/reactNativeDevServer.ts +145 -28
  158. package/src/plugins/rnCodegenPlugin.test.ts +30 -0
  159. package/src/plugins/workletImportsPlugin.ts +19 -37
  160. package/src/rn-commands/bundle/buildBundle.ts +5 -3
  161. package/src/swift-package-id.ts +1 -0
  162. package/src/utils/compileNativeDevBytecode.test.ts +168 -0
  163. package/src/utils/compileNativeDevBytecode.ts +117 -0
  164. package/src/utils/createNativeDevEngine.test.ts +643 -18
  165. package/src/utils/createNativeDevEngine.ts +278 -48
  166. package/src/utils/nativeRun.test.ts +125 -23
  167. package/src/utils/nativeRun.ts +51 -23
  168. package/src/utils/nativeSourceArtifacts.test.ts +39 -0
  169. package/src/utils/nativeSourceContract.test.ts +600 -0
  170. package/src/utils/nativeSourceContract.ts +1897 -0
  171. package/src/utils/scanDepsToOptimize.test.ts +80 -0
  172. package/src/utils/scanDepsToOptimize.ts +18 -7
  173. package/src/utils/swiftPackageId.ts +25 -1
  174. package/types/backgroundComputation.d.ts +28 -0
  175. package/types/backgroundComputation.d.ts.map +1 -0
  176. package/types/backgroundComputationNative.d.ts +2 -0
  177. package/types/backgroundComputationNative.d.ts.map +1 -0
  178. package/types/config/getBaseViteConfigOnly.d.ts.map +1 -1
  179. package/types/config/getOptimizeDeps.d.ts.map +1 -1
  180. package/types/config/getViteServerConfig.d.ts.map +1 -1
  181. package/types/constants/defaults.d.ts.map +1 -1
  182. package/types/exports/dev.d.ts.map +1 -1
  183. package/types/exports/prebuild.d.ts.map +1 -1
  184. package/types/exports/prebuildWithoutExpo.d.ts +6 -0
  185. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  186. package/types/index.d.ts +2 -0
  187. package/types/index.d.ts.map +1 -1
  188. package/types/native-source-contract.d.ts +2 -0
  189. package/types/native-source-contract.d.ts.map +1 -0
  190. package/types/patches/builtInDepPatches.d.ts.map +1 -1
  191. package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
  192. package/types/plugins/backgroundComputationPlugin.d.ts +3 -0
  193. package/types/plugins/backgroundComputationPlugin.d.ts.map +1 -0
  194. package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
  195. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  196. package/types/plugins/workletImportsPlugin.d.ts.map +1 -1
  197. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  198. package/types/swift-package-id.d.ts +2 -0
  199. package/types/swift-package-id.d.ts.map +1 -0
  200. package/types/utils/compileNativeDevBytecode.d.ts +3 -0
  201. package/types/utils/compileNativeDevBytecode.d.ts.map +1 -0
  202. package/types/utils/createNativeDevEngine.d.ts +7 -22
  203. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  204. package/types/utils/nativeRun.d.ts.map +1 -1
  205. package/types/utils/nativeSourceContract.d.ts +121 -0
  206. package/types/utils/nativeSourceContract.d.ts.map +1 -0
  207. package/types/utils/scanDepsToOptimize.d.ts +1 -0
  208. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
  209. package/types/utils/swiftPackageId.d.ts +1 -0
  210. 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
+ })