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.
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 +1133 -182
  44. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  45. package/dist/exports/prebuildWithoutExpo.native.js +1606 -244
  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 +1623 -46
  148. package/src/exports/prebuildWithoutExpo.ts +1606 -216
  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
@@ -4,7 +4,8 @@ import { dirname, isAbsolute, join } from 'node:path'
4
4
  import { statSync } from 'node:fs'
5
5
  import { rolldown, type Plugin } from 'rolldown'
6
6
  import { parseSync } from 'oxc-parser'
7
- import { getClosureVariables, JS_GLOBALS, transformHermesLoops } from '@vxrn/compiler'
7
+ import { createBackgroundWorkletModule } from '../backgroundComputation'
8
+ import { prepareBackgroundWorkletModule } from '../backgroundComputationNative'
8
9
  import { hermesCompatSWCPlugin, hermesLoopsPlugin } from '../utils/createNativeDevEngine'
9
10
 
10
11
  /**
@@ -40,7 +41,12 @@ export function workletImportsPlugin(imports: Record<string, readonly string[]>)
40
41
  const instance: Plugin = {
41
42
  name: 'vxrn:worklet-imports:bundle',
42
43
  async resolveId(source, importer, options) {
43
- const names = selections.get(source)
44
+ const marker = '?one-background='
45
+ const selected = source.indexOf(marker)
46
+ const names =
47
+ selected < 0
48
+ ? selections.get(source)
49
+ : [decodeURIComponent(source.slice(selected + marker.length))]
44
50
  if (!names) return
45
51
  if (!names.length)
46
52
  throw new Error(`[worklet imports] no exports selected for ${source}`)
@@ -48,7 +54,11 @@ export function workletImportsPlugin(imports: Record<string, readonly string[]>)
48
54
  throw new Error(`[worklet imports] ${source} requires a static ESM import`)
49
55
  }
50
56
  rejectRuntimeDependency(source)
51
- const resolved = await this.resolve(source, importer, { skipSelf: true })
57
+ const resolved = await this.resolve(
58
+ selected < 0 ? source : source.slice(0, selected),
59
+ importer,
60
+ { skipSelf: true }
61
+ )
52
62
  if (
53
63
  !resolved ||
54
64
  resolved.external ||
@@ -173,7 +183,7 @@ export function workletImportsPlugin(imports: Record<string, readonly string[]>)
173
183
  )
174
184
  }
175
185
  // also cover closure-bearing loops in helpers emitted by the bundler.
176
- code = transformHermesLoops(chunk.code, id)?.code ?? chunk.code
186
+ code = chunk.code
177
187
  for (const file of await bundle.watchFiles) {
178
188
  if (isAbsolute(file)) outerContext.addWatchFile(file)
179
189
  }
@@ -181,40 +191,12 @@ export function workletImportsPlugin(imports: Record<string, readonly string[]>)
181
191
  await bundle.close()
182
192
  }
183
193
 
184
- const loader = `__oneLoadWorkletModule_${selected.key}`
185
- const loaderCode = `function ${loader}() {
186
- 'worklet';
187
- const cache = globalThis.__oneWorkletImportCache || (globalThis.__oneWorkletImportCache = new WeakMap());
188
- let namespace = cache.get(${loader});
189
- if (namespace === undefined) {
190
- ${code}
191
- namespace = __oneWorkletBundle;
192
- cache.set(${loader}, namespace);
193
- }
194
- return namespace;
195
- }`
196
- const parsed = parseSync(id, loaderCode, { lang: 'js' })
197
- if (parsed.errors.length) throw new Error(parsed.errors[0].message)
198
- const captures = getClosureVariables(
199
- parsed.program.body[0],
200
- new Set([...JS_GLOBALS, 'arguments'])
194
+ const moduleSource = createBackgroundWorkletModule(
195
+ code,
196
+ selected.names,
197
+ selected.key
201
198
  )
202
- if (captures.length) {
203
- throw new Error(
204
- `[worklet imports] ${selected.entry} requires unsupported runtime globals: ${captures.join(', ')}`
205
- )
206
- }
207
- return `${loaderCode}\n${selected.names
208
- .map(
209
- (name, index) => `
210
- function __oneExport${index}(...args) {
211
- 'worklet';
212
- return (0, ${loader}().__export${index})(...args);
213
- }
214
- export { __oneExport${index} as ${JSON.stringify(name)} };
215
- `
216
- )
217
- .join('\n')}`
199
+ return prepareBackgroundWorkletModule(moduleSource, selected.entry)
218
200
  },
219
201
  }
220
202
  return {
@@ -93,17 +93,19 @@ export async function buildBundle(
93
93
 
94
94
  // write sourcemap if available and requested
95
95
  if (result.map && args.sourcemapOutput) {
96
- FSExtra.writeFileSync(args.sourcemapOutput, result.map, { encoding: 'utf8' })
96
+ FSExtra.outputFileSync(args.sourcemapOutput, result.map, { encoding: 'utf8' })
97
97
  }
98
98
 
99
99
  console.info(`Writing bundle to ${bundleOutput}...`)
100
- FSExtra.writeFileSync(bundleOutput, builtBundle, { encoding: bundleEncoding })
100
+ FSExtra.outputFileSync(bundleOutput, builtBundle, { encoding: bundleEncoding })
101
101
  console.info('Done.')
102
102
 
103
103
  // Prevent the process not getting exited for some unknown reason.
104
104
  // If the process is not exited, it might hang the native build process.
105
+ // unref so a clean build exits as soon as its work is done; the timer only
106
+ // fires when something else still holds the event loop.
105
107
  setTimeout(() => {
106
108
  console.info('Exiting process to prevent hanging.')
107
109
  process.exit()
108
- }, 30000)
110
+ }, 30000).unref()
109
111
  }
@@ -0,0 +1 @@
1
+ export { swiftPackageDirOf, swiftPackageDirectories, swiftPackageId } from './utils/swiftPackageId'
@@ -0,0 +1,168 @@
1
+ import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import { createRequire } from 'node:module'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { afterEach, beforeEach, expect, it } from 'vitest'
6
+ import { nativeDevBytecodeCompiler } from './compileNativeDevBytecode'
7
+
8
+ const savedHermescPath = process.env.HERMESC_PATH
9
+ beforeEach(() => {
10
+ delete process.env.HERMESC_PATH
11
+ })
12
+ afterEach(() => {
13
+ if (savedHermescPath === undefined) delete process.env.HERMESC_PATH
14
+ else process.env.HERMESC_PATH = savedHermescPath
15
+ })
16
+
17
+ it.each(['prebuilt', 'source'])(
18
+ 'shares compiled bytes and retries failures with %s hermes',
19
+ async (mode) => {
20
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-test-'))
21
+ const require = createRequire(import.meta.url)
22
+ const compilerRoot = dirname(require.resolve('hermes-compiler/package.json'))
23
+ const host = { darwin: 'osx-bin', linux: 'linux64-bin', win32: 'win64-bin' }[
24
+ process.platform
25
+ ]!
26
+ const pod = join(root, 'ios/Pods/hermes-engine/destroot/bin')
27
+ try {
28
+ await mkdir(pod, { recursive: true })
29
+ await symlink(
30
+ join(
31
+ compilerRoot,
32
+ 'hermesc',
33
+ host,
34
+ process.platform === 'win32' ? 'hermesc.exe' : 'hermesc'
35
+ ),
36
+ join(pod, 'hermesc')
37
+ )
38
+ const specs = join(root, 'ios/Pods/Local Podspecs')
39
+ await mkdir(specs, { recursive: true })
40
+ await writeFile(
41
+ join(specs, 'hermes-engine.podspec.json'),
42
+ JSON.stringify({
43
+ subspecs: [{ name: mode === 'prebuilt' ? 'Pre-built' : 'Hermes' }],
44
+ user_target_xcconfig: {
45
+ HERMES_CLI_PATH: '${PODS_ROOT}/hermes-engine/destroot/bin/hermesc',
46
+ },
47
+ })
48
+ )
49
+ const compiler = nativeDevBytecodeCompiler(root)
50
+ const compile = (bundle: { code: string; map: string }) =>
51
+ compiler(bundle, 'http://localhost:8081/index.bundle?platform=ios')
52
+ const first = { code: 'globalThis.answer = 42;', map: '' }
53
+ const pending = compile(first)
54
+ expect(compile(first)).toBe(pending)
55
+ const bytes = await pending
56
+ expect(bytes.subarray(0, 8).toString('hex')).toBe('c61fbc03c103191f')
57
+ expect(await compile(first)).toBe(bytes)
58
+ const changed = await compile({ code: 'globalThis.answer = 43;', map: '' })
59
+ expect(changed.equals(bytes)).toBe(false)
60
+ const invalid = { code: 'function {', map: '' }
61
+ const failed = compile(invalid)
62
+ await expect(failed).rejects.toThrow()
63
+ const retried = compile(invalid)
64
+ expect(retried).not.toBe(failed)
65
+ await expect(retried).rejects.toThrow()
66
+ } finally {
67
+ await rm(root, { recursive: true, force: true })
68
+ }
69
+ }
70
+ )
71
+
72
+ function hostHermesc(): string {
73
+ const require = createRequire(import.meta.url)
74
+ const compilerRoot = dirname(require.resolve('hermes-compiler/package.json'))
75
+ const host = { darwin: 'osx-bin', linux: 'linux64-bin', win32: 'win64-bin' }[
76
+ process.platform
77
+ ]!
78
+ return join(
79
+ compilerRoot,
80
+ 'hermesc',
81
+ host,
82
+ process.platform === 'win32' ? 'hermesc.exe' : 'hermesc'
83
+ )
84
+ }
85
+
86
+ it('uses HERMESC_PATH without any ios/Pods', async () => {
87
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-override-'))
88
+ process.env.HERMESC_PATH = hostHermesc()
89
+ try {
90
+ const compiler = nativeDevBytecodeCompiler(root)
91
+ const bytes = await compiler(
92
+ { code: 'globalThis.answer = 42;', map: '' },
93
+ 'http://localhost:8081/index.bundle?platform=ios'
94
+ )
95
+ expect(bytes.subarray(0, 8).toString('hex')).toBe('c61fbc03c103191f')
96
+ } finally {
97
+ await rm(root, { recursive: true, force: true })
98
+ }
99
+ })
100
+
101
+ it('treats empty HERMESC_PATH as unset', async () => {
102
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-empty-'))
103
+ process.env.HERMESC_PATH = ' '
104
+ try {
105
+ expect(() => nativeDevBytecodeCompiler(root)).toThrow(
106
+ /hermes-engine\.podspec\.json/
107
+ )
108
+ expect(() => nativeDevBytecodeCompiler(root)).not.toThrow(/HERMESC_PATH is set/)
109
+ } finally {
110
+ await rm(root, { recursive: true, force: true })
111
+ }
112
+ })
113
+
114
+ it('loudly rejects a HERMESC_PATH that points nowhere', async () => {
115
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-invalid-'))
116
+ process.env.HERMESC_PATH = join(root, 'no-such-hermesc')
117
+ try {
118
+ expect(() => nativeDevBytecodeCompiler(root)).toThrow(
119
+ /HERMESC_PATH is set but points nowhere/
120
+ )
121
+ } finally {
122
+ await rm(root, { recursive: true, force: true })
123
+ }
124
+ })
125
+
126
+ // windows has no executable bit, so this case is posix-only.
127
+ it.runIf(process.platform !== 'win32')(
128
+ 'loudly rejects a non-executable HERMESC_PATH',
129
+ async () => {
130
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-noexec-'))
131
+ const plain = join(root, 'not-executable')
132
+ await writeFile(plain, 'x')
133
+ await chmod(plain, 0o644)
134
+ process.env.HERMESC_PATH = plain
135
+ try {
136
+ expect(() => nativeDevBytecodeCompiler(root)).toThrow(
137
+ /HERMESC_PATH is not executable/
138
+ )
139
+ } finally {
140
+ await rm(root, { recursive: true, force: true })
141
+ }
142
+ }
143
+ )
144
+
145
+ it('loudly rejects a root without ios/Pods when the override is unset', async () => {
146
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-nopods-'))
147
+ try {
148
+ expect(() => nativeDevBytecodeCompiler(root)).toThrow(
149
+ /hermes-engine\.podspec\.json.*HERMESC_PATH/s
150
+ )
151
+ } finally {
152
+ await rm(root, { recursive: true, force: true })
153
+ }
154
+ })
155
+
156
+ it('loudly rejects a malformed podspec when the override is unset', async () => {
157
+ const root = await mkdtemp(join(tmpdir(), 'vxrn-dev-bytecode-malformed-'))
158
+ try {
159
+ const specs = join(root, 'ios/Pods/Local Podspecs')
160
+ await mkdir(specs, { recursive: true })
161
+ await writeFile(join(specs, 'hermes-engine.podspec.json'), '{oops')
162
+ expect(() => nativeDevBytecodeCompiler(root)).toThrow(
163
+ /hermes-engine\.podspec\.json/
164
+ )
165
+ } finally {
166
+ await rm(root, { recursive: true, force: true })
167
+ }
168
+ })
@@ -0,0 +1,117 @@
1
+ import { accessSync, constants, readFileSync, statSync } from 'node:fs'
2
+ import { execFile } from 'node:child_process'
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { dirname, join } from 'node:path'
6
+ import { promisify } from 'node:util'
7
+ import type { NativeDevBundle } from './createNativeDevEngine'
8
+
9
+ const execute = promisify(execFile)
10
+
11
+ // explicit compiler ownership for split layouts, where the dev server root
12
+ // is not the native project (the iOS test job serves tests/test while the
13
+ // container is built from tests/rn-test-container). empty means unset.
14
+ function useHermescOverride(): string | undefined {
15
+ const override = process.env.HERMESC_PATH?.trim()
16
+ if (!override) return undefined
17
+ let stat: ReturnType<typeof statSync> | undefined
18
+ try {
19
+ stat = statSync(override)
20
+ } catch {
21
+ stat = undefined
22
+ }
23
+ if (!stat?.isFile()) {
24
+ throw new Error(
25
+ `[vxrn] HERMESC_PATH is set but points nowhere: ${override}. ` +
26
+ `Point it at the hermesc binary the native build used, or unset it to resolve the pod compiler.`
27
+ )
28
+ }
29
+ try {
30
+ accessSync(override, constants.X_OK)
31
+ } catch {
32
+ throw new Error(
33
+ `[vxrn] HERMESC_PATH is not executable: ${override}. ` +
34
+ `Point it at the hermesc binary the native build used, or unset it to resolve the pod compiler.`
35
+ )
36
+ }
37
+ return override
38
+ }
39
+
40
+ // the pod compiler matches the actual native vm, including rn's prebuilt
41
+ // hermes replacement. the npm compiler can target a different bytecode version.
42
+ function resolvePodHermesc(root: string): string {
43
+ const pods = join(root, 'ios/Pods')
44
+ const podspecPath = join(pods, 'Local Podspecs/hermes-engine.podspec.json')
45
+ let podspec: {
46
+ subspecs: { name: string }[]
47
+ user_target_xcconfig?: { HERMES_CLI_PATH?: string }
48
+ }
49
+ try {
50
+ podspec = JSON.parse(readFileSync(podspecPath, 'utf8'))
51
+ } catch (error) {
52
+ throw new Error(
53
+ `[vxrn] native iOS dev bytecode needs the pod hermesc but ${root} has no usable native project: ` +
54
+ `cannot read ${podspecPath} (${error instanceof Error ? error.message : String(error)}). ` +
55
+ `Run prebuild and pod install for this app, or set HERMESC_PATH to the hermesc the native build used.`
56
+ )
57
+ }
58
+ if (!Array.isArray(podspec.subspecs)) {
59
+ throw new Error(
60
+ `[vxrn] native iOS dev bytecode needs the pod hermesc but ${podspecPath} declares no subspecs. ` +
61
+ `Reinstall pods for this app, or set HERMESC_PATH to the hermesc the native build used.`
62
+ )
63
+ }
64
+ const prebuilt = podspec.subspecs.some(
65
+ (spec: { name: string }) => spec.name === 'Pre-built'
66
+ )
67
+ if (prebuilt) {
68
+ return join(pods, 'hermes-engine/destroot/bin/hermesc')
69
+ }
70
+ const cliPath = podspec.user_target_xcconfig?.HERMES_CLI_PATH
71
+ if (!cliPath) {
72
+ throw new Error(
73
+ `[vxrn] native iOS dev bytecode needs the pod hermesc but ${podspecPath} declares no HERMES_CLI_PATH. ` +
74
+ `Reinstall pods for this app, or set HERMESC_PATH to the hermesc the native build used.`
75
+ )
76
+ }
77
+ return cliPath.replace(/\$\(PODS_ROOT\)|\$\{PODS_ROOT\}/g, pods)
78
+ }
79
+
80
+ // compile once per emitted bundle. the device can start a fresh hermes context
81
+ // without compiling every cold route's module closures on its js thread.
82
+ export function nativeDevBytecodeCompiler(root: string) {
83
+ const compiler = useHermescOverride() ?? resolvePodHermesc(root)
84
+ const compiled = new WeakMap<NativeDevBundle, Map<string, Promise<Buffer>>>()
85
+
86
+ return (bundle: NativeDevBundle, sourceURL: string): Promise<Buffer> => {
87
+ let urls = compiled.get(bundle)
88
+ if (!urls) compiled.set(bundle, (urls = new Map()))
89
+ const existing = urls.get(sourceURL)
90
+ if (existing) return existing
91
+ const result = (async () => {
92
+ const directory = await mkdtemp(join(tmpdir(), 'vxrn-dev-hermes-'))
93
+ try {
94
+ // hermesc records its input argument as the debug filename. use the
95
+ // served js url as a relative filename so stacks and devtools retain
96
+ // the original bundle coordinates without a second source map.
97
+ const source = join(directory, sourceURL)
98
+ await mkdir(dirname(source), { recursive: true })
99
+ const output = join(directory, 'index.hbc')
100
+ await writeFile(source, bundle.code)
101
+ await execute(
102
+ compiler,
103
+ ['-Og', '-g', '-emit-binary', '-out', output, sourceURL],
104
+ { cwd: directory, maxBuffer: 16 * 1024 * 1024 }
105
+ )
106
+ return await readFile(output)
107
+ } finally {
108
+ await rm(directory, { recursive: true, force: true })
109
+ }
110
+ })().catch((error) => {
111
+ urls.delete(sourceURL)
112
+ throw error
113
+ })
114
+ urls.set(sourceURL, result)
115
+ return result
116
+ }
117
+ }