vxrn 2.10.0 → 2.11.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 (78) hide show
  1. package/dist/config/getBaseVitePlugins.mjs +3 -1
  2. package/dist/config/getBaseVitePlugins.mjs.map +1 -1
  3. package/dist/config/getBaseVitePlugins.native.js +3 -1
  4. package/dist/config/getBaseVitePlugins.native.js.map +1 -1
  5. package/dist/constants.mjs +23 -1
  6. package/dist/constants.mjs.map +1 -1
  7. package/dist/constants.native.js +23 -1
  8. package/dist/constants.native.js.map +1 -1
  9. package/dist/exports/build.mjs +6 -4
  10. package/dist/exports/build.mjs.map +1 -1
  11. package/dist/exports/build.native.js +6 -4
  12. package/dist/exports/build.native.js.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.mjs +2 -1
  15. package/dist/index.native.js +2 -1
  16. package/dist/plugins/reactNativeDevServer.mjs +7 -5
  17. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  18. package/dist/plugins/reactNativeDevServer.native.js +8 -5
  19. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  20. package/dist/rn-commands/bundle/buildBundle.mjs +9 -0
  21. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  22. package/dist/rn-commands/bundle/buildBundle.native.js +9 -0
  23. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  24. package/dist/runtime/native-prelude.mjs +7 -1
  25. package/dist/runtime/native-prelude.mjs.map +1 -1
  26. package/dist/runtime/native-prelude.native.js +7 -1
  27. package/dist/runtime/native-prelude.native.js.map +1 -1
  28. package/dist/utils/compileNativeDevBytecode.mjs +13 -3
  29. package/dist/utils/compileNativeDevBytecode.mjs.map +1 -1
  30. package/dist/utils/compileNativeDevBytecode.native.js +13 -3
  31. package/dist/utils/compileNativeDevBytecode.native.js.map +1 -1
  32. package/dist/utils/createNativeDevEngine.mjs +375 -173
  33. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  34. package/dist/utils/createNativeDevEngine.native.js +429 -193
  35. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  36. package/dist/utils/environmentUtils.mjs +1 -1
  37. package/dist/utils/environmentUtils.mjs.map +1 -1
  38. package/dist/utils/environmentUtils.native.js +1 -1
  39. package/dist/utils/environmentUtils.native.js.map +1 -1
  40. package/dist/utils/scanDepsToOptimize.mjs +37 -15
  41. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  42. package/dist/utils/scanDepsToOptimize.native.js +45 -15
  43. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/config/getBaseVitePlugins.ts +2 -0
  46. package/src/constants.ts +24 -0
  47. package/src/exports/build.lifecycle.test.ts +2 -1
  48. package/src/exports/build.ts +11 -4
  49. package/src/index.ts +1 -0
  50. package/src/plugins/reactNativeCommonJsPlugin.ts +1 -1
  51. package/src/plugins/reactNativeDevServer.ts +19 -8
  52. package/src/rn-commands/bundle/buildBundle.ts +13 -1
  53. package/src/runtime/native-prelude.ts +7 -1
  54. package/src/types.ts +1 -1
  55. package/src/utils/compileNativeDevBytecode.test.ts +8 -8
  56. package/src/utils/compileNativeDevBytecode.ts +22 -2
  57. package/src/utils/createNativeDevEngine.test.ts +52 -25
  58. package/src/utils/createNativeDevEngine.ts +567 -254
  59. package/src/utils/environmentUtils.ts +6 -1
  60. package/src/utils/scanDepsToOptimize.test.ts +46 -2
  61. package/src/utils/scanDepsToOptimize.ts +78 -18
  62. package/types/config/getBaseVitePlugins.d.ts.map +1 -1
  63. package/types/constants.d.ts +2 -0
  64. package/types/constants.d.ts.map +1 -1
  65. package/types/exports/build.d.ts.map +1 -1
  66. package/types/index.d.ts +1 -0
  67. package/types/index.d.ts.map +1 -1
  68. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  69. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  70. package/types/runtime/native-prelude.d.ts.map +1 -1
  71. package/types/types.d.ts +1 -1
  72. package/types/types.d.ts.map +1 -1
  73. package/types/utils/compileNativeDevBytecode.d.ts +2 -1
  74. package/types/utils/compileNativeDevBytecode.d.ts.map +1 -1
  75. package/types/utils/createNativeDevEngine.d.ts +11 -5
  76. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  77. package/types/utils/environmentUtils.d.ts.map +1 -1
  78. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { createHash } from 'node:crypto'
10
- import { basename, dirname, extname, join, relative, resolve } from 'node:path'
10
+ import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'
11
11
  import {
12
12
  copyFileSync,
13
13
  existsSync,
@@ -28,7 +28,13 @@ import type { DevEngine } from 'rolldown/experimental'
28
28
  import { loadEnv as loadViteEnv, normalizePath } from 'vite'
29
29
  import { shouldStripFlow, transformHermesAsync } from '@vxrn/compiler'
30
30
  import { resolvePath } from '@vxrn/resolve'
31
+ import {
32
+ desktopReactNativeFile,
33
+ requireOnePackageRoot,
34
+ type DesktopPlatform,
35
+ } from '@vxrn/utils/desktopReactNativeResolve'
31
36
  import { nativeAppDefines } from '@vxrn/utils/nativeAppManifest'
37
+ import type { NativePlatform } from '@vxrn/utils/platformEnv'
32
38
  import { withExpoPublicEnvAliases } from '@vxrn/utils/publicEnv'
33
39
  import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
34
40
  import { getNativePrelude } from '../runtime/native-prelude'
@@ -64,15 +70,17 @@ export function getHermesSWCIncludes(dev: boolean): string[] {
64
70
 
65
71
  export interface NativePluginContext {
66
72
  root: string
67
- platform: 'ios' | 'android'
73
+ platform: NativePlatform
68
74
  dev: boolean
75
+ command?: 'serve' | 'build'
76
+ assetsDest?: string
69
77
  }
70
78
 
71
79
  interface NativeDevEngineOptions {
72
80
  root: string
73
81
  port: number
74
82
  host?: string
75
- platform: 'ios' | 'android'
83
+ platform: NativePlatform
76
84
  serverUrl?: string
77
85
  plugins?: Plugin[]
78
86
  onHmrUpdate?: (update: NativeHmrUpdate) => void
@@ -109,19 +117,34 @@ interface NativeDevEngineResult {
109
117
  close: () => Promise<void>
110
118
  }
111
119
 
112
- // shared resolve extensions for native builds
113
- function getResolveExtensions(platform: 'ios' | 'android'): string[] {
114
- const platformExts =
115
- platform === 'ios'
116
- ? ['.ios.tsx', '.ios.ts', '.ios.jsx', '.ios.js']
117
- : ['.android.tsx', '.android.ts', '.android.jsx', '.android.js']
118
- const nativeExts = ['.native.tsx', '.native.ts', '.native.jsx', '.native.js']
119
- const defaultExts = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json']
120
- return [...platformExts, ...nativeExts, ...defaultExts]
120
+ const NATIVE_PLATFORM_EXTENSIONS: Record<NativePlatform, string[]> = {
121
+ ios: ['.ios.tsx', '.ios.ts', '.ios.jsx', '.ios.js'],
122
+ android: ['.android.tsx', '.android.ts', '.android.jsx', '.android.js'],
123
+ macos: ['.macos.tsx', '.macos.ts', '.macos.jsx', '.macos.js'],
124
+ linux: ['.linux.tsx', '.linux.ts', '.linux.jsx', '.linux.js'],
125
+ }
126
+
127
+ const NATIVE_SHARED_EXTENSIONS = [
128
+ '.native.tsx',
129
+ '.native.ts',
130
+ '.native.jsx',
131
+ '.native.js',
132
+ ]
133
+
134
+ const PLAIN_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json']
135
+
136
+ // shared resolve extensions for native builds. macos and linux do not list .ios:
137
+ // that fallback is only for files inside the react-native package.
138
+ function getResolveExtensions(platform: NativePlatform): string[] {
139
+ return [
140
+ ...NATIVE_PLATFORM_EXTENSIONS[platform],
141
+ ...NATIVE_SHARED_EXTENSIONS,
142
+ ...PLAIN_EXTENSIONS,
143
+ ]
121
144
  }
122
145
 
123
146
  // shared rolldown resolve config for native builds
124
- function getNativeResolveConfig(platform: 'ios' | 'android') {
147
+ function getNativeResolveConfig(platform: NativePlatform) {
125
148
  return {
126
149
  extensions: getResolveExtensions(platform),
127
150
  // Rolldown supplies `import`, `require`, and `default` contextually. Adding
@@ -138,7 +161,7 @@ function getNativeResolveConfig(platform: 'ios' | 'android') {
138
161
 
139
162
  // shared rolldown transform config for native builds
140
163
  export function getNativeTransformConfig(
141
- platform: 'ios' | 'android',
164
+ platform: NativePlatform,
142
165
  dev: boolean,
143
166
  root: string
144
167
  ) {
@@ -254,6 +277,128 @@ export function getNativeTransformConfig(
254
277
  }
255
278
  }
256
279
 
280
+ function desktopReactNativeResolvePlugin(root: string, platform: DesktopPlatform): Plugin {
281
+ const oneRoot = requireOnePackageRoot(root, platform)
282
+ return {
283
+ name: `vxrn:${platform}-react-native-resolve`,
284
+ resolveId(source, importer) {
285
+ if (!importer) return
286
+ const file = desktopReactNativeFile(importer, source, oneRoot, platform)
287
+ if (file) return file
288
+ },
289
+ }
290
+ }
291
+
292
+ // third-party packages often ship a component as .ios and .android only.
293
+ // desktop platforms must fail at bundle time when no supported implementation exists.
294
+ const PLATFORM_VARIANT_EXTS = [
295
+ '.ios.tsx',
296
+ '.ios.ts',
297
+ '.ios.jsx',
298
+ '.ios.js',
299
+ '.android.tsx',
300
+ '.android.ts',
301
+ '.android.jsx',
302
+ '.android.js',
303
+ '.web.tsx',
304
+ '.web.ts',
305
+ '.web.jsx',
306
+ '.web.js',
307
+ ]
308
+
309
+ function desktopNormalFile(
310
+ target: string,
311
+ source: string,
312
+ platform: DesktopPlatform
313
+ ): boolean {
314
+ if (/\.(?:tsx|ts|jsx|js|mjs|cjs|json)$/.test(source)) return existsSync(target)
315
+ const extensions = getResolveExtensions(platform)
316
+ for (const ext of extensions) {
317
+ if (existsSync(target + ext)) return true
318
+ if (existsSync(join(target, 'index' + ext))) return true
319
+ }
320
+ return false
321
+ }
322
+
323
+ function desktopVariantSource(target: string): string | null {
324
+ for (const ext of PLATFORM_VARIANT_EXTS) {
325
+ const file = target + ext
326
+ if (existsSync(file)) return file
327
+ }
328
+ return null
329
+ }
330
+
331
+ function desktopPackageImportLabel(variantFile: string): string {
332
+ let dir = dirname(variantFile)
333
+ for (let i = 0; i < 8; i++) {
334
+ const manifest = join(dir, 'package.json')
335
+ if (existsSync(manifest)) {
336
+ try {
337
+ const name = JSON.parse(readFileSync(manifest, 'utf8')).name
338
+ if (typeof name === 'string' && name.length > 0) {
339
+ const rel = relative(dir, variantFile)
340
+ .split('\\')
341
+ .join('/')
342
+ .replace(/\.(?:ios|android|web)\.(?:tsx|ts|jsx|js)$/, '')
343
+ return `${name}/${rel}`
344
+ }
345
+ } catch {
346
+ // keep walking to a readable manifest
347
+ }
348
+ }
349
+ const parent = dirname(dir)
350
+ if (parent === dir) break
351
+ dir = parent
352
+ }
353
+ return variantFile
354
+ }
355
+
356
+ function desktopUnsupportedImportPlugin(platform: DesktopPlatform): Plugin {
357
+ const label = platform === 'macos' ? 'macOS' : 'Linux'
358
+ return {
359
+ name: `vxrn:${platform}-unsupported-import`,
360
+ onLog(_level, log) {
361
+ // rolldown otherwise warns and emits an external ESM import. native
362
+ // hosts evaluate scripts and cannot load that import at runtime.
363
+ if (log.code === 'UNRESOLVED_IMPORT') {
364
+ this.error({
365
+ ...log,
366
+ message: `[vxrn] ${label} bundle cannot resolve an import: ${log.message}`,
367
+ })
368
+ }
369
+ },
370
+ resolveId(source, importer) {
371
+ if (!importer || !source.startsWith('.') || source.includes('\0')) return
372
+ if (source.includes('?') || source.includes('#')) return
373
+ const target = resolve(dirname(importer), source)
374
+ if (desktopNormalFile(target, source, platform)) return
375
+ const variant = desktopVariantSource(target)
376
+ if (!variant) return
377
+ this.error(
378
+ `[vxrn] ${desktopPackageImportLabel(variant)} has no ${label} implementation: cannot resolve import "${source}" from "${importer}". Add a .${platform}, .native or plain file.`
379
+ )
380
+ },
381
+ }
382
+ }
383
+
384
+ // built packages import siblings by their full name (./one.native.js), so
385
+ // .macos and .linux resolution never sees them. when the build emitted a
386
+ // .<platform>.native.js beside the target, that platform loads that file
387
+ // instead. the variant itself still imports the shared file.
388
+ function desktopDistVariantPlugin(platform: DesktopPlatform): Plugin {
389
+ return {
390
+ name: `vxrn:${platform}-dist-variant`,
391
+ resolveId(source, importer) {
392
+ if (!importer || !source.startsWith('.') || !source.endsWith('.native.js')) return
393
+ if (source.endsWith(`.${platform}.native.js`)) return
394
+ const target = resolve(dirname(importer), source)
395
+ const variant = target.replace(/\.native\.js$/, `.${platform}.native.js`)
396
+ if (variant === importer || !existsSync(variant)) return
397
+ return variant
398
+ },
399
+ }
400
+ }
401
+
257
402
  // shared plugins used by both dev and prod native builds
258
403
  function getNativePlugins(
259
404
  root: string,
@@ -264,12 +409,30 @@ function getNativePlugins(
264
409
  onAsset?: (asset: NativeAssetData) => void,
265
410
  sourceMaps = false,
266
411
  userPlugins: Plugin[] = [],
267
- minify = false
412
+ minify = false,
413
+ command: 'serve' | 'build' = 'serve'
268
414
  ): Plugin[] {
269
- const context: NativePluginContext = { root, platform, dev }
270
- return [
415
+ const context: NativePluginContext = { root, platform, dev, command, assetsDest }
416
+ const desktopPlugins =
417
+ platform === 'macos' || platform === 'linux'
418
+ ? [
419
+ desktopReactNativeResolvePlugin(root, platform),
420
+ desktopDistVariantPlugin(platform),
421
+ desktopUnsupportedImportPlugin(platform),
422
+ ]
423
+ : []
424
+ // evaluate One's factory once, in its existing position after desktop setup.
425
+ const globalPlugins = globalThis.__vxrnAddNativePlugins?.(platform) || []
426
+ const trace: NativeCompilerTrace | undefined =
427
+ dev && command === 'serve'
428
+ ? [...globalPlugins, ...userPlugins].find(
429
+ (plugin) => plugin.api?.vxrnNativeCompilerTrace
430
+ )?.api.vxrnNativeCompilerTrace
431
+ : undefined
432
+ const plugins = [
433
+ ...desktopPlugins,
271
434
  // plugins provided by One (clientTreeShakePlugin for loader removal, etc.)
272
- ...(globalThis.__vxrnAddNativePlugins?.(platform) || []),
435
+ ...globalPlugins,
273
436
  // block .server.* and _middleware.* files from entering the native bundle
274
437
  serverFileExclusionPlugin(),
275
438
  // guard server-only / client-only / web-only / native-only imports
@@ -300,7 +463,7 @@ function getNativePlugins(
300
463
  rnCodegenPlugin({ projectRoot: root }),
301
464
  // lower Flow before worklets and react compiler parse the source with oxc.
302
465
  flowStripPlugin(),
303
- vxrnCompilerPlugin(platform, dev, root, sourceMaps),
466
+ vxrnCompilerPlugin(platform, dev, root, sourceMaps, trace),
304
467
  mixedNativeModulePlugin(sourceMaps),
305
468
  // guard undefined native methods in NativeAnimatedHelper
306
469
  nativeAnimatedGuardPlugin(),
@@ -317,6 +480,83 @@ function getNativePlugins(
317
480
  ].map((plugin: Plugin) =>
318
481
  plugin.api?.vxrnNative ? plugin.api.vxrnNative(context) : plugin
319
482
  )
483
+ if (!trace) return plugins
484
+
485
+ // temporary fixture-only diagnostics. keep the existing pre hooks in place;
486
+ // observe their returned promises without replacing them or awaiting again.
487
+ const start = (id: string, stage: string) => {
488
+ try {
489
+ const record = trace(id)
490
+ if (!record) return
491
+ const mark = (status: 'start' | 'end' | 'error') => {
492
+ try {
493
+ record(stage, status)
494
+ } catch {
495
+ // capture must not affect the hook.
496
+ }
497
+ }
498
+ mark('start')
499
+ return mark
500
+ } catch {
501
+ // capture must not affect the hook.
502
+ }
503
+ }
504
+ const traced = plugins.map((plugin, index) => {
505
+ if (
506
+ plugin.name !== 'one:use-dom-native' &&
507
+ plugin.name !== 'vxrn:rn-codegen' &&
508
+ plugin.name !== 'one:background-computation:native' &&
509
+ plugin.name !== 'one-client-tree-shake'
510
+ )
511
+ return plugin
512
+ const hook = plugin.transform
513
+ if (!hook || typeof hook === 'function' || hook.order !== 'pre') return plugin
514
+ return {
515
+ ...plugin,
516
+ transform: {
517
+ ...hook,
518
+ handler(this: any, ...args: any[]) {
519
+ const mark = start(args[1], `pre:${index}:${plugin.name}`)
520
+ if (!mark) return hook.handler.apply(this, args)
521
+ try {
522
+ const result = hook.handler.apply(this, args)
523
+ if (result instanceof Promise) {
524
+ // use the native observer, never a user-supplied .then override.
525
+ // its rejection handler consumes only the observer's branch.
526
+ try {
527
+ Promise.prototype.then.call(
528
+ result,
529
+ () => mark('end'),
530
+ () => mark('error')
531
+ )
532
+ } catch {
533
+ // an observer failure must not replace the original promise.
534
+ }
535
+ } else {
536
+ mark('end')
537
+ }
538
+ return result
539
+ } catch (error) {
540
+ mark('error')
541
+ throw error
542
+ }
543
+ },
544
+ },
545
+ }
546
+ })
547
+ return [
548
+ {
549
+ name: 'test-native-hmr-trace-first-pre',
550
+ transform: {
551
+ order: 'pre',
552
+ handler(_code: string, id: string) {
553
+ start(id, 'pre:first')?.('end')
554
+ return null
555
+ },
556
+ },
557
+ },
558
+ ...traced,
559
+ ]
320
560
  }
321
561
 
322
562
  // shared output options for native builds. `minify: false` keeps rolldown's
@@ -455,13 +695,11 @@ function interopNodeModeSpan(
455
695
  function isInteropHelperCallee(callee: InteropSyntaxNode): boolean {
456
696
  if (callee.type === 'Identifier') {
457
697
  return (
458
- typeof callee['name'] === 'string' &&
459
- NATIVE_INTEROP_HELPER.test(callee['name'])
698
+ typeof callee['name'] === 'string' && NATIVE_INTEROP_HELPER.test(callee['name'])
460
699
  )
461
700
  }
462
701
  if (
463
- (callee.type === 'MemberExpression' ||
464
- callee.type === 'StaticMemberExpression') &&
702
+ (callee.type === 'MemberExpression' || callee.type === 'StaticMemberExpression') &&
465
703
  callee['computed'] !== true
466
704
  ) {
467
705
  const property = callee['property']
@@ -597,6 +835,15 @@ export function postProcessNativeBundle(code: string): string {
597
835
  return code
598
836
  }
599
837
 
838
+ // the dev prelude ends with this line, which rolldown emits directly before its
839
+ // runtime. the module scope and the runtime transform locate themselves by text
840
+ // vxrn writes because rolldown's module comments change between releases
841
+ // (1.2.15 dropped `//#region`/`//#endregion`).
842
+ export const NATIVE_MODULE_SCOPE_MARKER = '// vxrn: native module scope'
843
+ // the last statement of getHmrRuntimeSource, which closes rolldown's runtime
844
+ export const NATIVE_DEV_RUNTIME_END =
845
+ 'globalThis.__rolldown_runtime__ = new ReactNativeDevRuntime();'
846
+
600
847
  /**
601
848
  * wrap dev module declarations in a function scope. script-level declarations
602
849
  * create global properties that can block react native's lazy polyfills.
@@ -607,10 +854,12 @@ export function postProcessNativeBundle(code: string): string {
607
854
  * inside this closure so it can reach module bindings and runtime helpers.
608
855
  */
609
856
  export function wrapNativeBundleModuleScope(code: string): string {
610
- // the prelude (intro) ends right before the rolldown runtime region
611
- const marker = '//#region \\0rolldown/runtime.js'
612
- const idx = code.indexOf(marker)
613
- if (idx === -1) return code
857
+ const idx = code.indexOf(NATIVE_MODULE_SCOPE_MARKER)
858
+ if (idx === -1) {
859
+ throw new Error(
860
+ '[vxrn] the dev bundle lost the end of its prelude, so its modules would run at script scope'
861
+ )
862
+ }
614
863
 
615
864
  // the opener shares the marker's line (the rest of that line is a comment) so
616
865
  // the wrap costs no lines, and the closer only appends past the last mapped
@@ -619,22 +868,22 @@ export function wrapNativeBundleModuleScope(code: string): string {
619
868
  }
620
869
 
621
870
  /**
622
- * Downlevel class fields in the rolldown runtime for Hermes compatibility.
871
+ * Downlevel class fields in the rolldown dev runtime for Hermes compatibility.
623
872
  * The runtime (\0rolldown/runtime.js) is injected directly into the output,
624
873
  * bypassing hermesCompatSWCPlugin. We extract just that section (~5KB) and
625
- * transform it rather than re-parsing the entire 6MB bundle.
874
+ * transform it rather than re-parsing the entire 6MB bundle. Only the dev
875
+ * runtime has classes; the production runtime is plain helper functions.
626
876
  */
627
877
  async function downlevelClassFieldsInBundle(code: string): Promise<string> {
628
- const startMarker = '//#region \\0rolldown/runtime.js'
629
- const endMarker = '//#endregion'
630
-
631
- const startIdx = code.indexOf(startMarker)
632
- if (startIdx === -1) return code
633
-
634
- const endIdx = code.indexOf(endMarker, startIdx)
635
- if (endIdx === -1) return code
878
+ const markerIdx = code.indexOf(NATIVE_MODULE_SCOPE_MARKER)
879
+ const endIdx = markerIdx === -1 ? -1 : code.indexOf(NATIVE_DEV_RUNTIME_END, markerIdx)
880
+ if (endIdx === -1) {
881
+ throw new Error('[vxrn] could not locate the rolldown runtime in the dev bundle')
882
+ }
636
883
 
637
- const runtimeEnd = endIdx + endMarker.length
884
+ // the marker line stays outside the transform so the scope wrapper finds it
885
+ const startIdx = code.indexOf('\n', markerIdx) + 1
886
+ const runtimeEnd = endIdx + NATIVE_DEV_RUNTIME_END.length
638
887
  const runtimeSection = code.slice(startIdx, runtimeEnd)
639
888
  const originalNewlines = runtimeSection.match(/\n/g)?.length ?? 0
640
889
 
@@ -648,16 +897,11 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
648
897
  if (result.errors?.length) {
649
898
  throw new Error(result.errors.map((e) => e.message).join('\n'))
650
899
  }
651
- const transformedCode = result.code.trimEnd()
652
- // SWC minification strips comments, but the dev bundle scope wrapper uses
653
- // this generated-runtime marker as its structural boundary.
654
- const transformed = transformedCode.includes(startMarker)
655
- ? transformedCode
656
- : `${startMarker}\n${transformedCode}`
900
+ const transformed = result.code.trimEnd()
657
901
  const transformedNewlines = transformed.match(/\n/g)?.length ?? 0
658
902
  if (transformedNewlines > originalNewlines) {
659
903
  throw new Error(
660
- '[vxrn] Hermes runtime transform added lines and would invalidate the production source map'
904
+ '[vxrn] Hermes runtime transform added lines and would invalidate the dev source map'
661
905
  )
662
906
  }
663
907
  const linePreservingRuntime =
@@ -669,14 +913,13 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
669
913
  * Run the native post-processing passes inside the bundle, where rolldown still
670
914
  * hands them unminified output.
671
915
  *
672
- * Rolldown minifies a chunk *after* plugin `renderChunk` hooks, and every pass
673
- * below reads the shape rolldown generates: `normalizeNativeCommonJSInterop`
674
- * matches `__toESM(require_x(), 1)` by name, and `downlevelClassFieldsInBundle`
675
- * locates the runtime by its `//#region` comment. Minification mangles the
676
- * first and strips the second, so running these on the finished chunk would
677
- * silently stop applying them as soon as `--minify` is on — a production bundle
678
- * whose CommonJS interop differs from the dev one. This hook is what keeps a
679
- * minified and an unminified bundle the same bundle.
916
+ * Rolldown minifies a chunk *after* plugin `renderChunk` hooks, and the passes
917
+ * read the shape rolldown generates: `normalizeNativeCommonJSInterop` matches
918
+ * `__toESM(require_x(), 1)` by name. Minification mangles that, so running the
919
+ * passes on the finished chunk would silently stop applying them as soon as
920
+ * `--minify` is on — a production bundle whose CommonJS interop differs from
921
+ * the dev one. This hook is what keeps a minified and an unminified bundle the
922
+ * same bundle.
680
923
  *
681
924
  * `map: null` records what the passes guarantee: they replace syntax in place
682
925
  * without moving a line, so rolldown's own map still describes the result.
@@ -684,8 +927,8 @@ async function downlevelClassFieldsInBundle(code: string): Promise<string> {
684
927
  function nativeBundlePostProcessPlugin(): Plugin {
685
928
  return {
686
929
  name: 'vxrn:native-post-process',
687
- async renderChunk(code) {
688
- const processed = await downlevelClassFieldsInBundle(postProcessNativeBundle(code))
930
+ renderChunk(code) {
931
+ const processed = postProcessNativeBundle(code)
689
932
  assertBundleLinesPreserved(code, processed, 'production')
690
933
  return { code: processed, map: null }
691
934
  },
@@ -789,7 +1032,7 @@ export async function createNativeDevEngine(
789
1032
  // generated once per full bundle build, not per edit: rebuildStrategy is
790
1033
  // 'never', so Fast Refresh patches never re-enter this path. a dev bundle
791
1034
  // is never minified.
792
- ...getNativeOutputOptions(prelude, true, false),
1035
+ ...getNativeOutputOptions(`${prelude}\n${NATIVE_MODULE_SCOPE_MARKER}`, true, false),
793
1036
  // open the HMR socket with RN's WebSocket module (not the global, which is
794
1037
  // only polyfilled once setup-env has run) and hand it to the runtime,
795
1038
  // which owns every message it carries. handed over immediately rather than
@@ -952,6 +1195,12 @@ try {
952
1195
  // skipped by the per-file SWC plugin) so old Hermes can parse them
953
1196
  code = await downlevelClassFieldsInBundle(code)
954
1197
  assertBundleLinesPreserved(chunk.code, code, 'dev')
1198
+
1199
+ // wrap module code in a function scope so top-level `var`s (e.g. RN
1200
+ // fetch.js's `Headers`/`Request`) don't leak as non-configurable
1201
+ // globals and break RN's polyfillGlobal (dev-only redbox). see fn doc.
1202
+ // it only appends past the last mapped line, so it needs no assertion.
1203
+ code = wrapNativeBundleModuleScope(code)
955
1204
  } catch (error) {
956
1205
  // a post-processing failure is a build failure: without a bundle to
957
1206
  // serve, record it and wake the waiting request with its message
@@ -973,12 +1222,6 @@ try {
973
1222
  return
974
1223
  }
975
1224
 
976
- // wrap module code in a function scope so top-level `var`s (e.g. RN
977
- // fetch.js's `Headers`/`Request`) don't leak as non-configurable
978
- // globals and break RN's polyfillGlobal (dev-only redbox). see fn doc.
979
- // it only appends past the last mapped line, so it needs no assertion.
980
- code = wrapNativeBundleModuleScope(code)
981
-
982
1225
  if (!chunk.map) {
983
1226
  throw new Error(
984
1227
  '[vxrn] rolldown produced no source map for the dev bundle, so /symbolicate cannot resolve a frame'
@@ -1092,7 +1335,7 @@ try {
1092
1335
 
1093
1336
  interface NativeBuildOptions {
1094
1337
  root: string
1095
- platform: 'ios' | 'android'
1338
+ platform: NativePlatform
1096
1339
  dev?: boolean
1097
1340
  serverUrl?: string
1098
1341
  entryFile?: string
@@ -1162,7 +1405,8 @@ export async function buildNativeBundle(
1162
1405
  undefined,
1163
1406
  sourcemap,
1164
1407
  userPlugins,
1165
- minify
1408
+ minify,
1409
+ 'build'
1166
1410
  ),
1167
1411
  // last, so it sees what every other plugin produced
1168
1412
  nativeBundlePostProcessPlugin(),
@@ -1217,7 +1461,7 @@ function nativeVirtualEntryPlugin(
1217
1461
  root: string,
1218
1462
  opts: {
1219
1463
  dev?: boolean
1220
- platform: 'ios' | 'android'
1464
+ platform: NativePlatform
1221
1465
  dynamicRoutes?: boolean
1222
1466
  }
1223
1467
  ): { plugin: Plugin; routes: NativeRouteRegistry } {
@@ -1236,8 +1480,21 @@ function nativeVirtualEntryPlugin(
1236
1480
  const setupFileImport = (() => {
1237
1481
  const sf = entryConfig.setupFile
1238
1482
  if (!sf) return ''
1239
- // resolve which file to use for ios (covers both formats)
1240
- const file = typeof sf === 'string' ? sf : 'native' in sf ? sf.native : sf.ios
1483
+ // string and native apply to every native platform. macos and linux do not borrow the ios file.
1484
+ const perPlatform = sf as {
1485
+ ios?: string
1486
+ android?: string
1487
+ macos?: string
1488
+ linux?: string
1489
+ }
1490
+ const file =
1491
+ typeof sf === 'string'
1492
+ ? sf
1493
+ : 'native' in sf
1494
+ ? sf.native
1495
+ : opts.platform === 'macos' || opts.platform === 'linux'
1496
+ ? perPlatform[opts.platform]
1497
+ : sf.ios
1241
1498
  if (!file) return ''
1242
1499
  // file:// URL is the canonical specifier; bare Windows absolute path is not
1243
1500
  const resolved = pathToFileURL(resolve(root, file)).href
@@ -1568,7 +1825,7 @@ export function hmrClientNoopPlugin(): Plugin {
1568
1825
  }
1569
1826
  }
1570
1827
 
1571
- function nativeIdentityAliasPlugin(platform: 'ios' | 'android'): Plugin {
1828
+ function nativeIdentityAliasPlugin(platform: NativePlatform): Plugin {
1572
1829
  const packages = new Map<
1573
1830
  string,
1574
1831
  { root: string; aliases: Record<string, unknown> } | null
@@ -1622,10 +1879,7 @@ function nativeIdentityAliasPlugin(platform: 'ios' | 'android'): Plugin {
1622
1879
  // `react-native` would give native its web build. the browser fallback is for
1623
1880
  // packages without a native target: where a package's exports name both,
1624
1881
  // resolve it as metro does, without `browser`.
1625
- function nativeExportsPrecedencePlugin(
1626
- root: string,
1627
- platform: 'ios' | 'android'
1628
- ): Plugin {
1882
+ function nativeExportsPrecedencePlugin(root: string, platform: NativePlatform): Plugin {
1629
1883
  const manifests = new Map<string, { name?: string; exports?: unknown } | null>()
1630
1884
  const readManifest = (directory: string) => {
1631
1885
  if (!manifests.has(directory)) {
@@ -1677,7 +1931,7 @@ function nativeExportsPrecedencePlugin(
1677
1931
  }
1678
1932
  }
1679
1933
 
1680
- function nativeDeclarationAliasPlugin(root: string, platform: 'ios' | 'android'): Plugin {
1934
+ function nativeDeclarationAliasPlugin(root: string, platform: NativePlatform): Plugin {
1681
1935
  const resolvers = new Map<string, import('rolldown/experimental').ResolverFactory>()
1682
1936
  return {
1683
1937
  name: 'vxrn:declaration-alias',
@@ -1841,6 +2095,12 @@ function cssStubPlugin(): Plugin {
1841
2095
  }
1842
2096
  }
1843
2097
 
2098
+ // temporary diagnostic hook consumed by the opt-in HMR test trace. callbacks are
2099
+ // synchronous and best-effort; they must never alter compiler results or errors.
2100
+ export type NativeCompilerTrace = (
2101
+ id: string
2102
+ ) => ((stage: string, status: 'start' | 'end' | 'error') => void) | undefined
2103
+
1844
2104
  /**
1845
2105
  * pipe files through @vxrn/compiler's babel transforms.
1846
2106
  * handles reanimated worklet compilation, async generator downleveling,
@@ -1850,7 +2110,8 @@ export function vxrnCompilerPlugin(
1850
2110
  platform: string,
1851
2111
  dev: boolean,
1852
2112
  projectRoot = process.cwd(),
1853
- sourceMaps = false
2113
+ sourceMaps = false,
2114
+ trace?: NativeCompilerTrace
1854
2115
  ): Plugin {
1855
2116
  let compiler: typeof import('@vxrn/compiler') | null = null
1856
2117
 
@@ -1867,148 +2128,151 @@ export function vxrnCompilerPlugin(
1867
2128
  if (!/\.[cm]?[jt]sx?$/.test(id)) return
1868
2129
  if (id.includes('\0') || id.includes('virtual:')) return
1869
2130
 
1870
- const needsRefresh = isRefreshCandidate(id)
1871
-
1872
- if (!compiler) compiler = await import('@vxrn/compiler')
1873
-
1874
- const props = {
1875
- id,
1876
- code,
1877
- projectRoot,
1878
- development: dev,
1879
- environment: platform as 'ios' | 'android',
1880
- reactForRNVersion: '19' as const,
2131
+ let active = 'transform'
2132
+ let failed = false
2133
+ let mark: ((stage: string, status: 'start' | 'end' | 'error') => void) | undefined
2134
+ if (trace) {
2135
+ try {
2136
+ const record = trace(id)
2137
+ if (record) {
2138
+ mark = (stage, status) => {
2139
+ active = status === 'start' ? stage : 'transform'
2140
+ try {
2141
+ record(stage, status)
2142
+ } catch {
2143
+ // capture must not affect compilation.
2144
+ }
2145
+ }
2146
+ }
2147
+ } catch {
2148
+ // capture must not affect compilation.
2149
+ }
1881
2150
  }
2151
+ mark?.('transform', 'start')
2152
+ try {
2153
+ const needsRefresh = isRefreshCandidate(id)
1882
2154
 
1883
- let babelOptions = compiler.getBabelOptions(props)
2155
+ if (!compiler) {
2156
+ mark?.('compiler-import', 'start')
2157
+ compiler = await import('@vxrn/compiler')
2158
+ mark?.('compiler-import', 'end')
2159
+ }
1884
2160
 
1885
- let curCode = code
1886
- const intermediateMaps: any[] = []
2161
+ const props = {
2162
+ id,
2163
+ code,
2164
+ projectRoot,
2165
+ development: dev,
2166
+ // the compiler's native gate is ios or android. macos and linux use the
2167
+ // ios transforms. module resolution stays .<platform> and does not follow .ios.
2168
+ environment: (platform === 'android' ? 'android' : 'ios') as 'ios' | 'android',
2169
+ reactForRNVersion: '19' as const,
2170
+ }
1887
2171
 
1888
- const isWorkletPlugin = (entry: any) => {
1889
- if (!entry) return false
1890
- const name =
1891
- typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0] : null
1892
- return (
1893
- typeof name === 'string' &&
1894
- (name.includes('react-native-worklets') ||
1895
- name.includes('react-native-reanimated'))
1896
- )
1897
- }
1898
- const workletEntry = babelOptions?.plugins?.find(isWorkletPlugin)
1899
- const workletPluginOptions = Array.isArray(workletEntry)
1900
- ? workletEntry[1]
1901
- : undefined
1902
- const useWorklets =
1903
- compiler.isNativeWorkletsEnabled() &&
1904
- (Boolean(workletEntry) || compiler.shouldTransformWorklets({ id, code }))
1905
- const compilerPluginIndex =
1906
- babelOptions?.plugins?.findIndex(
1907
- (x) => Array.isArray(x) && x[0] === 'babel-plugin-react-compiler'
1908
- ) ?? -1
1909
-
1910
- if (compilerPluginIndex !== -1) {
1911
- // preserve automatic worklet candidates before react compiler hoists them.
1912
- if (useWorklets) {
1913
- const prepared = compiler.prepareWorkletsForReactCompiler(
1914
- id,
1915
- curCode,
1916
- sourceMaps
2172
+ mark?.('options', 'start')
2173
+ let babelOptions = compiler.getBabelOptions(props)
2174
+ mark?.('options', 'end')
2175
+
2176
+ let curCode = code
2177
+ const intermediateMaps: any[] = []
2178
+
2179
+ const isWorkletPlugin = (entry: any) => {
2180
+ if (!entry) return false
2181
+ const name =
2182
+ typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0] : null
2183
+ return (
2184
+ typeof name === 'string' &&
2185
+ (name.includes('react-native-worklets') ||
2186
+ name.includes('react-native-reanimated'))
1917
2187
  )
1918
- if (prepared) {
1919
- curCode = prepared.code
1920
- if (prepared.map) intermediateMaps.push(prepared.map)
1921
- }
1922
2188
  }
1923
- const compilerPluginConfig = babelOptions!.plugins![compilerPluginIndex] as any[]
1924
- const compilerPluginOptions = compilerPluginConfig[1] || {}
1925
- const useBabelCompiler =
1926
- compilerPluginOptions.compiler === 'babel' ||
1927
- process.env.ONE_REACT_COMPILER === 'babel' ||
1928
- process.env.VXRN_REACT_COMPILER === 'babel'
1929
-
1930
- if (!useBabelCompiler) {
1931
- const compilerOut = await compiler.transformOxcReactCompiler(
1932
- id,
1933
- curCode,
1934
- compilerPluginOptions,
1935
- sourceMaps
1936
- )
1937
- if (compilerOut?.code) {
1938
- curCode = compilerOut.code
1939
- if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map)
2189
+ const workletEntry = babelOptions?.plugins?.find(isWorkletPlugin)
2190
+ const workletPluginOptions = Array.isArray(workletEntry)
2191
+ ? workletEntry[1]
2192
+ : undefined
2193
+ const useWorklets =
2194
+ compiler.isNativeWorkletsEnabled() &&
2195
+ (Boolean(workletEntry) || compiler.shouldTransformWorklets({ id, code }))
2196
+ const compilerPluginIndex =
2197
+ babelOptions?.plugins?.findIndex(
2198
+ (x) => Array.isArray(x) && x[0] === 'babel-plugin-react-compiler'
2199
+ ) ?? -1
2200
+
2201
+ if (compilerPluginIndex !== -1) {
2202
+ // preserve automatic worklet candidates before react compiler hoists them.
2203
+ if (useWorklets) {
2204
+ mark?.('worklets-prepare', 'start')
2205
+ const prepared = compiler.prepareWorkletsForReactCompiler(
2206
+ id,
2207
+ curCode,
2208
+ sourceMaps
2209
+ )
2210
+ mark?.('worklets-prepare', 'end')
2211
+ if (prepared) {
2212
+ curCode = prepared.code
2213
+ if (prepared.map) intermediateMaps.push(prepared.map)
2214
+ }
1940
2215
  }
1941
- babelOptions!.plugins!.splice(compilerPluginIndex, 1)
1942
- if (
1943
- babelOptions!.plugins!.length === 0 &&
1944
- !babelOptions!.configFile &&
1945
- !babelOptions!.babelrc
1946
- ) {
1947
- babelOptions = null
2216
+ const compilerPluginConfig = babelOptions!.plugins![
2217
+ compilerPluginIndex
2218
+ ] as any[]
2219
+ const compilerPluginOptions = compilerPluginConfig[1] || {}
2220
+ const useBabelCompiler =
2221
+ compilerPluginOptions.compiler === 'babel' ||
2222
+ process.env.ONE_REACT_COMPILER === 'babel' ||
2223
+ process.env.VXRN_REACT_COMPILER === 'babel'
2224
+
2225
+ if (!useBabelCompiler) {
2226
+ mark?.('oxc-react-compiler', 'start')
2227
+ const compilerOut = await compiler.transformOxcReactCompiler(
2228
+ id,
2229
+ curCode,
2230
+ compilerPluginOptions,
2231
+ sourceMaps
2232
+ )
2233
+ mark?.('oxc-react-compiler', 'end')
2234
+ if (compilerOut?.code) {
2235
+ curCode = compilerOut.code
2236
+ if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map)
2237
+ }
2238
+ babelOptions!.plugins!.splice(compilerPluginIndex, 1)
2239
+ if (
2240
+ babelOptions!.plugins!.length === 0 &&
2241
+ !babelOptions!.configFile &&
2242
+ !babelOptions!.babelrc
2243
+ ) {
2244
+ babelOptions = null
2245
+ }
1948
2246
  }
1949
2247
  }
1950
- }
1951
2248
 
1952
- if (useWorklets) {
1953
- const workletOut = await compiler.transformWorklets(id, curCode, sourceMaps, {
1954
- projectRoot,
1955
- ...workletPluginOptions,
1956
- })
1957
- if (workletOut?.code) {
1958
- curCode = workletOut.code
1959
- if (sourceMaps && workletOut.map) intermediateMaps.push(workletOut.map)
1960
- }
1961
- if (babelOptions?.plugins) {
1962
- babelOptions.plugins = babelOptions.plugins.filter(
1963
- (entry) => !isWorkletPlugin(entry)
1964
- )
1965
- if (
1966
- babelOptions.plugins.length === 0 &&
1967
- !babelOptions.configFile &&
1968
- !babelOptions.babelrc
1969
- ) {
1970
- babelOptions = null
2249
+ if (useWorklets) {
2250
+ mark?.('worklets', 'start')
2251
+ const workletOut = await compiler.transformWorklets(id, curCode, sourceMaps, {
2252
+ projectRoot,
2253
+ ...workletPluginOptions,
2254
+ })
2255
+ mark?.('worklets', 'end')
2256
+ if (workletOut?.code) {
2257
+ curCode = workletOut.code
2258
+ if (sourceMaps && workletOut.map) intermediateMaps.push(workletOut.map)
1971
2259
  }
1972
- }
1973
- }
1974
-
1975
- if (!babelOptions && !needsRefresh) {
1976
- if (curCode !== code) {
1977
- let finalMap: any = undefined
1978
- if (sourceMaps && intermediateMaps.length > 0) {
1979
- if (intermediateMaps.length === 1) {
1980
- finalMap = intermediateMaps[0]
1981
- } else {
1982
- const remapping = (await import('@jridgewell/remapping')).default
1983
- finalMap = remapping(intermediateMaps.slice().reverse(), () => null)
2260
+ if (babelOptions?.plugins) {
2261
+ babelOptions.plugins = babelOptions.plugins.filter(
2262
+ (entry) => !isWorkletPlugin(entry)
2263
+ )
2264
+ if (
2265
+ babelOptions.plugins.length === 0 &&
2266
+ !babelOptions.configFile &&
2267
+ !babelOptions.babelrc
2268
+ ) {
2269
+ babelOptions = null
1984
2270
  }
1985
2271
  }
1986
- return { code: curCode, map: finalMap }
1987
- }
1988
- return
1989
- }
1990
-
1991
- if (sourceMaps && babelOptions) {
1992
- // Individual stage maps are collected in intermediateMaps and composed
1993
- // once via remapping([wrapMap, refreshMap, ...intermediateMaps.reverse()]).
1994
- // Do not pass intermediateMaps as inputSourceMap to Babel, which would
1995
- // cause Babel to pre-compose and remapping to double-compose the compiler map.
1996
- babelOptions = {
1997
- ...babelOptions,
1998
- sourceMaps: true,
1999
- sourceFileName: id,
2000
2272
  }
2001
- delete (babelOptions as any).inputSourceMap
2002
- }
2003
2273
 
2004
- if (babelOptions) {
2005
- const result = await compiler.transformBabel(id, curCode, babelOptions)
2006
- if (result?.code) {
2007
- curCode = result.code
2008
- if (sourceMaps && result.map) {
2009
- intermediateMaps.push(result.map)
2010
- }
2011
- if (!needsRefresh) {
2274
+ if (!babelOptions && !needsRefresh) {
2275
+ if (curCode !== code) {
2012
2276
  let finalMap: any = undefined
2013
2277
  if (sourceMaps && intermediateMaps.length > 0) {
2014
2278
  if (intermediateMaps.length === 1) {
@@ -2020,40 +2284,82 @@ export function vxrnCompilerPlugin(
2020
2284
  }
2021
2285
  return { code: curCode, map: finalMap }
2022
2286
  }
2287
+ return
2023
2288
  }
2024
- }
2025
2289
 
2026
- if (needsRefresh) {
2027
- const { transformSync } = await import('oxc-transform-react')
2028
- const jsxImportSource =
2029
- compiler.configuration?.enableNativewind && !id.includes('node_modules')
2030
- ? 'nativewind'
2031
- : 'react'
2032
-
2033
- const res = transformSync(id, curCode, {
2034
- jsx: {
2035
- development: dev,
2036
- runtime: 'automatic',
2037
- importSource: jsxImportSource,
2038
- refresh: {
2039
- refreshReg: '__vxrnRefreshReg',
2040
- refreshSig: '__vxrnRefreshSig',
2041
- },
2042
- },
2043
- reactCompiler: false,
2044
- sourcemap: sourceMaps,
2045
- })
2290
+ if (sourceMaps && babelOptions) {
2291
+ // Individual stage maps are collected in intermediateMaps and composed
2292
+ // once via remapping([wrapMap, refreshMap, ...intermediateMaps.reverse()]).
2293
+ // Do not pass intermediateMaps as inputSourceMap to Babel, which would
2294
+ // cause Babel to pre-compose and remapping to double-compose the compiler map.
2295
+ babelOptions = {
2296
+ ...babelOptions,
2297
+ sourceMaps: true,
2298
+ sourceFileName: id,
2299
+ }
2300
+ delete (babelOptions as any).inputSourceMap
2301
+ }
2046
2302
 
2047
- if (res.fatal) {
2048
- throw new Error(
2049
- `[vxrn:compiler] oxc react transform failed on ${id}: ${(res.errors ?? [])
2050
- .map((e: any) => e.message ?? String(e))
2051
- .join(', ')}`
2052
- )
2303
+ if (babelOptions) {
2304
+ mark?.('babel', 'start')
2305
+ const result = await compiler.transformBabel(id, curCode, babelOptions)
2306
+ mark?.('babel', 'end')
2307
+ if (result?.code) {
2308
+ curCode = result.code
2309
+ if (sourceMaps && result.map) {
2310
+ intermediateMaps.push(result.map)
2311
+ }
2312
+ if (!needsRefresh) {
2313
+ let finalMap: any = undefined
2314
+ if (sourceMaps && intermediateMaps.length > 0) {
2315
+ if (intermediateMaps.length === 1) {
2316
+ finalMap = intermediateMaps[0]
2317
+ } else {
2318
+ const remapping = (await import('@jridgewell/remapping')).default
2319
+ finalMap = remapping(intermediateMaps.slice().reverse(), () => null)
2320
+ }
2321
+ }
2322
+ return { code: curCode, map: finalMap }
2323
+ }
2324
+ }
2053
2325
  }
2054
2326
 
2055
- const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
2056
- const header = `var __prevRefreshReg = globalThis.$RefreshReg$;
2327
+ if (needsRefresh) {
2328
+ mark?.('refresh-import', 'start')
2329
+ const { transformSync } = await import('oxc-transform-react')
2330
+ mark?.('refresh-import', 'end')
2331
+ const jsxImportSource =
2332
+ compiler.configuration?.enableNativewind && !id.includes('node_modules')
2333
+ ? 'nativewind'
2334
+ : 'react'
2335
+
2336
+ mark?.('refresh', 'start')
2337
+ const res = transformSync(id, curCode, {
2338
+ jsx: {
2339
+ development: dev,
2340
+ runtime: 'automatic',
2341
+ importSource: jsxImportSource,
2342
+ refresh: {
2343
+ refreshReg: '__vxrnRefreshReg',
2344
+ refreshSig: '__vxrnRefreshSig',
2345
+ },
2346
+ },
2347
+ reactCompiler: false,
2348
+ sourcemap: sourceMaps,
2349
+ })
2350
+
2351
+ if (res.fatal) {
2352
+ throw new Error(
2353
+ `[vxrn:compiler] oxc react transform failed on ${id}: ${(res.errors ?? [])
2354
+ .map((e: any) => e.message ?? String(e))
2355
+ .join(', ')}`
2356
+ )
2357
+ }
2358
+
2359
+ mark?.('refresh', 'end')
2360
+
2361
+ const escapedId = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
2362
+ const header = `var __prevRefreshReg = globalThis.$RefreshReg$;
2057
2363
  var __prevRefreshSig = globalThis.$RefreshSig$;
2058
2364
  if (globalThis.__ReactRefresh) {
2059
2365
  globalThis.$RefreshReg$ = function(type, id) {
@@ -2066,7 +2372,7 @@ var __vxrnRefreshReg = globalThis.$RefreshReg$;
2066
2372
  var __vxrnRefreshSig = globalThis.$RefreshSig$;
2067
2373
 
2068
2374
  `
2069
- const footer = `
2375
+ const footer = `
2070
2376
 
2071
2377
  globalThis.$RefreshReg$ = __prevRefreshReg;
2072
2378
  globalThis.$RefreshSig$ = __prevRefreshSig;
@@ -2078,29 +2384,36 @@ if (import.meta.hot) {
2078
2384
  });
2079
2385
  }
2080
2386
  `
2081
- let out: string
2082
- let finalMap: any = undefined
2083
-
2084
- if (sourceMaps) {
2085
- const MagicString = (await import('magic-string')).default
2086
- const ms = new MagicString(res.code)
2087
- ms.prepend(header)
2088
- ms.append(footer)
2089
- out = ms.toString()
2090
- const wrapMap = ms.generateMap({ source: id, hires: true })
2091
-
2092
- const allMaps = [
2093
- wrapMap,
2094
- res.map,
2095
- ...intermediateMaps.slice().reverse(),
2096
- ].filter(Boolean)
2097
- const remapping = (await import('@jridgewell/remapping')).default
2098
- finalMap = remapping(allMaps, () => null)
2099
- } else {
2100
- out = header + res.code + footer
2101
- }
2387
+ let out: string
2388
+ let finalMap: any = undefined
2102
2389
 
2103
- return { code: out, map: finalMap }
2390
+ if (sourceMaps) {
2391
+ const MagicString = (await import('magic-string')).default
2392
+ const ms = new MagicString(res.code)
2393
+ ms.prepend(header)
2394
+ ms.append(footer)
2395
+ out = ms.toString()
2396
+ const wrapMap = ms.generateMap({ source: id, hires: true })
2397
+
2398
+ const allMaps = [
2399
+ wrapMap,
2400
+ res.map,
2401
+ ...intermediateMaps.slice().reverse(),
2402
+ ].filter(Boolean)
2403
+ const remapping = (await import('@jridgewell/remapping')).default
2404
+ finalMap = remapping(allMaps, () => null)
2405
+ } else {
2406
+ out = header + res.code + footer
2407
+ }
2408
+
2409
+ return { code: out, map: finalMap }
2410
+ }
2411
+ } catch (error) {
2412
+ failed = true
2413
+ if (active !== 'transform') mark?.(active, 'error')
2414
+ throw error
2415
+ } finally {
2416
+ mark?.('transform', failed ? 'error' : 'end')
2104
2417
  }
2105
2418
  },
2106
2419
  }
@@ -2755,6 +3068,6 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
2755
3068
  }
2756
3069
  }
2757
3070
 
2758
- globalThis.__rolldown_runtime__ = new ReactNativeDevRuntime();
3071
+ ${NATIVE_DEV_RUNTIME_END}
2759
3072
  `
2760
3073
  }