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.
@@ -1,10 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
3
- import { copyFileSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import micromatch from "micromatch";
6
6
  import { loadEnv, normalizePath } from "vite";
7
7
  import { shouldStripFlow, transformHermesAsync } from "@vxrn/compiler";
8
+ import { resolvePath } from "@vxrn/resolve";
8
9
  import { DEFAULT_ASSET_EXTS } from "../constants/defaults.mjs";
9
10
  import { getNativePrelude } from "../runtime/native-prelude.mjs";
10
11
  import { rnCodegenPlugin } from "../plugins/rnCodegenPlugin.mjs";
@@ -116,7 +117,7 @@ function getNativePlugins(root, platform, viteImportGlobPlugin, dev, assetsDest,
116
117
  platform,
117
118
  dev
118
119
  };
119
- return [...(globalThis.__vxrnAddNativePlugins || []), serverFileExclusionPlugin(), environmentGuardPlugin(), hmrClientNoopPlugin(), cssStubPlugin(), viteImportGlobPlugin({
120
+ return [...(globalThis.__vxrnAddNativePlugins || []), serverFileExclusionPlugin(), environmentGuardPlugin(), hmrClientNoopPlugin(), ...(dev ? [reactNativeDedupePlugin(root)] : []), cssStubPlugin(), viteImportGlobPlugin({
120
121
  root
121
122
  }), vxrnCompilerPlugin(platform, dev, root, sourceMaps), rnCodegenPlugin({
122
123
  projectRoot: root
@@ -252,7 +253,9 @@ async function createNativeDevEngine(options) {
252
253
  let bundlePromise = null;
253
254
  const resolvedHost = host === "0.0.0.0" ? "localhost" : host;
254
255
  const virtualEntry = nativeVirtualEntryPlugin(root, {
255
- dev: true
256
+ dev: true,
257
+ platform,
258
+ dynamicRoutes: true
256
259
  });
257
260
  let engineWork = Promise.resolve();
258
261
  const queueEngineWork = work => {
@@ -298,30 +301,18 @@ try {
298
301
  };
299
302
  let outputProcessed = Promise.resolve();
300
303
  let engine;
301
- const rebuildIfNewRoute = async files => {
304
+ let routeWatcher;
305
+ const rebuildIfRouteGraphChanged = () => queueEngineWork(async () => {
302
306
  const {
303
- routeRoot,
304
307
  files: knownRoutes,
305
- isRouteFile
308
+ scanFiles,
309
+ updateFiles
306
310
  } = virtualEntry.routes;
307
- const routeRootPrefix = `${normalizePath(routeRoot)}/`;
308
- if (!files.some(file => {
309
- const normalized = normalizePath(file);
310
- if (isRouteFile(normalized)) return !knownRoutes.has(normalized);
311
- return normalized.startsWith(routeRootPrefix) && statSync(file, {
312
- throwIfNoEntry: false
313
- })?.isDirectory() === true;
314
- })) return false;
315
- await queueEngineWork(async () => {
316
- engine.triggerFullBuild();
317
- await engine.ensureLatestBuildOutput();
318
- await outputProcessed;
319
- });
320
- onHmrUpdate?.({
321
- type: "hmr:reload"
322
- });
311
+ const currentRoutes = scanFiles();
312
+ if (!(currentRoutes.size !== knownRoutes.size || [...currentRoutes].some(route => !knownRoutes.has(route)))) return false;
313
+ updateFiles(currentRoutes);
323
314
  return true;
324
- };
315
+ });
325
316
  engine = await dev(inputOptions, outputOptions, {
326
317
  onOutput: async result => {
327
318
  let finishOutput = () => {};
@@ -342,7 +333,7 @@ try {
342
333
  });
343
334
  return;
344
335
  }
345
- if (await rebuildIfNewRoute(result.changedFiles)) return;
336
+ if (await rebuildIfRouteGraphChanged()) return;
346
337
  for (const {
347
338
  clientId,
348
339
  update
@@ -398,6 +389,29 @@ try {
398
389
  }
399
390
  }
400
391
  await engine.run();
392
+ let routeWatchTarget = virtualEntry.routes.routeRoot;
393
+ while (!statSync(routeWatchTarget, {
394
+ throwIfNoEntry: false
395
+ })?.isDirectory()) {
396
+ const parent = dirname(routeWatchTarget);
397
+ if (parent === routeWatchTarget) break;
398
+ routeWatchTarget = parent;
399
+ }
400
+ const normalizedRouteRoot = normalizePath(virtualEntry.routes.routeRoot);
401
+ routeWatcher = watch(routeWatchTarget, {
402
+ recursive: true
403
+ }, (_eventType, file) => {
404
+ if (file === null) return;
405
+ const changed = normalizePath(resolve(routeWatchTarget, file.toString()));
406
+ if (changed !== normalizedRouteRoot && !changed.startsWith(`${normalizedRouteRoot}/`)) return;
407
+ rebuildIfRouteGraphChanged().catch(error => {
408
+ console.error(`[vxrn] rebuilding the native route map failed`, error);
409
+ });
410
+ });
411
+ routeWatcher.on("error", error => {
412
+ console.error(`[vxrn] watching the native route map failed`, error);
413
+ });
414
+ await rebuildIfRouteGraphChanged();
401
415
  return {
402
416
  engine,
403
417
  async getBundle() {
@@ -432,10 +446,8 @@ try {
432
446
  return assetRegistry.resolve(pathname, hash);
433
447
  },
434
448
  async close() {
449
+ routeWatcher?.close();
435
450
  await engine.close();
436
- },
437
- async handleAddedFile(file) {
438
- await rebuildIfNewRoute([file]);
439
451
  }
440
452
  };
441
453
  }
@@ -477,7 +489,8 @@ async function buildNativeBundle(options) {
477
489
  ".js": "jsx"
478
490
  },
479
491
  plugins: [...(entryFile ? [] : [nativeVirtualEntryPlugin(root, {
480
- dev
492
+ dev,
493
+ platform
481
494
  }).plugin]), ...getNativePlugins(root, platform, viteImportGlobPlugin, dev, assetsDest, void 0, sourcemap, userPlugins), nativeBundlePostProcessPlugin()],
482
495
  output: getNativeOutputOptions(prelude, sourcemap, minify)
483
496
  })).output.find(o => o.type === "chunk" && o.isEntry);
@@ -491,6 +504,7 @@ async function buildNativeBundle(options) {
491
504
  const VIRTUAL_NATIVE_ENTRY = "virtual:native-entry";
492
505
  function nativeVirtualEntryPlugin(root, opts) {
493
506
  const isDev = opts?.dev !== false;
507
+ const useDynamicRouteRegistry = opts.dynamicRoutes === true;
494
508
  const resolvedId = normalizePath(resolve(root, "__virtual-native-entry.tsx"));
495
509
  const entryConfig = globalThis.__vxrnNativeEntryConfig || {};
496
510
  const routerRoot = entryConfig.routerRoot || "app";
@@ -505,8 +519,7 @@ function nativeVirtualEntryPlugin(root, opts) {
505
519
  return `import ${JSON.stringify(resolved)};`;
506
520
  })();
507
521
  const routeGlobs = [`./${routerRoot}/**/*.tsx`, `./${routerRoot}/**/*.ts`, `!./${routerRoot}/**/*+api.*`, `!./${routerRoot}/**/*.test.*`, `!./${routerRoot}/**/*.d.ts`, `!./${routerRoot}/**/*.server.*`, `!./${routerRoot}/**/_middleware.*`, `!./${routerRoot}/**/*.web.*`, ...(entryConfig.ignoredRouteFiles || []).map(pattern => `!./${routerRoot}/${pattern}`)];
508
- const entryCode = `
509
- ${isDev ? `
522
+ const refreshSetup = isDev ? `
510
523
  // react-refresh/runtime MUST initialize before React loads
511
524
  import RefreshRuntime from 'react-refresh/runtime';
512
525
  RefreshRuntime.injectIntoGlobalHook(globalThis);
@@ -515,22 +528,25 @@ globalThis.$RefreshReg$ = function(type, id) {
515
528
  RefreshRuntime.register(type, id);
516
529
  };
517
530
  globalThis.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
518
- ` : ""}
531
+ ` : "";
532
+ const entryCode = () => `
533
+ ${refreshSetup}
519
534
  import * as ReactNativeInitializeCore from 'react-native/Libraries/Core/InitializeCore';
520
535
  import NativeWebSocket from 'react-native/Libraries/WebSocket/WebSocket';
536
+ import DevSettings from 'react-native/Libraries/Utilities/DevSettings';
521
537
  ${setupFileImport}
538
+ ${routeImports}
522
539
  import { createApp } from 'one';
523
540
 
524
541
  void ReactNativeInitializeCore;
525
542
  globalThis.WebSocket = NativeWebSocket;
543
+ globalThis.__VXRN_RELOAD_NATIVE_DEV_BUNDLE__ = function(reason) {
544
+ DevSettings.reload(reason);
545
+ };
526
546
 
527
- var _routes = import.meta.glob(${JSON.stringify(routeGlobs)}, { exhaustive: true });
528
- // fix route keys: One expects '/${routerRoot}/...' prefix but import.meta.glob returns './${routerRoot}/...'
529
- var routes = {};
530
- Object.keys(_routes).forEach(function(key) {
531
- var normalizedKey = key.replace(/^\\.\\//, '');
532
- routes['/' + normalizedKey] = _routes[key];
533
- });
547
+ // React Native reloads a dev bundle in the existing JS runtime. Advance One's
548
+ // route-cache version so a full reload cannot reuse the previous context.
549
+ globalThis.__vxrnVersion = (globalThis.__vxrnVersion || 0) + 1;
534
550
 
535
551
  createApp({
536
552
  routes: routes,
@@ -538,6 +554,8 @@ createApp({
538
554
  flags: ${JSON.stringify(flags)},
539
555
  linking: ${JSON.stringify(linking)},
540
556
  });
557
+
558
+ ${routeHmr}
541
559
  `;
542
560
  const routeRoot = resolve(root, routerRoot);
543
561
  const stripDot = pattern => pattern.replace(/^\.\//, "");
@@ -547,11 +565,61 @@ createApp({
547
565
  const relativeToRoot = normalizePath(relative(root, file));
548
566
  return micromatch.isMatch(relativeToRoot, includeGlobs) && !micromatch.isMatch(relativeToRoot, excludeGlobs);
549
567
  };
568
+ const scanFiles = () => {
569
+ const files = /* @__PURE__ */new Set();
570
+ const walkRouteRoot = dir => {
571
+ for (const entry of readdirSync(dir, {
572
+ withFileTypes: true
573
+ })) {
574
+ const child = resolve(dir, entry.name);
575
+ if (entry.isDirectory()) walkRouteRoot(child);else if (isRouteFile(child)) files.add(normalizePath(child));
576
+ }
577
+ };
578
+ if (statSync(routeRoot, {
579
+ throwIfNoEntry: false
580
+ })?.isDirectory()) walkRouteRoot(routeRoot);
581
+ return files;
582
+ };
583
+ const registrySpecifier = `./node_modules/.vxrn/native-routes-${opts.platform}.mjs`;
584
+ const registryPath = resolve(root, registrySpecifier);
550
585
  const routes = {
551
586
  routeRoot,
552
587
  files: /* @__PURE__ */new Set(),
553
- isRouteFile
588
+ isRouteFile,
589
+ scanFiles,
590
+ updateFiles(files) {
591
+ routes.files = files;
592
+ if (!useDynamicRouteRegistry) return;
593
+ const source = `export const routes = {
594
+ ${[...files].sort().map(file => {
595
+ const routeKey = `/${normalizePath(relative(root, file))}`;
596
+ let importPath = normalizePath(relative(dirname(registryPath), file));
597
+ if (!importPath.startsWith(".")) importPath = `./${importPath}`;
598
+ return ` ${JSON.stringify(routeKey)}: () => import(${JSON.stringify(importPath)})`;
599
+ }).join(",\n")}
600
+ }
601
+ `;
602
+ mkdirSync(dirname(registryPath), {
603
+ recursive: true
604
+ });
605
+ if (!existsSync(registryPath) || readFileSync(registryPath, "utf8") !== source) writeFileSync(registryPath, source);
606
+ }
554
607
  };
608
+ routes.updateFiles(routes.scanFiles());
609
+ const routeImports = useDynamicRouteRegistry ? `import { routes } from ${JSON.stringify(registrySpecifier)};` : `var _routes = import.meta.glob(${JSON.stringify(routeGlobs)}, { exhaustive: true });
610
+ // fix route keys: One expects '/${routerRoot}/...' prefix but import.meta.glob returns './${routerRoot}/...'
611
+ var routes = {};
612
+ Object.keys(_routes).forEach(function(key) {
613
+ var normalizedKey = key.replace(/^\\.\\//, '');
614
+ routes['/' + normalizedKey] = _routes[key];
615
+ });`;
616
+ const routeHmr = useDynamicRouteRegistry ? `if (import.meta.hot) {
617
+ import.meta.hot.accept(${JSON.stringify(registrySpecifier)}, function(next) {
618
+ if (globalThis.__VXRN_UPDATE_NATIVE_ROUTES__) {
619
+ globalThis.__VXRN_UPDATE_NATIVE_ROUTES__(next.routes);
620
+ }
621
+ });
622
+ }` : "";
555
623
  return {
556
624
  routes,
557
625
  plugin: {
@@ -561,20 +629,7 @@ createApp({
561
629
  },
562
630
  load(id) {
563
631
  if (id !== resolvedId) return;
564
- routes.files.clear();
565
- const walkRouteRoot = dir => {
566
- this.addWatchFile(dir);
567
- for (const entry of readdirSync(dir, {
568
- withFileTypes: true
569
- })) {
570
- const child = resolve(dir, entry.name);
571
- if (entry.isDirectory()) walkRouteRoot(child);else if (isRouteFile(child)) routes.files.add(normalizePath(child));
572
- }
573
- };
574
- if (statSync(routeRoot, {
575
- throwIfNoEntry: false
576
- })?.isDirectory()) walkRouteRoot(routeRoot);else this.addWatchFile(dirname(routeRoot));
577
- return entryCode;
632
+ return entryCode();
578
633
  }
579
634
  }
580
635
  };
@@ -660,6 +715,20 @@ export default HMRClient`,
660
715
  }
661
716
  };
662
717
  }
718
+ function reactNativeDedupePlugin(root) {
719
+ let reactNativeRoot;
720
+ return {
721
+ name: "vxrn:react-native-dedupe",
722
+ async resolveId(source, importer) {
723
+ if (source !== "react-native" && !source.startsWith("react-native/")) return;
724
+ reactNativeRoot ||= realpathSync(dirname(resolvePath("react-native/package.json", root)));
725
+ const subpath = source === "react-native" ? "index.js" : source.slice(13);
726
+ return this.resolve(normalizePath(resolve(reactNativeRoot, subpath)), importer, {
727
+ skipSelf: true
728
+ });
729
+ }
730
+ };
731
+ }
663
732
  function cssStubPlugin() {
664
733
  return {
665
734
  name: "vxrn:css-stub",
@@ -714,14 +783,16 @@ function vxrnCompilerPlugin(platform, dev, projectRoot = process.cwd(), sourceMa
714
783
  if (prepared.map) intermediateMaps.push(prepared.map);
715
784
  }
716
785
  }
717
- const compilerTarget = babelOptions.plugins[compilerPluginIndex][1]?.target ?? "19";
718
- const compilerOut = await compiler.transformOxcReactCompiler(id, curCode, compilerTarget, sourceMaps);
719
- if (compilerOut?.code) {
720
- curCode = compilerOut.code;
721
- if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map);
786
+ const compilerPluginOptions = babelOptions.plugins[compilerPluginIndex][1] || {};
787
+ if (!(compilerPluginOptions.compiler === "babel" || process.env.ONE_REACT_COMPILER === "babel" || process.env.VXRN_REACT_COMPILER === "babel")) {
788
+ const compilerOut = await compiler.transformOxcReactCompiler(id, curCode, compilerPluginOptions, sourceMaps);
789
+ if (compilerOut?.code) {
790
+ curCode = compilerOut.code;
791
+ if (sourceMaps && compilerOut.map) intermediateMaps.push(compilerOut.map);
792
+ }
793
+ babelOptions.plugins.splice(compilerPluginIndex, 1);
794
+ if (babelOptions.plugins.length === 0 && !babelOptions.configFile && !babelOptions.babelrc) babelOptions = null;
722
795
  }
723
- babelOptions.plugins.splice(compilerPluginIndex, 1);
724
- if (babelOptions.plugins.length === 0) babelOptions = null;
725
796
  }
726
797
  if (useWorklets) {
727
798
  const workletOut = await compiler.transformWorklets(id, curCode, sourceMaps, {
@@ -734,7 +805,7 @@ function vxrnCompilerPlugin(platform, dev, projectRoot = process.cwd(), sourceMa
734
805
  }
735
806
  if (babelOptions?.plugins) {
736
807
  babelOptions.plugins = babelOptions.plugins.filter(entry => !isWorkletPlugin(entry));
737
- if (babelOptions.plugins.length === 0) babelOptions = null;
808
+ if (babelOptions.plugins.length === 0 && !babelOptions.configFile && !babelOptions.babelrc) babelOptions = null;
738
809
  }
739
810
  }
740
811
  if (!babelOptions && !needsRefresh) {
@@ -1256,11 +1327,13 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
1256
1327
  }
1257
1328
  }
1258
1329
 
1259
- reload() {
1260
- var proxy = globalThis.__turboModuleProxy
1261
- ? globalThis.__turboModuleProxy('DevSettings')
1262
- : globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.DevSettings;
1263
- if (proxy && proxy.reload) proxy.reload();
1330
+ reload(reason) {
1331
+ var reloadNativeBundle = globalThis.__VXRN_RELOAD_NATIVE_DEV_BUNDLE__;
1332
+ if (typeof reloadNativeBundle === 'function') {
1333
+ reloadNativeBundle(reason);
1334
+ return;
1335
+ }
1336
+ console.error('[vxrn HMR]: DevSettings.reload is unavailable');
1264
1337
  }
1265
1338
 
1266
1339
  setup(socket) {
@@ -1282,10 +1355,10 @@ class ReactNativeDevRuntime extends BaseDevRuntime {
1282
1355
  if (message.type === 'hmr:update') {
1283
1356
  // a patch that cannot be applied in place is the reload signal
1284
1357
  if (!runtime.applyHmrUpdate(message.code, message.changedIds, message.seq)) {
1285
- runtime.reload();
1358
+ runtime.reload('native HMR update could not be applied');
1286
1359
  }
1287
1360
  } else if (message.type === 'hmr:reload') {
1288
- runtime.reload();
1361
+ runtime.reload('native route map changed');
1289
1362
  }
1290
1363
  } catch (error) {
1291
1364
  console.error('[vxrn HMR]: failed to apply update', error);