vxrn 1.26.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vxrn",
3
- "version": "1.26.1",
3
+ "version": "1.27.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -65,16 +65,16 @@
65
65
  "@jridgewell/trace-mapping": "^0.3.25",
66
66
  "@react-native/codegen": "^0.86.2",
67
67
  "@react-native/dev-middleware": "^0.86.2",
68
- "@vxrn/compiler": "1.26.1",
69
- "@vxrn/debug": "1.26.1",
70
- "@vxrn/query-string": "1.26.1",
71
- "@vxrn/resolve": "1.26.1",
72
- "@vxrn/safe-area": "1.26.1",
73
- "@vxrn/url-parse": "1.26.1",
74
- "@vxrn/utils": "1.26.1",
75
- "@vxrn/vendor": "1.26.1",
76
- "@vxrn/vite-flow": "1.26.1",
77
- "@vxrn/vite-plugin-metro": "1.26.1",
68
+ "@vxrn/compiler": "1.27.0",
69
+ "@vxrn/debug": "1.27.0",
70
+ "@vxrn/query-string": "1.27.0",
71
+ "@vxrn/resolve": "1.27.0",
72
+ "@vxrn/safe-area": "1.27.0",
73
+ "@vxrn/url-parse": "1.27.0",
74
+ "@vxrn/utils": "1.27.0",
75
+ "@vxrn/vendor": "1.27.0",
76
+ "@vxrn/vite-flow": "1.27.0",
77
+ "@vxrn/vite-plugin-metro": "1.27.0",
78
78
  "citty": "^0.1.6",
79
79
  "dotenv": "^17.2.1",
80
80
  "dotenv-expand": "^12.0.2",
@@ -95,7 +95,7 @@
95
95
  "perfect-debounce": "^1.0.0",
96
96
  "picocolors": "^1.0.0",
97
97
  "pkg-types": "^1.0.3",
98
- "rolldown": "^1.2.4",
98
+ "rolldown": "^1.2.8",
99
99
  "semver": "^7.6.3",
100
100
  "vite": "^8.2.2",
101
101
  "ws": "^8.21.0"
@@ -83,6 +83,7 @@ export function createReactNativeDevServerPlugin(
83
83
  > = {}
84
84
  const devEngineCreating: Record<string, Promise<unknown> | null> = {}
85
85
  const warnedProdBundleRequest = new Set<string>()
86
+ const pendingReloadPlatforms = new Set<'ios' | 'android'>()
86
87
 
87
88
  const devToolsSocketEndpoints = ['/inspector/device', '/inspector/debug']
88
89
  const reactNativeDevToolsUrl = `http://${host}:${getBoundPort(server)}`
@@ -210,6 +211,9 @@ export function createReactNativeDevServerPlugin(
210
211
  return
211
212
  }
212
213
  addConnectedNativeClient()
214
+ if (pendingReloadPlatforms.has(socket.vxrnPlatform)) {
215
+ socket.send(JSON.stringify({ type: 'hmr:reload' }))
216
+ }
213
217
 
214
218
  socket.on('message', (message) => {
215
219
  const value = message.toString()
@@ -244,66 +248,6 @@ export function createReactNativeDevServerPlugin(
244
248
  })
245
249
  })
246
250
 
247
- const reloadNativeClients = (platform: string) => {
248
- const reload = JSON.stringify({ type: 'hmr:reload' })
249
- hmrWSS.clients.forEach((client) => {
250
- const nativeClient = client as NativeHmrSocket
251
- if (nativeClient.readyState === 1 && nativeClient.vxrnPlatform === platform) {
252
- nativeClient.send(reload)
253
- }
254
- })
255
- }
256
-
257
- // a created route file: rolldown's addWatchFile on the route directories
258
- // is supposed to report it, and the engine rebuilds only then. github's
259
- // macos runners do not always surface that, so vite's watcher is the
260
- // backup, still scoped to route files inside handleAddedFile.
261
- //
262
- // a deletion it cannot survive. rolldown's dev engine panics its worker
263
- // when it retires a module that left the disk ("index out of bounds" out
264
- // of oxc_index), and from then on every bundle request answers 500 even
265
- // once the file comes back. that engine cannot be repaired, so drop it
266
- // and let the next bundle request build a fresh one.
267
- let deletionTimer: ReturnType<typeof setTimeout> | undefined
268
- let addTimer: ReturnType<typeof setTimeout> | undefined
269
- const addedFiles = new Set<string>()
270
- server.watcher.on('add', (file) => {
271
- if (file.split('/').some((segment) => segment.startsWith('.'))) return
272
- addedFiles.add(file)
273
- clearTimeout(addTimer)
274
- addTimer = setTimeout(() => {
275
- const files = [...addedFiles]
276
- addedFiles.clear()
277
- for (const platform of Object.keys(devEngines)) {
278
- const devEngine = devEngines[platform]
279
- if (!devEngine) continue
280
- for (const added of files) {
281
- devEngine.handleAddedFile(added).catch((error) => {
282
- console.error(`[vxrn] handling added ${added} for ${platform} failed`, error)
283
- })
284
- }
285
- }
286
- }, 50)
287
- })
288
- server.watcher.on('unlink', (file) => {
289
- // vite's watcher already skips node_modules and .git. keep out the rest
290
- // of what a dev server writes into a project while it runs: caches and
291
- // build output under a dot directory, editor swap files.
292
- if (file.split('/').some((segment) => segment.startsWith('.'))) return
293
- // one pass for a burst (a branch switch, a generator, a rename)
294
- clearTimeout(deletionTimer)
295
- deletionTimer = setTimeout(() => {
296
- for (const platform of Object.keys(devEngines)) {
297
- const devEngine = devEngines[platform]
298
- if (!devEngine) continue
299
- devEngines[platform] = null
300
- devEngineCreating[platform] = null
301
- devEngine.close().catch(() => {})
302
- reloadNativeClients(platform)
303
- }
304
- }, 500)
305
- })
306
-
307
251
  clientWSS.on('connection', (socket) => {
308
252
  socket.on('message', (messageRaw) => {
309
253
  const message = JSON.parse(messageRaw.toString()) as any as ClientMessage
@@ -335,10 +279,10 @@ export function createReactNativeDevServerPlugin(
335
279
  })
336
280
 
337
281
  const getDevEngine = async (platform: 'ios' | 'android') => {
338
- if (!devEngines[platform]) {
282
+ while (!devEngines[platform]) {
339
283
  // prevent duplicate creation from concurrent requests
340
284
  if (!devEngineCreating[platform]) {
341
- devEngineCreating[platform] = (async () => {
285
+ const creating = (async () => {
342
286
  try {
343
287
  console.info(`[vxrn] creating rolldown DevEngine for ${platform}...`)
344
288
  devEngines[platform] = await createNativeDevEngine({
@@ -352,6 +296,12 @@ export function createReactNativeDevServerPlugin(
352
296
  // an update with no clientId is for every client on the
353
297
  // platform: an error, or a reload after a full rebuild
354
298
  const target = 'clientId' in update ? update.clientId : undefined
299
+ if (update.type === 'hmr:reload' && !target) {
300
+ // keep this pending until the rebuilt bundle is served.
301
+ // a previous app instance can leave a live socket while
302
+ // its replacement is still mounting and connecting.
303
+ pendingReloadPlatforms.add(platform)
304
+ }
355
305
  hmrWSS.clients.forEach((client) => {
356
306
  const nativeClient = client as NativeHmrSocket
357
307
  if (
@@ -365,12 +315,12 @@ export function createReactNativeDevServerPlugin(
365
315
  },
366
316
  })
367
317
  console.info(`[vxrn] rolldown DevEngine ready for ${platform}`)
368
- } catch (err) {
369
- // clear so next request retries instead of permanently failing
318
+ } finally {
319
+ // clear so a failed creation can be retried.
370
320
  devEngineCreating[platform] = null
371
- throw err
372
321
  }
373
322
  })()
323
+ devEngineCreating[platform] = creating
374
324
  }
375
325
  await devEngineCreating[platform]
376
326
  }
@@ -403,8 +353,18 @@ export function createReactNativeDevServerPlugin(
403
353
 
404
354
  try {
405
355
  const bundle = await (await getDevEngine(platform)).getBundle()
406
-
407
- res.writeHead(200, { 'Content-Type': 'text/javascript' })
356
+ // a client that connects after this response starts from the current
357
+ // route map and does not need the pending reload intended for the
358
+ // previous runtime.
359
+ res.once('finish', () => pendingReloadPlatforms.delete(platform))
360
+
361
+ // a DevSettings reload requests this exact URL again. native URL
362
+ // loading may otherwise reuse the first response and restart the app
363
+ // on a route map from before a file was added or removed.
364
+ res.writeHead(200, {
365
+ 'Cache-Control': 'no-store',
366
+ 'Content-Type': 'text/javascript',
367
+ })
408
368
  res.end(bundle.code)
409
369
  } catch (err) {
410
370
  console.error(` Error building React Native bundle`)
@@ -16,6 +16,9 @@ import {
16
16
  readdirSync,
17
17
  statSync,
18
18
  readFileSync,
19
+ writeFileSync,
20
+ watch,
21
+ type FSWatcher,
19
22
  } from 'node:fs'
20
23
  import { pathToFileURL } from 'node:url'
21
24
  import micromatch from 'micromatch'
@@ -23,6 +26,7 @@ import type { InputOptions, OutputOptions, Plugin, RolldownOutput } from 'rolldo
23
26
  import type { DevEngine } from 'rolldown/experimental'
24
27
  import { loadEnv as loadViteEnv, normalizePath } from 'vite'
25
28
  import { shouldStripFlow, transformHermesAsync } from '@vxrn/compiler'
29
+ import { resolvePath } from '@vxrn/resolve'
26
30
  import { DEFAULT_ASSET_EXTS } from '../constants/defaults'
27
31
  import { getNativePrelude } from '../runtime/native-prelude'
28
32
  import { rnCodegenPlugin } from '../plugins/rnCodegenPlugin'
@@ -98,7 +102,6 @@ interface NativeDevEngineResult {
98
102
  getBundle: () => Promise<NativeDevBundle>
99
103
  getAsset: (pathname: string, hash?: string) => NativeDevAsset | undefined
100
104
  close: () => Promise<void>
101
- handleAddedFile: (file: string) => Promise<void>
102
105
  }
103
106
 
104
107
  // shared resolve extensions for native builds
@@ -262,6 +265,7 @@ function getNativePlugins(
262
265
  // rolldown-runtime WebSocket); RN's client otherwise opens a /hot socket and
263
266
  // red-boxes "unknown-message [object Object]" on every edit (new arch)
264
267
  hmrClientNoopPlugin(),
268
+ ...(dev ? [reactNativeDedupePlugin(root)] : []),
265
269
  // stub CSS imports — native doesn't support CSS and rolldown removed CSS bundling
266
270
  cssStubPlugin(),
267
271
  // handle import.meta.glob (used by One's route system)
@@ -556,7 +560,11 @@ export async function createNativeDevEngine(
556
560
  let bundlePromise: Promise<NativeDevBundle> | null = null
557
561
 
558
562
  const resolvedHost = host === '0.0.0.0' ? 'localhost' : host
559
- const virtualEntry = nativeVirtualEntryPlugin(root, { dev: true })
563
+ const virtualEntry = nativeVirtualEntryPlugin(root, {
564
+ dev: true,
565
+ platform,
566
+ dynamicRoutes: true,
567
+ })
560
568
 
561
569
  // one build at a time: a bundle request and a full rebuild can arrive
562
570
  // together (a reload lands on the bundle route while a new route file is
@@ -645,27 +653,26 @@ try {
645
653
  let outputProcessed: Promise<void> = Promise.resolve()
646
654
 
647
655
  let engine: Awaited<ReturnType<typeof dev>>
648
-
649
- const rebuildIfNewRoute = async (files: string[]): Promise<boolean> => {
650
- const { routeRoot, files: knownRoutes, isRouteFile } = virtualEntry.routes
651
- const routeRootPrefix = `${normalizePath(routeRoot)}/`
652
- const routeAdded = files.some((file) => {
653
- const normalized = normalizePath(file)
654
- if (isRouteFile(normalized)) return !knownRoutes.has(normalized)
655
- return (
656
- normalized.startsWith(routeRootPrefix) &&
657
- statSync(file, { throwIfNoEntry: false })?.isDirectory() === true
658
- )
656
+ let routeWatcher: FSWatcher | undefined
657
+
658
+ const rebuildIfRouteGraphChanged = (): Promise<boolean> =>
659
+ queueEngineWork(async () => {
660
+ // decide from the complete route set after all earlier engine work. a
661
+ // watcher can report a directory, a rename, or several events for one
662
+ // write, and none of those event shapes are the source of truth.
663
+ const { files: knownRoutes, scanFiles, updateFiles } = virtualEntry.routes
664
+ const currentRoutes = scanFiles()
665
+ const routeSetChanged =
666
+ currentRoutes.size !== knownRoutes.size ||
667
+ [...currentRoutes].some((route) => !knownRoutes.has(route))
668
+ if (!routeSetChanged) return false
669
+
670
+ // changing this small imported module lets Rolldown compile only the new
671
+ // route and the registry delta. rebuilding a multi-megabyte native bundle
672
+ // makes route creation scale with the entire app.
673
+ updateFiles(currentRoutes)
674
+ return true
659
675
  })
660
- if (!routeAdded) return false
661
- await queueEngineWork(async () => {
662
- engine.triggerFullBuild()
663
- await engine.ensureLatestBuildOutput()
664
- await outputProcessed
665
- })
666
- onHmrUpdate?.({ type: 'hmr:reload' })
667
- return true
668
- }
669
676
 
670
677
  engine = await dev(inputOptions, outputOptions, {
671
678
  onOutput: async (result) => {
@@ -687,12 +694,10 @@ try {
687
694
  return
688
695
  }
689
696
 
690
- // a route file that was just created has no module, so rolldown reports
691
- // the change and stops: in devMode it never re-scans, and the entry's
692
- // `import.meta.glob` was expanded back when it transformed the entry.
693
- // only a full build re-expands it, and rolldown tells no client that
694
- // happened, so the reload is sent from here.
695
- if (await rebuildIfNewRoute(result.changedFiles)) return
697
+ // adding or deleting a route changes a route map that was expanded when
698
+ // the entry transformed. only a full build re-expands it, and rolldown
699
+ // tells no client that happened, so the reload is sent from here.
700
+ if (await rebuildIfRouteGraphChanged()) return
696
701
 
697
702
  for (const { clientId, update } of result.updates) {
698
703
  if (update.type === 'Patch' && update.code) {
@@ -778,6 +783,35 @@ try {
778
783
 
779
784
  await engine.run()
780
785
 
786
+ // import.meta.glob records the files it matched, not the directory whose
787
+ // membership defines the route map. own that directory watch next to the
788
+ // engine and compare complete route sets on every event. this also covers a
789
+ // router root created after the dev server starts.
790
+ let routeWatchTarget = virtualEntry.routes.routeRoot
791
+ while (!statSync(routeWatchTarget, { throwIfNoEntry: false })?.isDirectory()) {
792
+ const parent = dirname(routeWatchTarget)
793
+ if (parent === routeWatchTarget) break
794
+ routeWatchTarget = parent
795
+ }
796
+ const normalizedRouteRoot = normalizePath(virtualEntry.routes.routeRoot)
797
+ routeWatcher = watch(routeWatchTarget, { recursive: true }, (_eventType, file) => {
798
+ if (file === null) return
799
+ const changed = normalizePath(resolve(routeWatchTarget, file.toString()))
800
+ if (
801
+ changed !== normalizedRouteRoot &&
802
+ !changed.startsWith(`${normalizedRouteRoot}/`)
803
+ ) {
804
+ return
805
+ }
806
+ rebuildIfRouteGraphChanged().catch((error) => {
807
+ console.error(`[vxrn] rebuilding the native route map failed`, error)
808
+ })
809
+ })
810
+ routeWatcher.on('error', (error) => {
811
+ console.error(`[vxrn] watching the native route map failed`, error)
812
+ })
813
+ await rebuildIfRouteGraphChanged()
814
+
781
815
  return {
782
816
  engine,
783
817
 
@@ -817,16 +851,9 @@ try {
817
851
  },
818
852
 
819
853
  async close() {
854
+ routeWatcher?.close()
820
855
  await engine.close()
821
856
  },
822
-
823
- // vite's watcher sees a created file even when rolldown's addWatchFile
824
- // directory watches do not, which is what happens on github's macos
825
- // runners. scoped to route files so a random write in the project is not
826
- // a full rebuild.
827
- async handleAddedFile(file: string) {
828
- await rebuildIfNewRoute([file])
829
- },
830
857
  }
831
858
  }
832
859
 
@@ -894,7 +921,7 @@ export async function buildNativeBundle(
894
921
  shimMissingExports: true,
895
922
  moduleTypes: { '.js': 'jsx' },
896
923
  plugins: [
897
- ...(entryFile ? [] : [nativeVirtualEntryPlugin(root, { dev }).plugin]),
924
+ ...(entryFile ? [] : [nativeVirtualEntryPlugin(root, { dev, platform }).plugin]),
898
925
  ...getNativePlugins(
899
926
  root,
900
927
  platform,
@@ -942,13 +969,20 @@ interface NativeRouteRegistry {
942
969
  routeRoot: string
943
970
  files: Set<string>
944
971
  isRouteFile: (file: string) => boolean
972
+ scanFiles: () => Set<string>
973
+ updateFiles: (files: Set<string>) => void
945
974
  }
946
975
 
947
976
  function nativeVirtualEntryPlugin(
948
977
  root: string,
949
- opts?: { dev?: boolean }
978
+ opts: {
979
+ dev?: boolean
980
+ platform: 'ios' | 'android'
981
+ dynamicRoutes?: boolean
982
+ }
950
983
  ): { plugin: Plugin; routes: NativeRouteRegistry } {
951
984
  const isDev = opts?.dev !== false
985
+ const useDynamicRouteRegistry = opts.dynamicRoutes === true
952
986
  // absolute for import.meta.glob resolution; forward-slash for module-graph convention
953
987
  const resolvedId = normalizePath(resolve(root, '__virtual-native-entry.tsx'))
954
988
 
@@ -1001,23 +1035,24 @@ globalThis.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
1001
1035
  `
1002
1036
  : ''
1003
1037
 
1004
- const entryCode = `
1038
+ const entryCode = () => `
1005
1039
  ${refreshSetup}
1006
1040
  import * as ReactNativeInitializeCore from 'react-native/Libraries/Core/InitializeCore';
1007
1041
  import NativeWebSocket from 'react-native/Libraries/WebSocket/WebSocket';
1042
+ import DevSettings from 'react-native/Libraries/Utilities/DevSettings';
1008
1043
  ${setupFileImport}
1044
+ ${routeImports}
1009
1045
  import { createApp } from 'one';
1010
1046
 
1011
1047
  void ReactNativeInitializeCore;
1012
1048
  globalThis.WebSocket = NativeWebSocket;
1049
+ globalThis.__VXRN_RELOAD_NATIVE_DEV_BUNDLE__ = function(reason) {
1050
+ DevSettings.reload(reason);
1051
+ };
1013
1052
 
1014
- var _routes = import.meta.glob(${JSON.stringify(routeGlobs)}, { exhaustive: true });
1015
- // fix route keys: One expects '/${routerRoot}/...' prefix but import.meta.glob returns './${routerRoot}/...'
1016
- var routes = {};
1017
- Object.keys(_routes).forEach(function(key) {
1018
- var normalizedKey = key.replace(/^\\.\\//, '');
1019
- routes['/' + normalizedKey] = _routes[key];
1020
- });
1053
+ // React Native reloads a dev bundle in the existing JS runtime. Advance One's
1054
+ // route-cache version so a full reload cannot reuse the previous context.
1055
+ globalThis.__vxrnVersion = (globalThis.__vxrnVersion || 0) + 1;
1021
1056
 
1022
1057
  createApp({
1023
1058
  routes: routes,
@@ -1025,6 +1060,8 @@ createApp({
1025
1060
  flags: ${JSON.stringify(flags)},
1026
1061
  linking: ${JSON.stringify(linking)},
1027
1062
  });
1063
+
1064
+ ${routeHmr}
1028
1065
  `
1029
1066
 
1030
1067
  const routeRoot = resolve(root, routerRoot)
@@ -1045,11 +1082,66 @@ createApp({
1045
1082
  !micromatch.isMatch(relativeToRoot, excludeGlobs)
1046
1083
  )
1047
1084
  }
1085
+ const scanFiles = () => {
1086
+ const files = new Set<string>()
1087
+ const walkRouteRoot = (dir: string) => {
1088
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1089
+ const child = resolve(dir, entry.name)
1090
+ if (entry.isDirectory()) walkRouteRoot(child)
1091
+ else if (isRouteFile(child)) files.add(normalizePath(child))
1092
+ }
1093
+ }
1094
+ if (statSync(routeRoot, { throwIfNoEntry: false })?.isDirectory()) {
1095
+ walkRouteRoot(routeRoot)
1096
+ }
1097
+ return files
1098
+ }
1099
+ const registrySpecifier = `./node_modules/.vxrn/native-routes-${opts.platform}.mjs`
1100
+ const registryPath = resolve(root, registrySpecifier)
1048
1101
  const routes: NativeRouteRegistry = {
1049
1102
  routeRoot,
1050
1103
  files: new Set(),
1051
1104
  isRouteFile,
1105
+ scanFiles,
1106
+ updateFiles(files) {
1107
+ routes.files = files
1108
+ if (!useDynamicRouteRegistry) return
1109
+
1110
+ const source = `export const routes = {\n${[...files]
1111
+ .sort()
1112
+ .map((file) => {
1113
+ const routeKey = `/${normalizePath(relative(root, file))}`
1114
+ let importPath = normalizePath(relative(dirname(registryPath), file))
1115
+ if (!importPath.startsWith('.')) importPath = `./${importPath}`
1116
+ return ` ${JSON.stringify(routeKey)}: () => import(${JSON.stringify(importPath)})`
1117
+ })
1118
+ .join(',\n')}\n}\n`
1119
+ mkdirSync(dirname(registryPath), { recursive: true })
1120
+ if (!existsSync(registryPath) || readFileSync(registryPath, 'utf8') !== source) {
1121
+ writeFileSync(registryPath, source)
1122
+ }
1123
+ },
1052
1124
  }
1125
+ routes.updateFiles(routes.scanFiles())
1126
+
1127
+ const routeImports = useDynamicRouteRegistry
1128
+ ? `import { routes } from ${JSON.stringify(registrySpecifier)};`
1129
+ : `var _routes = import.meta.glob(${JSON.stringify(routeGlobs)}, { exhaustive: true });
1130
+ // fix route keys: One expects '/${routerRoot}/...' prefix but import.meta.glob returns './${routerRoot}/...'
1131
+ var routes = {};
1132
+ Object.keys(_routes).forEach(function(key) {
1133
+ var normalizedKey = key.replace(/^\\.\\//, '');
1134
+ routes['/' + normalizedKey] = _routes[key];
1135
+ });`
1136
+ const routeHmr = useDynamicRouteRegistry
1137
+ ? `if (import.meta.hot) {
1138
+ import.meta.hot.accept(${JSON.stringify(registrySpecifier)}, function(next) {
1139
+ if (globalThis.__VXRN_UPDATE_NATIVE_ROUTES__) {
1140
+ globalThis.__VXRN_UPDATE_NATIVE_ROUTES__(next.routes);
1141
+ }
1142
+ });
1143
+ }`
1144
+ : ''
1053
1145
 
1054
1146
  return {
1055
1147
  routes,
@@ -1062,35 +1154,7 @@ createApp({
1062
1154
  },
1063
1155
  load(id) {
1064
1156
  if (id !== resolvedId) return
1065
- // rolldown expands `import.meta.glob` during transform and records a
1066
- // dependency on the files it matched, never on the directories it
1067
- // searched. registering those directories is what makes its own watcher
1068
- // report a route file appearing, and walking them is what lets the
1069
- // engine tell that report apart from an edit.
1070
- //
1071
- // watching a directory is not recursive, so every directory under the
1072
- // route root is registered. a new subdirectory is itself a change to
1073
- // its watched parent, and the full build that follows re-runs this
1074
- // hook, so the build that picks up `app/foo/index.tsx` is also what
1075
- // starts watching `app/foo`.
1076
- routes.files.clear()
1077
- const walkRouteRoot = (dir: string) => {
1078
- this.addWatchFile(dir)
1079
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1080
- const child = resolve(dir, entry.name)
1081
- if (entry.isDirectory()) walkRouteRoot(child)
1082
- else if (isRouteFile(child)) routes.files.add(normalizePath(child))
1083
- }
1084
- }
1085
- // a project can have no route root at all: `import.meta.glob` then
1086
- // expands to nothing and the app builds fine. watch the directory that
1087
- // would hold it, so creating it is the change that starts the walk.
1088
- if (statSync(routeRoot, { throwIfNoEntry: false })?.isDirectory()) {
1089
- walkRouteRoot(routeRoot)
1090
- } else {
1091
- this.addWatchFile(dirname(routeRoot))
1092
- }
1093
- return entryCode
1157
+ return entryCode()
1094
1158
  },
1095
1159
  },
1096
1160
  }
@@ -1226,6 +1290,29 @@ export function hmrClientNoopPlugin(): Plugin {
1226
1290
  }
1227
1291
  }
1228
1292
 
1293
+ function reactNativeDedupePlugin(root: string): Plugin {
1294
+ // package managers can install the same react native version at multiple
1295
+ // physical paths. rolldown assigns each path a module identity, so resolving
1296
+ // every entry through the app's instance prevents initializeCore from running twice.
1297
+ let reactNativeRoot: string | undefined
1298
+
1299
+ return {
1300
+ name: 'vxrn:react-native-dedupe',
1301
+ async resolveId(source, importer) {
1302
+ if (source !== 'react-native' && !source.startsWith('react-native/')) return
1303
+
1304
+ reactNativeRoot ||= realpathSync(
1305
+ dirname(resolvePath('react-native/package.json', root))
1306
+ )
1307
+ const subpath =
1308
+ source === 'react-native' ? 'index.js' : source.slice('react-native/'.length)
1309
+ return this.resolve(normalizePath(resolve(reactNativeRoot, subpath)), importer, {
1310
+ skipSelf: true,
1311
+ })
1312
+ },
1313
+ }
1314
+ }
1315
+
1229
1316
  /**
1230
1317
  * Stub CSS imports for native builds.
1231
1318
  * Native doesn't support CSS and rolldown removed CSS bundling support.
@@ -1324,20 +1411,33 @@ export function vxrnCompilerPlugin(
1324
1411
  if (prepared.map) intermediateMaps.push(prepared.map)
1325
1412
  }
1326
1413
  }
1327
- const compilerTarget =
1328
- (babelOptions!.plugins![compilerPluginIndex] as any[])[1]?.target ?? '19'
1329
- const compilerOut = await compiler.transformOxcReactCompiler(
1330
- id,
1331
- curCode,
1332
- compilerTarget,
1333
- sourceMaps
1334
- )
1335
- if (compilerOut?.code) {
1336
- curCode = compilerOut.code
1337
- if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map)
1414
+ const compilerPluginConfig = babelOptions!.plugins![compilerPluginIndex] as any[]
1415
+ const compilerPluginOptions = compilerPluginConfig[1] || {}
1416
+ const useBabelCompiler =
1417
+ compilerPluginOptions.compiler === 'babel' ||
1418
+ process.env.ONE_REACT_COMPILER === 'babel' ||
1419
+ process.env.VXRN_REACT_COMPILER === 'babel'
1420
+
1421
+ if (!useBabelCompiler) {
1422
+ const compilerOut = await compiler.transformOxcReactCompiler(
1423
+ id,
1424
+ curCode,
1425
+ compilerPluginOptions,
1426
+ sourceMaps
1427
+ )
1428
+ if (compilerOut?.code) {
1429
+ curCode = compilerOut.code
1430
+ if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map)
1431
+ }
1432
+ babelOptions!.plugins!.splice(compilerPluginIndex, 1)
1433
+ if (
1434
+ babelOptions!.plugins!.length === 0 &&
1435
+ !babelOptions!.configFile &&
1436
+ !babelOptions!.babelrc
1437
+ ) {
1438
+ babelOptions = null
1439
+ }
1338
1440
  }
1339
- babelOptions!.plugins!.splice(compilerPluginIndex, 1)
1340
- if (babelOptions!.plugins!.length === 0) babelOptions = null
1341
1441
  }
1342
1442
 
1343
1443
  if (useWorklets) {
@@ -1353,7 +1453,13 @@ export function vxrnCompilerPlugin(
1353
1453
  babelOptions.plugins = babelOptions.plugins.filter(
1354
1454
  (entry) => !isWorkletPlugin(entry)
1355
1455
  )
1356
- if (babelOptions.plugins.length === 0) babelOptions = null
1456
+ if (
1457
+ babelOptions.plugins.length === 0 &&
1458
+ !babelOptions.configFile &&
1459
+ !babelOptions.babelrc
1460
+ ) {
1461
+ babelOptions = null
1462
+ }
1357
1463
  }
1358
1464
  }
1359
1465
 
@@ -2069,11 +2175,13 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
2069
2175
  }
2070
2176
  }
2071
2177
 
2072
- reload() {
2073
- var proxy = globalThis.__turboModuleProxy
2074
- ? globalThis.__turboModuleProxy('DevSettings')
2075
- : globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.DevSettings;
2076
- if (proxy && proxy.reload) proxy.reload();
2178
+ reload(reason) {
2179
+ var reloadNativeBundle = globalThis.__VXRN_RELOAD_NATIVE_DEV_BUNDLE__;
2180
+ if (typeof reloadNativeBundle === 'function') {
2181
+ reloadNativeBundle(reason);
2182
+ return;
2183
+ }
2184
+ console.error('[vxrn HMR]: DevSettings.reload is unavailable');
2077
2185
  }
2078
2186
 
2079
2187
  setup(socket) {
@@ -2095,10 +2203,10 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
2095
2203
  if (message.type === 'hmr:update') {
2096
2204
  // a patch that cannot be applied in place is the reload signal
2097
2205
  if (!runtime.applyHmrUpdate(message.code, message.changedIds, message.seq)) {
2098
- runtime.reload();
2206
+ runtime.reload('native HMR update could not be applied');
2099
2207
  }
2100
2208
  } else if (message.type === 'hmr:reload') {
2101
- runtime.reload();
2209
+ runtime.reload('native route map changed');
2102
2210
  }
2103
2211
  } catch (error) {
2104
2212
  console.error('[vxrn HMR]: failed to apply update', error);
@@ -1 +1 @@
1
- {"version":3,"file":"reactNativeDevServer.d.ts","sourceRoot":"","sources":["../../src/plugins/reactNativeDevServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAO1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAwBnE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9D;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,CAAC,EAAE,OAAO,CACf,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAAC,CACrF,GACA,MAAM,CAyZR"}
1
+ {"version":3,"file":"reactNativeDevServer.d.ts","sourceRoot":"","sources":["../../src/plugins/reactNativeDevServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAO1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAwBnE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9D;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,CAAC,EAAE,OAAO,CACf,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAAC,CACrF,GACA,MAAM,CAiXR"}
@@ -49,7 +49,6 @@ interface NativeDevEngineResult {
49
49
  getBundle: () => Promise<NativeDevBundle>;
50
50
  getAsset: (pathname: string, hash?: string) => NativeDevAsset | undefined;
51
51
  close: () => Promise<void>;
52
- handleAddedFile: (file: string) => Promise<void>;
53
52
  }
54
53
  export declare function getNativeTransformConfig(platform: 'ios' | 'android', dev: boolean, root: string): {
55
54
  jsx: {