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/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/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: [ [ 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", } /**