From cb03a41a676df983178f6e277f183d4f92992086 Mon Sep 17 00:00:00 2001 From: arrzdev <55147716+arrzdev@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:19:13 +0000 Subject: [PATCH 1/2] fix(router): a link tapped mid-preload no longer throws match._nonReactive A link preloads its route on intent, and the tap that follows navigates while that preload is still loading the route's chunk. The navigation's commit clears the expired cache, which drops the cached match of a route with no loader; the preload then read the evicted match and threw `match._nonReactive` (TanStack/router#7759). The router caught it and console.error'd it, which failed icon.spec.ts's StrictMode client navigation about 1 run in 8 on webkit. It was not a version skew: react-router 1.170.18 pins router-core 1.171.15 exactly and only one copy is installed. The fix is upstream (TanStack/router#7805, first released in router-core 1.171.16). This moves react-start to 1.168.36, the first release that carries it, which brings react-router 1.170.19 and router-core 1.171.16, and re-pins the two in-memory engine edits to router-generator 1.167.22 and start-plugin-core 1.171.27, whose edited files are byte-identical. These are dependencies, not peers: the published peer range does not change. The rewrite renames the match flag for the root not-found boundary from `globalNotFound` to `_notFound` (still dehydrated), so the shell's not-found check reads the new name; shell-layout.test.tsx's two 404 splash tests fail without it. create-adaptv-router.test.ts reproduces the race with a lazy route and fails on the old pins. The skew notes in link.spec.ts and capabilities-smoke.spec.ts are gone. --- CHANGELOG.md | 9 ++ package.json | 6 +- playground/e2e/capabilities-smoke.spec.ts | 10 +- playground/e2e/link.spec.ts | 4 +- pnpm-lock.yaml | 155 +++++++++++----------- src/shell/create-adaptv-router.test.ts | 52 ++++++++ src/shell/shell-layout.tsx | 6 +- src/vite/engine-hooks.ts | 4 +- 8 files changed, 148 insertions(+), 98 deletions(-) create mode 100644 src/shell/create-adaptv-router.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b92ac5..97fbead6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ API. - **adaptv and `create-adaptv` are MIT-licensed.** They were `UNLICENSED`. `LICENSE` is at the repo root and in `packages/create-adaptv`. +- **The router moves to its release with the preload fix** (TanStack Router 1.170.19, router core + 1.171.16, Start 1.168.36), and adaptv's two in-memory engine edits are pinned to the matching + versions. See Fixed. - **Hovers and type errors show adaptv's type names, not the router's.** This covers `notFoundScreen` and `notFoundComponent` (`NotFoundScreenComponent`, `NotFoundScreenProps`), `notFound` (`NotFoundOptions`), `isRedirect` (`RouteRedirect`), the route DSL (`RouteNode`, @@ -38,6 +41,12 @@ API. default without `!important`. A selector that targeted one of adaptv's Tailwind classes no longer matches; target the `data-adaptv` / `data-part` attributes instead. +### Fixed + +- **Tapping a link while its route is still preloading no longer logs a `TypeError`.** The tap + navigated mid-preload; the navigation evicted the preloaded route from the cache, and the preload + then read the evicted entry (`match._nonReactive`). The navigation itself always worked. + ## 0.1.0-alpha.1 The first public alpha. Not published to npm yet: publishing waits on approval, and until then the diff --git a/package.json b/package.json index 295312b1..dd03a1e9 100644 --- a/package.json +++ b/package.json @@ -144,10 +144,10 @@ "@capacitor/splash-screen": "8.0.2", "@capacitor/status-bar": "8.0.2", "@capawesome/capacitor-live-update": "8.3.0", - "@tanstack/react-router": "1.170.18", - "@tanstack/react-start": "1.168.32", + "@tanstack/react-router": "1.170.19", + "@tanstack/react-start": "1.168.36", "@tanstack/react-virtual": "3.14.6", - "@tanstack/router-core": "1.171.15", + "@tanstack/router-core": "1.171.16", "@tanstack/virtual-file-routes": "1.162.0", "@vitejs/plugin-react": "5.2.0", "commander": "15.0.0", diff --git a/playground/e2e/capabilities-smoke.spec.ts b/playground/e2e/capabilities-smoke.spec.ts index eace267b..af74f9c7 100644 --- a/playground/e2e/capabilities-smoke.spec.ts +++ b/playground/e2e/capabilities-smoke.spec.ts @@ -28,15 +28,7 @@ import { awaitClientHandover } from "./support/hydrated" /** * Console errors this spec does NOT fail on. Each entry is a filed, understood * error with a reason; there is no wildcard, and a new error is a failure until - * someone writes its line here. Empty on purpose: the one known handled error in - * the app — TanStack Router's preload path throwing `_nonReactive` on a - * react-router/router-core version skew (see link.spec.ts) — fires from a link - * being hovered or tapped, which nothing here does: a full run on both engines - * and six repeats of this spec never saw it on these routes. It is not excused - * ahead of time; if it ever shows up here the failure names it, and the line is - * one entry — with the follow-on the debug telemetry adds when it forwards that - * console.error to a log sink that is not running, which is why an entry that - * matched only the TypeError would not even make the test pass. + * someone writes its line here. Empty on purpose. */ const KNOWN_CONSOLE_ERRORS: { pattern: RegExp; reason: string }[] = [] diff --git a/playground/e2e/link.spec.ts b/playground/e2e/link.spec.ts index d4a322bf..8b55afd8 100644 --- a/playground/e2e/link.spec.ts +++ b/playground/e2e/link.spec.ts @@ -17,9 +17,7 @@ import { awaitClientHandover } from "./support/hydrated" * pop-vs-push. Those fire through the shared gesture engine, which — exactly like * Checkbox/Switch — does not respond to Playwright's synthetic mouse/keyboard/ * dispatched input; only a real finger or the OS drives it, so they are walked on - * the simulator. (Separately: TanStack Router's PRELOAD path logs `_nonReactive` - * here from a react-router/router-core version skew. It is caught and non-fatal, - * orthogonal to these two components, and flagged for its own cleanup.) + * the simulator. */ test.describe("Link & ExternalLink", () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 487f7d58..482e3e31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,17 +83,17 @@ importers: specifier: 8.3.0 version: 8.3.0(patch_hash=bde38c758771ca4a578a76b47f58ad30ebfec7c01dd91fd24390b86efab706fc)(@capacitor/core@8.4.3) '@tanstack/react-router': - specifier: 1.170.18 - version: 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 1.170.19 + version: 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: 1.168.32 - version: 1.168.32(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) + specifier: 1.168.36 + version: 1.168.36(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) '@tanstack/react-virtual': specifier: 3.14.6 version: 3.14.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-core': - specifier: 1.171.15 - version: 1.171.15 + specifier: 1.171.16 + version: 1.171.16 '@tanstack/virtual-file-routes': specifier: 1.162.0 version: 1.162.0 @@ -1847,26 +1847,26 @@ packages: resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} - '@tanstack/react-router@1.170.18': - resolution: {integrity: sha512-wpbGYZEp/fmz1q4bn7BD8VZ+/VZ7GBqSJv5V969pU+chP8y7dquWDmKTFMohvUegb9lg12m1uPVvD6kB2wORvQ==} + '@tanstack/react-router@1.170.19': + resolution: {integrity: sha512-cK7VUsc9+8gYZMqwJjCAhrmL9wPhkwxEcZ7cxlHdBjjmqHbZyNxXAml/KfLmSuxRDQGVk2iofpoHclDTSrkmmg==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-client@1.168.16': - resolution: {integrity: sha512-1OfHgy0wpHwe2tlB3FxMeA+IMX6Il/QAMf+8UdXuimReIc2Lz3BkMLBL38k4GIxBguX9sI8EMLO5jlTZ4e1olw==} + '@tanstack/react-start-client@1.168.17': + resolution: {integrity: sha512-FBlebCkUpxmObLzgHQJaCDp3abT6lnoidsHR/5B7hl4thqrWCjeytbuvc3m3t/i60I29GDs3PoRLbZJJMFOhcA==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-rsc@0.1.31': - resolution: {integrity: sha512-WxjkXYflq550vTNJpdPyMaPC+Vyh88L5wOL+SiDTjPMGne9ad7FZmoJxqfCFEv1e7HVKMH/mMoE8619TsTNVzQ==} + '@tanstack/react-start-rsc@0.1.35': + resolution: {integrity: sha512-G4/hNp529X4JvztM7jimLdKzNtqk4jBAYRICwv/vVarkkssQrjk5fHaLP7Lor8PdS2DuLs52JneQnRMxZ1lalQ==} engines: {node: '>=22.12.0'} peerDependencies: '@rspack/core': '>=2.0.0-0' - '@vitejs/plugin-rsc': '>=0.5.20' + '@vitejs/plugin-rsc': '>=0.5.30' react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' react-server-dom-rspack: '>=0.0.2' @@ -1878,15 +1878,15 @@ packages: react-server-dom-rspack: optional: true - '@tanstack/react-start-server@1.167.22': - resolution: {integrity: sha512-eH2PeHuLfL3R5YzE9+y2FfcE4Ld1LNV2ZfrCNVPJMMJFt+9nXDaRHg9BsEmc+JkTAGzz3FKLyQEoWwpbG6Ehqg==} + '@tanstack/react-start-server@1.167.24': + resolution: {integrity: sha512-2sJ7D+9EslcpCtjeQ9pNA8Xd6gndmzqcrW0xKObylMBTHJQHQOwipYEjO7HRS8Bk3I7tCAKH7wMTbfPsl7LF3Q==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start@1.168.32': - resolution: {integrity: sha512-y1WXHo+jPfHxiuuN1m+br06IcriiBQnEWryBdbKdEOS5vw2PmnOj+Cgf1/YcGOqtSougScWFeE2rL1FXWvsLLg==} + '@tanstack/react-start@1.168.36': + resolution: {integrity: sha512-Mg1JojcfZp9gYxoZ7bwPnzeSfrMKIywlTBJw7HzeM5YR45zRBlHMPfkj5zkKWigID/kth0psD6LfG7aqYnBGVg==} engines: {node: '>=22.12.0'} peerDependencies: '@rsbuild/core': ^2.0.0 @@ -1914,20 +1914,20 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.171.15': - resolution: {integrity: sha512-IILCDcLaItMZQ2jEmCABHY1Nhjjn5XUvwpQp3e4Nmu+vfg0BgYFuu/QASz2SwE2ZNbVMrvt8X/wxa+Gg5aErxA==} + '@tanstack/router-core@1.171.16': + resolution: {integrity: sha512-/qwawP5a45puA9DQtG8syrChWCV7uGMYs1p5rwgrICgiS/0pHo7zNOFyfVI02J1azWhCfFc6w6+SuiivPESYAA==} engines: {node: '>=20.19'} - '@tanstack/router-generator@1.167.21': - resolution: {integrity: sha512-m3oXZyienj8owialdyoZ0txHQrnEx/Ra+D9kWtar5fC2cWZr5Pvxl86VY2mX5RRLC5QLKLeRGT1x4HV95wHVDQ==} + '@tanstack/router-generator@1.167.22': + resolution: {integrity: sha512-tiKrH6xuCdTfmm4UuHSpLsZ5q2pwmhNbGpExp5u/CCRObjQM5nxz8Dce/X3w86ST6VSZFQMBNjLcsUMnVlJw/Q==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.168.23': - resolution: {integrity: sha512-0+PIcvnaAimFwjoEIeV3h7LKjzC8zNnp7pH2UamdKwQ9QlY99WU9V0Xl0zbM0i9hrUa/mKgWPDAzELmPUu5fMA==} + '@tanstack/router-plugin@1.168.24': + resolution: {integrity: sha512-FfPW45gfZHDC25AHo4MJB3ZljXEKFJenEgIDTELwnI6FtS90vXTQXjOjdTVNs/7B4R8+wLf7nWVrPSvBMp7zWw==} engines: {node: '>=20.19'} peerDependencies: '@rsbuild/core': '>=1.0.2 || ^2.0.0' - '@tanstack/react-router': ^1.170.18 + '@tanstack/react-router': ^1.170.19 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' vite-plugin-solid: ^2.11.10 || ^3.0.0-0 webpack: '>=5.92.0' @@ -1947,16 +1947,16 @@ packages: resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} engines: {node: '>=20.19'} - '@tanstack/start-client-core@1.170.14': - resolution: {integrity: sha512-yasBgEIFSWysL4EiFIGwp638nCoXXKiTqkc48EP2oty4OyNsZPTC1yfJ82zjq2KGkTAYtIaeMl7otqqRl1n85Q==} + '@tanstack/start-client-core@1.170.15': + resolution: {integrity: sha512-jzQ19jYFHcFeGADbZul6jNALP3ihTraKDkAnPzDIFyOKf/8B8py5qOi8pAl+0QlCBEXmxGw+5u+IU2bCr3SI5Q==} engines: {node: '>=22.12.0'} '@tanstack/start-fn-stubs@1.162.0': resolution: {integrity: sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==} engines: {node: '>=22.12.0'} - '@tanstack/start-plugin-core@1.171.24': - resolution: {integrity: sha512-l/tm+T0ntXHeIzr9kJDTJ2IDNZC0yFazjkvbEVeZsDOrJ8F+HiZmY+tXYqI5/nDYkwxY0DVQr+kGsTRVb6y2Jw==} + '@tanstack/start-plugin-core@1.171.27': + resolution: {integrity: sha512-VV9vUJ4qUYotWj6VqaUh1g6wTpijDWijHuMNymQKC3uAJQ/EVWJir+Jp/XhhSFtlmOeZXmCTLgwKdJwUzJtTxg==} engines: {node: '>=22.12.0'} peerDependencies: '@rsbuild/core': ^2.0.0 @@ -1967,12 +1967,12 @@ packages: vite: optional: true - '@tanstack/start-server-core@1.169.17': - resolution: {integrity: sha512-u0N+PHJhMHnzfnlXYI9F+A/qweDe3E2X0mfkORPGIEkNQgvS548RA9fjwvixR2en5b848CfpEqUzwFhm/tQ40Q==} + '@tanstack/start-server-core@1.169.19': + resolution: {integrity: sha512-q+RE1YI9SGckFOINW2RE/z1KmuXKrUnUWf/xQUWVYN0xXqZNxQvWrNEhoV2Fr+1TT6Fp0hy8oP1yTrNoteNg6Q==} engines: {node: '>=22.12.0'} - '@tanstack/start-storage-context@1.167.17': - resolution: {integrity: sha512-ntkDyGx0PE0opIlWNAMpkMb8qkjR4uyCUOfC0CiT0STM25+EcwPuwYNfDXXeVObMrTAPgsQ4yOj3xdY0Xr4ptw==} + '@tanstack/start-storage-context@1.167.18': + resolution: {integrity: sha512-Hpr/yvwYKIcXlfUt7MZ8mBHt9f2VNEXYwZXq9bIJVUvYhKcF/zzdMnc8UStTyRAynkgK/rZ36UvK9RrX4dcjeg==} engines: {node: '>=22.12.0'} '@tanstack/store@0.9.3': @@ -5992,33 +5992,32 @@ snapshots: '@tanstack/history@1.162.0': {} - '@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/history': 1.162.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.171.15 + '@tanstack/router-core': 1.171.16 isbot: 5.2.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-start-client@1.168.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-start-client@1.168.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.171.15 - '@tanstack/start-client-core': 1.170.14 + '@tanstack/react-router': 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.16 + '@tanstack/start-client-core': 1.170.15 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-start-rsc@0.1.31(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': + '@tanstack/react-start-rsc@0.1.35(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': dependencies: - '@tanstack/react-router': 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.171.15 + '@tanstack/react-router': 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.16 '@tanstack/router-utils': 1.162.2 - '@tanstack/start-client-core': 1.170.14 + '@tanstack/start-client-core': 1.170.15 '@tanstack/start-fn-stubs': 1.162.0 - '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) - '@tanstack/start-server-core': 1.169.17(crossws@0.4.10(srvx@0.11.22)) - '@tanstack/start-storage-context': 1.167.17 + '@tanstack/start-plugin-core': 1.171.27(@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) + '@tanstack/start-storage-context': 1.167.18 pathe: 2.0.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -6036,26 +6035,26 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/react-start-server@1.167.22(crossws@0.4.10(srvx@0.11.22))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-start-server@1.167.24(crossws@0.4.10(srvx@0.11.22))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.171.15 - '@tanstack/start-server-core': 1.169.17(crossws@0.4.10(srvx@0.11.22)) + '@tanstack/react-router': 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.16 + '@tanstack/start-server-core': 1.169.19(crossws@0.4.10(srvx@0.11.22)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - crossws - '@tanstack/react-start@1.168.32(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': + '@tanstack/react-start@1.168.36(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': dependencies: - '@tanstack/react-router': 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-client': 1.168.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-rsc': 0.1.31(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) - '@tanstack/react-start-server': 1.167.22(crossws@0.4.10(srvx@0.11.22))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-client': 1.168.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-rsc': 0.1.35(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) + '@tanstack/react-start-server': 1.167.24(crossws@0.4.10(srvx@0.11.22))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-utils': 1.162.2 - '@tanstack/start-client-core': 1.170.14 - '@tanstack/start-plugin-core': 1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) - '@tanstack/start-server-core': 1.169.17(crossws@0.4.10(srvx@0.11.22)) + '@tanstack/start-client-core': 1.170.15 + '@tanstack/start-plugin-core': 1.171.27(@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) + '@tanstack/start-server-core': 1.169.19(crossws@0.4.10(srvx@0.11.22)) pathe: 2.0.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -6088,17 +6087,17 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/router-core@1.171.15': + '@tanstack/router-core@1.171.16': dependencies: '@tanstack/history': 1.162.0 cookie-es: 3.1.1 seroval: 1.5.5 seroval-plugins: 1.5.5(seroval@1.5.5) - '@tanstack/router-generator@1.167.21': + '@tanstack/router-generator@1.167.22': dependencies: '@babel/types': 7.29.7 - '@tanstack/router-core': 1.171.15 + '@tanstack/router-core': 1.171.16 '@tanstack/router-utils': 1.162.2 '@tanstack/virtual-file-routes': 1.162.0 jiti: 2.7.0 @@ -6108,19 +6107,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': + '@tanstack/router-plugin@1.168.24(@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': dependencies: '@babel/core': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - '@tanstack/router-core': 1.171.15 - '@tanstack/router-generator': 1.167.21 + '@tanstack/router-core': 1.171.16 + '@tanstack/router-generator': 1.167.22 '@tanstack/router-utils': 1.162.2 chokidar: 5.0.0 unplugin: 3.3.0(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) zod: 4.4.3 optionalDependencies: - '@tanstack/react-router': 1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0) transitivePeerDependencies: - '@farmfe/core' @@ -6145,25 +6144,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/start-client-core@1.170.14': + '@tanstack/start-client-core@1.170.15': dependencies: - '@tanstack/router-core': 1.171.15 + '@tanstack/router-core': 1.171.16 '@tanstack/start-fn-stubs': 1.162.0 - '@tanstack/start-storage-context': 1.167.17 + '@tanstack/start-storage-context': 1.167.18 seroval: 1.5.5 '@tanstack/start-fn-stubs@1.162.0': {} - '@tanstack/start-plugin-core@1.171.24(@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': + '@tanstack/start-plugin-core@1.171.27(@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.10(srvx@0.11.22))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.7 '@babel/types': 7.29.7 - '@tanstack/router-core': 1.171.15 - '@tanstack/router-generator': 1.167.21 - '@tanstack/router-plugin': 1.168.23(@tanstack/react-router@1.170.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) + '@tanstack/router-core': 1.171.16 + '@tanstack/router-generator': 1.167.22 + '@tanstack/router-plugin': 1.168.24(@tanstack/react-router@1.170.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.0)(rolldown@1.2.0)(rollup@4.62.2)(vite@8.0.11(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.49.0)) '@tanstack/router-utils': 1.162.2 - '@tanstack/start-server-core': 1.169.17(crossws@0.4.10(srvx@0.11.22)) + '@tanstack/start-server-core': 1.169.19(crossws@0.4.10(srvx@0.11.22)) exsolve: 1.1.0 lightningcss: 1.32.0 pathe: 2.0.3 @@ -6192,21 +6191,21 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/start-server-core@1.169.17(crossws@0.4.10(srvx@0.11.22))': + '@tanstack/start-server-core@1.169.19(crossws@0.4.10(srvx@0.11.22))': dependencies: '@tanstack/history': 1.162.0 - '@tanstack/router-core': 1.171.15 - '@tanstack/start-client-core': 1.170.14 - '@tanstack/start-storage-context': 1.167.17 + '@tanstack/router-core': 1.171.16 + '@tanstack/start-client-core': 1.170.15 + '@tanstack/start-storage-context': 1.167.18 fetchdts: 0.1.7 h3-v2: h3@2.0.1-rc.20(crossws@0.4.10(srvx@0.11.22)) seroval: 1.5.5 transitivePeerDependencies: - crossws - '@tanstack/start-storage-context@1.167.17': + '@tanstack/start-storage-context@1.167.18': dependencies: - '@tanstack/router-core': 1.171.15 + '@tanstack/router-core': 1.171.16 '@tanstack/store@0.9.3': {} diff --git a/src/shell/create-adaptv-router.test.ts b/src/shell/create-adaptv-router.test.ts new file mode 100644 index 00000000..207dedd0 --- /dev/null +++ b/src/shell/create-adaptv-router.test.ts @@ -0,0 +1,52 @@ +import { + createLazyRoute, + createMemoryHistory, + createRootRoute, + createRoute, +} from "@tanstack/react-router" +import { afterEach, describe, expect, it, vi } from "vitest" +import { createAdaptvRouter } from "#adaptv/shell/create-adaptv-router" + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe("createAdaptvRouter", () => { + //A link preloads its route on intent (hover, touchstart), and the tap that follows + //navigates while that preload is still in flight. The navigation's commit clears + //the expired cache, which drops the cached match of a route with no loader, so the + //preload came back to a match that was gone and threw `match._nonReactive` (the + //route engine caught it and console.error'd it). Seen ~1 in 8 on webkit in + //playground/e2e/icon.spec.ts, the StrictMode client-navigation test. + it("a navigation that commits during a preload leaves the preload to finish quietly", async () => { + let release = () => {} + const chunk = new Promise((resolve) => { + release = resolve + }) + const rootRoute = createRootRoute() + const routeTree = rootRoute.addChildren([ + createRoute({ getParentRoute: () => rootRoute, path: "/" }), + //code-split like every app route, and no loader: its cached match is the one + //a commit evicts while the chunk is still loading + createRoute({ + getParentRoute: () => rootRoute, + path: "/preloaded", + }).lazy(() => chunk.then(() => createLazyRoute("/preloaded")({}))), + createRoute({ getParentRoute: () => rootRoute, path: "/elsewhere" }), + ]) + const router = createAdaptvRouter({ + routeTree, + options: { history: createMemoryHistory({ initialEntries: ["/"] }) }, + }) + await router.load() + const error = vi.spyOn(console, "error").mockImplementation(() => {}) + + const preload = router.preloadRoute({ to: "/preloaded" }) + await router.navigate({ to: "/elsewhere" }) + release() + await preload + + expect(router.state.location.pathname).toBe("/elsewhere") + expect(error).not.toHaveBeenCalled() + }) +}) diff --git a/src/shell/shell-layout.tsx b/src/shell/shell-layout.tsx index 199cd420..425f7bc9 100644 --- a/src/shell/shell-layout.tsx +++ b/src/shell/shell-layout.tsx @@ -173,12 +173,12 @@ type RoutingShellProps = { */ function useNotFoundBoundary() { return useRouterState({ - //`globalNotFound` marks the boundary that renders the not-found; `notFound` + //`_notFound` marks the boundary that renders the not-found; `notFound` //status is the nested (`notFoundMode: "fuzzy"`) form of the same thing. Both //cut the tree off below that point. select: (state) => state.matches.some( - (match) => match.globalNotFound || match.status === "notFound", + (match) => match._notFound || match.status === "notFound", ), }) } @@ -287,7 +287,7 @@ export function RoutingShell({ // //adaptv mounts the splash, so adaptv retires it — there is no boot left to cover //when the app tree is never going to mount. During render, not in an effect, so - //the SSR pass and hydration agree (`globalNotFound` is dehydrated) and a native + //the SSR pass and hydration agree (`_notFound` is dehydrated) and a native //cold start never paints the splash even for a frame. Latched, because retiring //is one-way: re-mounting on the navigation *out* of a 404 would hand a fresh //splash an already-set ready gate and replay it over a booted app. diff --git a/src/vite/engine-hooks.ts b/src/vite/engine-hooks.ts index de4e3303..ee3cb8ba 100644 --- a/src/vite/engine-hooks.ts +++ b/src/vite/engine-hooks.ts @@ -53,7 +53,7 @@ export const ENGINE_EDITS: readonly EngineEdit[] = [ //the import the generator writes into route files names the framework barrel //(ADAPTV_ROUTER_PKG, set by adaptv's vite plugin). → src/vite/router-autoimport.ts pkg: "@tanstack/router-generator", - version: "1.167.21", + version: "1.167.22", file: "dist/esm/transform/transform.js", replace: [ [ @@ -67,7 +67,7 @@ export const ENGINE_EDITS: readonly EngineEdit[] = [ //`#tanstack-router-entry` stay: they are subpath imports declared in //start-server-core's own `imports` map, and never reach the HTML. pkg: "@tanstack/start-plugin-core", - version: "1.171.24", + version: "1.171.27", file: "dist/esm/constants.js", replace: [ [ From db77cc01478c08c69e3d64cf115f89ccc28e623a Mon Sep 17 00:00:00 2001 From: arrzdev <55147716+arrzdev@users.noreply.github.com> Date: Thu, 8 Oct 2026 02:30:20 +0000 Subject: [PATCH 2/2] fix(router): a missing route chunk reloads with nothing drawn over the reload react-router 1.170.19, the first release with the #7759 preload fix, checks the stale-chunk reload key at render time instead of latching the decision when the import fails. React renders a suspended route more than once, so the second render found the key set and threw the import error to the boundary: the app's error screen drew over adaptv's stale-chunk recovery reload, and e2e-sw/stale-chunk.spec.ts failed on both engines. adaptv now serves the app's modules a @tanstack/react-router that re-exports the router with its own lazyRouteComponent, the 1.170.18 latch, shipped as a dist entry like the other modules the consumer's build compiles. No public export changes. lazy-route-component.test.ts fails against the router's own function; tanstack-resolve.test.ts covers the redirect and the served module. --- src/execution-boundary.test.ts | 1 + src/routes/lazy-route-component.test.ts | 83 +++++++++++++++++++++++++ src/routes/lazy-route-component.ts | 80 ++++++++++++++++++++++++ src/vite/package-files.ts | 11 +++- src/vite/tanstack-resolve.test.ts | 37 ++++++++++- src/vite/tanstack-resolve.ts | 28 ++++++++- tsdown.config.ts | 1 + 7 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 src/routes/lazy-route-component.test.ts create mode 100644 src/routes/lazy-route-component.ts diff --git a/src/execution-boundary.test.ts b/src/execution-boundary.test.ts index 6690ac94..802950e7 100644 --- a/src/execution-boundary.test.ts +++ b/src/execution-boundary.test.ts @@ -83,6 +83,7 @@ const CONSUMER_BUILD_ENTRIES = [ "src/routes/client-entry.tsx", "src/routes/router-entry.tsx", "src/components/boot-error.tsx", + "src/routes/lazy-route-component.ts", "src/sw/default-worker.ts", ] diff --git a/src/routes/lazy-route-component.test.ts b/src/routes/lazy-route-component.test.ts new file mode 100644 index 00000000..c3c3d1d3 --- /dev/null +++ b/src/routes/lazy-route-component.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it, vi } from "vitest" +import { lazyRouteComponent } from "#adaptv/routes/lazy-route-component.ts" + +/* + * A split route whose chunk is gone reloads the page once. React renders the suspended + * route more than once before the page goes, and with react-router 1.170.19's + * `lazyRouteComponent` the second render threw the import error, so the app's error + * screen drew over the reload. The end-to-end proof is `e2e-sw/stale-chunk.spec.ts`. + */ + +const MISSING = new TypeError( + "Failed to fetch dynamically imported module: /assets/home-old.js", +) + +afterEach(() => { + sessionStorage.clear() + vi.restoreAllMocks() +}) + +function stubReload() { + const reload = vi.fn() + vi.spyOn(window, "location", "get").mockReturnValue({ + ...window.location, + reload, + }) + return reload +} + +describe("lazyRouteComponent", () => { + it("suspends every render after a missing chunk, until the reload lands", async () => { + const reload = stubReload() + const Lazy = lazyRouteComponent(() => Promise.reject(MISSING)) + await Lazy.preload() + + for (let render = 0; render < 3; render++) { + let thrown: unknown + try { + Lazy({}) + } catch (value) { + thrown = value + } + expect(thrown).toBeInstanceOf(Promise) + } + expect(reload).toHaveBeenCalled() + expect( + sessionStorage.getItem(`tanstack_router_reload:${MISSING.message}`), + ).toBe("1") + }) + + it("throws the error when the reload already happened once for that chunk", async () => { + const reload = stubReload() + sessionStorage.setItem( + `tanstack_router_reload:${MISSING.message}`, + "1", + ) + const Lazy = lazyRouteComponent(() => Promise.reject(MISSING)) + await Lazy.preload() + + expect(() => Lazy({})).toThrow(MISSING) + expect(reload).not.toHaveBeenCalled() + }) + + it("throws any other import error without reloading", async () => { + const reload = stubReload() + const failure = new Error("boom") + const Lazy = lazyRouteComponent(() => Promise.reject(failure)) + await Lazy.preload() + + expect(() => Lazy({})).toThrow(failure) + expect(reload).not.toHaveBeenCalled() + }) + + it("renders the named export once the chunk has loaded", async () => { + const Page = () => null + const Lazy = lazyRouteComponent( + () => Promise.resolve({ Page }), + "Page", + ) + await Lazy.preload() + + expect(Lazy({})).toMatchObject({ type: Page }) + }) +}) diff --git a/src/routes/lazy-route-component.ts b/src/routes/lazy-route-component.ts new file mode 100644 index 00000000..92e4c96d --- /dev/null +++ b/src/routes/lazy-route-component.ts @@ -0,0 +1,80 @@ +import type { ComponentType } from "react" +import { createElement, use } from "react" + +/** + * The `lazyRouteComponent` TanStack's code-splitter writes into every split route of the + * app. `tanstack-resolve.ts` serves the app this one in place of the router's own. + * + * A route whose chunk is gone (the deploy moved on under an open tab) reloads the page + * once, guarded by a `tanstack_router_reload:` sessionStorage key so a chunk that is + * really missing does not loop. The decision is LATCHED when the import fails: every + * render after it suspends until the reload lands. + * + * react-router 1.170.19 moved the decision into the render: the first render sets the key + * and reloads, and the next one, before the page has gone, finds the key set and throws + * the import error to the route's error boundary. React renders a suspended route more + * than once, so the app's error screen drew over the reload, and `stale-chunk.spec.ts` + * failed on both engines. That release is also the first with the fix for + * TanStack/router#7759 (a preload reading a match a navigation had just evicted), so + * adaptv keeps the router and owns this one function, as 1.170.18 shipped it. + */ +export function lazyRouteComponent< + TModule extends Record, + TKey extends keyof TModule = "default", +>(importer: () => Promise, exportName?: TKey) { + let loadPromise: Promise | undefined + let comp: ComponentType | undefined + let error: unknown + let reload = false + + const load = () => { + if (!loadPromise) + loadPromise = importer() + .then((res) => { + loadPromise = undefined + comp = res[exportName ?? "default"] as ComponentType + }) + .catch((err: unknown) => { + error = err + if ( + isModuleNotFoundError(err) && + typeof window !== "undefined" && + typeof sessionStorage !== "undefined" + ) { + const storageKey = `tanstack_router_reload:${err.message}` + if (!sessionStorage.getItem(storageKey)) { + sessionStorage.setItem(storageKey, "1") + reload = true + } + } + }) + return loadPromise + } + + const lazyComp = function Lazy(props: object) { + if (reload) { + window.location.reload() + throw new Promise(() => {}) + } + if (error) throw error + if (!comp) use(load()) + return createElement(comp as ComponentType, props) + } + lazyComp.preload = load + return lazyComp +} + +//router-core's `isModuleNotFoundError`, copied: router-core is not adaptv's dependency. +//The three engines' messages for a dynamic import that failed to fetch. +function isModuleNotFoundError(error: unknown): error is Error { + if (!(error instanceof Error)) return false + return ( + error.message.startsWith( + "Failed to fetch dynamically imported module", + ) || + error.message.startsWith( + "error loading dynamically imported module", + ) || + error.message.startsWith("Importing a module script failed") + ) +} diff --git a/src/vite/package-files.ts b/src/vite/package-files.ts index fd5c1f9f..3d1c468c 100644 --- a/src/vite/package-files.ts +++ b/src/vite/package-files.ts @@ -22,9 +22,10 @@ const PACKAGE_NAME = "@arrzdev/adaptv" /** * The modules adaptv hands to the consumer's own build, which compiles them as part of the * app: Start's client and router entries, the root route the route DSL points at, the - * service worker esbuild bundles, and the boot screen prerendered into the shell. In a - * checkout they are source; in a published package each is its own tsdown entry, so the - * consumer gets the same file the framework's own entries share chunks with. + * service worker esbuild bundles, the boot screen prerendered into the shell, and the + * `lazyRouteComponent` the app's split routes import. In a checkout they are source; in a + * published package each is its own tsdown entry, so the consumer gets the same file the + * framework's own entries share chunks with. * → tsdown.config.ts */ export const SHIPPED_FILES = { @@ -48,6 +49,10 @@ export const SHIPPED_FILES = { src: "src/components/boot-error.tsx", dist: "dist/boot-error.mjs", }, + "lazy-route-component": { + src: "src/routes/lazy-route-component.ts", + dist: "dist/lazy-route-component.mjs", + }, } as const export type ShippedFile = keyof typeof SHIPPED_FILES diff --git a/src/vite/tanstack-resolve.test.ts b/src/vite/tanstack-resolve.test.ts index 624cd4a3..cd20e36c 100644 --- a/src/vite/tanstack-resolve.test.ts +++ b/src/vite/tanstack-resolve.test.ts @@ -13,6 +13,7 @@ import { adaptvTanstackResolvePlugin } from "#adaptv/vite/tanstack-resolve.ts" const APP = path.resolve("/work/my-app") const ADAPTV = path.resolve("/pkgs/adaptv") +const LAZY = path.join(ADAPTV, "dist/lazy-route-component.mjs") function resolveFrom(source: string, importer: string | undefined) { const plugin = adaptvTanstackResolvePlugin(APP, ADAPTV) @@ -28,14 +29,48 @@ function resolveFrom(source: string, importer: string | undefined) { } describe("adaptvTanstackResolvePlugin", () => { - it("resolves a TanStack import in an app route module from adaptv's package", () => { + it("resolves a TanStack import in an app module from adaptv's package", () => { const { resolve } = resolveFrom( + "@tanstack/react-start", + path.join(APP, "src/app.tsx"), + ) + expect(resolve).toHaveBeenCalledWith( + "@tanstack/react-start", + path.join(ADAPTV, "package.json"), + { ssr: false, skipSelf: true }, + ) + }) + + //react-router 1.170.19 checks the stale-chunk reload key at render time, so the + //second render of a route whose chunk was gone threw to the error screen while the + //reload was in flight. The end-to-end proof is `e2e-sw/stale-chunk.spec.ts`. + it("gives an app route module the router with adaptv's lazyRouteComponent", async () => { + const { result } = resolveFrom( "@tanstack/react-router", path.join( APP, "src/routing/pages/home.page.tsx?tsr-split=component", ), ) + const id = await result + expect(id).toBe("\0virtual:adaptv/react-router") + + const plugin = adaptvTanstackResolvePlugin(APP, ADAPTV, LAZY) + const load = plugin.load as (id: string) => string | null + expect(load(id as string)).toBe( + [ + 'export * from "@tanstack/react-router"', + `export { lazyRouteComponent } from ${JSON.stringify(LAZY)}`, + "", + ].join("\n"), + ) + }) + + it("resolves that module's own router import from adaptv's package", () => { + const { resolve } = resolveFrom( + "@tanstack/react-router", + "\0virtual:adaptv/react-router", + ) expect(resolve).toHaveBeenCalledWith( "@tanstack/react-router", path.join(ADAPTV, "package.json"), diff --git a/src/vite/tanstack-resolve.ts b/src/vite/tanstack-resolve.ts index 13ebf128..e1bd54f6 100644 --- a/src/vite/tanstack-resolve.ts +++ b/src/vite/tanstack-resolve.ts @@ -1,6 +1,9 @@ import path from "node:path" import type { Plugin } from "vite" -import { adaptvPackageRoot } from "#adaptv/vite/package-files.ts" +import { + adaptvPackageRoot, + adaptvShippedFile, +} from "#adaptv/vite/package-files.ts" //The entries Start adds to `resolve.dedupe` (`@tanstack/react-start`'s `plugin/vite.js`). //Only these go: an app's own `@tanstack/*` entry, such as `@tanstack/react-query`, stays. @@ -17,6 +20,10 @@ const ROUTER_CORE_PREBUNDLE = [ "@tanstack/router-core/scroll-restoration-script", ].map((entry) => `@arrzdev/adaptv > @tanstack/react-router > ${entry}`) +const ROUTER = "@tanstack/react-router" +//The router the app's modules get: TanStack's, with adaptv's `lazyRouteComponent`. +const APP_ROUTER_ID = "\0virtual:adaptv/react-router" + /** * Resolve the `@tanstack/*` imports TanStack writes into the APP's modules from adaptv's * own dependencies. @@ -66,6 +73,11 @@ const ROUTER_CORE_PREBUNDLE = [ * Node answered every page with a 500. A build bundles it either way. Every app needs * the entry, so the plugin adds it and an app's `vite.config.ts` carries no `ssr` key. * + * The app's `@tanstack/react-router` is not the router itself but a module that re-exports + * it with adaptv's own `lazyRouteComponent` in place of TanStack's. The split routes are + * the only importers of that function, and react-router 1.170.19 broke the stale-chunk + * reload it does. → src/routes/lazy-route-component.ts + * * In dev the browser's copy of the router is pre-bundled up front. Start puts every * package that peers on it, the router and adaptv included, in the client's * `optimizeDeps.exclude`, so the router is served as source, and Vite met its @@ -78,6 +90,7 @@ const ROUTER_CORE_PREBUNDLE = [ export function adaptvTanstackResolvePlugin( appRoot: string, packageRoot = adaptvPackageRoot(), + lazyRouteFile?: string, ): Plugin { const from = path.join(packageRoot, "package.json") const appDir = `${path.resolve(appRoot)}${path.sep}` @@ -108,13 +121,26 @@ export function adaptvTanstackResolvePlugin( }, resolveId(source, importer, options) { if (!source.startsWith("@tanstack/") || !importer) return null + if (importer === APP_ROUTER_ID) + return this.resolve(source, from, { ...options, skipSelf: true }) const file = importer.split("?")[0] ?? importer if ( !file.startsWith(appDir) || file.includes(`${path.sep}node_modules${path.sep}`) ) return null + if (source === ROUTER) return APP_ROUTER_ID return this.resolve(source, from, { ...options, skipSelf: true }) }, + load(id) { + if (id !== APP_ROUTER_ID) return null + const lazy = + lazyRouteFile ?? adaptvShippedFile("lazy-route-component") + return [ + `export * from ${JSON.stringify(ROUTER)}`, + `export { lazyRouteComponent } from ${JSON.stringify(lazy)}`, + "", + ].join("\n") + }, } } diff --git a/tsdown.config.ts b/tsdown.config.ts index 60a742ff..a5f018ec 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -57,6 +57,7 @@ const browserEntry = { "client-entry": "src/routes/client-entry.tsx", "router-entry": "src/routes/router-entry.tsx", "boot-error": "src/components/boot-error.tsx", + "lazy-route-component": "src/routes/lazy-route-component.ts", } /**