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
@@ -31,6 +31,7 @@ import { withExpoPublicEnvAliases } from '@vxrn/utils/publicEnv'
31
31
  import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
32
32
  import { getNativePrelude } from '../runtime/native-prelude'
33
33
  import { rnCodegenPlugin } from '../plugins/rnCodegenPlugin'
34
+ import { dedupe } from '../config/getBaseViteConfigOnly'
34
35
 
35
36
  // Hermes needs the whole class shape lowered *together*. downleveling only the
36
37
  // class fields while leaving `class ... extends` as modern ES6 produces a
@@ -124,8 +125,11 @@ function getNativeResolveConfig(platform: 'ios' | 'android') {
124
125
  // both here makes a CommonJS require eligible for an export map's ESM
125
126
  // `import` target (for example @babel/runtime), which turns callable CJS
126
127
  // helpers into namespace objects at runtime.
127
- conditionNames: ['react-native'],
128
- mainFields: ['react-native', 'module', 'main'],
128
+ conditionNames: ['react-native', 'browser'],
129
+ // Native CommonJS consumers need the package's main export shape (punycode
130
+ // exposes ucs2 there, but only its default export contains it in module).
131
+ mainFields: ['react-native', 'browser', 'main', 'module'],
132
+ aliasFields: [['react-native'], ['browser']],
129
133
  }
130
134
  }
131
135
 
@@ -265,22 +269,13 @@ function getNativePlugins(
265
269
  serverFileExclusionPlugin(),
266
270
  // guard server-only / client-only / web-only / native-only imports
267
271
  environmentGuardPlugin(),
272
+ missingDynamicImportPlugin(),
268
273
  // alias RN's Metro HMR client to a no-op; vxrn drives HMR itself (the
269
274
  // rolldown-runtime WebSocket); RN's client otherwise opens a /hot socket and
270
275
  // red-boxes "unknown-message [object Object]" on every edit (new arch)
271
276
  hmrClientNoopPlugin(),
272
- // react native 0.87 removed @react-native/assets-registry. libraries like
273
- // react-native-svg still import its registry, which is now the same
274
- // singleton at react-native/asset-registry. unresolved, rolldown would
275
- // leave it as an external import that throws when the module runs.
276
- {
277
- name: 'vxrn:legacy-asset-registry',
278
- resolveId(source, importer) {
279
- if (source === '@react-native/assets-registry/registry') {
280
- return this.resolve('react-native/asset-registry', importer, { skipSelf: true })
281
- }
282
- },
283
- } satisfies Plugin,
277
+ nativeIdentityAliasPlugin(platform),
278
+ nativeAssetRegistryPlugin(root),
284
279
  // route every `react-native/*` import through the app's react native copy.
285
280
  // two reasons: package managers can install the same react native version at
286
281
  // multiple physical paths and rolldown assigns each path its own module
@@ -291,6 +286,7 @@ function getNativePlugins(
291
286
  // @react-native/virtualized-lists itself makes — fail resolution against
292
287
  // the export map. metro resolves those from the filesystem, so do the same.
293
288
  reactNativeDedupePlugin(root),
289
+ nativePackageDedupePlugin(root),
294
290
  // stub CSS imports — native doesn't support CSS and rolldown removed CSS bundling
295
291
  cssStubPlugin(),
296
292
  // handle import.meta.glob (used by One's route system)
@@ -317,6 +313,8 @@ function getNativePlugins(
317
313
  // loops the earlier lowering steps emit.
318
314
  hermesLoopsPlugin(sourceMaps),
319
315
  ...userPlugins,
316
+ nativeExportsPrecedencePlugin(root, platform),
317
+ nativeDeclarationAliasPlugin(root, platform),
320
318
  ].map((plugin: Plugin) =>
321
319
  plugin.api?.vxrnNative ? plugin.api.vxrnNative(context) : plugin
322
320
  )
@@ -445,28 +443,13 @@ export function postProcessNativeBundle(code: string): string {
445
443
  }
446
444
 
447
445
  /**
448
- * Wrap the dev bundle body in a function scope so module top-level
449
- * `var`/`function` declarations don't leak onto the global object.
450
- *
451
- * rolldown's dev() emits the bundle as a *script*. A top-level `var` in a
452
- * script creates a NON-configurable property on the global object. RN's
453
- * `Libraries/Network/fetch.js` declares `var ... Headers, Request, ...`, so
454
- * `global.Headers`/`global.Request` become non-configurable. RN's `setUpXHR`
455
- * then calls `polyfillGlobal('Headers', ...)`, whose `polyfillObjectProperty`
456
- * does `Object.defineProperty(global, 'Headers', { configurable: true, ... })`
457
- * — which throws "Cannot redefine property" and RN converts to
458
- * `console.error('Failed to set polyfill. Headers is not configurable.')`.
459
- * In dev that console.error becomes a blocking LogBox redbox, so the app never
460
- * mounts (every appium navigation then times out). The prod build is immune:
461
- * its modules are wrapped in closures (no global leak) and it has no LogBox.
446
+ * wrap dev module declarations in a function scope. script-level declarations
447
+ * create global properties that can block react native's lazy polyfills.
448
+ * production uses rolldown's iife format; the dev engine needs this wrapper.
462
449
  *
463
- * Wrapping everything after the prelude in an IIFE makes those module vars
464
- * function-scoped, matching prod, so `polyfillGlobal` succeeds. The prelude
465
- * stays at script scope because it intentionally installs globals
466
- * (`globalThis.global`/`__DEV__`/`process`/...). Intentional globals survive:
467
- * the runtime is assigned via `globalThis.__rolldown_runtime__ = ...`, and HMR
468
- * updates run through a *direct* `eval` inside this scope, so they still see
469
- * the closure's `__esmMin`/`__toCommonJS`/module bindings.
450
+ * keep the prelude at script scope because it installs intentional globals.
451
+ * the dev runtime assigns itself to globalThis, and hmr's direct eval stays
452
+ * inside this closure so it can reach module bindings and runtime helpers.
470
453
  */
471
454
  export function wrapNativeBundleModuleScope(code: string): string {
472
455
  // the prelude (intro) ends right before the rolldown runtime region
@@ -584,7 +567,6 @@ export async function createNativeDevEngine(
584
567
  let bundleReject: ((error: Error) => void) | null = null
585
568
  let bundlePromise: Promise<NativeDevBundle> | null = null
586
569
 
587
- const resolvedHost = host === '0.0.0.0' ? 'localhost' : host
588
570
  const virtualEntry = nativeVirtualEntryPlugin(root, {
589
571
  dev: true,
590
572
  platform,
@@ -657,11 +639,15 @@ export async function createNativeDevEngine(
657
639
  // only polyfilled once InitializeCore has run) and hand it to the runtime,
658
640
  // which owns every message it carries. handed over immediately rather than
659
641
  // from an open handler: a host that does not honor `socket.onopen = fn`
660
- // would otherwise silently never get Fast Refresh.
642
+ // would otherwise silently never get Fast Refresh. the socket goes to the
643
+ // origin the device loaded this bundle from (RN's getDevServer), never the
644
+ // server's own host:port: an emulator behind `adb reverse tcp:8081 tcp:<port>`
645
+ // or a phone on the LAN cannot reach the server's localhost:<port>.
661
646
  outro: `
662
647
  try {
663
648
  var __WS = (init_WebSocket(), __toCommonJS(WebSocket_exports)).default;
664
- var __hmrUrl = 'ws://${resolvedHost}:${port}/hot?platform=${platform}&clientId=' + encodeURIComponent(__rolldown_runtime__.clientId);
649
+ var __devServer = (init_getDevServer(), __toCommonJS(getDevServer_exports)).default();
650
+ var __hmrUrl = __devServer.url.replace(/^http/, 'ws') + 'hot?platform=${platform}&clientId=' + encodeURIComponent(__rolldown_runtime__.clientId);
665
651
  __rolldown_runtime__.setup(new __WS(__hmrUrl));
666
652
  } catch(e) {
667
653
  // a swallowed failure here leaves the app permanently without Fast Refresh and
@@ -960,7 +946,11 @@ export async function buildNativeBundle(
960
946
  // last, so it sees what every other plugin produced
961
947
  nativeBundlePostProcessPlugin(),
962
948
  ],
963
- output: getNativeOutputOptions(prelude, sourcemap, minify),
949
+ output: {
950
+ ...getNativeOutputOptions(prelude, sourcemap, minify),
951
+ // native hosts evaluate a script; module bindings must stay out of globals.
952
+ format: 'iife',
953
+ },
964
954
  })
965
955
  const chunk = result.output.find((o) => o.type === 'chunk' && o.isEntry)
966
956
 
@@ -1132,16 +1122,18 @@ ${routeHmr}
1132
1122
  routes.files = files
1133
1123
  if (!useDynamicRouteRegistry) return
1134
1124
 
1125
+ mkdirSync(dirname(registryPath), { recursive: true })
1126
+ // rolldown resolves the registry through symlinked node_modules before its imports.
1127
+ const registryDir = realpathSync(dirname(registryPath))
1135
1128
  const source = `export const routes = {\n${[...files]
1136
1129
  .sort()
1137
1130
  .map((file) => {
1138
1131
  const routeKey = `/${normalizePath(relative(root, file))}`
1139
- let importPath = normalizePath(relative(dirname(registryPath), file))
1132
+ let importPath = normalizePath(relative(registryDir, file))
1140
1133
  if (!importPath.startsWith('.')) importPath = `./${importPath}`
1141
1134
  return ` ${JSON.stringify(routeKey)}: () => import(${JSON.stringify(importPath)})`
1142
1135
  })
1143
1136
  .join(',\n')}\n}\n`
1144
- mkdirSync(dirname(registryPath), { recursive: true })
1145
1137
  if (!existsSync(registryPath) || readFileSync(registryPath, 'utf8') !== source) {
1146
1138
  writeFileSync(registryPath, source)
1147
1139
  }
@@ -1243,6 +1235,36 @@ function serverFileExclusionPlugin(): Plugin {
1243
1235
  }
1244
1236
  }
1245
1237
 
1238
+ /**
1239
+ * An `import()` of a package that is not installed stays a raw dynamic import
1240
+ * in the bundle, which Hermes cannot parse. Metro resolves it as an optional
1241
+ * dependency: the import rejects with "Cannot find module" when it runs. Do the
1242
+ * same, so a library that probes an optional peer (`await import('x')` inside
1243
+ * try) builds and falls back at runtime.
1244
+ */
1245
+ function missingDynamicImportPlugin(): Plugin {
1246
+ const PREFIX = '\0vxrn-missing-module:'
1247
+ return {
1248
+ name: 'vxrn:missing-dynamic-import',
1249
+ async resolveId(source, importer, options) {
1250
+ if (options.kind !== 'dynamic-import') return
1251
+ if (source.startsWith('.') || source.startsWith('/') || source.includes(':')) return
1252
+ if (source.startsWith('\0')) return
1253
+ const resolved = await this.resolve(source, importer, {
1254
+ skipSelf: true,
1255
+ kind: options.kind,
1256
+ })
1257
+ if (resolved) return resolved
1258
+ return { id: `${PREFIX}${source}`, external: false }
1259
+ },
1260
+ load(id) {
1261
+ if (!id.startsWith(PREFIX)) return
1262
+ const source = JSON.stringify(`Cannot find module '${id.slice(PREFIX.length)}'`)
1263
+ return { code: `throw new Error(${source});`, moduleType: 'js' as any }
1264
+ },
1265
+ }
1266
+ }
1267
+
1246
1268
  /**
1247
1269
  * Guard environment-specific bare imports in native bundles.
1248
1270
  * - server-only, client-only, web-only → throw at runtime
@@ -1315,6 +1337,203 @@ export function hmrClientNoopPlugin(): Plugin {
1315
1337
  }
1316
1338
  }
1317
1339
 
1340
+ function nativeIdentityAliasPlugin(platform: 'ios' | 'android'): Plugin {
1341
+ const packages = new Map<
1342
+ string,
1343
+ { root: string; aliases: Record<string, unknown> } | null
1344
+ >()
1345
+ let resolver: import('rolldown/experimental').ResolverFactory | undefined
1346
+ const findPackage = (
1347
+ directory: string
1348
+ ): { root: string; aliases: Record<string, unknown> } | null => {
1349
+ if (packages.has(directory)) return packages.get(directory)!
1350
+ let result: { root: string; aliases: Record<string, unknown> } | null = null
1351
+ const manifest = join(directory, 'package.json')
1352
+ if (existsSync(manifest)) {
1353
+ try {
1354
+ const field = JSON.parse(readFileSync(manifest, 'utf8'))['react-native']
1355
+ result = {
1356
+ root: directory,
1357
+ aliases: field && typeof field === 'object' ? field : {},
1358
+ }
1359
+ } catch {}
1360
+ } else {
1361
+ const parent = dirname(directory)
1362
+ if (parent !== directory) result = findPackage(parent)
1363
+ }
1364
+ packages.set(directory, result)
1365
+ return result
1366
+ }
1367
+ return {
1368
+ name: 'vxrn:native-identity-alias',
1369
+ async resolveId(source, importer) {
1370
+ if (!importer || !source.startsWith('.')) return
1371
+ const pkg = findPackage(dirname(importer))
1372
+ if (!pkg) return
1373
+ const key = `./${normalizePath(relative(pkg.root, resolve(dirname(importer), source)))}`
1374
+ if (pkg.aliases[key] !== key) return
1375
+ // A native identity mapping deliberately keeps the original implementation
1376
+ // even when the browser field maps it elsewhere. Avoid alias recursion.
1377
+ if (!resolver) {
1378
+ const { ResolverFactory } = await import('rolldown/experimental')
1379
+ resolver = new ResolverFactory({
1380
+ ...getNativeResolveConfig(platform),
1381
+ aliasFields: [],
1382
+ })
1383
+ }
1384
+ const result = resolver.sync(dirname(importer), source)
1385
+ if (result.path) return result.path
1386
+ },
1387
+ }
1388
+ }
1389
+
1390
+ // export maps resolve in key order, so a package listing `browser` before
1391
+ // `react-native` would give native its web build. the browser fallback is for
1392
+ // packages without a native target: where a package's exports name both,
1393
+ // resolve it as metro does, without `browser`.
1394
+ function nativeExportsPrecedencePlugin(
1395
+ root: string,
1396
+ platform: 'ios' | 'android'
1397
+ ): Plugin {
1398
+ const manifests = new Map<string, { name?: string; exports?: unknown } | null>()
1399
+ const readManifest = (directory: string) => {
1400
+ if (!manifests.has(directory)) {
1401
+ let manifest = null
1402
+ try {
1403
+ manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'))
1404
+ } catch {}
1405
+ manifests.set(directory, manifest)
1406
+ }
1407
+ return manifests.get(directory)
1408
+ }
1409
+ const resolvers = new Map<string, import('rolldown/experimental').ResolverFactory>()
1410
+ return {
1411
+ name: 'vxrn:native-exports-precedence',
1412
+ async resolveId(source, importer, options) {
1413
+ if (source.startsWith('.') || source.startsWith('/') || source.includes(':')) return
1414
+ const resolved = await this.resolve(source, importer, {
1415
+ skipSelf: true,
1416
+ kind: options.kind,
1417
+ })
1418
+ if (!resolved || resolved.external || resolved.id.startsWith('\0')) return resolved
1419
+ const name = source
1420
+ .split('/')
1421
+ .slice(0, source.startsWith('@') ? 2 : 1)
1422
+ .join('/')
1423
+ let directory = dirname(resolved.id)
1424
+ while (readManifest(directory)?.name !== name) {
1425
+ if (dirname(directory) === directory) return resolved
1426
+ directory = dirname(directory)
1427
+ }
1428
+ const exportsMap = JSON.stringify(readManifest(directory)?.exports ?? null)
1429
+ if (!exportsMap.includes('"react-native"') || !exportsMap.includes('"browser"')) {
1430
+ return resolved
1431
+ }
1432
+ const kind = options.kind === 'require-call' ? 'require' : 'import'
1433
+ let resolver = resolvers.get(kind)
1434
+ if (!resolver) {
1435
+ const { ResolverFactory } = await import('rolldown/experimental')
1436
+ resolver = new ResolverFactory({
1437
+ ...getNativeResolveConfig(platform),
1438
+ conditionNames: ['react-native', kind, 'default'],
1439
+ })
1440
+ resolvers.set(kind, resolver)
1441
+ }
1442
+ const native = resolver.sync(importer ? dirname(importer) : root, source)
1443
+ if (!native.path || native.path === resolved.id) return resolved
1444
+ return this.resolve(native.path, importer, { skipSelf: true, kind: options.kind })
1445
+ },
1446
+ }
1447
+ }
1448
+
1449
+ function nativeDeclarationAliasPlugin(root: string, platform: 'ios' | 'android'): Plugin {
1450
+ const resolvers = new Map<string, import('rolldown/experimental').ResolverFactory>()
1451
+ return {
1452
+ name: 'vxrn:declaration-alias',
1453
+ async resolveId(source, importer, options) {
1454
+ if (source.startsWith('.') || source.startsWith('/') || source.includes(':')) return
1455
+ const resolved = await this.resolve(source, importer, {
1456
+ skipSelf: true,
1457
+ kind: options.kind,
1458
+ })
1459
+ if (!resolved || resolved.external || !/\.d\.[cm]?ts$/.test(resolved.id)) {
1460
+ return resolved
1461
+ }
1462
+ // tsconfig paths can supply types for a package without replacing its
1463
+ // runtime export. Resolve that package with the same native conditions,
1464
+ // but without the declaration alias, and preserve import/require selection.
1465
+ const kind = options.kind === 'require-call' ? 'require' : 'import'
1466
+ let resolver = resolvers.get(kind)
1467
+ if (!resolver) {
1468
+ const { ResolverFactory } = await import('rolldown/experimental')
1469
+ resolver = new ResolverFactory({
1470
+ ...getNativeResolveConfig(platform),
1471
+ conditionNames: ['react-native', 'browser', kind, 'default'],
1472
+ })
1473
+ resolvers.set(kind, resolver)
1474
+ }
1475
+ const runtime = resolver.sync(importer ? dirname(importer) : root, source)
1476
+ if (!runtime.path || /\.d\.[cm]?ts$/.test(runtime.path)) {
1477
+ this.error(
1478
+ `Native import ${source} resolves to a declaration without a runtime export`
1479
+ )
1480
+ }
1481
+ return this.resolve(runtime.path, importer, { skipSelf: true, kind: options.kind })
1482
+ },
1483
+ }
1484
+ }
1485
+
1486
+ function nativeAssetRegistryPlugin(root: string): Plugin {
1487
+ let registryPath: string | null | undefined
1488
+ return {
1489
+ name: 'vxrn:asset-registry',
1490
+ async resolveId(source, importer) {
1491
+ if (
1492
+ source !== 'react-native/asset-registry' &&
1493
+ source !== '@react-native/assets-registry/registry'
1494
+ ) {
1495
+ return
1496
+ }
1497
+ if (registryPath === undefined) {
1498
+ try {
1499
+ const reactNativeRoot = dirname(resolvePath('react-native/package.json', root))
1500
+ // RN 0.87 moved the singleton into RN and exports this file as
1501
+ // react-native/asset-registry. Older RN versions own it in a package.
1502
+ const exportedRegistry = join(reactNativeRoot, 'src/asset-registry.js')
1503
+ registryPath = existsSync(exportedRegistry)
1504
+ ? exportedRegistry
1505
+ : resolvePath('@react-native/assets-registry/registry', reactNativeRoot)
1506
+ } catch {
1507
+ registryPath = null
1508
+ }
1509
+ }
1510
+ if (registryPath) {
1511
+ return this.resolve(normalizePath(registryPath), importer, { skipSelf: true })
1512
+ }
1513
+ },
1514
+ }
1515
+ }
1516
+
1517
+ function nativePackageDedupePlugin(root: string): Plugin {
1518
+ // these packages share one native runtime; nested copies cannot initialize it twice.
1519
+ const packages = new Set([
1520
+ ...dedupe,
1521
+ 'react-native-worklets',
1522
+ 'react-native-nitro-modules',
1523
+ ])
1524
+ return {
1525
+ name: 'vxrn:native-package-dedupe',
1526
+ async resolveId(source) {
1527
+ const parts = source.split('/')
1528
+ const packageName = source.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
1529
+ if (!packages.has(packageName)) return
1530
+ // use the app's package copy for contexts and subpaths, preserving the
1531
+ // native export conditions selected by rolldown.
1532
+ return this.resolve(source, join(root, 'package.json'), { skipSelf: true })
1533
+ },
1534
+ }
1535
+ }
1536
+
1318
1537
  function reactNativeDedupePlugin(root: string): Plugin {
1319
1538
  // package managers can install the same react native version at multiple
1320
1539
  // physical paths. rolldown assigns each path a module identity, so resolving
@@ -1928,7 +2147,7 @@ function copyNativeAssetFiles(
1928
2147
 
1929
2148
  /**
1930
2149
  * SWC transform for Hermes compatibility.
1931
- * Transforms class properties and private fields that Hermes doesn't support.
2150
+ * Lowers classes, lexical bindings, and async syntax for legacy Hermes.
1932
2151
  * Inspired by rollipop's swc-plugin.ts.
1933
2152
  */
1934
2153
  export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin {
@@ -1940,14 +2159,14 @@ export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin
1940
2159
  if (!/\.[cm]?[jt]sx?$/.test(id)) return
1941
2160
  if (id.includes('\0') || id.includes('virtual:')) return
1942
2161
  // skip files that don't need transformation
1943
- const hasClass = code.includes('class ') || code.includes('class{')
2162
+ const hasClass = /\bclass(?:\s|\{)/.test(code)
1944
2163
  const hasAsync = code.includes('async')
1945
- const hasBlockScopedLoop = /\bfor\s*\(\s*(?:const|let)\b/.test(code)
1946
- if (!hasClass && !hasAsync && !hasBlockScopedLoop) return
2164
+ const hasBlockScopedBinding = /\b(?:const|let)\s/.test(code)
2165
+ if (!hasClass && !hasAsync && !hasBlockScopedBinding) return
1947
2166
  let output: { code: string; map?: any } | undefined
1948
- // keep the existing oxc limit for large prebuilt files. async lowering
1949
- // below has no size exemption and runs after the worklet transform.
1950
- if (code.length <= 500_000) {
2167
+ // Keep the Oxc limit for other large prebuilt files. Class, lexical binding,
2168
+ // and async lowering below have no size exemption for legacy Hermes.
2169
+ if (code.length <= 500_000 || hasClass) {
1951
2170
  if (!oxc) oxc = await import('oxc-transform')
1952
2171
  const lang = /\.[cm]?ts$/.test(id) ? 'ts' : id.endsWith('.tsx') ? 'tsx' : 'jsx'
1953
2172
  const result = oxc.transformSync(id, code, {
@@ -1967,6 +2186,17 @@ export function hermesCompatSWCPlugin(dev: boolean, sourceMaps = false): Plugin
1967
2186
  map: sourceMaps ? result.map : undefined,
1968
2187
  }
1969
2188
  }
2189
+ if (hasClass || hasBlockScopedBinding) {
2190
+ const { transformHermesClasses } = await import('@vxrn/compiler')
2191
+ const classes = await transformHermesClasses(output?.code ?? code, id, sourceMaps)
2192
+ if (classes) {
2193
+ if (classes.map && output?.map) {
2194
+ const remapping = (await import('@jridgewell/remapping')).default
2195
+ classes.map = remapping([classes.map, output.map], () => null)
2196
+ }
2197
+ output = classes
2198
+ }
2199
+ }
1970
2200
  const lowered = await transformHermesAsync(output?.code ?? code, id, sourceMaps)
1971
2201
  if (!lowered) return output
1972
2202
  if (lowered.map && output?.map) {
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { createServer } from 'node:http'
4
4
  import { dirname, join } from 'node:path'
@@ -17,6 +17,49 @@ vi.mock('../utils/patches', () => ({
17
17
  const here = dirname(fileURLToPath(import.meta.url))
18
18
  const workspaceRoot = join(here, '..', '..', '..')
19
19
 
20
+ // an android sdk whose adb reports exactly these connected serials
21
+ function fakeAndroidSdk(serials: string[]) {
22
+ const sdk = mkdtempSync(join(tmpdir(), 'nativerun-sdk-'))
23
+ mkdirSync(join(sdk, 'platform-tools'))
24
+ const listing = ['List of devices attached', ...serials.map((serial) => `${serial}\tdevice`)]
25
+ writeFileSync(
26
+ join(sdk, 'platform-tools', 'adb'),
27
+ `#!/bin/sh\nprintf '%s\\n' ${listing.map((line) => `'${line}'`).join(' ')}\n`
28
+ )
29
+ chmodSync(join(sdk, 'platform-tools', 'adb'), 0o755)
30
+ return sdk
31
+ }
32
+
33
+ async function withAndroidSdk<T>(serials: string[], serial: string | undefined, run: () => Promise<T>) {
34
+ const sdk = fakeAndroidSdk(serials)
35
+ const previous = { home: process.env.ANDROID_HOME, serial: process.env.ANDROID_SERIAL }
36
+ process.env.ANDROID_HOME = sdk
37
+ if (serial) process.env.ANDROID_SERIAL = serial
38
+ else delete process.env.ANDROID_SERIAL
39
+ try {
40
+ return await run()
41
+ } finally {
42
+ if (previous.home === undefined) delete process.env.ANDROID_HOME
43
+ else process.env.ANDROID_HOME = previous.home
44
+ if (previous.serial === undefined) delete process.env.ANDROID_SERIAL
45
+ else process.env.ANDROID_SERIAL = previous.serial
46
+ rmSync(sdk, { recursive: true, force: true })
47
+ }
48
+ }
49
+
50
+ async function withDevServer<T>(run: (port: number) => Promise<T>) {
51
+ const server = createServer((req, res) => {
52
+ res.end(req.url === '/status' ? 'packager-status:running' : 'ok')
53
+ })
54
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
55
+ const address = server.address()
56
+ try {
57
+ return await run(typeof address === 'object' && address ? address.port : 0)
58
+ } finally {
59
+ server.close()
60
+ }
61
+ }
62
+
20
63
  describe('expo-free run commands', () => {
21
64
  it('delegates build/install/launch to community cli with no packager', () => {
22
65
  expect(buildNativeRunCommand({ platform: 'ios', port: 8082 })).toEqual({
@@ -32,22 +75,34 @@ describe('expo-free run commands', () => {
32
75
  })
33
76
 
34
77
  it('forwards an explicit simulator target instead of letting the cli pick', () => {
35
- expect(
36
- buildNativeRunCommand({ platform: 'ios', simulator: 'iPhone 16' })
37
- ).toEqual({
78
+ expect(buildNativeRunCommand({ platform: 'ios', simulator: 'iPhone 16' })).toEqual({
38
79
  command: 'run-ios',
39
80
  argv: ['--no-packager', '--port', '8081', '--simulator', 'iPhone 16'],
40
81
  port: 8081,
41
82
  })
42
- expect(
43
- buildNativeRunCommand({ platform: 'ios', udid: 'some-udid' })
44
- ).toEqual({
83
+ expect(buildNativeRunCommand({ platform: 'ios', udid: 'some-udid' })).toEqual({
45
84
  command: 'run-ios',
46
85
  argv: ['--no-packager', '--port', '8081', '--udid', 'some-udid'],
47
86
  port: 8081,
48
87
  })
49
88
  })
50
89
 
90
+ it('targets the selected Android device rather than launching on every emulator', () => {
91
+ vi.stubEnv('ANDROID_SERIAL', 'emulator-5582')
92
+ try {
93
+ expect(buildNativeRunCommand({ platform: 'android', port: 8097 }).argv).toEqual([
94
+ '--no-packager',
95
+ '--port',
96
+ '8097',
97
+ '--device',
98
+ 'emulator-5582',
99
+ ])
100
+ expect(buildNativeRunCommand({ platform: 'ios' }).argv).not.toContain('--device')
101
+ } finally {
102
+ vi.unstubAllEnvs()
103
+ }
104
+ })
105
+
51
106
  it('never resolves the expo cli', () => {
52
107
  const source = readFileSync(join(here, 'nativeRun.ts'), 'utf8')
53
108
  expect(source).not.toContain('expo/cli')
@@ -65,9 +120,10 @@ describe('expo-free run commands', () => {
65
120
  const port = typeof address === 'object' && address ? address.port : 0
66
121
  const calls: Array<{ executable: string; argv: string[] }> = []
67
122
  const previousMetroPort = process.env.RCT_METRO_PORT
123
+ const root = mkdtempSync(join(tmpdir(), 'vxrn-run-no-app-cli-'))
68
124
  try {
69
125
  await nativeRun({
70
- root: workspaceRoot,
126
+ root,
71
127
  platform: 'ios',
72
128
  port,
73
129
  spawn: (executable, argv) => {
@@ -84,6 +140,7 @@ describe('expo-free run commands', () => {
84
140
  if (previousMetroPort === undefined) delete process.env.RCT_METRO_PORT
85
141
  else process.env.RCT_METRO_PORT = previousMetroPort
86
142
  server.close()
143
+ rmSync(root, { recursive: true, force: true })
87
144
  }
88
145
  })
89
146
 
@@ -101,18 +158,20 @@ describe('expo-free run commands', () => {
101
158
  )
102
159
 
103
160
  try {
104
- const launch = nativeRun({
105
- root: workspaceRoot,
106
- platform: 'android',
107
- port,
108
- spawn: (_executable, argv) => calls.push(argv),
109
- })
110
- await new Promise<void>((resolve) => setImmediate(resolve))
111
- expect(calls).toHaveLength(0)
161
+ await withAndroidSdk(['emulator-5554'], undefined, async () => {
162
+ const launch = nativeRun({
163
+ root: workspaceRoot,
164
+ platform: 'android',
165
+ port,
166
+ spawn: (_executable, argv) => calls.push(argv),
167
+ })
168
+ await new Promise<void>((resolve) => setImmediate(resolve))
169
+ expect(calls).toHaveLength(0)
112
170
 
113
- finishPatches?.()
114
- await launch
115
- expect(calls).toHaveLength(1)
171
+ finishPatches?.()
172
+ await launch
173
+ expect(calls).toHaveLength(1)
174
+ })
116
175
  } finally {
117
176
  server.close()
118
177
  }
@@ -141,14 +200,17 @@ describe('expo-free run commands', () => {
141
200
  }
142
201
  })
143
202
 
144
- it('resolves the ios bundle id from app.json', () => {
203
+ it('resolves the ios bundle id from the prebuilt ios project', () => {
145
204
  const dir = mkdtempSync(join(tmpdir(), 'nativerun-'))
146
205
  try {
147
206
  expect(resolveIosBundleId(dir)).toBeNull()
148
- writeFileSync(join(dir, 'app.json'), JSON.stringify({ expo: { ios: { bundleIdentifier: 'dev.example.app' } } }))
149
- expect(resolveIosBundleId(dir)).toBe('dev.example.app')
150
- writeFileSync(join(dir, 'app.json'), 'not json')
207
+ mkdirSync(join(dir, 'ios', 'Example.xcodeproj'), { recursive: true })
151
208
  expect(resolveIosBundleId(dir)).toBeNull()
209
+ writeFileSync(
210
+ join(dir, 'ios', 'Example.xcodeproj', 'project.pbxproj'),
211
+ 'buildSettings = {\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dev.example.app;\n\t\t\t};'
212
+ )
213
+ expect(resolveIosBundleId(dir)).toBe('dev.example.app')
152
214
  } finally {
153
215
  rmSync(dir, { recursive: true, force: true })
154
216
  }
@@ -166,4 +228,44 @@ describe('expo-free run commands', () => {
166
228
  })
167
229
  ).rejects.toThrow(/No dev server running/)
168
230
  })
231
+
232
+ it('fails before launching when no android device is connected', async () => {
233
+ const spawn = vi.fn()
234
+ await withDevServer((port) =>
235
+ withAndroidSdk([], undefined, () =>
236
+ expect(
237
+ nativeRun({ root: workspaceRoot, platform: 'android', port, spawn })
238
+ ).rejects.toThrow(/No Android device or emulator is connected/)
239
+ )
240
+ )
241
+ expect(spawn).not.toHaveBeenCalled()
242
+ })
243
+
244
+ it('fails before launching when the chosen android serial is not connected', async () => {
245
+ const spawn = vi.fn()
246
+ await withDevServer((port) =>
247
+ withAndroidSdk(['emulator-5554'], 'emulator-5556', () =>
248
+ expect(
249
+ nativeRun({ root: workspaceRoot, platform: 'android', port, spawn })
250
+ ).rejects.toThrow(/emulator-5556 is not connected \(connected: emulator-5554\)/)
251
+ )
252
+ )
253
+ expect(spawn).not.toHaveBeenCalled()
254
+ })
255
+
256
+ it('launches on the chosen android serial when it is connected', async () => {
257
+ const calls: string[][] = []
258
+ await withDevServer((port) =>
259
+ withAndroidSdk(['emulator-5554', 'emulator-5556'], 'emulator-5556', () =>
260
+ nativeRun({
261
+ root: workspaceRoot,
262
+ platform: 'android',
263
+ port,
264
+ spawn: (_executable, argv) => calls.push(argv),
265
+ })
266
+ )
267
+ )
268
+ expect(calls).toHaveLength(1)
269
+ expect(calls[0]).toEqual(expect.arrayContaining(['--device', 'emulator-5556']))
270
+ })
169
271
  })