hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy
zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)#
z)0C|?$o>jzh<|-cpfK7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_
zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg
z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf`
z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi
z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S
z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W
zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie
zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j
literal 0
HcmV?d00001
diff --git a/examples/react-router-8/react-router.config.ts b/examples/react-router-8/react-router.config.ts
new file mode 100644
index 00000000..f26e5657
--- /dev/null
+++ b/examples/react-router-8/react-router.config.ts
@@ -0,0 +1,6 @@
+export default {
+ ssr: true,
+ routeDiscovery: { mode: 'initial' },
+ splitRouteModules: true,
+ subResourceIntegrity: true,
+};
diff --git a/examples/react-router-8/rsbuild.config.ts b/examples/react-router-8/rsbuild.config.ts
new file mode 100644
index 00000000..36cc0bc3
--- /dev/null
+++ b/examples/react-router-8/rsbuild.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from '@rsbuild/core';
+import { pluginReact } from '@rsbuild/plugin-react';
+import { pluginReactRouter } from 'rsbuild-plugin-react-router';
+
+export default defineConfig({
+ plugins: [pluginReactRouter(), pluginReact()],
+});
diff --git a/examples/react-router-8/tests/e2e/react-router-8.test.ts b/examples/react-router-8/tests/e2e/react-router-8.test.ts
new file mode 100644
index 00000000..ea1187b1
--- /dev/null
+++ b/examples/react-router-8/tests/e2e/react-router-8.test.ts
@@ -0,0 +1,44 @@
+import { expect, test } from '@playwright/test';
+
+test('renders the React Router 8 default template without browser errors', async ({
+ page,
+}) => {
+ const browserProblems: string[] = [];
+ page.on('console', message => {
+ if (message.type() === 'error') {
+ browserProblems.push(`console error: ${message.text()}`);
+ }
+ });
+ page.on('pageerror', error => {
+ browserProblems.push(`page error: ${error.message}`);
+ });
+ page.on('response', response => {
+ if (response.status() >= 500) {
+ browserProblems.push(`${response.status()} response: ${response.url()}`);
+ }
+ });
+ page.on('requestfailed', request => {
+ if (request.resourceType() !== 'websocket') {
+ browserProblems.push(
+ `${request.method()} ${request.url()} failed: ${
+ request.failure()?.errorText ?? 'unknown error'
+ }`
+ );
+ }
+ });
+
+ const response = await page.goto('/');
+ expect(response?.ok()).toBe(true);
+ await expect(
+ page.getByRole('heading', { name: 'Welcome to React Router' })
+ ).toBeVisible();
+ await expect(page).toHaveTitle('New React Router App');
+ await page.waitForFunction(
+ () =>
+ (window as Window & { __reactRouterRouteModules?: unknown })
+ .__reactRouterRouteModules !== undefined
+ );
+ await page.waitForTimeout(250);
+
+ expect(browserProblems).toEqual([]);
+});
diff --git a/examples/react-router-8/tsconfig.json b/examples/react-router-8/tsconfig.json
new file mode 100644
index 00000000..31a91e13
--- /dev/null
+++ b/examples/react-router-8/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["node", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true,
+ "rootDirs": [".", ".react-router/types/"],
+ "skipLibCheck": true,
+ "strict": true
+ }
+}
diff --git a/package.json b/package.json
index 5c9148a1..797a04b1 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"bench:baseline": "node scripts/bench-builds.mjs --profile default --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/baseline",
"bench:full": "node scripts/bench-builds.mjs --profile full --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/full",
"bench:large": "node scripts/bench-builds.mjs --profile large --iterations 1 --warmup 0 --clean cold --format both --out .benchmark/results/large",
- "e2e": "pnpm build && pnpm test:package-interop && pnpm test:react-router-matrix && pnpm --filter './examples/{default-template,spa-mode,prerender,custom-node-server,cloudflare,client-only}' test:e2e",
+ "e2e": "pnpm build && pnpm test:package-interop && pnpm --filter './examples/{default-template,spa-mode,prerender,custom-node-server,cloudflare,client-only,react-router-8}' test:e2e",
"dev": "rslib build --watch",
"test": "rstest run",
"test:watch": "rstest watch",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aa58b4ab..e5c63cb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,7 +56,7 @@ importers:
version: 2.31.0(@types/node@25.0.10)
'@react-router/dev':
specifier: ^8.0.1
- version: 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/config':
specifier: workspace:*
version: link:config
@@ -146,7 +146,7 @@ importers:
dependencies:
'@react-router/express':
specifier: ^7.13.0
- version: 7.13.0(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ version: 7.13.0(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/node':
specifier: ^7.13.0
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
@@ -1584,6 +1584,70 @@ importers:
specifier: ^6.0.5
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
+ examples/react-router-8:
+ dependencies:
+ '@react-router/fs-routes':
+ specifier: ^8.0.1
+ version: 8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ '@react-router/node':
+ specifier: ^8.0.1
+ version: 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@react-router/serve':
+ specifier: ^8.0.1
+ version: 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@vanilla-extract/css':
+ specifier: ^1.20.1
+ version: 1.21.0
+ react:
+ specifier: ^19.2.4
+ version: 19.2.7
+ react-dom:
+ specifier: ^19.2.4
+ version: 19.2.7(react@19.2.7)
+ react-router:
+ specifier: ^8.0.1
+ version: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ serialize-javascript:
+ specifier: ^6.0.1
+ version: 6.0.2
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.58.0
+ version: 1.58.0
+ '@react-router/dev':
+ specifier: ^8.0.1
+ version: 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ '@rsbuild/core':
+ specifier: 2.1.0
+ version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ '@rsbuild/plugin-react':
+ specifier: 2.1.0
+ version: 2.1.0(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))
+ '@types/node':
+ specifier: ^25.0.10
+ version: 25.0.10
+ '@types/react':
+ specifier: ^19.2.10
+ version: 19.2.10
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.10)
+ cross-env:
+ specifier: ^10.1.0
+ version: 10.1.0
+ rsbuild-plugin-react-router:
+ specifier: workspace:*
+ version: link:../..
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
+ vite-env-only:
+ specifier: ^3.0.3
+ version: 3.0.3(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
+
examples/spa-mode:
dependencies:
'@react-router/express':
@@ -2084,6 +2148,9 @@ packages:
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+ '@emotion/hash@0.9.2':
+ resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
+
'@epic-web/cachified@5.6.1':
resolution: {integrity: sha512-+VKwMhqM43l2s+gX28Telcf6bUJk1Zaj0Ix2i8K4R2QW8WgPE0q3THCnr0xZg5chw35/B4SkHS43an2fqKOFnQ==}
@@ -4037,6 +4104,27 @@ packages:
typescript:
optional: true
+ '@react-router/express@8.0.1':
+ resolution: {integrity: sha512-FWErptC9nFtaRo3SRsHgO60C1bCpUU35ATDvJulQIYXxDsXUdicyhJWCrl5DeEO2pUeqyPA4taP7l7aWkz2qZQ==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ express: ^4.22.2 || ^5
+ react-router: 8.0.1
+ typescript: ^5.1.0 || ^6.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@react-router/fs-routes@8.0.1':
+ resolution: {integrity: sha512-AvVOgD51NONhWECESN8kATjq7bKRZg/PmHA/vk9DrRIYFxCPFe3xiEvtIIk5q6Ng61lQUHfUzx5e5K95VpIAiA==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ '@react-router/dev': ^8.0.1
+ typescript: ^5.1.0 || ^6.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@react-router/node@7.13.0':
resolution: {integrity: sha512-Mhr3fAou19oc/S93tKMIBHwCPfqLpWyWM/m0NWd3pJh/wZin8/9KhAdjwxhYbXw1TrTBZBLDENa35uZ+Y7oh3A==}
engines: {node: '>=20.0.0'}
@@ -4084,6 +4172,13 @@ packages:
peerDependencies:
react-router: 7.18.0
+ '@react-router/serve@8.0.1':
+ resolution: {integrity: sha512-7kCZhE4cT0y4JMHpG1bJoIfy9tYWSqDqzZUYylQL9UCLYg9vq84X33UC6Xi9eQB9SRAuDM5iKQtTrGEstIMVKA==}
+ engines: {node: '>=22.22.0'}
+ hasBin: true
+ peerDependencies:
+ react-router: 8.0.1
+
'@remix-run/node-fetch-server@0.13.3':
resolution: {integrity: sha512-UfjOXed/DQteaM5VyTfqTeGpHwyL2J5aoRGY6cydip4tt1ehNNeSwuXCC7AEGE0RWBs/7bgKxYkL/B/+UDe4AA==}
@@ -5321,6 +5416,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@vanilla-extract/css@1.21.0':
+ resolution: {integrity: sha512-lqdRtP622Z85RprHlJemV5+ipdi+g48J115LaL8nrI64iixIp4SWPlvAEPf3o9pwEZaZPb5/ZfRwiXLE4p3+kQ==}
+
+ '@vanilla-extract/private@1.0.9':
+ resolution: {integrity: sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==}
+
'@vitejs/plugin-react@5.1.2':
resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -6085,6 +6186,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-es@3.1.1:
+ resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
+
cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
@@ -6288,6 +6392,9 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ deep-object-diff@1.1.9:
+ resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==}
+
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -6878,6 +6985,10 @@ packages:
resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==}
engines: {node: '>=16'}
+ get-port@7.2.0:
+ resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==}
+ engines: {node: '>=16'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -7660,6 +7771,9 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+ media-query-parser@2.0.2:
+ resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==}
+
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -7808,6 +7922,9 @@ packages:
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ modern-ahocorasick@1.1.0:
+ resolution: {integrity: sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==}
+
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
@@ -8467,6 +8584,9 @@ packages:
resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==}
engines: {node: '>=0.12'}
+ randombytes@2.1.0:
+ resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+
range-parser@1.2.0:
resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
engines: {node: '>= 0.6'}
@@ -8585,6 +8705,16 @@ packages:
react-dom:
optional: true
+ react-router@8.0.1:
+ resolution: {integrity: sha512-5EL/fANovVUhRK50NLS8RYfX0BxrimoKsHWUPPy8v5UEl8i6vzF7e4POo3u+AhPItDwccUAJjMfIOmydxBJmQw==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ react: '>=19.2.7'
+ react-dom: '>=19.2.7'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -8987,6 +9117,9 @@ packages:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
+ serialize-javascript@6.0.2:
+ resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
+
seroval-plugins@1.5.4:
resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
engines: {node: '>=10'}
@@ -10379,6 +10512,8 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@emotion/hash@0.9.2': {}
+
'@epic-web/cachified@5.6.1': {}
'@epic-web/client-hints@1.3.8': {}
@@ -12209,7 +12344,7 @@ snapshots:
- tsx
- yaml
- '@react-router/dev@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
+ '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
@@ -12241,19 +12376,51 @@ snapshots:
valibot: 1.4.2(typescript@5.9.3)
vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
optionalDependencies:
+ '@react-router/serve': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
typescript: 5.9.3
wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
- '@react-router/express@7.13.0(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
dependencies:
- '@react-router/node': 7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
- express: 4.22.2
- react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@babel/core': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
+ '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7)
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ '@react-router/node': 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@remix-run/node-fetch-server': 0.13.3
+ arg: 5.0.2
+ babel-dead-code-elimination: 1.0.12
+ chokidar: 5.0.0
+ dedent: 1.7.2
+ es-module-lexer: 2.1.0
+ exit-hook: 5.1.0
+ isbot: 5.1.44
+ jsesc: 3.1.0
+ lodash: 4.18.1
+ p-map: 7.0.4
+ pathe: 2.0.3
+ picocolors: 1.1.1
+ pkg-types: 2.3.1
+ prettier: 3.9.1
+ react-refresh: 0.18.0
+ react-router: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ valibot: 1.4.2(typescript@5.9.3)
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
optionalDependencies:
+ '@react-router/serve': 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
typescript: 5.9.3
+ wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1)
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
'@react-router/express@7.13.0(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
@@ -12271,6 +12438,30 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ '@react-router/express@8.0.1(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/node': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ express: 5.2.1
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ optionalDependencies:
+ typescript: 5.9.3
+ optional: true
+
+ '@react-router/express@8.0.1(express@5.2.1)(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/node': 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ express: 5.2.1
+ react-router: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ optionalDependencies:
+ typescript: 5.9.3
+
+ '@react-router/fs-routes@8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/dev': 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ minimatch: 10.2.5
+ optionalDependencies:
+ typescript: 5.9.3
+
'@react-router/node@7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
@@ -12292,6 +12483,13 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ '@react-router/node@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@remix-run/node-fetch-server': 0.13.3
+ react-router: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ optionalDependencies:
+ typescript: 5.9.3
+
'@react-router/remix-routes-option-adapter@7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
dependencies:
'@react-router/dev': 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
@@ -12313,6 +12511,37 @@ snapshots:
- supports-color
- typescript
+ '@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/express': 8.0.1(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@react-router/node': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@remix-run/node-fetch-server': 0.13.3
+ compression: 1.8.1
+ express: 5.2.1
+ get-port: 7.2.0
+ morgan: 1.10.1
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ source-map-support: 0.5.21
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+ optional: true
+
+ '@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/express': 8.0.1(express@5.2.1)(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@react-router/node': 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@remix-run/node-fetch-server': 0.13.3
+ compression: 1.8.1
+ express: 5.2.1
+ get-port: 7.2.0
+ morgan: 1.10.1
+ react-router: 8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ source-map-support: 0.5.21
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
'@remix-run/node-fetch-server@0.13.3': {}
'@remix-run/react@2.17.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)':
@@ -13643,6 +13872,24 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
+ '@vanilla-extract/css@1.21.0':
+ dependencies:
+ '@emotion/hash': 0.9.2
+ '@vanilla-extract/private': 1.0.9
+ css-what: 6.2.2
+ csstype: 3.2.3
+ dedent: 1.7.2
+ deep-object-diff: 1.1.9
+ deepmerge: 4.3.1
+ lru-cache: 10.4.3
+ media-query-parser: 2.0.2
+ modern-ahocorasick: 1.1.0
+ picocolors: 1.1.1
+ transitivePeerDependencies:
+ - babel-plugin-macros
+
+ '@vanilla-extract/private@1.0.9': {}
+
'@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))':
dependencies:
'@babel/core': 7.29.7
@@ -14396,6 +14643,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-es@3.1.1: {}
+
cookie-signature@1.0.7: {}
cookie-signature@1.2.2: {}
@@ -14576,6 +14825,8 @@ snapshots:
deep-is@0.1.4: {}
+ deep-object-diff@1.1.9: {}
+
deepmerge@4.3.1: {}
defaults@1.0.4:
@@ -15381,6 +15632,8 @@ snapshots:
get-port@7.1.0: {}
+ get-port@7.2.0: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
@@ -16117,6 +16370,10 @@ snapshots:
mdn-data@2.27.1: {}
+ media-query-parser@2.0.2:
+ dependencies:
+ '@babel/runtime': 7.29.7
+
media-typer@0.3.0: {}
media-typer@1.1.0: {}
@@ -16219,6 +16476,8 @@ snapshots:
mkdirp-classic@0.5.3: {}
+ modern-ahocorasick@1.1.0: {}
+
module-details-from-path@1.0.4: {}
moo@0.5.3: {}
@@ -16764,6 +17023,10 @@ snapshots:
discontinuous-range: 1.0.0
ret: 0.1.15
+ randombytes@2.1.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
range-parser@1.2.0: {}
range-parser@1.2.1: {}
@@ -16907,6 +17170,13 @@ snapshots:
optionalDependencies:
react-dom: 19.2.7(react@19.2.7)
+ react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ cookie-es: 3.1.1
+ react: 19.2.7
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+
react-style-singleton@2.2.3(@types/react@19.2.10)(react@19.2.7):
dependencies:
get-nonce: 1.0.1
@@ -17324,6 +17594,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ serialize-javascript@6.0.2:
+ dependencies:
+ randombytes: 2.1.0
+
seroval-plugins@1.5.4(seroval@1.5.4):
dependencies:
seroval: 1.5.4
diff --git a/src/index.ts b/src/index.ts
index 49e38d77..13761bea 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from 'node:fs';
+import { createRequire } from 'node:module';
import fsExtra from 'fs-extra';
import type { Config } from './react-router-config.js';
import type { RouteConfigEntry } from '@react-router/dev/routes';
@@ -89,6 +90,24 @@ export { loadReactRouterServerBuild } from './dev-generation.js';
export { resolveReactRouterServerBuild };
const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;
+const requireFromApp = createRequire(resolve(process.cwd(), 'package.json'));
+
+const resolveAppPackagePath = (specifier: string): string | undefined => {
+ try {
+ return requireFromApp.resolve(specifier);
+ } catch {
+ return undefined;
+ }
+};
+
+const createReactRouterPackageAliases = () => {
+ const reactRouterPath = resolveAppPackagePath('react-router');
+ const reactRouterDomPath = resolveAppPackagePath('react-router/dom');
+ return {
+ ...(reactRouterPath ? { 'react-router$': reactRouterPath } : {}),
+ ...(reactRouterDomPath ? { 'react-router/dom$': reactRouterDomPath } : {}),
+ };
+};
type ReactRouterPresetResolvedConfig = Parameters<
NonNullable<
@@ -811,6 +830,7 @@ export const pluginReactRouter = (
routeCount >= 256 &&
(config.performance?.printFileSize === undefined ||
config.performance.printFileSize === true);
+ const reactRouterAliases = createReactRouterPackageAliases();
return mergeRsbuildConfig(config, {
...(shouldCompactFileSizeReport
@@ -846,6 +866,10 @@ export const pluginReactRouter = (
},
tools: {
rspack: {
+ resolve:
+ Object.keys(reactRouterAliases).length > 0
+ ? { alias: reactRouterAliases }
+ : undefined,
plugins: [vmodPlugin],
},
},
diff --git a/src/plugin-utils.ts b/src/plugin-utils.ts
index 51561985..6c5f9adc 100644
--- a/src/plugin-utils.ts
+++ b/src/plugin-utils.ts
@@ -35,41 +35,15 @@ export function findEntryFile(basePath: string): string {
export function generateWithProps() {
return `
- import { createElement as h } from "react";
- import { useActionData, useLoaderData, useMatches, useParams, useRouteError } from "react-router";
-
- export function withComponentProps(Component) {
- return function Wrapped() {
- const props = {
- params: useParams(),
- loaderData: useLoaderData(),
- actionData: useActionData(),
- matches: useMatches(),
- };
- return h(Component, props);
- };
- }
-
- export function withHydrateFallbackProps(HydrateFallback) {
- return function Wrapped() {
- const props = {
- params: useParams(),
- };
- return h(HydrateFallback, props);
- };
- }
-
- export function withErrorBoundaryProps(ErrorBoundary) {
- return function Wrapped() {
- const props = {
- params: useParams(),
- loaderData: useLoaderData(),
- actionData: useActionData(),
- error: useRouteError(),
- };
- return h(ErrorBoundary, props);
- };
- }
+ import {
+ UNSAFE_withComponentProps,
+ UNSAFE_withErrorBoundaryProps,
+ UNSAFE_withHydrateFallbackProps,
+ } from "react-router";
+
+ export const withComponentProps = UNSAFE_withComponentProps;
+ export const withHydrateFallbackProps = UNSAFE_withHydrateFallbackProps;
+ export const withErrorBoundaryProps = UNSAFE_withErrorBoundaryProps;
`;
}
diff --git a/src/route-component-transform.ts b/src/route-component-transform.ts
index c1dbea68..15205ac3 100644
--- a/src/route-component-transform.ts
+++ b/src/route-component-transform.ts
@@ -124,7 +124,7 @@ export const transformRoute = (ast: ParseResult | AnyNode): void => {
function getHocUid(hocName: string) {
const uid = getUid(hocName);
- hocs.push([hocName, uid]);
+ hocs.push([`UNSAFE_${hocName}`, uid]);
return identifier(uid);
}
@@ -301,7 +301,7 @@ export const transformRoute = (ast: ParseResult | AnyNode): void => {
0,
importDeclaration(
hocs.map(([name, local]) => ({ imported: name, local })),
- 'virtual/react-router/with-props'
+ 'react-router'
)
);
}
diff --git a/tests/index.test.ts b/tests/index.test.ts
index a1df1e72..c170d903 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -1,6 +1,7 @@
import { createStubRsbuild } from '@scripts/test-helper';
import { describe, expect, it, rstest } from '@rstest/core';
import * as fs from 'node:fs';
+import { createRequire } from 'node:module';
import { pluginReactRouter, shouldParallelizeEnvironmentBuilds } from '../src';
type ReactRouterTestGlobal = typeof globalThis & {
@@ -33,6 +34,8 @@ const getLazyCompilationTest = (
return lazyCompilation.test;
};
+const requireFromHere = createRequire(import.meta.url);
+
const captureEnv = (keys: string[]) => {
const previousValues = new Map(
keys.map(key => [key, process.env[key]] as const)
@@ -64,6 +67,20 @@ describe('pluginReactRouter', () => {
expect(config.dev.lazyCompilation).toBeUndefined();
});
+ it('aliases React Router packages to the app install', async () => {
+ const rsbuild = await createStubRsbuild({
+ rsbuildConfig: {},
+ });
+
+ rsbuild.addPlugins([pluginReactRouter()]);
+ const config = await rsbuild.unwrapConfig();
+
+ expect(config.tools.rspack.resolve.alias).toMatchObject({
+ 'react-router$': requireFromHere.resolve('react-router'),
+ 'react-router/dom$': requireFromHere.resolve('react-router/dom'),
+ });
+ });
+
it('adds the committed custom-server build entry only in development', async () => {
const devRsbuild = await createStubRsbuild({ rsbuildConfig: {} });
devRsbuild.addPlugins([pluginReactRouter({ customServer: true })]);
diff --git a/tests/plugin-utils.test.ts b/tests/plugin-utils.test.ts
index 31e0b1aa..f2aaa541 100644
--- a/tests/plugin-utils.test.ts
+++ b/tests/plugin-utils.test.ts
@@ -89,10 +89,7 @@ describe('plugin-utils', () => {
it('should generate withComponentProps HOC', () => {
const result = generateWithProps();
expect(result).toContain('withComponentProps');
- expect(result).toContain('useLoaderData');
- expect(result).toContain('useActionData');
- expect(result).toContain('useParams');
- expect(result).toContain('useMatches');
+ expect(result).toContain('UNSAFE_withComponentProps');
});
it('should generate withHydrateFallbackProps HOC', () => {
@@ -103,7 +100,7 @@ describe('plugin-utils', () => {
it('should generate withErrorBoundaryProps HOC', () => {
const result = generateWithProps();
expect(result).toContain('withErrorBoundaryProps');
- expect(result).toContain('useRouteError');
+ expect(result).toContain('UNSAFE_withErrorBoundaryProps');
});
it('should import from react-router', () => {
@@ -221,8 +218,11 @@ describe('plugin-utils', () => {
export { Route as default };
`);
- expect(result.indexOf("'use client'")).toBeLessThan(
- result.indexOf('virtual/react-router/with-props')
+ expect(result.search(/['"]use client['"]/)).toBeLessThan(
+ result.search(/from ['"]react-router['"]/)
+ );
+ expect(result).toContain(
+ 'UNSAFE_withComponentProps as _withComponentProps'
);
expect(result).toContain('withComponentProps');
expect(result).not.toContain('withdefaultProps');
From 53162189db698c98ace354e7de2ea4efebff8c7f Mon Sep 17 00:00:00 2001
From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com>
Date: Mon, 29 Jun 2026 01:06:12 +0000
Subject: [PATCH 05/15] chore: deslop React Router 8 PR
---
.changeset/fresh-router-matrix.md | 9 -
.changeset/react-router-8-default-template.md | 5 +
.changeset/stable-prerender-concurrency.md | 6 -
package.json | 1 -
scripts/test-react-router-version-matrix.mjs | 216 ------------------
5 files changed, 5 insertions(+), 232 deletions(-)
delete mode 100644 .changeset/fresh-router-matrix.md
create mode 100644 .changeset/react-router-8-default-template.md
delete mode 100644 .changeset/stable-prerender-concurrency.md
delete mode 100644 scripts/test-react-router-version-matrix.mjs
diff --git a/.changeset/fresh-router-matrix.md b/.changeset/fresh-router-matrix.md
deleted file mode 100644
index 5575a7a9..00000000
--- a/.changeset/fresh-router-matrix.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'rsbuild-plugin-react-router': minor
----
-
-Add React Router 8 compatibility while preserving React Router 7 behavior.
-Stable `subResourceIntegrity` and `prerender.concurrency` config fields are now
-supported alongside React Router 7 aliases, prerender data requests default to
-the correct React Router major-version format, and the package test suite now
-packs the plugin and smoke-builds real React Router 7 and 8 apps.
diff --git a/.changeset/react-router-8-default-template.md b/.changeset/react-router-8-default-template.md
new file mode 100644
index 00000000..adef8ec2
--- /dev/null
+++ b/.changeset/react-router-8-default-template.md
@@ -0,0 +1,5 @@
+---
+'rsbuild-plugin-react-router': minor
+---
+
+Add React Router 8 compatibility while preserving React Router 7 behavior. The plugin now supports stable React Router 8 config fields, resolves prerender data requests for the installed React Router major version, and includes a copied React Router 8 default-template example with dev and production e2e coverage.
diff --git a/.changeset/stable-prerender-concurrency.md b/.changeset/stable-prerender-concurrency.md
deleted file mode 100644
index 83c49967..00000000
--- a/.changeset/stable-prerender-concurrency.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'rsbuild-plugin-react-router': patch
----
-
-Support React Router's stable `prerender.concurrency` config while preserving
-the existing `unstable_concurrency` fallback.
diff --git a/package.json b/package.json
index 797a04b1..1fe9a04c 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,6 @@
"test:core": "rstest run -c ./rstest.config.ts",
"test:core:watch": "rstest watch -c ./rstest.config.ts",
"test:package-interop": "node scripts/test-package-interop.mjs",
- "test:react-router-matrix": "pnpm build && node scripts/test-react-router-version-matrix.mjs",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
"changeset": "changeset",
diff --git a/scripts/test-react-router-version-matrix.mjs b/scripts/test-react-router-version-matrix.mjs
deleted file mode 100644
index a82bc918..00000000
--- a/scripts/test-react-router-version-matrix.mjs
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/usr/bin/env node
-import { spawn } from 'node:child_process';
-import {
- access,
- mkdir,
- mkdtemp,
- readdir,
- rm,
- writeFile,
-} from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import path from 'node:path';
-
-const rootDir = process.cwd();
-const versions = (process.env.RR_COMPAT_VERSIONS ?? '7.13.0,8.0.1')
- .split(',')
- .map(version => version.trim())
- .filter(Boolean);
-
-const run = (command, args, options = {}) =>
- new Promise((resolve, reject) => {
- const child = spawn(command, args, {
- cwd: options.cwd ?? rootDir,
- env: {
- ...process.env,
- COREPACK_ENABLE_DOWNLOAD_PROMPT: '0',
- ...options.env,
- },
- stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
- });
-
- let stdout = '';
- let stderr = '';
- if (options.capture) {
- child.stdout?.on('data', chunk => {
- stdout += chunk;
- });
- child.stderr?.on('data', chunk => {
- stderr += chunk;
- });
- }
-
- child.on('error', reject);
- child.on('close', code => {
- if (code === 0) {
- resolve({ stdout, stderr });
- return;
- }
- reject(
- new Error(
- `${command} ${args.join(' ')} failed with exit code ${code}\n${stderr}`
- )
- );
- });
- });
-
-const writeFixture = async ({ appDir, version, tarball }) => {
- await mkdir(path.join(appDir, 'app', 'routes'), { recursive: true });
- const writeAppFile = (file, contents) =>
- writeFile(path.join(appDir, file), contents);
-
- await writeAppFile(
- 'package.json',
- JSON.stringify(
- {
- private: true,
- type: 'module',
- scripts: {
- build: 'rsbuild build',
- },
- dependencies: {
- '@react-router/dev': version,
- '@react-router/node': version,
- '@rsbuild/core': '2.1.0',
- '@rsbuild/plugin-react': '2.1.0',
- react: '^19.2.4',
- 'react-dom': '^19.2.4',
- 'react-router': version,
- 'rsbuild-plugin-react-router': `file:${tarball}`,
- },
- devDependencies: {
- '@types/node': '^25.0.10',
- '@types/react': '^19.2.10',
- '@types/react-dom': '^19.2.3',
- typescript: '^5.9.3',
- },
- },
- null,
- 2
- ) + '\n'
- );
-
- await writeAppFile(
- 'rsbuild.config.ts',
- `import { defineConfig } from '@rsbuild/core';
-import { pluginReact } from '@rsbuild/plugin-react';
-import { pluginReactRouter } from 'rsbuild-plugin-react-router';
-
-export default defineConfig({
- plugins: [pluginReactRouter(), pluginReact()],
-});
-`
- );
-
- await writeAppFile(
- 'react-router.config.ts',
- `export default {
- ssr: true,
- routeDiscovery: { mode: 'initial' },
- splitRouteModules: true,
- subResourceIntegrity: true,
- prerender: { paths: ['/'], concurrency: 1 },
-};
-`
- );
-
- await writeAppFile(
- path.join('app', 'root.tsx'),
- `import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from 'react-router';
-
-export default function Root() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-`
- );
-
- await writeAppFile(
- path.join('app', 'routes.ts'),
- `import { index } from '@react-router/dev/routes';
-
-export default [index('routes/index.tsx')];
-`
- );
-
- await writeAppFile(
- path.join('app', 'routes', 'index.tsx'),
- `export function loader() {
- return { message: 'React Router ${version}' };
-}
-
-export default function Index() {
- return React Router ${version} compatibility ;
-}
-`
- );
-};
-
-const assertFile = async file => {
- try {
- await access(file);
- } catch {
- throw new Error(`Expected ${file} to exist`);
- }
-};
-
-const assertBrowserManifest = async clientDir => {
- const jsDir = path.join(clientDir, 'static', 'js');
- const files = await readdir(jsDir, { recursive: true });
- const hasBrowserManifest = files.some(
- file =>
- file.startsWith('manifest-') ||
- file === path.join('virtual', 'react-router', 'browser-manifest.js')
- );
- if (!hasBrowserManifest) {
- throw new Error(`Expected ${jsDir} to contain a React Router manifest`);
- }
-};
-
-const main = async () => {
- const tempRoot = await mkdtemp(path.join(tmpdir(), 'rr-version-matrix-'));
- try {
- const packResult = await run(
- 'npm',
- ['pack', '--json', '--pack-destination', tempRoot],
- { capture: true }
- );
- const [packInfo] = JSON.parse(packResult.stdout);
- const tarball = path.join(tempRoot, packInfo.filename);
-
- for (const version of versions) {
- const appDir = path.join(tempRoot, `rr-${version}`);
- await mkdir(appDir, { recursive: true });
- await writeFixture({ appDir, version, tarball });
- await run('pnpm', ['install'], { cwd: appDir });
- await run('pnpm', ['build'], { cwd: appDir });
-
- await assertFile(path.join(appDir, 'build', 'client', 'index.html'));
- await assertFile(path.join(appDir, 'build', 'server', 'index.js'));
- await assertBrowserManifest(path.join(appDir, 'build', 'client'));
-
- console.log(`React Router ${version} package smoke passed.`);
- }
- } finally {
- await rm(tempRoot, { recursive: true, force: true });
- }
-};
-
-await main();
From 527d123becf9e04a1dfbe9a5d5a5411ec4f1e4b6 Mon Sep 17 00:00:00 2001
From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com>
Date: Mon, 29 Jun 2026 03:26:37 +0000
Subject: [PATCH 06/15] test: port React Router framework suite
---
package.json | 27 +-
pnpm-lock.yaml | 1511 +++++-
tests/react-router-framework/README.md | 36 +
.../integration/CHANGELOG.md | 14 +
.../integration/abort-signal-test.ts | 65 +
.../integration/action-test.ts | 232 +
.../integration/assets/toupload.txt | 1 +
.../integration/assets/touploadtoobig.txt | 1 +
.../integration/blocking-test.ts | 113 +
.../integration/browser-entry-test.ts | 322 ++
.../integration/bug-report-test.ts | 127 +
.../integration/catch-boundary-data-test.ts | 258 +
.../integration/catch-boundary-test.ts | 379 ++
.../integration/cli-test.ts | 201 +
.../integration/client-data-test.ts | 1772 +++++++
.../integration/custom-entry-server-test.ts | 60 +
.../integration/deduped-route-modules-test.ts | 287 +
.../integration/defer-loader-test.ts | 110 +
.../integration/defer-test.ts | 699 +++
.../integration/error-boundary-test.ts | 1020 ++++
.../integration/error-boundary-v2-test.ts | 257 +
.../integration/error-data-request-test.ts | 168 +
.../integration/error-sanitization-test.ts | 707 +++
.../integration/fetch-globals-test.ts | 42 +
.../integration/fetcher-layout-test.ts | 268 +
.../integration/fetcher-test.ts | 593 +++
.../integration/fog-of-war-test.ts | 1798 +++++++
.../integration/form-data-test.ts | 58 +
.../integration/form-test.ts | 1142 ++++
.../integration/fs-routes-test.ts | 454 ++
.../integration/headers-test.ts | 472 ++
.../integration/helpers/cleanup.mjs | 27 +
.../integration/helpers/create-fixture.ts | 592 +++
.../integration/helpers/express.ts | 52 +
.../integration/helpers/fixtures.ts | 146 +
.../integration/helpers/playwright-fixture.ts | 364 ++
.../integration/helpers/rsbuild-adapter.ts | 197 +
.../helpers/rsc-vite-framework/.gitignore | 2 +
.../helpers/rsc-vite-framework/app/root.tsx | 18 +
.../helpers/rsc-vite-framework/app/routes.ts | 4 +
.../rsc-vite-framework/app/routes/_index.tsx | 16 +
.../helpers/rsc-vite-framework/package.json | 40 +
.../rsc-vite-framework/public/favicon.ico | Bin 0 -> 15086 bytes
.../helpers/rsc-vite-framework/start.js | 18 +
.../helpers/rsc-vite-framework/tsconfig.json | 17 +
.../helpers/rsc-vite-framework/vite.config.ts | 13 +
.../integration/helpers/rsc-vite/.gitignore | 2 +
.../integration/helpers/rsc-vite/package.json | 32 +
.../helpers/rsc-vite/public/favicon.ico | Bin 0 -> 15086 bytes
.../integration/helpers/rsc-vite/server.js | 26 +
.../helpers/rsc-vite/src/config/basename.ts | 2 +
.../rsc-vite/src/config/get-context.ts | 2 +
.../rsc-vite/src/config/request-context.ts | 2 +
.../helpers/rsc-vite/src/entry.browser.tsx | 45 +
.../helpers/rsc-vite/src/entry.rsc.tsx | 40 +
.../helpers/rsc-vite/src/entry.ssr.tsx | 32 +
.../helpers/rsc-vite/src/routes.ts | 16 +
.../helpers/rsc-vite/src/routes/home.tsx | 3 +
.../helpers/rsc-vite/src/routes/root.tsx | 22 +
.../helpers/rsc-vite/tsconfig.json | 11 +
.../helpers/rsc-vite/vite.config.ts | 16 +
.../integration/helpers/stream.ts | 40 +
.../integration/helpers/templates.ts | 23 +
.../helpers/vite-7-template/.gitignore | 6 +
.../helpers/vite-7-template/app/root.tsx | 19 +
.../helpers/vite-7-template/app/routes.ts | 4 +
.../vite-7-template/app/routes/_index.tsx | 16 +
.../helpers/vite-7-template/env.d.ts | 2 +
.../helpers/vite-7-template/package.json | 41 +
.../vite-7-template/public/favicon.ico | Bin 0 -> 15086 bytes
.../helpers/vite-7-template/tsconfig.json | 16 +
.../helpers/vite-7-template/vite.config.ts | 7 +
.../helpers/vite-8-template/.gitignore | 6 +
.../helpers/vite-8-template/app/root.tsx | 19 +
.../helpers/vite-8-template/app/routes.ts | 4 +
.../vite-8-template/app/routes/_index.tsx | 16 +
.../helpers/vite-8-template/env.d.ts | 2 +
.../helpers/vite-8-template/package.json | 40 +
.../vite-8-template/public/favicon.ico | Bin 0 -> 15086 bytes
.../helpers/vite-8-template/tsconfig.json | 16 +
.../helpers/vite-8-template/vite.config.ts | 7 +
.../.gitignore | 11 +
.../app/cloudflare.ts | 10 +
.../app/entry.server.tsx | 43 +
.../app/root.tsx | 19 +
.../app/routes.ts | 4 +
.../app/routes/_index.tsx | 16 +
.../package.json | 36 +
.../public/favicon.ico | Bin 0 -> 15086 bytes
.../react-router.config.ts | 3 +
.../tsconfig.cloudflare.json | 27 +
.../tsconfig.json | 12 +
.../tsconfig.node.json | 13 +
.../vite.config.ts | 35 +
.../workers/app.ts | 16 +
.../wrangler.toml | 8 +
.../integration/helpers/vite.ts | 549 ++
.../integration/hook-useSubmit-test.ts | 135 +
.../integration/http-test.ts | 87 +
.../integration/layout-route-test.ts | 66 +
.../integration/link-test.ts | 667 +++
.../integration/loader-test.ts | 128 +
.../integration/matches-test.ts | 197 +
.../integration/mdx-test.ts | 114 +
.../integration/middleware-test.ts | 2947 +++++++++++
.../integration/multiple-cookies-test.ts | 78 +
.../integration/navigation-state-test.ts | 467 ++
.../integration/package.json | 52 +
.../integration/passthrough-requests-test.ts | 159 +
.../integration/playwright.config.ts | 40 +
.../integration/prefetch-test.ts | 725 +++
.../integration/react-router-serve-test.ts | 84 +
.../integration/redirects-test.ts | 308 ++
.../integration/rendering-test.ts | 73 +
.../integration/request-test.ts | 177 +
.../integration/resource-routes-test.ts | 397 ++
.../integration/revalidate-test.ts | 367 ++
.../integration/root-route-test.ts | 414 ++
.../integration/route-collisions-test.ts | 144 +
.../integration/route-config-test.ts | 275 +
.../integration/rsc/rsc-nojs-test.ts | 291 +
.../integration/rsc/rsc-prerender-test.ts | 185 +
.../integration/rsc/rsc-test.ts | 2762 ++++++++++
.../integration/rsc/utils.ts | 74 +
.../integration/scroll-test.ts | 124 +
.../integration/server-entry-test.ts | 66 +
.../session-storage-denied-test.ts | 115 +
.../set-cookie-revalidation-test.ts | 136 +
.../integration/single-fetch-test.ts | 4662 +++++++++++++++++
.../integration/splat-routes-test.ts | 129 +
.../integration/split-route-modules-test.ts | 559 ++
.../integration/sri-test.ts | 87 +
.../integration/transition-test.ts | 338 ++
.../integration/tsconfig.json | 13 +
.../integration/typegen-test.ts | 890 ++++
.../integration/use-route-test.ts | 138 +
.../integration/vite-absolute-base-test.ts | 40 +
.../integration/vite-basename-test.ts | 741 +++
.../integration/vite-build-test.ts | 384 ++
.../integration/vite-css-lazy-loading-test.ts | 280 +
.../integration/vite-css-test.ts | 585 +++
.../integration/vite-dev-custom-entry-test.ts | 103 +
.../integration/vite-dev-test.ts | 557 ++
.../integration/vite-dot-client-test.ts | 47 +
.../integration/vite-dot-server-test.ts | 270 +
.../integration/vite-dotenv-test.ts | 115 +
.../vite-extra-server-environment-test.ts | 118 +
.../integration/vite-hmr-hdr-rsc-test.ts | 392 ++
.../integration/vite-hmr-hdr-test.ts | 362 ++
.../integration/vite-loader-context-test.ts | 82 +
.../integration/vite-manifests-test.ts | 143 +
.../integration/vite-node-env-test.ts | 79 +
.../vite-plugin-cloudflare-test.ts | 133 +
.../vite-plugin-order-validation-test.ts | 84 +
.../integration/vite-prerender-test.ts | 3066 +++++++++++
.../integration/vite-presets-test.ts | 280 +
.../integration/vite-preview-test.ts | 381 ++
.../integration/vite-route-added-test.ts | 76 +
...e-route-exports-modified-offscreen-test.ts | 107 +
.../integration/vite-server-bundles-test.ts | 459 ++
.../integration/vite-server-fs-allow-test.ts | 51 +
.../integration/vite-spa-mode-test.ts | 1285 +++++
.../vite-unused-route-exports-test.ts | 37 +
.../__tests__/fixtures/basic/.gitignore | 6 +
.../__tests__/fixtures/basic/app/root.tsx | 19 +
.../__tests__/fixtures/basic/app/routes.ts | 5 +
.../fixtures/basic/app/routes/_index.tsx | 16 +
.../__tests__/fixtures/basic/package.json | 28 +
.../fixtures/basic/public/favicon.ico | Bin 0 -> 15086 bytes
.../__tests__/fixtures/basic/tsconfig.json | 16 +
.../__tests__/route-config-test.ts | 565 ++
.../rsc-virtual-route-modules-test.ts | 632 +++
.../__tests__/setupAfterEnv.ts | 3 +
.../react-router-dev/__tests__/styles-test.ts | 60 +
.../__tests__/utils/captureError.ts | 14 +
.../react-router-dev/__tests__/utils/cli.ts | 105 +
.../react-router-dev/__tests__/utils/eol.ts | 2 +
.../react-router-dev/__tests__/utils/git.ts | 18 +
.../__tests__/utils/withApp.ts | 46 +
.../__tests__/watcher-ignored-test.ts | 70 +
180 files changed, 46528 insertions(+), 41 deletions(-)
create mode 100644 tests/react-router-framework/README.md
create mode 100644 tests/react-router-framework/integration/CHANGELOG.md
create mode 100644 tests/react-router-framework/integration/abort-signal-test.ts
create mode 100644 tests/react-router-framework/integration/action-test.ts
create mode 100644 tests/react-router-framework/integration/assets/toupload.txt
create mode 100644 tests/react-router-framework/integration/assets/touploadtoobig.txt
create mode 100644 tests/react-router-framework/integration/blocking-test.ts
create mode 100644 tests/react-router-framework/integration/browser-entry-test.ts
create mode 100644 tests/react-router-framework/integration/bug-report-test.ts
create mode 100644 tests/react-router-framework/integration/catch-boundary-data-test.ts
create mode 100644 tests/react-router-framework/integration/catch-boundary-test.ts
create mode 100644 tests/react-router-framework/integration/cli-test.ts
create mode 100644 tests/react-router-framework/integration/client-data-test.ts
create mode 100644 tests/react-router-framework/integration/custom-entry-server-test.ts
create mode 100644 tests/react-router-framework/integration/deduped-route-modules-test.ts
create mode 100644 tests/react-router-framework/integration/defer-loader-test.ts
create mode 100644 tests/react-router-framework/integration/defer-test.ts
create mode 100644 tests/react-router-framework/integration/error-boundary-test.ts
create mode 100644 tests/react-router-framework/integration/error-boundary-v2-test.ts
create mode 100644 tests/react-router-framework/integration/error-data-request-test.ts
create mode 100644 tests/react-router-framework/integration/error-sanitization-test.ts
create mode 100644 tests/react-router-framework/integration/fetch-globals-test.ts
create mode 100644 tests/react-router-framework/integration/fetcher-layout-test.ts
create mode 100644 tests/react-router-framework/integration/fetcher-test.ts
create mode 100644 tests/react-router-framework/integration/fog-of-war-test.ts
create mode 100644 tests/react-router-framework/integration/form-data-test.ts
create mode 100644 tests/react-router-framework/integration/form-test.ts
create mode 100644 tests/react-router-framework/integration/fs-routes-test.ts
create mode 100644 tests/react-router-framework/integration/headers-test.ts
create mode 100644 tests/react-router-framework/integration/helpers/cleanup.mjs
create mode 100644 tests/react-router-framework/integration/helpers/create-fixture.ts
create mode 100644 tests/react-router-framework/integration/helpers/express.ts
create mode 100644 tests/react-router-framework/integration/helpers/fixtures.ts
create mode 100644 tests/react-router-framework/integration/helpers/playwright-fixture.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsbuild-adapter.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/.gitignore
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/app/root.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/app/routes.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/app/routes/_index.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/package.json
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/public/favicon.ico
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/start.js
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/tsconfig.json
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite-framework/vite.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/.gitignore
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/package.json
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/public/favicon.ico
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/server.js
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/config/basename.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/config/get-context.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/config/request-context.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/entry.browser.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/entry.rsc.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/entry.ssr.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/routes.ts
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/routes/home.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/src/routes/root.tsx
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/tsconfig.json
create mode 100644 tests/react-router-framework/integration/helpers/rsc-vite/vite.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/stream.ts
create mode 100644 tests/react-router-framework/integration/helpers/templates.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/.gitignore
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/app/root.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/app/routes.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/app/routes/_index.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/env.d.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/package.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/public/favicon.ico
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/tsconfig.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-7-template/vite.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/.gitignore
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/app/root.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/app/routes.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/app/routes/_index.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/env.d.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/package.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/public/favicon.ico
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/tsconfig.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-8-template/vite.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/.gitignore
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/app/cloudflare.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/app/root.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/app/routes.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/app/routes/_index.tsx
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/package.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/public/favicon.ico
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/react-router.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/tsconfig.cloudflare.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/tsconfig.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/tsconfig.node.json
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/vite.config.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/workers/app.ts
create mode 100644 tests/react-router-framework/integration/helpers/vite-plugin-cloudflare-template/wrangler.toml
create mode 100644 tests/react-router-framework/integration/helpers/vite.ts
create mode 100644 tests/react-router-framework/integration/hook-useSubmit-test.ts
create mode 100644 tests/react-router-framework/integration/http-test.ts
create mode 100644 tests/react-router-framework/integration/layout-route-test.ts
create mode 100644 tests/react-router-framework/integration/link-test.ts
create mode 100644 tests/react-router-framework/integration/loader-test.ts
create mode 100644 tests/react-router-framework/integration/matches-test.ts
create mode 100644 tests/react-router-framework/integration/mdx-test.ts
create mode 100644 tests/react-router-framework/integration/middleware-test.ts
create mode 100644 tests/react-router-framework/integration/multiple-cookies-test.ts
create mode 100644 tests/react-router-framework/integration/navigation-state-test.ts
create mode 100644 tests/react-router-framework/integration/package.json
create mode 100644 tests/react-router-framework/integration/passthrough-requests-test.ts
create mode 100644 tests/react-router-framework/integration/playwright.config.ts
create mode 100644 tests/react-router-framework/integration/prefetch-test.ts
create mode 100644 tests/react-router-framework/integration/react-router-serve-test.ts
create mode 100644 tests/react-router-framework/integration/redirects-test.ts
create mode 100644 tests/react-router-framework/integration/rendering-test.ts
create mode 100644 tests/react-router-framework/integration/request-test.ts
create mode 100644 tests/react-router-framework/integration/resource-routes-test.ts
create mode 100644 tests/react-router-framework/integration/revalidate-test.ts
create mode 100644 tests/react-router-framework/integration/root-route-test.ts
create mode 100644 tests/react-router-framework/integration/route-collisions-test.ts
create mode 100644 tests/react-router-framework/integration/route-config-test.ts
create mode 100644 tests/react-router-framework/integration/rsc/rsc-nojs-test.ts
create mode 100644 tests/react-router-framework/integration/rsc/rsc-prerender-test.ts
create mode 100644 tests/react-router-framework/integration/rsc/rsc-test.ts
create mode 100644 tests/react-router-framework/integration/rsc/utils.ts
create mode 100644 tests/react-router-framework/integration/scroll-test.ts
create mode 100644 tests/react-router-framework/integration/server-entry-test.ts
create mode 100644 tests/react-router-framework/integration/session-storage-denied-test.ts
create mode 100644 tests/react-router-framework/integration/set-cookie-revalidation-test.ts
create mode 100644 tests/react-router-framework/integration/single-fetch-test.ts
create mode 100644 tests/react-router-framework/integration/splat-routes-test.ts
create mode 100644 tests/react-router-framework/integration/split-route-modules-test.ts
create mode 100644 tests/react-router-framework/integration/sri-test.ts
create mode 100644 tests/react-router-framework/integration/transition-test.ts
create mode 100644 tests/react-router-framework/integration/tsconfig.json
create mode 100644 tests/react-router-framework/integration/typegen-test.ts
create mode 100644 tests/react-router-framework/integration/use-route-test.ts
create mode 100644 tests/react-router-framework/integration/vite-absolute-base-test.ts
create mode 100644 tests/react-router-framework/integration/vite-basename-test.ts
create mode 100644 tests/react-router-framework/integration/vite-build-test.ts
create mode 100644 tests/react-router-framework/integration/vite-css-lazy-loading-test.ts
create mode 100644 tests/react-router-framework/integration/vite-css-test.ts
create mode 100644 tests/react-router-framework/integration/vite-dev-custom-entry-test.ts
create mode 100644 tests/react-router-framework/integration/vite-dev-test.ts
create mode 100644 tests/react-router-framework/integration/vite-dot-client-test.ts
create mode 100644 tests/react-router-framework/integration/vite-dot-server-test.ts
create mode 100644 tests/react-router-framework/integration/vite-dotenv-test.ts
create mode 100644 tests/react-router-framework/integration/vite-extra-server-environment-test.ts
create mode 100644 tests/react-router-framework/integration/vite-hmr-hdr-rsc-test.ts
create mode 100644 tests/react-router-framework/integration/vite-hmr-hdr-test.ts
create mode 100644 tests/react-router-framework/integration/vite-loader-context-test.ts
create mode 100644 tests/react-router-framework/integration/vite-manifests-test.ts
create mode 100644 tests/react-router-framework/integration/vite-node-env-test.ts
create mode 100644 tests/react-router-framework/integration/vite-plugin-cloudflare-test.ts
create mode 100644 tests/react-router-framework/integration/vite-plugin-order-validation-test.ts
create mode 100644 tests/react-router-framework/integration/vite-prerender-test.ts
create mode 100644 tests/react-router-framework/integration/vite-presets-test.ts
create mode 100644 tests/react-router-framework/integration/vite-preview-test.ts
create mode 100644 tests/react-router-framework/integration/vite-route-added-test.ts
create mode 100644 tests/react-router-framework/integration/vite-route-exports-modified-offscreen-test.ts
create mode 100644 tests/react-router-framework/integration/vite-server-bundles-test.ts
create mode 100644 tests/react-router-framework/integration/vite-server-fs-allow-test.ts
create mode 100644 tests/react-router-framework/integration/vite-spa-mode-test.ts
create mode 100644 tests/react-router-framework/integration/vite-unused-route-exports-test.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/.gitignore
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/app/root.tsx
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/app/routes.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/app/routes/_index.tsx
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/package.json
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/public/favicon.ico
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/fixtures/basic/tsconfig.json
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/route-config-test.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/rsc-virtual-route-modules-test.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/setupAfterEnv.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/styles-test.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/utils/captureError.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/utils/cli.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/utils/eol.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/utils/git.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/utils/withApp.ts
create mode 100644 tests/react-router-framework/react-router-dev/__tests__/watcher-ignored-test.ts
diff --git a/package.json b/package.json
index 1fe9a04c..801a8352 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,8 @@
"test:core": "rstest run -c ./rstest.config.ts",
"test:core:watch": "rstest watch -c ./rstest.config.ts",
"test:package-interop": "node scripts/test-package-interop.mjs",
+ "test:react-router-framework": "pnpm build && playwright test --config tests/react-router-framework/integration/playwright.config.ts",
+ "test:react-router-framework:smoke": "pnpm build && playwright test --config tests/react-router-framework/integration/playwright.config.ts use-route-test.ts loader-test.ts",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
"changeset": "changeset",
@@ -74,7 +76,7 @@
},
"dependencies": {
"@react-router/node": "^7.0.0 || ^8.0.0",
- "@remix-run/node-fetch-server": "^0.13.0",
+ "@remix-run/node-fetch-server": "^0.13.3",
"@rspack/plugin-react-refresh": "^2.0.2",
"execa": "^9.6.1",
"fs-extra": "11.3.3",
@@ -89,9 +91,15 @@
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
+ "@playwright/test": "^1.61.1",
"@react-router/dev": "^8.0.1",
+ "@react-router/express": "^8.0.1",
+ "@react-router/fs-routes": "^8.0.1",
+ "@react-router/remix-routes-option-adapter": "^8.0.1",
+ "@react-router/serve": "^8.0.1",
"@rsbuild/config": "workspace:*",
"@rsbuild/core": "2.1.0",
+ "@rsbuild/plugin-mdx": "^1.1.3",
"@rsbuild/plugin-react": "2.1.0",
"@rslib/core": "^0.22.1",
"@rspack/core": "2.1.0",
@@ -103,16 +111,29 @@
"@types/node": "^25.0.10",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
+ "@vanilla-extract/css": "^1.20.1",
+ "cheerio": "^1.2.0",
+ "cross-spawn": "^7.0.6",
+ "dedent": "^1.7.2",
"es-module-lexer": "1.7.0",
+ "express": "^4.22.2",
+ "fast-glob": "^3.3.3",
+ "get-port": "7.1.0",
"kill-port": "^2.0.1",
"pkg-pr-new": "^0.0.75",
- "playwright": "1.58.0",
+ "playwright": "^1.61.1",
"prettier": "3.8.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "^7.13.0",
"react-router-dom": "^7.13.0",
- "typescript": "^5.9.3"
+ "semver": "^7.8.5",
+ "shelljs": "^0.10.0",
+ "strip-ansi": "^7.2.0",
+ "strip-indent": "^4.1.1",
+ "type-fest": "^5.7.0",
+ "typescript": "^5.9.3",
+ "wait-on": "^9.0.10"
},
"peerDependencies": {
"@react-router/dev": "^7.0.0 || ^8.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5c63cb3..5a396a46 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,7 +15,7 @@ importers:
specifier: ^7.0.0 || ^8.0.0
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@remix-run/node-fetch-server':
- specifier: ^0.13.0
+ specifier: ^0.13.3
version: 0.13.3
'@rspack/plugin-react-refresh':
specifier: ^2.0.2
@@ -54,21 +54,39 @@ importers:
'@changesets/cli':
specifier: ^2.29.8
version: 2.31.0(@types/node@25.0.10)
+ '@playwright/test':
+ specifier: ^1.61.1
+ version: 1.61.1
'@react-router/dev':
specifier: ^8.0.1
- version: 8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ '@react-router/express':
+ specifier: ^8.0.1
+ version: 8.0.1(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@react-router/fs-routes':
+ specifier: ^8.0.1
+ version: 8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ '@react-router/remix-routes-option-adapter':
+ specifier: ^8.0.1
+ version: 8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ '@react-router/serve':
+ specifier: ^8.0.1
+ version: 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@rsbuild/config':
specifier: workspace:*
version: link:config
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ '@rsbuild/plugin-mdx':
+ specifier: ^1.1.3
+ version: 1.1.3(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(webpack@5.108.1(lightningcss@1.32.0))
'@rsbuild/plugin-react':
specifier: 2.1.0
version: 2.1.0(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))
'@rslib/core':
specifier: ^0.22.1
- version: 0.22.1(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)(typescript@5.9.3)
+ version: 0.22.1(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)(typescript@5.9.3)
'@rspack/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23)
@@ -96,9 +114,30 @@ importers:
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.10)
+ '@vanilla-extract/css':
+ specifier: ^1.20.1
+ version: 1.21.0
+ cheerio:
+ specifier: ^1.2.0
+ version: 1.2.0
+ cross-spawn:
+ specifier: ^7.0.6
+ version: 7.0.6
+ dedent:
+ specifier: ^1.7.2
+ version: 1.7.2
es-module-lexer:
specifier: 1.7.0
version: 1.7.0
+ express:
+ specifier: ^4.22.2
+ version: 4.22.2
+ fast-glob:
+ specifier: ^3.3.3
+ version: 3.3.3
+ get-port:
+ specifier: 7.1.0
+ version: 7.1.0
kill-port:
specifier: ^2.0.1
version: 2.0.1
@@ -106,8 +145,8 @@ importers:
specifier: ^0.0.75
version: 0.0.75
playwright:
- specifier: 1.58.0
- version: 1.58.0
+ specifier: ^1.61.1
+ version: 1.61.1
prettier:
specifier: 3.8.1
version: 3.8.1
@@ -123,9 +162,27 @@ importers:
react-router-dom:
specifier: ^7.13.0
version: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ semver:
+ specifier: ^7.8.5
+ version: 7.8.5
+ shelljs:
+ specifier: ^0.10.0
+ version: 0.10.0
+ strip-ansi:
+ specifier: ^7.2.0
+ version: 7.2.0
+ strip-indent:
+ specifier: ^4.1.1
+ version: 4.1.1
+ type-fest:
+ specifier: ^5.7.0
+ version: 5.7.0
typescript:
specifier: ^5.9.3
version: 5.9.3
+ wait-on:
+ specifier: ^9.0.10
+ version: 9.0.10
config:
devDependencies:
@@ -146,7 +203,7 @@ importers:
dependencies:
'@react-router/express':
specifier: ^7.13.0
- version: 7.13.0(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ version: 7.13.0(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/node':
specifier: ^7.13.0
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
@@ -171,7 +228,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -229,7 +286,7 @@ importers:
version: 7.18.0(@cloudflare/workers-types@4.20260628.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -290,7 +347,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -354,7 +411,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -480,7 +537,7 @@ importers:
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/remix-routes-option-adapter':
specifier: 7.13.0
- version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
'@remix-run/server-runtime':
specifier: 2.17.4
version: 2.17.4(typescript@5.9.3)
@@ -597,7 +654,7 @@ importers:
version: 3.0.2(remix-auth@4.2.0)
remix-utils:
specifier: 9.0.0
- version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(@standard-schema/spec@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
rsbuild-plugin-react-router:
specifier: workspace:*
version: link:../..
@@ -643,7 +700,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rstest/core':
specifier: 0.8.1
version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0))
@@ -862,7 +919,7 @@ importers:
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/remix-routes-option-adapter':
specifier: 7.13.0
- version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
'@remix-run/server-runtime':
specifier: 2.17.4
version: 2.17.4(typescript@5.9.3)
@@ -979,7 +1036,7 @@ importers:
version: 3.0.2(remix-auth@4.2.0)
remix-utils:
specifier: 9.0.0
- version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(@standard-schema/spec@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
rsbuild-plugin-react-router:
specifier: workspace:*
version: link:../../..
@@ -1022,7 +1079,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rstest/core':
specifier: 0.8.1
version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0))
@@ -1226,7 +1283,7 @@ importers:
version: 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
'@react-router/remix-routes-option-adapter':
specifier: 7.13.0
- version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ version: 7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
'@remix-run/server-runtime':
specifier: 2.17.4
version: 2.17.4(typescript@5.9.3)
@@ -1343,7 +1400,7 @@ importers:
version: 3.0.2(remix-auth@4.2.0)
remix-utils:
specifier: 9.0.0
- version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
+ version: 9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(@standard-schema/spec@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
rsbuild-plugin-react-router:
specifier: workspace:*
version: link:../../..
@@ -1386,7 +1443,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rstest/core':
specifier: 0.8.1
version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0))
@@ -1534,7 +1591,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -1588,7 +1645,7 @@ importers:
dependencies:
'@react-router/fs-routes':
specifier: ^8.0.1
- version: 8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
+ version: 8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)
'@react-router/node':
specifier: ^8.0.1
version: 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
@@ -1616,7 +1673,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^8.0.1
- version: 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -1677,7 +1734,7 @@ importers:
version: 1.58.0
'@react-router/dev':
specifier: ^7.13.0
- version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ version: 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
'@rsbuild/core':
specifier: 2.1.0
version: 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -2548,6 +2605,26 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@hapi/address@5.1.1':
+ resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==}
+ engines: {node: '>=14.0.0'}
+
+ '@hapi/formula@3.0.2':
+ resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==}
+
+ '@hapi/hoek@11.0.7':
+ resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==}
+
+ '@hapi/pinpoint@2.0.1':
+ resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==}
+
+ '@hapi/tlds@1.1.7':
+ resolution: {integrity: sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==}
+ engines: {node: '>=14.0.0'}
+
+ '@hapi/topo@6.0.2':
+ resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -2781,6 +2858,17 @@ packages:
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
+ '@mdx-js/loader@3.1.1':
+ resolution: {integrity: sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==}
+ peerDependencies:
+ webpack: '>=5'
+ peerDependenciesMeta:
+ webpack:
+ optional: true
+
+ '@mdx-js/mdx@3.1.1':
+ resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+
'@mjackson/form-data-parser@0.9.1':
resolution: {integrity: sha512-GQqet5qTAm8LfUOsMdfdInqnOBpuDO5GK/y7tBgpXs+DQhJY9Rf1fxMuFXiXczoNMRu8UIG5/RFSBDeaF1bbrw==}
@@ -3294,6 +3382,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@playwright/test@1.61.1':
+ resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@poppinss/colors@4.1.6':
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
@@ -4165,6 +4258,16 @@ packages:
typescript:
optional: true
+ '@react-router/remix-routes-option-adapter@8.0.1':
+ resolution: {integrity: sha512-t8xFxE/LDOAxJTDdyVHG34i5N3HnsPjQ/ZsrOQEnIcJwS7GZSLoHnNa045+Be/12Ma4Kwxj23fq3skxhKoR/fQ==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ '@react-router/dev': ^8.0.1
+ typescript: ^5.1.0 || ^6.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@react-router/serve@7.18.0':
resolution: {integrity: sha512-IrF0cLcJNGBBavnRBm3HxaEGwRrLrLF8E4EzQFuCpkgP1sRli1x2xEOOTJl4zBgUbyIn0ey4TAD6ytg45MAUBQ==}
engines: {node: '>=20.0.0'}
@@ -4222,6 +4325,9 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
'@rollup/rollup-android-arm-eabi@4.62.2':
resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
cpu: [arm]
@@ -4388,6 +4494,14 @@ packages:
'@rsbuild/core':
optional: true
+ '@rsbuild/plugin-mdx@1.1.3':
+ resolution: {integrity: sha512-2o/Pg+VExsyfr6uzzXj+cBxzjiFbQ2yIUxzM5kQnrPC5J0D8ZhWyXuWN5rTtsolAS23QdfsycZ0iAtlRuZVwCw==}
+ peerDependencies:
+ '@rsbuild/core': ^1.0.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@rsbuild/core':
+ optional: true
+
'@rsbuild/plugin-react@2.1.0':
resolution: {integrity: sha512-RQTIAWB/CwPjoWt9iAl+8HixeQVgZ7kEIBrWPCixfITyHdiD84h0YpUTpEUuz6kGHw1KXT9mHZ3Rwy6WG7aRDA==}
peerDependencies:
@@ -4919,6 +5033,9 @@ packages:
'@speed-highlight/core@1.2.17':
resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -5145,12 +5262,18 @@ packages:
'@types/d3-hierarchy@1.1.11':
resolution: {integrity: sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==}
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/eslint@9.6.1':
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@@ -5170,6 +5293,9 @@ packages:
resolution: {integrity: sha512-00UxlRaIUvYm4R4W9WYkN8/J+kV8fmOQ7okeH6YFtGWFMt3odD45tpG5yA5wnL7HE6lLgjaTW5n14ju2hl2NNA==}
deprecated: This is a stub types definition. glob provides its own type definitions, so you do not need this installed.
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
@@ -5182,9 +5308,18 @@ packages:
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/mdx@2.0.14':
+ resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==}
+
'@types/morgan@1.9.10':
resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==}
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
'@types/mysql@2.15.27':
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
@@ -5244,6 +5379,12 @@ packages:
'@types/tedious@4.0.14':
resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -5306,6 +5447,9 @@ packages:
resolution: {integrity: sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@ungap/structured-clone@1.3.2':
+ resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==}
+
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==}
cpu: [arm]
@@ -5428,6 +5572,17 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@vitejs/plugin-rsc@0.5.27':
+ resolution: {integrity: sha512-s1fd5DUkPXk86DDHPM/kP93WrvI0MoA8klxdDZmD1fMSaA9xujfgunsm8ZoUH0FemR+63vNalFsIDR0AJH4ktg==}
+ peerDependencies:
+ react: '*'
+ react-dom: '*'
+ react-server-dom-webpack: '*'
+ vite: '*'
+ peerDependenciesMeta:
+ react-server-dom-webpack:
+ optional: true
+
'@vitest/eslint-plugin@1.6.20':
resolution: {integrity: sha512-xRwWHFG0Utp6hXtbGiWk4VdKXCGdExD8kbWrrmFEiG5dk8anOJ+vbWbeOa8EbkocKQRTsx7JAWETccZiBgFp/Q==}
engines: {node: '>=18'}
@@ -5840,10 +5995,17 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ astring@1.9.0:
+ resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
+ hasBin: true
+
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
autoprefixer@10.4.23:
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
engines: {node: ^10 || ^12 || >=14}
@@ -5855,9 +6017,15 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
+ axios@1.18.1:
+ resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
+
babel-dead-code-elimination@1.0.12:
resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -6018,6 +6186,9 @@ packages:
caniuse-lite@1.0.30001799:
resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==}
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
chain-function@1.0.1:
resolution: {integrity: sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==}
@@ -6041,9 +6212,28 @@ packages:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
chardet@2.2.0:
resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==}
+ cheerio-select@2.1.0:
+ resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
+
+ cheerio@1.2.0:
+ resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
+ engines: {node: '>=20.18.1'}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -6114,6 +6304,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
+ collapse-white-space@2.1.0:
+ resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
+
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -6130,6 +6323,13 @@ packages:
colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
@@ -6369,6 +6569,9 @@ packages:
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -6410,6 +6613,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -6433,6 +6640,9 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
@@ -6504,6 +6714,9 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
+ encoding-sniffer@0.2.1:
+ resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
+
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
@@ -6541,6 +6754,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
entities@8.0.0:
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
engines: {node: '>=20.19.0'}
@@ -6608,6 +6825,12 @@ packages:
es-toolkit@1.49.0:
resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
+ esast-util-from-estree@2.0.0:
+ resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
+
+ esast-util-from-js@2.0.1:
+ resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+
esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'}
@@ -6744,6 +6967,27 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-attach-comments@3.0.0:
+ resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
+
+ estree-util-build-jsx@3.0.1:
+ resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
+
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
+ estree-util-scope@1.0.0:
+ resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
+
+ estree-util-to-js@2.0.0:
+ resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
+
+ estree-util-visit@2.0.0:
+ resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -6801,6 +7045,9 @@ packages:
exsolve@1.1.0:
resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==}
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
extendable-error@0.1.7:
resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
@@ -6882,6 +7129,15 @@ packages:
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+ follow-redirects@1.16.0:
+ resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@@ -6890,6 +7146,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
+ form-data@4.0.6:
+ resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
+ engines: {node: '>= 6'}
+
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
@@ -7109,6 +7369,15 @@ packages:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
+ hast-util-to-estree@3.1.3:
+ resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
+
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -7141,6 +7410,9 @@ packages:
htmlparser2@10.0.0:
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
+ htmlparser2@10.1.0:
+ resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
+
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
@@ -7232,6 +7504,9 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ inline-style-parser@0.2.7:
+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
+
input-otp@1.4.2:
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
peerDependencies:
@@ -7254,6 +7529,12 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -7293,6 +7574,9 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
@@ -7322,6 +7606,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
is-interactive@2.0.0:
resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
engines: {node: '>=12'}
@@ -7485,9 +7772,16 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
+ joi@18.2.3:
+ resolution: {integrity: sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==}
+ engines: {node: '>= 20'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
hasBin: true
@@ -7718,6 +8012,9 @@ packages:
long-timeout@0.1.1:
resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==}
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -7759,6 +8056,10 @@ packages:
resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==}
engines: {node: '>=18'}
+ markdown-extensions@2.0.0:
+ resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
+ engines: {node: '>=16'}
+
marked@15.0.12:
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
engines: {node: '>= 18'}
@@ -7768,6 +8069,33 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdx@3.0.0:
+ resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
@@ -7804,6 +8132,90 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-mdx-expression@3.0.1:
+ resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
+
+ micromark-extension-mdx-jsx@3.0.2:
+ resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==}
+
+ micromark-extension-mdx-md@2.0.0:
+ resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
+
+ micromark-extension-mdxjs@3.0.0:
+ resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-mdx-expression@2.0.3:
+ resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-events-to-acorn@2.0.3:
+ resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -8181,6 +8593,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
parse-json@4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
@@ -8201,6 +8616,15 @@ packages:
resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
engines: {node: '>=0.10.0'}
+ parse5-htmlparser2-tree-adapter@7.1.0:
+ resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
+
+ parse5-parser-stream@7.1.2:
+ resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
parse5@8.0.1:
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
@@ -8545,6 +8969,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ property-information@7.2.0:
+ resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -8552,6 +8979,10 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ proxy-from-env@2.1.0:
+ resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+ engines: {node: '>=10'}
+
prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
@@ -8759,6 +9190,20 @@ packages:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
+ recma-build-jsx@1.0.0:
+ resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
+
+ recma-jsx@1.0.1:
+ resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ recma-parse@1.0.0:
+ resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
+
+ recma-stringify@1.0.0:
+ resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -8781,6 +9226,18 @@ packages:
resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==}
engines: {node: '>=0.10.0'}
+ rehype-recma@1.0.0:
+ resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+
+ remark-mdx@3.1.1:
+ resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
+
+ remark-parse@11.0.0:
+ resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
+
+ remark-rehype@11.1.2:
+ resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+
remix-auth-github@3.0.2:
resolution: {integrity: sha512-3XxykdwMrcPSyMsdGtBDl3DBc19gJM3t7q/1uzfz3g/SJRsxEytjGiQ17ztKykebCGM454Z0lVJMvSb+LF/yHA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=20.0.0}
@@ -9201,6 +9658,10 @@ packages:
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
engines: {node: '>= 0.4'}
+ shelljs@0.10.0:
+ resolution: {integrity: sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==}
+ engines: {node: '>=18'}
+
side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'}
@@ -9275,6 +9736,9 @@ packages:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
@@ -9302,6 +9766,11 @@ packages:
resolution: {integrity: sha512-kTYRg5FIcvsDtYUG2Qn9pYT6xKwiLJN5TTIvc5Mur6hIg4pSfdpHu8Yyu5bqESLHnVM3mXzD446cb2+uEaKZXg==}
hasBin: true
+ srvx@0.11.17:
+ resolution: {integrity: sha512-43yM4luKfCJamyCMhrUeHUPOrf8TdZe7kN8s5zayZCH5OeprYqi49Aso5ZvHXR4aB+DHaRNO/diNFgZSMNG8Xw==}
+ engines: {node: '>=20.16.0'}
+ hasBin: true
+
stable-hash-x@0.2.0:
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
engines: {node: '>=12.0.0'}
@@ -9361,6 +9830,9 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -9389,6 +9861,10 @@ packages:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
+ strip-indent@4.1.1:
+ resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
+ engines: {node: '>=12'}
+
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -9397,6 +9873,15 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ strip-literal@3.1.0:
+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
+ style-to-js@1.1.21:
+ resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
+
+ style-to-object@1.0.14:
+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
@@ -9524,6 +10009,12 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
@@ -9555,6 +10046,9 @@ packages:
turbo-stream@2.4.1:
resolution: {integrity: sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw==}
+ turbo-stream@3.2.0:
+ resolution: {integrity: sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -9633,6 +10127,27 @@ packages:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position-from-estree@2.0.0:
+ resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
@@ -9720,6 +10235,12 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
vite-env-only@3.0.3:
resolution: {integrity: sha512-iAb7cTXRrvFShaF1n+G8f6Yqq7sRJcxipNYNQQu0DN5N9P55vJMmLG5lNU5moYGpd+ZH1WhBHdkWi5WjrfImHg==}
peerDependencies:
@@ -9775,10 +10296,23 @@ packages:
yaml:
optional: true
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
+ wait-on@9.0.10:
+ resolution: {integrity: sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==}
+ engines: {node: '>=20.0.0'}
+ hasBin: true
+
warning@3.0.0:
resolution: {integrity: sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==}
@@ -9813,6 +10347,11 @@ packages:
webpack-cli:
optional: true
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
@@ -9977,6 +10516,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
snapshots:
'@acemir/cssom@0.9.31': {}
@@ -10001,7 +10543,7 @@ snapshots:
'@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- lru-cache: 11.2.5
+ lru-cache: 11.5.1
'@asamuzakjp/dom-selector@6.8.1':
dependencies:
@@ -10795,6 +11337,22 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@hapi/address@5.1.1':
+ dependencies:
+ '@hapi/hoek': 11.0.7
+
+ '@hapi/formula@3.0.2': {}
+
+ '@hapi/hoek@11.0.7': {}
+
+ '@hapi/pinpoint@2.0.1': {}
+
+ '@hapi/tlds@1.1.7': {}
+
+ '@hapi/topo@6.0.2':
+ dependencies:
+ '@hapi/hoek': 11.0.7
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -10996,6 +11554,45 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
+ '@mdx-js/loader@3.1.1(webpack@5.108.1(lightningcss@1.32.0))':
+ dependencies:
+ '@mdx-js/mdx': 3.1.1
+ source-map: 0.7.6
+ optionalDependencies:
+ webpack: 5.108.1(lightningcss@1.32.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mdx-js/mdx@3.1.1':
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdx': 2.0.14
+ acorn: 8.17.0
+ collapse-white-space: 2.1.0
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-util-scope: 1.0.0
+ estree-walker: 3.0.3
+ hast-util-to-jsx-runtime: 2.3.6
+ markdown-extensions: 2.0.0
+ recma-build-jsx: 1.0.0
+ recma-jsx: 1.0.1(acorn@8.17.0)
+ recma-stringify: 1.0.0
+ rehype-recma: 1.0.0
+ remark-mdx: 3.1.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ source-map: 0.7.6
+ unified: 11.0.5
+ unist-util-position-from-estree: 2.0.0
+ unist-util-stringify-position: 4.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
'@mjackson/form-data-parser@0.9.1':
dependencies:
'@mjackson/multipart-parser': 0.10.1
@@ -11650,6 +12247,10 @@ snapshots:
dependencies:
playwright: 1.58.0
+ '@playwright/test@1.61.1':
+ dependencies:
+ playwright: 1.61.1
+
'@poppinss/colors@4.1.6':
dependencies:
kleur: 4.1.5
@@ -12293,7 +12894,7 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
- '@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
+ '@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
@@ -12327,6 +12928,7 @@ snapshots:
vite-node: 3.2.4(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
optionalDependencies:
'@react-router/serve': 7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
typescript: 5.9.3
wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1)
transitivePeerDependencies:
@@ -12344,7 +12946,7 @@ snapshots:
- tsx
- yaml
- '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
+ '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
@@ -12377,13 +12979,14 @@ snapshots:
vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
optionalDependencies:
'@react-router/serve': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
typescript: 5.9.3
wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
- '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
+ '@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))':
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
@@ -12416,12 +13019,21 @@ snapshots:
vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
optionalDependencies:
'@react-router/serve': 8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
typescript: 5.9.3
wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
+ '@react-router/express@7.13.0(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/node': 7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ express: 4.22.2
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ optionalDependencies:
+ typescript: 5.9.3
+
'@react-router/express@7.13.0(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
'@react-router/node': 7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
@@ -12438,6 +13050,14 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ '@react-router/express@8.0.1(express@4.22.2)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/node': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
+ express: 4.22.2
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ optionalDependencies:
+ typescript: 5.9.3
+
'@react-router/express@8.0.1(express@5.2.1)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
'@react-router/node': 8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)
@@ -12445,7 +13065,6 @@ snapshots:
react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
optionalDependencies:
typescript: 5.9.3
- optional: true
'@react-router/express@8.0.1(express@5.2.1)(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
@@ -12455,17 +13074,24 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
- '@react-router/fs-routes@8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
+ '@react-router/fs-routes@8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
dependencies:
- '@react-router/dev': 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ '@react-router/dev': 8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
minimatch: 10.2.5
optionalDependencies:
typescript: 5.9.3
- '@react-router/node@7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ '@react-router/fs-routes@8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
dependencies:
- '@mjackson/node-fetch-server': 0.2.0
- react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ '@react-router/dev': 8.0.1(@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ minimatch: 10.2.5
+ optionalDependencies:
+ typescript: 5.9.3
+
+ '@react-router/node@7.13.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
+ dependencies:
+ '@mjackson/node-fetch-server': 0.2.0
+ react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
optionalDependencies:
typescript: 5.9.3
@@ -12490,9 +13116,15 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
- '@react-router/remix-routes-option-adapter@7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
+ '@react-router/remix-routes-option-adapter@7.13.0(@react-router/dev@7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
+ dependencies:
+ '@react-router/dev': 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ optionalDependencies:
+ typescript: 5.9.3
+
+ '@react-router/remix-routes-option-adapter@8.0.1(@react-router/dev@8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1)))(typescript@5.9.3)':
dependencies:
- '@react-router/dev': 7.18.0(@react-router/serve@7.18.0(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
+ '@react-router/dev': 8.0.1(@react-router/serve@8.0.1(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)))(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))(wrangler@4.105.0(@cloudflare/workers-types@4.20260628.1))
optionalDependencies:
typescript: 5.9.3
@@ -12525,7 +13157,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- typescript
- optional: true
'@react-router/serve@8.0.1(react-router@8.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@5.9.3)':
dependencies:
@@ -12586,6 +13217,9 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.53': {}
+ '@rolldown/pluginutils@1.0.1':
+ optional: true
+
'@rollup/rollup-android-arm-eabi@4.62.2':
optional: true
@@ -12669,6 +13303,15 @@ snapshots:
core-js: 3.47.0
jiti: 2.7.0
+ '@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)':
+ dependencies:
+ '@rspack/core': 2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23)
+ '@swc/helpers': 0.5.23
+ optionalDependencies:
+ core-js: 3.47.0
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+
'@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)':
dependencies:
'@rspack/core': 2.0.8(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23)
@@ -12709,6 +13352,15 @@ snapshots:
- '@rspack/core'
- webpack
+ '@rsbuild/plugin-mdx@1.1.3(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(webpack@5.108.1(lightningcss@1.32.0))':
+ dependencies:
+ '@mdx-js/loader': 3.1.1(webpack@5.108.1(lightningcss@1.32.0))
+ optionalDependencies:
+ '@rsbuild/core': 2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ transitivePeerDependencies:
+ - supports-color
+ - webpack
+
'@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))':
dependencies:
'@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))(react-refresh@0.18.0)
@@ -12831,6 +13483,17 @@ snapshots:
- '@rspack/core'
- webpack
+ '@rslib/core@0.22.1(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)(typescript@5.9.3)':
+ dependencies:
+ '@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ rsbuild-plugin-dts: 0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(typescript@5.9.3)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+ - '@typescript/native-preview'
+ - core-js
+
'@rslib/core@0.22.1(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)(typescript@5.9.3)':
dependencies:
'@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)
@@ -12997,6 +13660,13 @@ snapshots:
optionalDependencies:
'@swc/helpers': 0.5.23
+ '@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23)':
+ dependencies:
+ '@rspack/binding': 2.0.8
+ optionalDependencies:
+ '@module-federation/runtime-tools': 2.5.1(node-fetch@2.7.0(encoding@0.1.13))
+ '@swc/helpers': 0.5.23
+
'@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23)':
dependencies:
'@rspack/binding': 2.0.8
@@ -13334,6 +14004,8 @@ snapshots:
'@speed-highlight/core@1.2.17': {}
+ '@standard-schema/spec@1.1.0': {}
+
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -13593,6 +14265,10 @@ snapshots:
'@types/d3-hierarchy@1.1.11': {}
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
'@types/deep-eql@4.0.2': {}
'@types/eslint@9.6.1':
@@ -13600,6 +14276,10 @@ snapshots:
'@types/estree': 1.0.9
'@types/json-schema': 7.0.15
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.9
+
'@types/estree@1.0.5': {}
'@types/estree@1.0.9': {}
@@ -13626,6 +14306,10 @@ snapshots:
dependencies:
glob: 13.0.0
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/http-errors@2.0.5': {}
'@types/jsesc@3.0.3': {}
@@ -13636,10 +14320,18 @@ snapshots:
dependencies:
'@types/node': 25.0.10
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/mdx@2.0.14': {}
+
'@types/morgan@1.9.10':
dependencies:
'@types/node': 25.0.10
+ '@types/ms@2.1.0': {}
+
'@types/mysql@2.15.27':
dependencies:
'@types/node': 25.0.10
@@ -13707,6 +14399,10 @@ snapshots:
dependencies:
'@types/node': 25.0.10
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.0.10
@@ -13802,6 +14498,8 @@ snapshots:
'@typescript-eslint/types': 8.62.0
eslint-visitor-keys: 5.0.1
+ '@ungap/structured-clone@1.3.2': {}
+
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
optional: true
@@ -13902,6 +14600,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ es-module-lexer: 2.1.0
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ srvx: 0.11.17
+ strip-literal: 3.1.0
+ turbo-stream: 3.2.0
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
+ vitefu: 1.1.3(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0))
+ optional: true
+
'@vitest/eslint-plugin@1.6.20(@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.2(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.2(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.2(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.62.0
@@ -14284,8 +14997,12 @@ snapshots:
assertion-error@2.0.1: {}
+ astring@1.9.0: {}
+
async-function@1.0.0: {}
+ asynckit@0.4.0: {}
+
autoprefixer@10.4.23(postcss@8.5.16):
dependencies:
browserslist: 4.28.4
@@ -14299,6 +15016,16 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
+ axios@1.18.1:
+ dependencies:
+ follow-redirects: 1.16.0
+ form-data: 4.0.6
+ https-proxy-agent: 5.0.1
+ proxy-from-env: 2.1.0
+ transitivePeerDependencies:
+ - debug
+ - supports-color
+
babel-dead-code-elimination@1.0.12:
dependencies:
'@babel/core': 7.29.7
@@ -14308,6 +15035,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ bail@2.0.2: {}
+
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -14487,6 +15216,8 @@ snapshots:
caniuse-lite@1.0.30001799: {}
+ ccount@2.0.1: {}
+
chain-function@1.0.1: {}
chalk-template@0.4.0:
@@ -14508,8 +15239,39 @@ snapshots:
chalk@5.6.2: {}
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
chardet@2.2.0: {}
+ cheerio-select@2.1.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-select: 5.2.2
+ css-what: 6.2.2
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+
+ cheerio@1.2.0:
+ dependencies:
+ cheerio-select: 2.1.0
+ dom-serializer: 2.0.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ encoding-sniffer: 0.2.1
+ htmlparser2: 10.1.0
+ parse5: 7.3.0
+ parse5-htmlparser2-tree-adapter: 7.1.0
+ parse5-parser-stream: 7.1.2
+ undici: 7.28.0
+ whatwg-mimetype: 4.0.0
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -14578,6 +15340,8 @@ snapshots:
clsx@2.1.1: {}
+ collapse-white-space@2.1.0: {}
+
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -14592,6 +15356,12 @@ snapshots:
colorjs.io@0.5.2: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ comma-separated-tokens@2.0.3: {}
+
commander@11.1.0: {}
commander@2.20.3: {}
@@ -14720,7 +15490,7 @@ snapshots:
'@asamuzakjp/css-color': 4.1.2
'@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1)
css-tree: 3.2.1
- lru-cache: 11.2.5
+ lru-cache: 11.5.1
csstype@3.2.3: {}
@@ -14811,6 +15581,10 @@ snapshots:
decimal.js@10.6.0: {}
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -14845,6 +15619,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ delayed-stream@1.0.0: {}
+
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -14857,6 +15633,10 @@ snapshots:
detect-node-es@1.1.0: {}
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
dijkstrajs@1.0.3: {}
dir-glob@3.0.1:
@@ -14919,6 +15699,11 @@ snapshots:
encodeurl@2.0.0: {}
+ encoding-sniffer@0.2.1:
+ dependencies:
+ iconv-lite: 0.6.3
+ whatwg-encoding: 3.1.1
+
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
@@ -14967,6 +15752,8 @@ snapshots:
entities@6.0.1: {}
+ entities@7.0.1: {}
+
entities@8.0.0: {}
envinfo@7.21.0: {}
@@ -15101,6 +15888,20 @@ snapshots:
es-toolkit@1.49.0: {}
+ esast-util-from-estree@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ unist-util-position-from-estree: 2.0.0
+
+ esast-util-from-js@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ acorn: 8.17.0
+ esast-util-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
esbuild@0.27.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
@@ -15316,6 +16117,39 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-attach-comments@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ estree-util-build-jsx@3.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-walker: 3.0.3
+
+ estree-util-is-identifier-name@3.0.0: {}
+
+ estree-util-scope@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+
+ estree-util-to-js@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ astring: 1.9.0
+ source-map: 0.7.6
+
+ estree-util-visit@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/unist': 3.0.3
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
esutils@2.0.3: {}
etag@1.8.1: {}
@@ -15447,6 +16281,8 @@ snapshots:
exsolve@1.1.0: {}
+ extend@3.0.2: {}
+
extendable-error@0.1.7: {}
fast-deep-equal@3.1.3: {}
@@ -15537,6 +16373,8 @@ snapshots:
flatted@3.4.2: {}
+ follow-redirects@1.16.0: {}
+
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@@ -15546,6 +16384,14 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ form-data@4.0.6:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.4
+ mime-types: 2.1.35
+
forwarded-parse@2.1.2: {}
forwarded@0.2.0: {}
@@ -15753,6 +16599,51 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hast-util-to-estree@3.1.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-attach-comments: 3.0.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ zwitch: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
he@1.2.0: {}
headers-polyfill@4.0.3: {}
@@ -15788,6 +16679,13 @@ snapshots:
domutils: 3.2.2
entities: 6.0.1
+ htmlparser2@10.1.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 7.0.1
+
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
@@ -15882,6 +16780,8 @@ snapshots:
ini@1.3.8: {}
+ inline-style-parser@0.2.7: {}
+
input-otp@1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
dependencies:
react: 19.2.7
@@ -15899,6 +16799,13 @@ snapshots:
ipaddr.js@1.9.1: {}
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.9
@@ -15945,6 +16852,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
+ is-decimal@2.0.1: {}
+
is-docker@2.2.1: {}
is-document.all@1.0.0:
@@ -15971,6 +16880,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-hexadecimal@2.0.1: {}
+
is-interactive@2.0.0: {}
is-map@2.0.3: {}
@@ -16113,8 +17024,21 @@ snapshots:
jiti@2.7.0: {}
+ joi@18.2.3:
+ dependencies:
+ '@hapi/address': 5.1.1
+ '@hapi/formula': 3.0.2
+ '@hapi/hoek': 11.0.7
+ '@hapi/pinpoint': 2.0.1
+ '@hapi/tlds': 1.1.7
+ '@hapi/topo': 6.0.2
+ '@standard-schema/spec': 1.1.0
+
js-tokens@4.0.0: {}
+ js-tokens@9.0.1:
+ optional: true
+
js-yaml@3.15.0:
dependencies:
argparse: 1.0.10
@@ -16331,6 +17255,8 @@ snapshots:
long-timeout@0.1.1: {}
+ longest-streak@3.1.0: {}
+
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@@ -16364,10 +17290,111 @@ snapshots:
make-dir@5.1.0:
optional: true
+ markdown-extensions@2.0.0: {}
+
marked@15.0.12: {}
math-intrinsics@1.1.0: {}
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx@3.0.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.2
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
mdn-data@2.27.1: {}
media-query-parser@2.0.2:
@@ -16390,6 +17417,212 @@ snapshots:
methods@1.1.2: {}
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-expression@3.0.1:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-jsx@3.0.2:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-extension-mdx-md@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-extension-mdxjs@3.0.0:
+ dependencies:
+ acorn: 8.17.0
+ acorn-jsx: 5.3.2(acorn@8.17.0)
+ micromark-extension-mdx-expression: 3.0.1
+ micromark-extension-mdx-jsx: 3.0.2
+ micromark-extension-mdx-md: 2.0.0
+ micromark-extension-mdxjs-esm: 3.0.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-mdx-expression@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-events-to-acorn@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -16763,6 +17996,16 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
parse-json@4.0.0:
dependencies:
error-ex: 1.3.4
@@ -16781,6 +18024,19 @@ snapshots:
parse-passwd@1.0.0: {}
+ parse5-htmlparser2-tree-adapter@7.1.0:
+ dependencies:
+ domhandler: 5.0.3
+ parse5: 7.3.0
+
+ parse5-parser-stream@7.1.2:
+ dependencies:
+ parse5: 7.3.0
+
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
parse5@8.0.1:
dependencies:
entities: 8.0.0
@@ -16984,6 +18240,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ property-information@7.2.0: {}
+
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -16991,6 +18249,8 @@ snapshots:
proxy-from-env@1.1.0: {}
+ proxy-from-env@2.1.0: {}
+
prr@1.0.1:
optional: true
@@ -17221,6 +18481,35 @@ snapshots:
readdirp@5.0.0: {}
+ recma-build-jsx@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-util-build-jsx: 3.0.1
+ vfile: 6.0.3
+
+ recma-jsx@1.0.1(acorn@8.17.0):
+ dependencies:
+ acorn: 8.17.0
+ acorn-jsx: 5.3.2(acorn@8.17.0)
+ estree-util-to-js: 2.0.0
+ recma-parse: 1.0.0
+ recma-stringify: 1.0.0
+ unified: 11.0.5
+
+ recma-parse@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ esast-util-from-js: 2.0.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ recma-stringify@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-util-to-js: 2.0.0
+ unified: 11.0.5
+ vfile: 6.0.3
+
redent@3.0.0:
dependencies:
indent-string: 4.0.0
@@ -17257,6 +18546,38 @@ snapshots:
dependencies:
rc: 1.2.8
+ rehype-recma@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/hast': 3.0.4
+ hast-util-to-estree: 3.1.3
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-mdx@3.1.1:
+ dependencies:
+ mdast-util-mdx: 3.0.0
+ micromark-extension-mdxjs: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-parse@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ micromark-util-types: 2.0.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-rehype@11.1.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ mdast-util-to-hast: 13.2.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
remix-auth-github@3.0.2(remix-auth@4.2.0):
dependencies:
'@mjackson/headers': 0.9.0
@@ -17273,12 +18594,13 @@ snapshots:
fs-extra: 11.3.3
minimatch: 10.2.5
- remix-utils@9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
+ remix-utils@9.0.0(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(@standard-schema/spec@1.1.0)(intl-parse-accept-language@1.0.0)(react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
dependencies:
type-fest: 4.41.0
optionalDependencies:
'@oslojs/crypto': 1.0.1
'@oslojs/encoding': 1.1.0
+ '@standard-schema/spec': 1.1.0
intl-parse-accept-language: 1.0.0
react: 19.2.7
react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -17387,6 +18709,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rsbuild-plugin-dts@0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(typescript@5.9.3):
+ dependencies:
+ '@ast-grep/napi': 0.37.0
+ '@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ optionalDependencies:
+ typescript: 5.9.3
+
rsbuild-plugin-dts@0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0))(typescript@5.9.3):
dependencies:
'@ast-grep/napi': 0.37.0
@@ -17727,6 +19056,11 @@ snapshots:
shell-quote@1.9.0: {}
+ shelljs@0.10.0:
+ dependencies:
+ execa: 5.1.1
+ fast-glob: 3.3.3
+
side-channel-list@1.0.1:
dependencies:
es-errors: 1.3.0
@@ -17825,6 +19159,8 @@ snapshots:
source-map@0.7.6: {}
+ space-separated-tokens@2.0.2: {}
+
spawndamnit@3.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -17855,6 +19191,9 @@ snapshots:
argparse: 2.0.1
nearley: 2.20.1
+ srvx@0.11.17:
+ optional: true
+
stable-hash-x@0.2.0: {}
statuses@2.0.2: {}
@@ -17943,6 +19282,11 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -17963,10 +19307,25 @@ snapshots:
dependencies:
min-indent: 1.0.1
+ strip-indent@4.1.1: {}
+
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {}
+ strip-literal@3.1.0:
+ dependencies:
+ js-tokens: 9.0.1
+ optional: true
+
+ style-to-js@1.1.21:
+ dependencies:
+ style-to-object: 1.0.14
+
+ style-to-object@1.0.14:
+ dependencies:
+ inline-style-parser: 0.2.7
+
supports-color@10.2.2: {}
supports-color@5.5.0:
@@ -18072,6 +19431,10 @@ snapshots:
tree-kill@1.2.2: {}
+ trim-lines@3.0.1: {}
+
+ trough@2.2.0: {}
+
ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -18095,6 +19458,9 @@ snapshots:
turbo-stream@2.4.1: {}
+ turbo-stream@3.2.0:
+ optional: true
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -18185,6 +19551,43 @@ snapshots:
unicorn-magic@0.3.0: {}
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position-from-estree@2.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
universalify@0.1.2: {}
universalify@2.0.1: {}
@@ -18278,6 +19681,16 @@ snapshots:
vary@1.1.2: {}
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
vite-env-only@3.0.3(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)):
dependencies:
'@babel/core': 7.29.7
@@ -18341,10 +19754,26 @@ snapshots:
terser: 5.48.0
tsx: 4.21.0
+ vitefu@1.1.3(vite@7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)):
+ optionalDependencies:
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.7.0)(less@4.6.7)(lightningcss@1.32.0)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.21.0)
+ optional: true
+
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
+ wait-on@9.0.10:
+ dependencies:
+ axios: 1.18.1
+ joi: 18.2.3
+ lodash: 4.18.1
+ minimist: 1.2.8
+ rxjs: 7.8.2
+ transitivePeerDependencies:
+ - debug
+ - supports-color
+
warning@3.0.0:
dependencies:
loose-envify: 1.4.0
@@ -18442,6 +19871,10 @@ snapshots:
- postcss
- uglify-js
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
whatwg-mimetype@4.0.0: {}
whatwg-mimetype@5.0.0: {}
@@ -18681,3 +20114,5 @@ snapshots:
'@yuku-parser/binding-win32-x64': 0.5.39
zod@3.25.76: {}
+
+ zwitch@2.0.4: {}
diff --git a/tests/react-router-framework/README.md b/tests/react-router-framework/README.md
new file mode 100644
index 00000000..e976ff83
--- /dev/null
+++ b/tests/react-router-framework/README.md
@@ -0,0 +1,36 @@
+# React Router Framework Tests
+
+This folder contains copied React Router upstream framework-mode tests adapted
+to exercise `rsbuild-plugin-react-router`.
+
+## Source
+
+- `integration/` is copied from `/home/zack/projects/react-router/integration`.
+- `react-router-dev/__tests__/` is copied from
+ `/home/zack/projects/react-router/packages/react-router-dev/__tests__`.
+
+Keep upstream test files as close to source as practical so the suite can be
+refreshed by copying those folders again.
+
+## Rsbuild Adapter
+
+The copied integration harness still has Vite-oriented names because the
+upstream test suite does. Execution is redirected through
+`integration/helpers/rsbuild-adapter.ts` and the patched helper files:
+
+- fixture projects get `rsbuild.config.ts`, not `vite.config.ts`
+- builds run `@rsbuild/core`
+- dev servers run `rsbuild dev`
+- production servers run `react-router-serve`
+- MDX routes use the official `@rsbuild/plugin-mdx`
+
+Do not add a local ignore list for Rsbuild gaps. Upstream `test.skip` and
+`test.fixme` calls should remain intact, but otherwise unsupported cases should
+fail visibly.
+
+## Commands
+
+```sh
+pnpm test:react-router-framework:smoke
+pnpm test:react-router-framework
+```
diff --git a/tests/react-router-framework/integration/CHANGELOG.md b/tests/react-router-framework/integration/CHANGELOG.md
new file mode 100644
index 00000000..2cf67d87
--- /dev/null
+++ b/tests/react-router-framework/integration/CHANGELOG.md
@@ -0,0 +1,14 @@
+# integration-tests
+
+## 0.0.0
+
+### Minor Changes
+
+- Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590))
+ - `remix build` 👉 `vite build && vite build --ssr`
+ - `remix dev` 👉 `vite dev`
+
+ Other runtimes (e.g. Deno, Cloudflare) not yet supported.
+ Custom server (e.g. Express) not yet supported.
+
+ See "Future > Vite" in the Remix Docs for details.
diff --git a/tests/react-router-framework/integration/abort-signal-test.ts b/tests/react-router-framework/integration/abort-signal-test.ts
new file mode 100644
index 00000000..f650ec6f
--- /dev/null
+++ b/tests/react-router-framework/integration/abort-signal-test.ts
@@ -0,0 +1,65 @@
+import { test } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { useActionData, useLoaderData, Form } from "react-router";
+
+ export async function action ({ request }) {
+ // New event loop causes express request to close
+ await new Promise(r => setTimeout(r, 0));
+ return { aborted: request.signal.aborted };
+ }
+
+ export function loader({ request }) {
+ return { aborted: request.signal.aborted };
+ }
+
+ export default function Index() {
+ let actionData = useActionData();
+ let data = useLoaderData();
+ return (
+
+
{actionData ? String(actionData.aborted) : "empty"}
+
{String(data.aborted)}
+
+
+ )
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using playwright.
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+test("should not abort the request in a new event loop", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await page.waitForSelector(`.action:has-text("empty")`);
+ await page.waitForSelector(`.loader:has-text("false")`);
+
+ await app.clickElement('button[type="submit"]');
+
+ await page.waitForSelector(`.action:has-text("false")`);
+ await page.waitForSelector(`.loader:has-text("false")`);
+});
diff --git a/tests/react-router-framework/integration/action-test.ts b/tests/react-router-framework/integration/action-test.ts
new file mode 100644
index 00000000..6e9a3a57
--- /dev/null
+++ b/tests/react-router-framework/integration/action-test.ts
@@ -0,0 +1,232 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createFixture,
+ createAppFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture, selectHtml } from "./helpers/playwright-fixture.js";
+import { type TemplateName } from "./helpers/vite.js";
+
+const templateNames = [
+ "vite-7-template",
+ "rsc-vite-framework",
+] as const satisfies TemplateName[];
+
+test.describe("actions", () => {
+ for (const templateName of templateNames) {
+ test.describe(`template: ${templateName}`, () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ let FIELD_NAME = "message";
+ let WAITING_VALUE = "Waiting...";
+ let SUBMITTED_VALUE = "Submission";
+ let THROWS_REDIRECT = "redirect-throw";
+ let REDIRECT_TARGET = "page";
+ let PAGE_TEXT = "PAGE_TEXT";
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ templateName,
+ files: {
+ "app/routes/urlencoded.tsx": js`
+ import { Form, useActionData } from "react-router";
+
+ export let action = async ({ request }) => {
+ let formData = await request.formData();
+ return formData.get("${FIELD_NAME}");
+ };
+
+ export default function Actions() {
+ let data = useActionData()
+
+ return (
+
+ );
+ }
+ `,
+
+ "app/routes/request-text.tsx": js`
+ import { Form, useActionData } from "react-router";
+
+ export let action = async ({ request }) => {
+ let text = await request.text();
+ return text;
+ };
+
+ export default function Actions() {
+ let data = useActionData()
+
+ return (
+
+ );
+ }
+ `,
+
+ [`app/routes/${THROWS_REDIRECT}.jsx`]: js`
+ import { redirect, Form } from "react-router";
+
+ export function action() {
+ throw redirect("/${REDIRECT_TARGET}")
+ }
+
+ export default function () {
+ return (
+
+ )
+ }
+ `,
+
+ [`app/routes/${REDIRECT_TARGET}.jsx`]: js`
+ export default function () {
+ return ${PAGE_TEXT}
+ }
+ `,
+
+ "app/routes/no-action.tsx": js`
+ import { Form } from "react-router";
+
+ export default function Component() {
+ return (
+
+ );
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ let logs: string[] = [];
+
+ test.beforeEach(({ page }) => {
+ page.on("console", (msg) => {
+ logs.push(msg.text());
+ });
+ });
+
+ test.afterEach(() => {
+ expect(logs).toHaveLength(0);
+ });
+
+ test("is not called on document GET requests", async () => {
+ let res = await fixture.requestDocument("/urlencoded");
+ let html = await selectHtml(await res.text(), "#text");
+ expect(html).toMatch(WAITING_VALUE);
+ });
+
+ test("is called on document POST requests", async () => {
+ let FIELD_VALUE = "cheeseburger";
+
+ let params = new URLSearchParams();
+ params.append(FIELD_NAME, FIELD_VALUE);
+
+ let res = await fixture.postDocument("/urlencoded", params);
+
+ let html = await selectHtml(await res.text(), "#text");
+ expect(html).toMatch(FIELD_VALUE);
+ });
+
+ test("is called on script transition POST requests", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/urlencoded`);
+ await page.waitForSelector(`#text:has-text("${WAITING_VALUE}")`);
+
+ await page.click("button[type=submit]");
+ await page.waitForSelector("#action-text");
+ await page.waitForSelector(`#text:has-text("${SUBMITTED_VALUE}")`);
+ });
+
+ test("throws a 405 when no action exists", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/no-action`);
+ await page.click("button[type=submit]");
+ await page.waitForSelector(`h1:has-text("405 Method Not Allowed")`);
+ expect(logs.length).toBe(2);
+ expect(logs[0]).toMatch(
+ 'Route "routes/no-action" does not have an action',
+ );
+ // logs[1] is the raw ErrorResponse instance from the boundary but playwright
+ // seems to just log the name of the constructor, which in the minified code
+ // is meaningless so we don't bother asserting
+
+ // The rest of the tests in this suite assert no logs, so clear this out to
+ // avoid failures in afterEach
+ logs = [];
+ });
+
+ test("properly encodes form data for request.text() usage", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/request-text`);
+ await page.waitForSelector(`#text:has-text("${WAITING_VALUE}")`);
+
+ await page.click("button[type=submit]");
+ await page.waitForSelector("#action-text");
+ expect(await app.getHtml("#action-text")).toBe(
+ 'a=1&b=2 ',
+ );
+ });
+
+ test("redirects a thrown response on document requests", async () => {
+ let params = new URLSearchParams();
+ let res = await fixture.postDocument(`/${THROWS_REDIRECT}`, params);
+ expect(res.status).toBe(302);
+ expect(res.headers.get("Location")).toBe(`/${REDIRECT_TARGET}`);
+ });
+
+ test("redirects a thrown response on script transitions", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/${THROWS_REDIRECT}`);
+ let responses = app.collectSingleFetchResponses();
+ await app.clickSubmitButton(`/${THROWS_REDIRECT}`);
+
+ await page.waitForSelector(`#${REDIRECT_TARGET}`);
+
+ // In RSC, every route implicitly has a loader, so we get an extra
+ // response for the page we've redirected to. To keep the rest of the
+ // test RSC-agnostic, we drop the last response.
+ if (templateName.includes("rsc")) {
+ responses = responses.slice(0, -1);
+ }
+
+ expect(responses).toHaveLength(1);
+ expect(responses[0].status()).toBe(202);
+
+ expect(new URL(page.url()).pathname).toBe(`/${REDIRECT_TARGET}`);
+ expect(await app.getHtml()).toMatch(PAGE_TEXT);
+ });
+ });
+ }
+});
diff --git a/tests/react-router-framework/integration/assets/toupload.txt b/tests/react-router-framework/integration/assets/toupload.txt
new file mode 100644
index 00000000..b45ef6fe
--- /dev/null
+++ b/tests/react-router-framework/integration/assets/toupload.txt
@@ -0,0 +1 @@
+Hello, World!
\ No newline at end of file
diff --git a/tests/react-router-framework/integration/assets/touploadtoobig.txt b/tests/react-router-framework/integration/assets/touploadtoobig.txt
new file mode 100644
index 00000000..8811b052
--- /dev/null
+++ b/tests/react-router-framework/integration/assets/touploadtoobig.txt
@@ -0,0 +1 @@
+Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!
\ No newline at end of file
diff --git a/tests/react-router-framework/integration/blocking-test.ts b/tests/react-router-framework/integration/blocking-test.ts
new file mode 100644
index 00000000..1929f02b
--- /dev/null
+++ b/tests/react-router-framework/integration/blocking-test.ts
@@ -0,0 +1,113 @@
+import { test, expect } from "@playwright/test";
+
+import type { AppFixture, Fixture } from "./helpers/create-fixture.js";
+import {
+ createFixture,
+ js,
+ createAppFixture,
+} from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.afterAll(() => appFixture.close());
+
+test("handles synchronous proceeding correctly", async ({ page }) => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+
+
Index
+ /a
+
+ )
+ }
+ `,
+ "app/routes/a.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+
+
A
+ /b
+
+ )
+ }
+ `,
+ "app/routes/b.tsx": js`
+ import * as React from "react";
+ import { Form, useBlocker } from "react-router";
+ export default function Component() {
+ return (
+
+
B
+
+
+ )
+ }
+ function ImportantForm() {
+ let [value, setValue] = React.useState("");
+ let shouldBlock = React.useCallback(
+ ({ currentLocation, nextLocation }) =>
+ value !== "" && currentLocation.pathname !== nextLocation.pathname,
+ [value]
+ );
+ let blocker = useBlocker(shouldBlock);
+ // Reset the blocker if the user cleans the form
+ React.useEffect(() => {
+ if (blocker.state === "blocked") {
+ blocker.proceed();
+ }
+ }, [blocker]);
+ return (
+ <>
+
+ Is the form dirty?{" "}
+ {value !== "" ? (
+ Yes
+ ) : (
+ No
+ )}
+
+
+ >
+ );
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using puppeteer.
+ appFixture = await createAppFixture(fixture);
+
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/");
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ await app.clickLink("/b");
+ await page.waitForSelector("#b");
+ await page.getByLabel("Enter some important data:").fill("Hello Remix!");
+
+ // Going back should:
+ // - block
+ // - immediately call blocker.proceed() once we enter the blocked state
+ // - and land back one history entry (/a)
+ await page.goBack();
+ await page.waitForSelector("#a");
+ expect(await app.getHtml()).toContain("A");
+});
diff --git a/tests/react-router-framework/integration/browser-entry-test.ts b/tests/react-router-framework/integration/browser-entry-test.ts
new file mode 100644
index 00000000..ec1731f5
--- /dev/null
+++ b/tests/react-router-framework/integration/browser-entry-test.ts
@@ -0,0 +1,322 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createFixture,
+ js,
+ createAppFixture,
+} from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test(
+ "expect to be able to browse backward out of a remix app, then forward " +
+ "twice in history and have pages render correctly",
+ async ({ page, browserName }) => {
+ test.skip(
+ browserName === "firefox",
+ "FireFox doesn't support browsing to an empty page (aka about:blank)",
+ );
+
+ let fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+
+ export default function Index() {
+ return (
+
+ )
+ }
+ `,
+
+ "app/routes/burgers.tsx": js`
+ export default function Index() {
+ return cheeseburger
;
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using puppeteer.
+ let appFixture = await createAppFixture(fixture);
+
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Slow down the entry chunk on the second load so the bug surfaces
+ let isSecondLoad = false;
+ await page.route(/entry/, async (route) => {
+ if (isSecondLoad) {
+ await new Promise((r) => setTimeout(r, 1000));
+ }
+ route.continue();
+ });
+
+ // This sets up the Remix modules cache in memory, priming the error case.
+ await app.goto("/");
+ await app.clickLink("/burgers");
+ await page.waitForSelector("#cheeseburger");
+ expect(await page.content()).toContain("cheeseburger");
+ await page.goBack();
+ await page.waitForSelector("#pizza");
+ expect(await app.getHtml()).toContain("pizza");
+
+ // Takes the browser out of the Remix app
+ await page.goBack();
+ expect(page.url()).toContain("about:blank");
+
+ // Forward to / and immediately again to /burgers. This will trigger the
+ // error since we'll load __routeModules for / but then try to hydrate /burgers
+ isSecondLoad = true;
+ await page.goForward();
+ await page.goForward();
+ await page.waitForSelector("#cheeseburger");
+
+ // If we resolve the error, we should hard reload and eventually
+ // successfully render /burgers
+ await page.waitForSelector("#cheeseburger");
+ expect(await app.getHtml()).toContain("cheeseburger");
+
+ appFixture.close();
+ },
+);
+
+test("allows users to pass a client side context to HydratedRouter", async ({
+ page,
+}) => {
+ let fixture = await createFixture({
+ files: {
+ "app/entry.client.tsx": js`
+ import { createContext, RouterContextProvider } from "react-router";
+ import { HydratedRouter } from "react-router/dom";
+ import { startTransition, StrictMode } from "react";
+ import { hydrateRoot } from "react-dom/client";
+
+ export const myContext = new createContext('foo');
+
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+ {
+ return new RouterContextProvider([
+ [myContext, 'bar']
+ ]);
+ }}
+ />
+
+ );
+ });
+ `,
+ "app/routes/_index.tsx": js`
+ import { myContext } from "../entry.client";
+
+ export function clientLoader({ context }) {
+ return context.get(myContext);
+ }
+ export default function Index({ loaderData }) {
+ return Hello, {loaderData}
+ }
+ `,
+ },
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ expect(await app.getHtml()).toContain("Hello, bar");
+
+ appFixture.close();
+});
+
+test("allows users to pass an onError function to HydratedRouter", async ({
+ page,
+ browserName,
+}) => {
+ let fixture = await createFixture({
+ files: {
+ "app/entry.client.tsx": js`
+ import { HydratedRouter } from "react-router/dom";
+ import { startTransition, StrictMode } from "react";
+ import { hydrateRoot } from "react-dom/client";
+
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+ {
+ console.log(error.message, JSON.stringify(errorInfo))
+ }}
+ />
+
+ );
+ });
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return Go to Page;
+ }
+ `,
+ "app/routes/page.tsx": js`
+ export default function Page() {
+ throw new Error("Render error");
+ }
+ export function ErrorBoundary({ error }) {
+ return Error: {error.message}
+ }
+ `,
+ },
+ });
+
+ let logs: string[] = [];
+ page.on("console", (msg) => logs.push(msg.text()));
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ await page.click('a[href="/page"]');
+ await page.waitForSelector("[data-error]");
+
+ expect(await app.getHtml()).toContain("Error: Render error");
+ expect(logs.length).toBe(2);
+ // First one is react logging the error
+ if (browserName === "firefox") {
+ expect(logs[0]).toContain("Error");
+ } else {
+ expect(logs[0]).toContain("Error: Render error");
+ }
+ expect(logs[0]).not.toContain("componentStack");
+ // Second one is ours
+ expect(logs[1]).toContain("Render error");
+ expect(logs[1]).toContain('"componentStack":');
+
+ appFixture.close();
+});
+
+test("allows users to instrument the client side router via HydratedRouter", async ({
+ page,
+}) => {
+ let fixture = await createFixture({
+ files: {
+ "app/entry.client.tsx": js`
+ import { HydratedRouter } from "react-router/dom";
+ import { startTransition, StrictMode } from "react";
+ import { hydrateRoot } from "react-dom/client";
+
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+ });
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return Go to Page;
+ }
+ `,
+ "app/routes/page.tsx": js`
+ import { useFetcher } from "react-router";
+ export function loader() {
+ return { data: "hello world" };
+ }
+ export function action() {
+ return "OK";
+ }
+ export default function Page({ loaderData }) {
+ let fetcher = useFetcher({ key: 'a' });
+ return (
+ <>
+ {loaderData.data} ;
+ fetcher.submit({ key: 'value' }, {
+ method: 'post',
+ action: "/page"
+ })}>
+ Fetch
+
+ {fetcher.data ? {fetcher.data} : null}
+ >
+ );
+ }
+ `,
+ },
+ });
+
+ let logs: string[] = [];
+ page.on("console", (msg) => logs.push(msg.text()));
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ await page.click('a[href="/page"]');
+ await page.waitForSelector("[data-page]");
+
+ expect(await app.getHtml()).toContain("hello world");
+ expect(logs).toEqual([
+ 'start navigate [["currentUrl","/"],["to","/page"]]',
+ "start loader root /page",
+ "start loader routes/page /page",
+ "end loader root /page",
+ "end loader routes/page /page",
+ 'end navigate [["currentUrl","/"],["to","/page"]]',
+ ]);
+ logs.splice(0);
+
+ await page.click("[data-fetch]");
+ await page.waitForSelector("[data-fetcher-data]");
+ await expect(page.locator("[data-fetcher-data]")).toContainText("OK");
+ expect(logs).toEqual([
+ 'start fetch [["body",{"key":"value"}],["currentUrl","/page"],["fetcherKey","a"],["formData",null],["formEncType","application/x-www-form-urlencoded"],["formMethod","post"],["href","/page"]]',
+ "start action routes/page /page",
+ "end action routes/page /page",
+ "start loader root /page",
+ "start loader routes/page /page",
+ "end loader root /page",
+ "end loader routes/page /page",
+ 'end fetch [["body",{"key":"value"}],["currentUrl","/page"],["fetcherKey","a"],["formData",null],["formEncType","application/x-www-form-urlencoded"],["formMethod","post"],["href","/page"]]',
+ ]);
+
+ appFixture.close();
+});
diff --git a/tests/react-router-framework/integration/bug-report-test.ts b/tests/react-router-framework/integration/bug-report-test.ts
new file mode 100644
index 00000000..8be63c7f
--- /dev/null
+++ b/tests/react-router-framework/integration/bug-report-test.ts
@@ -0,0 +1,127 @@
+import { test, expect } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+////////////////////////////////////////////////////////////////////////////////
+// 👋 Hola! I'm here to help you write a great bug report pull request.
+//
+// You don't need to fix the bug, this is just to report one.
+//
+// The pull request you are submitting is supposed to fail when created, to let
+// the team see the erroneous behavior, and understand what's going wrong.
+//
+// If you happen to have a fix as well, it will have to be applied in a subsequent
+// commit to this pull request, and your now-succeeding test will have to be moved
+// to the appropriate file.
+//
+// First, make sure to install dependencies and build React Router. From the root of
+// the project, run this:
+//
+// ```
+// pnpm install && pnpm build
+// ```
+//
+// If you have never installed playwright on your system before, you may also need
+// to install a browser engine:
+//
+// ```
+// pnpm exec playwright install chromium
+// ```
+//
+// Now try running this test:
+//
+// ```
+// pnpm test:integration bug-report --project chromium
+// ```
+//
+// You can add `--watch` to the end to have it re-run on file changes:
+//
+// ```
+// pnpm test:integration bug-report --project chromium --watch
+// ```
+////////////////////////////////////////////////////////////////////////////////
+
+test.beforeEach(async ({ context }) => {
+ await context.route(/\.data$/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ route.continue();
+ });
+});
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ ////////////////////////////////////////////////////////////////////////////
+ // 💿 Next, add files to this object, just like files in a real app,
+ // `createFixture` will make an app and run your tests against it.
+ ////////////////////////////////////////////////////////////////////////////
+ files: {
+ "app/routes/_index.tsx": js`
+ import { useLoaderData, Link } from "react-router";
+
+ export function loader() {
+ return "pizza";
+ }
+
+ export default function Index() {
+ let data = useLoaderData();
+ return (
+
+ {data}
+ Other Route
+
+ )
+ }
+ `,
+
+ "app/routes/burgers.tsx": js`
+ export default function Index() {
+ return cheeseburger
;
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using playwright.
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+////////////////////////////////////////////////////////////////////////////////
+// 💿 Almost done, now write your failing test case(s) down here Make sure to
+// add a good description for what you expect React Router to do 👇🏽
+////////////////////////////////////////////////////////////////////////////////
+
+test("[description of what you expect it to do]", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ // You can test any request your app might get using `fixture`.
+ let response = await fixture.requestDocument("/");
+ expect(await response.text()).toMatch("pizza");
+
+ // If you need to test interactivity use the `app`
+ await app.goto("/");
+ await app.clickLink("/burgers");
+ await page.waitForSelector("text=cheeseburger");
+
+ // If you're not sure what's going on, you can "poke" the app, it'll
+ // automatically open up in your browser for 20 seconds, so be quick!
+ // await app.poke(20);
+
+ // Go check out the other tests to see what else you can do.
+});
+
+////////////////////////////////////////////////////////////////////////////////
+// 💿 Finally, push your changes to your fork of React Router
+// and open a pull request!
+////////////////////////////////////////////////////////////////////////////////
diff --git a/tests/react-router-framework/integration/catch-boundary-data-test.ts b/tests/react-router-framework/integration/catch-boundary-data-test.ts
new file mode 100644
index 00000000..7deba08e
--- /dev/null
+++ b/tests/react-router-framework/integration/catch-boundary-data-test.ts
@@ -0,0 +1,258 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { type TemplateName } from "./helpers/vite.js";
+
+const templateNames = [
+ "vite-7-template",
+ "rsc-vite-framework",
+] as const satisfies TemplateName[];
+
+let ROOT_BOUNDARY_TEXT = "ROOT_TEXT" as const;
+let LAYOUT_BOUNDARY_TEXT = "LAYOUT_BOUNDARY_TEXT" as const;
+let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT" as const;
+
+let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const;
+let NO_BOUNDARY_LOADER = "/no/loader" as const;
+
+let HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE =
+ "/yes.loader-layout-boundary" as const;
+let HAS_BOUNDARY_LAYOUT_NESTED_LOADER = "/yes/loader-layout-boundary" as const;
+
+let HAS_BOUNDARY_NESTED_LOADER_FILE = "/yes.loader-self-boundary" as const;
+let HAS_BOUNDARY_NESTED_LOADER = "/yes/loader-self-boundary" as const;
+
+let ROOT_DATA = "root data";
+let LAYOUT_DATA = "root data";
+
+test.describe("ErrorBoundary (thrown responses)", () => {
+ for (const templateName of templateNames) {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.describe(`template: ${templateName}`, () => {
+ test.beforeEach(async ({ context }) => {
+ await context.route(/.(data|rsc)/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ route.continue();
+ });
+ });
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ templateName,
+ files: {
+ "app/root.tsx": js`
+ import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ useLoaderData,
+ useMatches,
+ } from "react-router";
+
+ export const loader = () => "${ROOT_DATA}";
+
+ export default function Root() {
+ const loaderData = useLoaderData();
+
+ return (
+
+
+
+
+
+
+ {loaderData}
+
+
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ let matches = useMatches();
+ let { loaderData } = matches.find(match => match.id === "root");
+
+ return (
+
+
+
+ ${ROOT_BOUNDARY_TEXT}
+ {loaderData}
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return (
+
+ ${NO_BOUNDARY_LOADER}
+ ${HAS_BOUNDARY_LAYOUT_NESTED_LOADER}
+ ${HAS_BOUNDARY_NESTED_LOADER}
+
+ );
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`
+ export function loader() {
+ throw new Response("", { status: 401 });
+ }
+ export default function Index() {
+ return
;
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js`
+ import { useMatches } from "react-router";
+ export function loader() {
+ return "${LAYOUT_DATA}";
+ }
+ export default function Layout() {
+ return
;
+ }
+ export function ErrorBoundary() {
+ let matches = useMatches();
+ let { loaderData } = matches.find(match => match.id === "routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}");
+
+ return (
+
+
${LAYOUT_BOUNDARY_TEXT}
+
{loaderData}
+
+ );
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js`
+ export function loader() {
+ throw new Response("", { status: 401 });
+ }
+ export default function Index() {
+ return
;
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js`
+ import { Outlet, useLoaderData } from "react-router";
+ export function loader() {
+ return "${LAYOUT_DATA}";
+ }
+ export default function Layout() {
+ let loaderData = useLoaderData();
+ return (
+
+ );
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js`
+ export function loader() {
+ throw new Response("", { status: 401 });
+ }
+ export default function Index() {
+ return
;
+ }
+ export function ErrorBoundary() {
+ return (
+ ${OWN_BOUNDARY_TEXT}
+ );
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("renders root boundary with data available", async () => {
+ let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);
+ expect(res.status).toBe(401);
+ let html = await res.text();
+ expect(html).toMatch(ROOT_BOUNDARY_TEXT);
+ expect(html).toMatch(ROOT_DATA);
+ });
+
+ test("renders root boundary with data available on transition", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(NO_BOUNDARY_LOADER);
+ await page.waitForSelector("#root-boundary");
+ await page.waitForSelector(
+ `#root-boundary-data:has-text("${ROOT_DATA}")`,
+ );
+ });
+
+ test("renders layout boundary with data available", async () => {
+ let res = await fixture.requestDocument(
+ HAS_BOUNDARY_LAYOUT_NESTED_LOADER,
+ );
+ expect(res.status).toBe(401);
+ let html = await res.text();
+ expect(html).toMatch(ROOT_DATA);
+ expect(html).toMatch(LAYOUT_BOUNDARY_TEXT);
+ expect(html).toMatch(LAYOUT_DATA);
+ });
+
+ test("renders layout boundary with data available on transition", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER);
+ await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`);
+ await page.waitForSelector(
+ `#layout-boundary:has-text("${LAYOUT_BOUNDARY_TEXT}")`,
+ );
+ await page.waitForSelector(
+ `#layout-boundary-data:has-text("${LAYOUT_DATA}")`,
+ );
+ });
+
+ test("renders self boundary with layout data available", async () => {
+ let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER);
+ expect(res.status).toBe(401);
+ let html = await res.text();
+ expect(html).toMatch(ROOT_DATA);
+ expect(html).toMatch(LAYOUT_DATA);
+ expect(html).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("renders self boundary with layout data available on transition", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(HAS_BOUNDARY_NESTED_LOADER);
+ await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`);
+ await page.waitForSelector(`#layout-data:has-text("${LAYOUT_DATA}")`);
+ await page.waitForSelector(
+ `#own-boundary:has-text("${OWN_BOUNDARY_TEXT}")`,
+ );
+ });
+ });
+ }
+});
diff --git a/tests/react-router-framework/integration/catch-boundary-test.ts b/tests/react-router-framework/integration/catch-boundary-test.ts
new file mode 100644
index 00000000..c4f2a8fe
--- /dev/null
+++ b/tests/react-router-framework/integration/catch-boundary-test.ts
@@ -0,0 +1,379 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test.describe("ErrorBoundary (thrown responses)", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+ let originalConsoleError: typeof console.error;
+ let originalConsoleWarn: typeof console.warn;
+
+ let ROOT_BOUNDARY_TEXT = "ROOT_TEXT" as const;
+ let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT" as const;
+
+ let HAS_BOUNDARY_LOADER = "/yes/loader" as const;
+ let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const;
+ let HAS_BOUNDARY_ACTION = "/yes/action" as const;
+ let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const;
+ let NO_BOUNDARY_ACTION = "/no/action" as const;
+ let NO_BOUNDARY_ACTION_FILE = "/no.action" as const;
+ let NO_BOUNDARY_LOADER = "/no/loader" as const;
+ let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const;
+
+ let NOT_FOUND_HREF = "/not/found";
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts, useMatches } from "react-router";
+
+ export function loader() {
+ return { data: "ROOT LOADER" };
+ }
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ let matches = useMatches()
+ return (
+
+
+
+ ${ROOT_BOUNDARY_TEXT}
+ {JSON.stringify(matches)}
+
+
+
+ )
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link, Form } from "react-router";
+ export default function() {
+ return (
+
+ ${NOT_FOUND_HREF}
+
+
+
+
+ ${HAS_BOUNDARY_LOADER}
+
+
+ ${HAS_BOUNDARY_LOADER}/child
+
+
+ ${NO_BOUNDARY_LOADER}
+
+
+ )
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js`
+ import { Form } from "react-router";
+ export async function action() {
+ throw new Response("", { status: 401 })
+ }
+ export function ErrorBoundary() {
+ return ${OWN_BOUNDARY_TEXT}
+ }
+ export default function Index() {
+ return (
+
+ );
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js`
+ import { Form } from "react-router";
+ export function action() {
+ throw new Response("", { status: 401 })
+ }
+ export default function Index() {
+ return (
+
+ )
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js`
+ import { useRouteError } from "react-router";
+ export function loader() {
+ throw new Response("", { status: 401 })
+ }
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+ <>
+ ${OWN_BOUNDARY_TEXT}
+ {error.status}
+ >
+ );
+ }
+ export default function Index() {
+ return
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js`
+ export function loader() {
+ throw new Response("", { status: 404 })
+ }
+ export default function Index() {
+ return
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`
+ export function loader() {
+ throw new Response("", { status: 401 })
+ }
+ export default function Index() {
+ return
+ }
+ `,
+
+ "app/routes/action.tsx": js`
+ import { Outlet, useLoaderData } from "react-router";
+
+ export function loader() {
+ return "PARENT";
+ }
+
+ export default function () {
+ return (
+
+ )
+ }
+ `,
+
+ "app/routes/action.child-catch.tsx": js`
+ import { Form, useLoaderData, useRouteError } from "react-router";
+
+ export function loader() {
+ return "CHILD";
+ }
+
+ export function action() {
+ throw new Response("Caught!", { status: 400 });
+ }
+
+ export default function () {
+ return (
+ <>
+ {useLoaderData()}
+
+ >
+ )
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError()
+ return {error.status} {error.data}
;
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ originalConsoleError = console.error;
+ console.error = () => {};
+ originalConsoleWarn = console.warn;
+ console.warn = () => {};
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ console.error = originalConsoleError;
+ console.warn = originalConsoleWarn;
+ });
+
+ test("non-matching urls on document requests", async () => {
+ let oldConsoleError;
+ oldConsoleError = console.error;
+ console.error = () => {};
+
+ let res = await fixture.requestDocument(NOT_FOUND_HREF);
+ expect(res.status).toBe(404);
+ let html = await res.text();
+ expect(html).toMatch(ROOT_BOUNDARY_TEXT);
+
+ // There should be no loader data on the root route
+ let expected = JSON.stringify([
+ { id: "root", pathname: "", params: {} },
+ ]).replace(/"/g, """);
+ expect(html).toContain(`${expected} `);
+
+ console.error = oldConsoleError;
+ });
+
+ test("non-matching urls on client transitions", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(NOT_FOUND_HREF, { wait: false });
+ await page.waitForSelector("#root-boundary");
+
+ // Root loader data sticks around from previous load
+ let expected = JSON.stringify([
+ {
+ id: "root",
+ pathname: "",
+ params: {},
+ loaderData: { data: "ROOT LOADER" },
+ },
+ ]);
+ expect(await app.getHtml("#matches")).toContain(expected);
+ });
+
+ test("own boundary, action, document request", async () => {
+ let params = new URLSearchParams();
+ let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params);
+ expect(res.status).toBe(401);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, action, client transition from other route", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(HAS_BOUNDARY_ACTION);
+ await page.waitForSelector("#action-boundary");
+ });
+
+ test("own boundary, action, client transition from itself", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(HAS_BOUNDARY_ACTION);
+ await app.clickSubmitButton(HAS_BOUNDARY_ACTION);
+ await page.waitForSelector("#action-boundary");
+ });
+
+ test("bubbles to parent in action document requests", async () => {
+ let params = new URLSearchParams();
+ let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params);
+ expect(res.status).toBe(401);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in action script transitions from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(NO_BOUNDARY_ACTION);
+ await page.waitForSelector("#root-boundary");
+ });
+
+ test("bubbles to parent in action script transitions from self", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(NO_BOUNDARY_ACTION);
+ await app.clickSubmitButton(NO_BOUNDARY_ACTION);
+ await page.waitForSelector("#root-boundary");
+ });
+
+ test("own boundary, loader, document request", async () => {
+ let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER);
+ expect(res.status).toBe(401);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, loader, client transition", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(HAS_BOUNDARY_LOADER);
+ await page.waitForSelector("#boundary-loader");
+ });
+
+ test("bubbles to parent in loader document requests", async () => {
+ let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);
+ expect(res.status).toBe(401);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in loader transitions from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(NO_BOUNDARY_LOADER);
+ await page.waitForSelector("#root-boundary");
+ });
+
+ test("uses correct catch boundary on server action errors", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/action/child-catch`);
+ expect(await app.getHtml("#parent-data")).toMatch("PARENT");
+ expect(await app.getHtml("#child-data")).toMatch("CHILD");
+ await page.click("button[type=submit]");
+ await page.waitForSelector("#child-catch");
+ // Preserves parent loader data
+ expect(await app.getHtml("#parent-data")).toMatch("PARENT");
+ expect(await app.getHtml("#child-catch")).toMatch("400");
+ expect(await app.getHtml("#child-catch")).toMatch("Caught!");
+ });
+
+ test("prefers parent catch when child loader also bubbles, document request", async () => {
+ let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`);
+ expect(res.status).toBe(401);
+ let text = await res.text();
+ expect(text).toMatch(OWN_BOUNDARY_TEXT);
+ expect(text).toMatch('401 ');
+ });
+
+ test("prefers parent catch when child loader also bubbles, client transition", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`);
+ await page.waitForSelector("#boundary-loader");
+ expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT);
+ expect(await app.getHtml("#status")).toMatch("401");
+ });
+});
diff --git a/tests/react-router-framework/integration/cli-test.ts b/tests/react-router-framework/integration/cli-test.ts
new file mode 100644
index 00000000..70e73554
--- /dev/null
+++ b/tests/react-router-framework/integration/cli-test.ts
@@ -0,0 +1,201 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, rmSync } from "node:fs";
+import * as path from "node:path";
+
+import { expect, test } from "@playwright/test";
+import dedent from "dedent";
+import semver from "semver";
+
+import { createProject } from "./helpers/vite";
+
+const nodeBin = process.argv[0];
+const reactRouterBin = "node_modules/@react-router/dev/dist/cli/index.js";
+
+const run = (command: string[], options: Parameters[2]) =>
+ spawnSync(nodeBin, [reactRouterBin, ...command], options);
+
+const helpText = dedent`
+ react-router
+
+ Usage:
+ $ react-router build [projectDir]
+ $ react-router dev [projectDir]
+ $ react-router routes [projectDir]
+
+ Options:
+ --help, -h Print this help message and exit
+ --version, -v Print the CLI version and exit
+ --no-color Disable ANSI colors in console output
+ \`build\` Options:
+ --assetsInlineLimit Static asset base64 inline threshold in bytes (default: 4096) (number)
+ --clearScreen Allow/disable clear screen when logging (boolean)
+ --config, -c Use specified config file (string)
+ --emptyOutDir Force empty outDir when it's outside of root (boolean)
+ --logLevel, -l Info | warn | error | silent (string)
+ --minify Enable/disable minification, or specify minifier to use (default: "esbuild") (boolean | "terser" | "esbuild")
+ --mode, -m Set env mode (string)
+ --profile Start built-in Node.js inspector
+ --sourcemapClient Output source maps for client build (default: false) (boolean | "inline" | "hidden")
+ --sourcemapServer Output source maps for server build (default: false) (boolean | "inline" | "hidden")
+ \`dev\` Options:
+ --clearScreen Allow/disable clear screen when logging (boolean)
+ --config, -c Use specified config file (string)
+ --cors Enable CORS (boolean)
+ --force Force the optimizer to ignore the cache and re-bundle (boolean)
+ --host Specify hostname (string)
+ --logLevel, -l Info | warn | error | silent (string)
+ --mode, -m Set env mode (string)
+ --open Open browser on startup (boolean | string)
+ --port Specify port (number)
+ --profile Start built-in Node.js inspector
+ --strictPort Exit if specified port is already in use (boolean)
+ \`routes\` Options:
+ --config, -c Use specified Vite config file (string)
+ --json Print the routes as JSON
+ \`reveal\` Options:
+ --config, -c Use specified Vite config file (string)
+ --no-typescript Generate plain JavaScript files
+ \`typegen\` Options:
+ --watch Automatically regenerate types whenever route config (\`routes.ts\`) or route modules change
+
+ Build your project:
+
+ $ react-router build
+
+ Run your project locally in development:
+
+ $ react-router dev
+
+ Show all routes in your app:
+
+ $ react-router routes
+ $ react-router routes my-app
+ $ react-router routes --json
+ $ react-router routes --config vite.react-router.config.ts
+
+ Reveal the used entry point:
+
+ $ react-router reveal entry.client
+ $ react-router reveal entry.server
+ $ react-router reveal entry.client --no-typescript
+ $ react-router reveal entry.server --no-typescript
+ $ react-router reveal entry.server --config vite.react-router.config.ts
+
+ Generate types for route modules:
+
+ $ react-router typegen
+ $ react-router typegen --watch
+`;
+
+test.describe("cli", () => {
+ test("--help", async () => {
+ const cwd = await createProject();
+ const { stdout, stderr, status } = run(["--help"], {
+ cwd,
+ env: {
+ NO_COLOR: "1",
+ },
+ });
+ expect(stdout.toString().trim()).toBe(helpText);
+ expect(stderr.toString()).toBe("");
+ expect(status).toBe(0);
+ });
+
+ test("--version", async () => {
+ const cwd = await createProject();
+ let { stdout, stderr, status } = run(["--version"], { cwd });
+ expect(semver.valid(stdout.toString().trim())).not.toBeNull();
+ expect(stderr.toString()).toBe("");
+ expect(status).toBe(0);
+ });
+
+ test("routes", async () => {
+ const cwd = await createProject();
+ let { stdout, stderr, status } = run(["routes"], { cwd });
+
+ // Filter out future flag warnings for the format:
+ // ⚠️ Future Flag Warning: [Something] is changing in React Router v8.
+ // You can use the `future.v8_[whatever]` flag to opt in early.
+ // -> https://reactrouter.com/upgrading/future-flags#v8_[whatever]
+ let filteredStdOut = stdout.toString().split("\n");
+ while (filteredStdOut[0]?.includes("Future Flag Warning:")) {
+ filteredStdOut.splice(0, 3);
+ }
+
+ expect(filteredStdOut.join("\n").trim()).toBe(dedent`
+
+
+
+
+
+ `);
+ expect(stderr.toString()).toBe("");
+ expect(status).toBe(0);
+ });
+
+ test.describe("reveal", async () => {
+ test("generates entry.{server,client}.tsx in the app directory", async () => {
+ const cwd = await createProject();
+ let entryClientFile = path.join(cwd, "app", "entry.client.tsx");
+ let entryServerFile = path.join(cwd, "app", "entry.server.tsx");
+
+ expect(existsSync(entryServerFile)).toBeFalsy();
+ expect(existsSync(entryClientFile)).toBeFalsy();
+
+ run(["reveal"], { cwd });
+
+ expect(existsSync(entryServerFile)).toBeTruthy();
+ expect(existsSync(entryClientFile)).toBeTruthy();
+ });
+
+ test("rsc generates entry.{ssr,rsc,client}.tsx in the app directory", async () => {
+ const cwd = await createProject({}, "rsc-vite-framework");
+ let entrySSRFile = path.join(cwd, "app", "entry.ssr.tsx");
+ let entryRSCFile = path.join(cwd, "app", "entry.rsc.tsx");
+ let entryClientFile = path.join(cwd, "app", "entry.client.tsx");
+
+ expect(existsSync(entrySSRFile)).toBeFalsy();
+ expect(existsSync(entryRSCFile)).toBeFalsy();
+ expect(existsSync(entryClientFile)).toBeFalsy();
+
+ run(["reveal"], { cwd });
+
+ expect(existsSync(entrySSRFile)).toBeTruthy();
+ expect(existsSync(entryRSCFile)).toBeTruthy();
+ expect(existsSync(entryClientFile)).toBeTruthy();
+ });
+
+ test("generates specified entries in the app directory", async () => {
+ const cwd = await createProject();
+
+ let entryClientFile = path.join(cwd, "app", "entry.client.tsx");
+ let entryServerFile = path.join(cwd, "app", "entry.server.tsx");
+
+ expect(existsSync(entryServerFile)).toBeFalsy();
+ expect(existsSync(entryClientFile)).toBeFalsy();
+
+ run(["reveal", "entry.server"], { cwd });
+ expect(existsSync(entryServerFile)).toBeTruthy();
+ expect(existsSync(entryClientFile)).toBeFalsy();
+ rmSync(entryServerFile);
+
+ run(["reveal", "entry.client"], { cwd });
+ expect(existsSync(entryClientFile)).toBeTruthy();
+ expect(existsSync(entryServerFile)).toBeFalsy();
+ });
+
+ test("generates entry.{server,client}.jsx in the app directory with --no-typescript", async () => {
+ const cwd = await createProject();
+ let entryClientFile = path.join(cwd, "app", "entry.client.jsx");
+ let entryServerFile = path.join(cwd, "app", "entry.server.jsx");
+
+ expect(existsSync(entryServerFile)).toBeFalsy();
+ expect(existsSync(entryClientFile)).toBeFalsy();
+
+ run(["reveal", "--no-typescript"], { cwd });
+
+ expect(existsSync(entryServerFile)).toBeTruthy();
+ expect(existsSync(entryClientFile)).toBeTruthy();
+ });
+ });
+});
diff --git a/tests/react-router-framework/integration/client-data-test.ts b/tests/react-router-framework/integration/client-data-test.ts
new file mode 100644
index 00000000..aa9b3777
--- /dev/null
+++ b/tests/react-router-framework/integration/client-data-test.ts
@@ -0,0 +1,1772 @@
+import { test, expect } from "@playwright/test";
+
+import { UNSAFE_ServerMode as ServerMode } from "react-router";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { type TemplateName, reactRouterConfig } from "./helpers/vite.js";
+
+const templateNames = [
+ "vite-7-template",
+ "rsc-vite-framework",
+] as const satisfies TemplateName[];
+
+test.describe("Client Data", () => {
+ for (const templateName of templateNames) {
+ function getFiles(
+ routeBaseFileName: string,
+ {
+ parentClientLoader,
+ parentClientLoaderHydrate,
+ parentAdditions,
+ childClientLoader,
+ childClientLoaderHydrate,
+ childAdditions,
+ }: {
+ parentClientLoader: boolean;
+ parentClientLoaderHydrate: boolean;
+ parentAdditions?: string;
+ childClientLoader: boolean;
+ childClientLoaderHydrate: boolean;
+ childAdditions?: string;
+ },
+ ) {
+ return {
+ [`app/routes/${routeBaseFileName}.parent.tsx`]: js`
+ import { Outlet, useLoaderData } from "react-router"
+ export function loader() {
+ return { message: 'Parent Server Loader' };
+ }
+ ${
+ parentClientLoader
+ ? js`
+ export async function clientLoader({ serverLoader }) {
+ // Need a small delay to ensure we capture the server-rendered
+ // fallbacks for assertions
+ await new Promise(r => setTimeout(r, 100))
+ let data = await serverLoader();
+ return { message: data.message + " (mutated by client)" };
+ }
+ `
+ : ""
+ }
+ ${
+ parentClientLoaderHydrate
+ ? js`
+ clientLoader.hydrate = true;
+ export function HydrateFallback() {
+ return Parent Fallback
+ }
+ `
+ : ""
+ }
+ ${parentAdditions || ""}
+ export default function Component() {
+ let data = useLoaderData();
+ return (
+ <>
+ {data.message}
+
+ >
+ );
+ }
+ `,
+ [`app/routes/${routeBaseFileName}.parent.child.tsx`]: js`
+ import { Form, Outlet, useActionData, useLoaderData } from "react-router"
+ export function loader() {
+ return { message: 'Child Server Loader' };
+ }
+ export function action() {
+ return { message: 'Child Server Action' };
+ }
+ ${
+ childClientLoader
+ ? js`
+ export async function clientLoader({ serverLoader }) {
+ // Need a small delay to ensure we capture the server-rendered
+ // fallbacks for assertions
+ await new Promise(r => setTimeout(r, 100))
+ let data = await serverLoader();
+ return { message: data.message + " (mutated by client)" };
+ }
+ `
+ : ""
+ }
+ ${
+ childClientLoaderHydrate
+ ? js`
+ clientLoader.hydrate = true;
+ export function HydrateFallback() {
+ return Child Fallback
+ }
+ `
+ : ""
+ }
+ ${childAdditions || ""}
+ export default function Component() {
+ let data = useLoaderData();
+ let actionData = useActionData();
+ return (
+ <>
+ {data.message}
+
+ >
+ );
+ }
+ `,
+ };
+ }
+
+ test.describe(`template: ${templateName}`, () => {
+ for (const splitRouteModules of [true, false]) {
+ test.describe(`splitRouteModules: ${splitRouteModules}`, () => {
+ test.skip(
+ templateName.includes("rsc") && splitRouteModules,
+ "RSC Framework Mode doesn't support splitRouteModules",
+ );
+
+ test.skip(
+ ({ browserName }) =>
+ Boolean(process.env.CI) &&
+ splitRouteModules &&
+ (browserName === "webkit" || process.platform === "win32"),
+ "Webkit/Windows tests only run on a single worker in CI and splitRouteModules is not OS/browser-specific",
+ );
+
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ appFixture = await createAppFixture(
+ await createFixture(
+ {
+ templateName,
+ files: {
+ "react-router.config.ts": reactRouterConfig({
+ splitRouteModules,
+ }),
+ "app/root.tsx": js`
+ import { Form, Outlet, Scripts } from "react-router"
+
+ export const middleware = [
+ async ({ request }, next) => {
+ let response = await next();
+
+ if (
+ request.method === "GET" &&
+ response instanceof Response &&
+ response.status === 200 &&
+ request.headers.get("sec-purpose") === "prefetch" &&
+ !response.headers.has("Cache-Control")
+ ) {
+ let cachedResponse = new Response(response.body, response);
+ cachedResponse.headers.set("Cache-Control", "max-age=5");
+ return cachedResponse;
+ }
+ return response;
+ }
+ ];
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router"
+ export default function Component() {
+ return (
+
+ /client-loader-lazy/no-client-loaders-or-fallbacks/parent/child
+ /client-loader-lazy/parent-client-loader/parent/child
+ /client-loader-lazy/child-client-loader/parent/child
+ /client-loader-lazy/parent-client-loader-child-client-loader/parent/child
+ /client-loader-lazy/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child
+ /client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent
+ /client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent/child
+ /client-action-lazy/child-client-action/parent/child
+ /client-action-lazy/child-client-action-parent-child-loader/parent/child
+ /client-action-lazy/child-client-action-child-client-loader/parent/child
+ /client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child
+ /client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child
+
+ );
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.no-client-loaders-or-fallbacks",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.parent-client-loader-child-client-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.parent-client-loader-hydrate-child-client-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: true,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.parent-client-loader-child-client-loader-hydrate",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: true,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.parent-client-loader-child-client-loader-hydrate-both",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: true,
+ childClientLoader: true,
+ childClientLoaderHydrate: true,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.handles-synchronous-client-loaders",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ parentAdditions: js`
+ export function clientLoader() {
+ return { message: "Parent Client Loader" };
+ }
+ clientLoader.hydrate=true
+ export function HydrateFallback() {
+ return Parent Fallback
+ }
+ `,
+ childAdditions: js`
+ export function clientLoader() {
+ return { message: "Child Client Loader" };
+ }
+ clientLoader.hydrate=true
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.handles-deferred-data-through-client-loaders",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.handles-deferred-data-through-client-loaders.parent.child.tsx": js`
+ import * as React from 'react';
+ import { Await, useLoaderData } from "react-router"
+ export function loader() {
+ return {
+ message: 'Child Server Loader',
+ lazy: new Promise(r => setTimeout(() => r("Child Deferred Data"), 1000)),
+ };
+ }
+ export async function clientLoader({ serverLoader }) {
+ let data = await serverLoader();
+ return {
+ ...data,
+ message: data.message + " (mutated by client)",
+ };
+ }
+ clientLoader.hydrate = true;
+ export function HydrateFallback() {
+ return Child Fallback
+ }
+ export default function Component() {
+ let data = useLoaderData();
+ return (
+ <>
+ {data.message}
+ Loading Deferred Data... }>
+
+ {(value) => {value}
}
+
+
+ >
+ );
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.allows-hydration-without-rendering-a-fallback",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientLoader() {
+ await new Promise(r => setTimeout(r, 100));
+ return { message: "Child Client Loader" };
+ }
+ clientLoader.hydrate=true
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-critical.hydrate-fallback-not-rendered-if-not-set-with-server-loader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.hydrate-fallback-not-rendered-if-not-set-with-server-loader.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData } from "react-router";
+ export function loader() {
+ return { message: "Child Server Loader Data" };
+ }
+ export async function clientLoader({ serverLoader }) {
+ await new Promise(r => setTimeout(r, 100));
+ return { message: "Child Client Loader Data" };
+ }
+ export function HydrateFallback() {
+ return SHOULD NOT SEE ME
+ }
+ export default function Component() {
+ let data = useLoaderData();
+ return {data.message}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData } from "react-router";
+ // Even without setting hydrate=true, this should run on hydration
+ export async function clientLoader({ serverLoader }) {
+ await new Promise(r => setTimeout(r, 100));
+ return {
+ message: "Loader Data (clientLoader only)",
+ };
+ }
+ export function HydrateFallback() {
+ return Child Fallback
+ }
+ export default function Component() {
+ let data = useLoaderData();
+ return {data.message}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData } from "react-router";
+ // Even without setting hydrate=true, this should run on hydration
+ export async function clientLoader({ serverLoader }) {
+ await new Promise(r => setTimeout(r, 100));
+ return {
+ message: "Loader Data (clientLoader only)",
+ };
+ }
+ export default function Component() {
+ let data = useLoaderData();
+ return {data.message}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData, useRouteError } from "react-router";
+ export async function clientLoader({ serverLoader }) {
+ return await serverLoader();
+ }
+ export default function Component() {
+ return Child
;
+ }
+ export function HydrateFallback() {
+ return Loading...
;
+ }
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return {error.status} {error.data}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.initial-hydration-data-check-functions-properly",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.initial-hydration-data-check-functions-properly.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData, useRevalidator } from "react-router";
+ let isFirstCall = true;
+ export async function loader({ serverLoader }) {
+ if (isFirstCall) {
+ isFirstCall = false
+ return { message: "Child Server Loader Data (1)" };
+ }
+ return { message: "Child Server Loader Data (2+)" };
+ }
+ export async function clientLoader({ serverLoader }) {
+ await new Promise(r => setTimeout(r, 100));
+ let serverData = await serverLoader();
+ return {
+ message: serverData.message + " (mutated by client)",
+ };
+ }
+ clientLoader.hydrate=true;
+ export default function Component() {
+ let data = useLoaderData();
+ let revalidator = useRevalidator();
+ return (
+ <>
+ {data.message}
+ revalidator.revalidate()}>Revalidate
+ >
+ );
+ }
+ export function HydrateFallback() {
+ return Loading...
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData, useRevalidator } from "react-router";
+ let isFirstCall = true;
+ export async function loader({ serverLoader }) {
+ if (isFirstCall) {
+ isFirstCall = false
+ return { message: "Child Server Loader Data (1)" };
+ }
+ return { message: "Child Server Loader Data (2+)" };
+ }
+ let isFirstClientCall = true;
+ export async function clientLoader({ serverLoader }) {
+ await new Promise(r => setTimeout(r, 100));
+ if (isFirstClientCall) {
+ isFirstClientCall = false;
+ // First time through - don't even call serverLoader
+ return {
+ message: "Child Client Loader Data",
+ };
+ }
+ // Only call the serverLoader on subsequent calls and this
+ // should *not* return us the initialData any longer
+ let serverData = await serverLoader();
+ return {
+ message: serverData.message + " (mutated by client)",
+ };
+ }
+ clientLoader.hydrate=true;
+ export default function Component() {
+ let data = useLoaderData();
+ let revalidator = useRevalidator();
+ return (
+ <>
+ {data.message}
+ revalidator.revalidate()}>Revalidate
+ >
+ );
+ }
+ export function HydrateFallback() {
+ return Loading...
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.server-loader-errors-are-re-thrown-from-serverloader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.server-loader-errors-are-re-thrown-from-serverloader.parent.child.tsx": js`
+ import { useRouteError } from "react-router";
+
+ export function loader() {
+ throw new Error("Broken!")
+ }
+
+ export async function clientLoader({ serverLoader }) {
+ return await serverLoader();
+ }
+ clientLoader.hydrate = true;
+
+ export default function Index() {
+ return Should not see me ;
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return {error.message}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent.tsx": js`
+ import { Outlet, useLoaderData, useRouteLoaderData, useRouteError } from 'react-router'
+ export function loader() {
+ return { message: 'Parent Server Loader' };
+ }
+ export async function clientLoader({ serverLoader }) {
+ console.log('running parent client loader')
+ // Need a small delay to ensure we capture the server-rendered
+ // fallbacks for assertions
+ await new Promise(r => setTimeout(r, 100));
+ let data = await serverLoader();
+ return { message: data.message + " (mutated by client)" };
+ }
+ clientLoader.hydrate = true;
+ export default function Component() {
+ let data = useLoaderData();
+ return (
+ <>
+ {data.message}
+
+ >
+ );
+ }
+ export function ErrorBoundary() {
+ let data = useRouteLoaderData("routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent")
+ let error = useRouteError();
+ return (
+ <>
+ Parent Error
+ {data?.message}
+ {error?.message}
+ >
+ );
+ }
+ `,
+ "app/routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent.child.tsx": js`
+ import { useLoaderData } from 'react-router'
+ export function loader() {
+ throw new Error('Child Server Error');
+ }
+ export function clientLoader() {
+ console.log('running child client loader')
+ return "Should not see me";
+ }
+ clientLoader.hydrate = true;
+ export default function Component() {
+ let data = useLoaderData()
+ return (
+ <>
+ Should not see me
+ {data}
;
+ >
+ );
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-lazy.no-client-loaders-or-fallbacks",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles("client-loader-lazy.parent-client-loader", {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ }),
+
+ ...getFiles("client-loader-lazy.child-client-loader", {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ }),
+
+ ...getFiles(
+ "client-loader-lazy.parent-client-loader-child-client-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles(
+ "client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child.tsx": js`
+ import * as React from 'react';
+ import { useLoaderData, useRouteError } from "react-router";
+ export async function clientLoader({ serverLoader }) {
+ return await serverLoader();
+ }
+ export default function Component() {
+ return Child
;
+ }
+ export function HydrateFallback() {
+ return Loading...
;
+ }
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return {error.status} {error.data}
;
+ }
+ `,
+
+ ...getFiles(
+ "client-loader-lazy.does-not-prefetch-server-loader-if-a-client-loader-is-present",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+
+ ...getFiles("client-action-critical.child-client-action", {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ }),
+
+ ...getFiles(
+ "client-action-critical.child-client-action-parent-child-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-critical.child-client-action-child-client-loader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-critical.child-client-action-parent-child-loader-child-client-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child.tsx": js`
+ import * as React from 'react';
+ import { Form, useRouteError } from "react-router";
+ export async function clientAction({ serverAction }) {
+ return await serverAction();
+ }
+ export default function Component() {
+ return (
+
+ );
+ }
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return {error.status} {error.data}
;
+ }
+ `,
+
+ ...getFiles("client-action-lazy.child-client-action", {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ }),
+
+ ...getFiles(
+ "client-action-lazy.child-client-action-parent-child-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-lazy.child-client-action-child-client-loader",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-lazy.child-client-action-parent-child-loader-child-client-loader",
+ {
+ parentClientLoader: true,
+ parentClientLoaderHydrate: false,
+ childClientLoader: true,
+ childClientLoaderHydrate: false,
+ childAdditions: js`
+ export async function clientAction({ serverAction }) {
+ let data = await serverAction();
+ return {
+ message: data.message + " (mutated by client)"
+ }
+ }
+ `,
+ },
+ ),
+
+ ...getFiles(
+ "client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action",
+ {
+ parentClientLoader: false,
+ parentClientLoaderHydrate: false,
+ childClientLoader: false,
+ childClientLoaderHydrate: false,
+ },
+ ),
+ "app/routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child.tsx": js`
+ import * as React from 'react';
+ import { Form, useRouteError } from "react-router";
+ export async function clientAction({ serverAction }) {
+ return await serverAction();
+ }
+ export default function Component() {
+ return (
+
+ );
+ }
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return {error.status} {error.data}
;
+ }
+ `,
+
+ "app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.tsx": js`
+ import { Outlet } from 'react-router'
+
+ let count = 1;
+ export function loader() {
+ return count++;
+ }
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ {loaderData}
+
+ >
+ );
+ }
+ `,
+
+ "app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.tsx": js`
+ import { Outlet } from 'react-router'
+ let count = 1;
+ export function loader() {
+ return count++;
+ }
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ {loaderData}
+
+ >
+ );
+ }
+ export function shouldRevalidate() {
+ return false;
+ }
+ `,
+ "app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.a.tsx": js`
+ import { redirect } from 'react-router'
+ export function clientLoader() {
+ return redirect('/client-loader-critical/hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server/parent/b');
+ }
+ clientLoader.hydrate = true;
+ export default function Component({ loaderData }) {
+ return Should not see me
;
+ }
+ `,
+ "app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.b.tsx": js`
+ export default function Component({ loaderData }) {
+ return Hi!
;
+ }
+ `,
+
+ "app/routes/client-loader-critical.aborted-hydration-fetches-fresh-data.tsx": js`
+ import { Link } from "react-router";
+
+ export function loader({ request }) {
+ return { query: new URL(request.url).searchParams.get("q") || "empty" };
+ }
+
+ export async function clientLoader({ serverLoader, request }) {
+ let q = new URL(request.url).searchParams.get("q") || "empty";
+
+ // Delay the initial invocation
+ if (q === "initial") {
+ if (!window.__hydrationBlock) {
+ let { promise, resolve } = Promise.withResolvers();
+ window.__resolveHydrationBlock = resolve
+ window.__hydrationBlock = promise;
+ await window.__hydrationBlock;
+ }
+ }
+
+ let serverData = await serverLoader();
+ return {
+ ...serverData,
+ clientLoaderRan: true,
+ clientLoaderQuery: q,
+ };
+ }
+
+ clientLoader.hydrate = true;
+
+ export default function Component({ loaderData }) {
+ return (
+
+
{loaderData.query}
+
{String(loaderData.clientLoaderQuery ?? "none")}
+
+ Update query
+
+
+ );
+ }
+ `,
+ },
+ },
+ ServerMode.Development, // Avoid error sanitization
+ ),
+ ServerMode.Development, // Avoid error sanitization
+ );
+ });
+
+ test.afterAll(() => {
+ appFixture?.close();
+ });
+
+ test.describe("clientLoader - critical route module", () => {
+ test("no client loaders or fallbacks", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Full SSR - normal loader behavior due to lack of clientLoader
+ await app.goto(
+ "/client-loader-critical/no-client-loaders-or-fallbacks/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ });
+
+ test("parent.clientLoader/child.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Full SSR - normal loader behavior due to lack of HydrateFallback components
+ await app.goto(
+ "/client-loader-critical/parent-client-loader-child-client-loader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ });
+
+ test("parent.clientLoader.hydrate/child.clientLoader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/parent-client-loader-hydrate-child-client-loader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Fallback");
+ expect(html).not.toMatch("Parent Server Loader");
+ expect(html).not.toMatch("Child Server Loader");
+
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).not.toMatch("Parent Fallback");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader");
+ });
+
+ test("parent.clientLoader/child.clientLoader.hydrate", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/parent-client-loader-child-client-loader-hydrate/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Fallback");
+ expect(html).not.toMatch("Child Server Loader");
+
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).not.toMatch("Child Fallback");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ });
+
+ test("parent.clientLoader.hydrate/child.clientLoader.hydrate", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/parent-client-loader-child-client-loader-hydrate-both/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Fallback");
+ expect(html).not.toMatch("Parent Server Loader");
+ expect(html).not.toMatch("Child Fallback");
+ expect(html).not.toMatch("Child Server Loader");
+
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).not.toMatch("Parent Fallback");
+ expect(html).not.toMatch("Child Fallback");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ });
+
+ test("handles synchronous client loaders", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Ensure we SSR the fallbacks
+ let response = await app.goto(
+ "/client-loader-critical/handles-synchronous-client-loaders/parent/child",
+ );
+ let html = await response?.text();
+ expect(html).toMatch("Parent Fallback");
+
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Client Loader");
+ expect(html).toMatch("Child Client Loader");
+ });
+
+ test("handles deferred data through client loaders", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Ensure initial document request contains the child fallback _and_ the
+ // subsequent streamed/resolved deferred data
+ let response = await app.goto(
+ "/client-loader-critical/handles-deferred-data-through-client-loaders/parent/child",
+ );
+ let html = await response?.text();
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Fallback");
+ expect(html).toMatch("Child Deferred Data");
+
+ await page.waitForSelector("#child-deferred-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ // app.goto() doesn't resolve until the document finishes loading so by
+ // then the HTML has updated via the streamed suspense updates
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ expect(html).toMatch("Child Deferred Data");
+ });
+
+ test("allows hydration execution without rendering a fallback", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-loader-critical/allows-hydration-without-rendering-a-fallback/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Child Server Loader");
+ await page.waitForSelector(':has-text("Child Client Loader")');
+ html = await app.getHtml("main");
+ expect(html).toMatch("Child Client Loader");
+ });
+
+ test("HydrateFallback is not rendered if clientLoader.hydrate is not set (w/server loader)", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Ensure initial document request contains the child fallback _and_ the
+ // subsequent streamed/resolved deferred data
+ let response = await app.goto(
+ "/client-loader-critical/hydrate-fallback-not-rendered-if-not-set-with-server-loader/parent/child",
+ );
+ let html = await response?.text();
+ expect(html).toMatch("Child Server Loader Data");
+ expect(html).not.toMatch("SHOULD NOT SEE ME");
+
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Child Server Loader Data");
+ });
+
+ test("clientLoader.hydrate is automatically implied when no server loader exists (w HydrateFallback)", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Child Fallback");
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Loader Data (clientLoader only)");
+ });
+
+ test("clientLoader.hydrate is automatically implied when no server loader exists (w/o HydrateFallback)", async ({
+ page,
+ }) => {
+ test.skip(
+ templateName.includes("rsc"),
+ "RSC Framework Mode doesn't need to provide a default root HydrateFallback since it doesn't need to ensure is rendered, and you already get a console warning",
+ );
+
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback/parent/child",
+ );
+ let html = await app.getHtml();
+ // Production builds strip dev-only warning logs, but we should
+ // still render the default root loading shell until hydration runs.
+ expect(html).toMatch("Loading... ");
+ expect(html).not.toMatch("child-data");
+ await page.waitForSelector("#child-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Loader Data (clientLoader only)");
+ });
+
+ test("throws a 400 if you call serverLoader without a server loader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child",
+ );
+ await page.waitForSelector("#child-error");
+ let html = await app.getHtml("#child-error");
+ expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
+ "400 Error: You are trying to call serverLoader() on a route that does " +
+ 'not have a server loader (routeId: "routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")',
+ );
+ });
+
+ test("initial hydration data check functions properly", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/initial-hydration-data-check-functions-properly/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml();
+ expect(html).toMatch(
+ "Child Server Loader Data (1) (mutated by client)",
+ );
+ app.clickElement("button");
+ await page.waitForSelector(
+ ':has-text("Child Server Loader Data (2+)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch(
+ "Child Server Loader Data (2+) (mutated by client)",
+ );
+ });
+
+ test("initial hydration data check functions properly even if serverLoader isn't called on hydration", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml();
+ expect(html).toMatch("Child Client Loader Data");
+ app.clickElement("button");
+ await page.waitForSelector(
+ ':has-text("Child Server Loader Data (2+)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch(
+ "Child Server Loader Data (2+) (mutated by client)",
+ );
+ });
+
+ test("server loader errors are re-thrown from serverLoader()", async ({
+ page,
+ }) => {
+ let _consoleError = console.error;
+ console.error = () => {};
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/server-loader-errors-are-re-thrown-from-serverloader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Broken!");
+ // Ensure we hydrate and remain on the boundary
+ await new Promise((r) => setTimeout(r, 100));
+ html = await app.getHtml("main");
+ expect(html).toMatch("Broken!");
+ expect(html).not.toMatch("Should not see me");
+ console.error = _consoleError;
+ });
+
+ test("bubbled server loader errors are persisted for hydrating routes", async ({
+ page,
+ }) => {
+ // test.skip(browserName === "firefox", "this test fails there due to extra debug logs.")
+ let _consoleError = console.error;
+ console.error = () => {};
+ let app = new PlaywrightFixture(appFixture, page);
+ let logs: string[] = [];
+ page.on("console", (msg) => {
+ let text = msg.text();
+ // Firefox surfaces React performance track labels on the console
+ // during hydration, so only capture the application log this
+ // assertion actually cares about.
+ if (text === "running parent client loader") {
+ logs.push(text);
+ }
+ });
+ await app.goto(
+ "/client-loader-critical/bubbled-server-loader-errors-are-persisted-for-hydrating-routes/parent/child",
+ false,
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Error");
+ expect(html).not.toMatch("Should not see me");
+ // Ensure we hydrate and remain on the boundary
+ await page.waitForSelector(
+ ":has-text('Parent Server Loader (mutated by client)')",
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch(
+ "Parent Server Loader (mutated by client)",
+ );
+ expect(html).toMatch("Child Server Error");
+ expect(html).not.toMatch("Should not see me");
+ expect(logs).toEqual(["running parent client loader"]);
+ console.error = _consoleError;
+ });
+
+ test("hydrating clientLoader redirects trigger new data requests to the server", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server/parent/a",
+ );
+ await page.waitForSelector("#b");
+ // 1st route parent re-runs
+ await expect(page.locator("#parent-1-data")).toHaveText("2");
+ // But 2nd parent opted out of revalidation
+ await expect(page.locator("#parent-2-data")).toHaveText("1");
+ await expect(page.locator("#b")).toHaveText("Hi!");
+ });
+
+ // When a same-route navigation aborts the pending hydration
+ // POP, serverLoader() must fetch fresh data — not return the
+ // stale SSR initialData captured for the original URL.
+ test("serverLoader() fetches fresh data when a same-route navigation aborts hydration", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto(
+ "/client-loader-critical/aborted-hydration-fetches-fresh-data?q=initial",
+ );
+
+ // SSR shows the server loader's data; clientLoader hasn't completed yet
+ await expect(page.locator("[data-server-query]")).toHaveText(
+ "initial",
+ );
+ await expect(
+ page.locator("[data-client-loader-query]"),
+ ).toHaveText("none");
+
+ // Click before hydration completes to abort the hydration clientLoader call before it calls serverLoader
+ await app.clickLink(
+ "/client-loader-critical/aborted-hydration-fetches-fresh-data?q=updated",
+ { wait: false },
+ );
+
+ await page.waitForURL(/q=updated/);
+
+ // PUSH ran the clientLoader as call #2 and saw the new URL and the serverLoader
+ // invocation doesn't return hydrationData
+ await expect(page.locator("[data-server-query]")).toHaveText(
+ "updated",
+ );
+ await expect(
+ page.locator("[data-client-loader-query]"),
+ ).toHaveText("updated");
+
+ // Release the still-pending hydration call so it can unwind.
+ await page.evaluate(() =>
+ (window as any).__resolveHydrationBlock(),
+ );
+
+ await expect(page.locator("[data-server-query]")).toHaveText(
+ "updated",
+ );
+ await expect(
+ page.locator("[data-client-loader-query]"),
+ ).toHaveText("updated");
+ });
+ });
+
+ test.describe("clientLoader - lazy route module", () => {
+ test("no client loaders or fallbacks", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-loader-lazy/no-client-loaders-or-fallbacks/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+
+ // Normal Remix behavior due to lack of clientLoader
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ });
+
+ test("parent.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-loader-lazy/parent-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader");
+ });
+
+ test("child.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-loader-lazy/child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ });
+
+ test("parent.clientLoader/child.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-loader-lazy/parent-client-loader-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader (mutated by client");
+ });
+
+ test("throws a 400 if you call serverLoader without a server loader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-loader-lazy/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child",
+ );
+ await page.waitForSelector("#child-error");
+ let html = await app.getHtml("#child-error");
+ expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
+ "400 Error: You are trying to call serverLoader() on a route that does " +
+ 'not have a server loader (routeId: "routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child")',
+ );
+ });
+
+ test("does not prefetch server loader if a client loader is present", async ({
+ page,
+ browserName,
+ }) => {
+ test.skip(
+ templateName.includes("rsc"),
+ "This test is specific to non-RSC Framework Mode",
+ );
+
+ let dataUrls: string[] = [];
+ page.on("request", (request) => {
+ let url = request.url();
+ if (url.includes(".data") || url.includes(".rsc")) {
+ dataUrls.push(url);
+ }
+ });
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+
+ if (browserName === "webkit") {
+ // No prefetch support :/
+ expect(dataUrls).toEqual([]);
+ } else {
+ // Only prefetch child server loader since parent has a `clientLoader`
+ expect(dataUrls).toEqual([
+ expect.stringMatching(
+ /client-loader-lazy\/does-not-prefetch-server-loader-if-a-client-loader-is-present\/parent\/child\.data\?_routes=routes%2Fclient-loader-lazy\.does-not-prefetch-server-loader-if-a-client-loader-is-present\.parent\.child/,
+ ),
+ ]);
+ }
+ });
+ });
+
+ test.describe("clientAction - critical route module", () => {
+ test("child.clientAction", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-action-critical/child-client-action/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-critical/child-client-action/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/parent.childLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-action-critical/child-client-action-parent-child-loader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-critical/child-client-action-parent-child-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Parent Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/child.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-action-critical/child-client-action-child-client-loader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-critical/child-client-action-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Child Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/parent.childLoader/child.clientLoader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-action-critical/child-client-action-parent-child-loader-child-client-loader/parent/child",
+ );
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-critical/child-client-action-parent-child-loader-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Child Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("throws a 400 if you call serverAction without a server action", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(
+ "/client-action-critical/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child",
+ );
+ app.clickSubmitButton(
+ "/client-action-critical/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child",
+ );
+ await page.waitForSelector("#child-error");
+ let html = await app.getHtml("#child-error");
+ expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
+ "400 Error: You are trying to call serverAction() on a route that does " +
+ 'not have a server action (routeId: "routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")',
+ );
+ });
+ });
+
+ test.describe("clientAction - lazy route module", () => {
+ test("child.clientAction", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-action-lazy/child-client-action/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-lazy/child-client-action/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/parent.childLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-action-lazy/child-client-action-parent-child-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-lazy/child-client-action-parent-child-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Parent Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/child.clientLoader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-action-lazy/child-client-action-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-lazy/child-client-action-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Child Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("child.clientAction/parent.childLoader/child.clientLoader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.clickLink(
+ "/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-data");
+ let html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader");
+ expect(html).toMatch("Child Server Loader");
+ expect(html).not.toMatch("Child Server Action");
+
+ app.clickSubmitButton(
+ "/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child",
+ );
+ await page.waitForSelector("#child-action-data");
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Loader"); // still revalidating
+ expect(html).toMatch("Child Server Action (mutated by client)");
+
+ await page.waitForSelector(
+ ':has-text("Child Server Loader (mutated by client)")',
+ );
+ html = await app.getHtml("main");
+ expect(html).toMatch("Parent Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Loader (mutated by client)");
+ expect(html).toMatch("Child Server Action (mutated by client)");
+ });
+
+ test("throws a 400 if you call serverAction without a server action", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await app.goto(
+ "/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child",
+ );
+ await page.waitForSelector("form");
+ app.clickSubmitButton(
+ "/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child",
+ );
+ await page.waitForSelector("#child-error");
+ let html = await app.getHtml("#child-error");
+ expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
+ "400 Error: You are trying to call serverAction() on a route that does " +
+ 'not have a server action (routeId: "routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child")',
+ );
+ });
+ });
+ });
+ }
+ });
+ }
+});
diff --git a/tests/react-router-framework/integration/custom-entry-server-test.ts b/tests/react-router-framework/integration/custom-entry-server-test.ts
new file mode 100644
index 00000000..ed8240b1
--- /dev/null
+++ b/tests/react-router-framework/integration/custom-entry-server-test.ts
@@ -0,0 +1,60 @@
+import { expect, test } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/entry.server.tsx": js`
+ import * as React from "react";
+ import { ServerRouter } from "react-router";
+ import { renderToString } from "react-dom/server";
+
+ export default function handleRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ ) {
+ let markup = renderToString(
+
+ );
+ responseHeaders.set("Content-Type", "text/html");
+ responseHeaders.set("x-custom-header", "custom-value");
+ return new Response('' + markup, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ });
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Hello World
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+test("allows user specified entry.server", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ let responses = app.collectResponses((url) => url.pathname === "/");
+ await app.goto("/");
+ let header = await responses[0].headerValues("x-custom-header");
+ expect(header).toEqual(["custom-value"]);
+});
diff --git a/tests/react-router-framework/integration/deduped-route-modules-test.ts b/tests/react-router-framework/integration/deduped-route-modules-test.ts
new file mode 100644
index 00000000..0a6d9056
--- /dev/null
+++ b/tests/react-router-framework/integration/deduped-route-modules-test.ts
@@ -0,0 +1,287 @@
+import { test, expect } from "@playwright/test";
+
+import { createFixture, createAppFixture } from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { type TemplateName, viteConfig } from "./helpers/vite.js";
+
+const templateNames = [
+ "vite-7-template",
+ "rsc-vite-framework",
+] as const satisfies TemplateName[];
+
+// This test ensures that code is not accidentally duplicated when a route is
+// imported within user code since they're not importing one of our internal
+// virtual route modules.
+test.describe("Deduped route modules", () => {
+ for (const templateName of templateNames) {
+ test.describe(`template: ${templateName}`, () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ templateName,
+ files: {
+ "vite.config.js": await viteConfig.basic({
+ templateName,
+ }),
+ "app/routes/client-first.a.tsx": `
+ import { Link } from "react-router";
+
+ export const customExport = (() => {
+ globalThis.custom_export_count = (globalThis.custom_export_count || 0) + 1;
+ return () => true;
+ })();
+
+ export const loader = (() => {
+ globalThis.loader_count = (globalThis.loader_count || 0) + 1;
+ return () => ({
+ customExportCount: globalThis.custom_export_count,
+ loaderCount: globalThis.loader_count,
+ componentCount: globalThis.component_count,
+ });
+ })();
+
+ export const clientLoader = (() => {
+ globalThis.client_loader_count = (globalThis.client_loader_count || 0) + 1;
+ return async ({ serverLoader }) => {
+ const loaderData = await serverLoader();
+ return {
+ loaderCount: loaderData.loaderCount,
+ clientLoaderCount: globalThis.client_loader_count,
+ serverCustomExportCount: loaderData.customExportCount,
+ clientCustomExportCount: globalThis.custom_export_count,
+ serverComponentCount: loaderData.componentCount,
+ clientComponentCount: globalThis.component_count,
+ };
+ };
+ })();
+ clientLoader.hydrate = true;
+
+ const RouteA = (() => {
+ globalThis.component_count = (globalThis.component_count || 0) + 1;
+ return ({ loaderData }: Route.ComponentProps) => {
+ return (
+ <>
+ Module Count
+ Loader count: {loaderData.loaderCount}
+ Client loader count: {loaderData.clientLoaderCount}
+ Server custom export count: {loaderData.serverCustomExportCount}
+ Client custom export count: {loaderData.clientCustomExportCount}
+ Server component count: {loaderData.serverComponentCount}
+ Client component count: {loaderData.clientComponentCount}
+ Go to Route B
+ >
+ );
+ };
+ })();
+
+ export default RouteA;
+ `,
+ "app/routes/client-first.b.tsx": `
+ import { Link } from "react-router";
+
+ import { customExport } from "./client-first.a";
+
+ export default function RouteB() {
+ return customExport && (
+ <>
+ Route B
+ This route imports the route module from Route A, so could potentially cause code duplication.
+ Go to Route A
+ >
+ );
+ }
+ `,
+
+ ...(templateName.includes("rsc")
+ ? {
+ "app/routes/rsc-server-first.a/route.tsx": `
+ import { Link } from "react-router";
+ import { ModuleCounts, clientLoader } from "./client";
+
+ export const customExport = (() => {
+ globalThis.rsc_custom_export_count = (globalThis.rsc_custom_export_count || 0) + 1;
+ return () => true;
+ })();
+
+ export const loader = (() => {
+ globalThis.rsc_loader_count = (globalThis.rsc_loader_count || 0) + 1;
+ return () => ({
+ customExportCount: globalThis.rsc_custom_export_count,
+ loaderCount: globalThis.rsc_loader_count,
+ componentCount: globalThis.rsc_component_count,
+ });
+ })();
+
+ export { clientLoader };
+
+ export const ServerComponent = (() => {
+ globalThis.rsc_component_count = (globalThis.rsc_component_count || 0) + 1;
+ return () => {
+ return (
+ <>
+ RSC Server-First Module Count
+
+ Go to RSC Route B
+ >
+ );
+ };
+ })();
+ `,
+ "app/routes/rsc-server-first.a/client.tsx": `
+ "use client";
+
+ import { useLoaderData } from "react-router";
+
+ export const clientLoader = (() => {
+ globalThis.rsc_client_loader_count = (globalThis.rsc_client_loader_count || 0) + 1;
+ return async ({ serverLoader }) => {
+ const loaderData = await serverLoader();
+ return {
+ loaderCount: loaderData.loaderCount,
+ clientLoaderCount: globalThis.rsc_client_loader_count,
+ serverCustomExportCount: loaderData.customExportCount,
+ clientCustomExportCount: globalThis.rsc_custom_export_count,
+ serverComponentCount: loaderData.componentCount,
+ };
+ };
+ })();
+ clientLoader.hydrate = true;
+
+ export function ModuleCounts() {
+ const loaderData = useLoaderData();
+ return (
+ <>
+ Loader count: {loaderData.loaderCount}
+ Client loader count: {loaderData.clientLoaderCount}
+ Server custom export count: {loaderData.serverCustomExportCount}
+ Client custom export count: {loaderData.clientCustomExportCount}
+ Server component count: {loaderData.serverComponentCount}
+ >
+ );
+ }
+ `,
+ "app/routes/rsc-server-first.b.tsx": `
+ import { Link } from "react-router";
+
+ import { customExport } from "./rsc-server-first.a/route";
+
+ // Ensure custom export is used in the client build in this route
+ export const handle = customExport;
+
+ export function ServerComponent() {
+ return customExport && (
+ <>
+ RSC Route B
+ This route imports the route module from RSC Route A, so could potentially cause code duplication.
+ Go to RSC Route A
+ >
+ );
+ }
+ `,
+ }
+ : {}),
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ let logs: string[] = [];
+
+ test.beforeEach(({ page }) => {
+ page.on("console", (msg) => {
+ logs.push(msg.text());
+ });
+ });
+
+ test.afterEach(() => {
+ expect(logs).toHaveLength(0);
+ });
+
+ test("Client-first routes", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let pageErrors: unknown[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await app.goto(`/client-first/b`, true);
+ expect(pageErrors).toEqual([]);
+
+ await app.clickLink("/client-first/a");
+ await page.waitForSelector("[data-loader-count]");
+ expect(await page.locator("[data-loader-count]").textContent()).toBe(
+ "1",
+ );
+ expect(
+ await page.locator("[data-client-loader-count]").textContent(),
+ ).toBe("1");
+ expect(
+ await page.locator("[data-server-custom-export-count]").textContent(),
+ ).toBe(
+ templateName.includes("rsc")
+ ? // In RSC, custom exports are present in both the react-server and react-client
+ // environments (so they're available to be imported by both),
+ // which means the Node server actually gets 2 copies
+ "2"
+ : "1",
+ );
+ expect(
+ await page.locator("[data-client-custom-export-count]").textContent(),
+ ).toBe("1");
+ expect(
+ await page.locator("[data-server-component-count]").textContent(),
+ ).toBe("1");
+ expect(
+ await page.locator("[data-client-component-count]").textContent(),
+ ).toBe("1");
+ expect(pageErrors).toEqual([]);
+ });
+
+ test("Server-first routes", async ({ page }) => {
+ test.skip(
+ !templateName.includes("rsc"),
+ "Server-first routes are an RSC-only feature",
+ );
+
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let pageErrors: unknown[] = [];
+ page.on("pageerror", (error) => pageErrors.push(error));
+
+ await app.goto(`/rsc-server-first/b`, true);
+ expect(pageErrors).toEqual([]);
+
+ await app.clickLink("/rsc-server-first/a");
+ await page.waitForSelector("[data-loader-count]");
+ expect(await page.locator("[data-loader-count]").textContent()).toBe(
+ "1",
+ );
+ expect(
+ await page.locator("[data-client-loader-count]").textContent(),
+ ).toBe("1");
+ expect(
+ await page.locator("[data-server-custom-export-count]").textContent(),
+ ).toBe(
+ // In RSC, custom exports are present in both the react-server and react-client
+ // environments (so they're available to be imported by both),
+ // which means the Node server actually gets 2 copies
+ "2",
+ );
+ expect(
+ await page.locator("[data-client-custom-export-count]").textContent(),
+ ).toBe("1");
+ expect(
+ await page.locator("[data-server-component-count]").textContent(),
+ ).toBe("1");
+ expect(pageErrors).toEqual([]);
+ });
+ });
+ }
+});
diff --git a/tests/react-router-framework/integration/defer-loader-test.ts b/tests/react-router-framework/integration/defer-loader-test.ts
new file mode 100644
index 00000000..9fc9beb6
--- /dev/null
+++ b/tests/react-router-framework/integration/defer-loader-test.ts
@@ -0,0 +1,110 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.describe("deferred loaders", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { useLoaderData, Link } from "react-router";
+ export default function Index() {
+ return (
+
+ Redirect
+ Direct Promise Access
+
+ )
+ }
+ `,
+
+ "app/routes/redirect.tsx": js`
+ import { data } from 'react-router';
+ export function loader() {
+ return data(
+ { food: "pizza" },
+ {
+ status: 301,
+ headers: {
+ Location: "/?redirected"
+ }
+ }
+ );
+ }
+ export default function Redirect() {
+ return null;
+ }
+ `,
+
+ "app/routes/direct-promise-access.tsx": js`
+ import * as React from "react";
+ import { useLoaderData, Link, Await } from "react-router";
+ export function loader() {
+ return {
+ bar: new Promise(async (resolve, reject) => {
+ resolve("hamburger");
+ }),
+ };
+ }
+ let count = 0;
+ export default function Index() {
+ let {bar} = useLoaderData();
+ React.useEffect(() => {
+ let aborted = false;
+ bar.then((data) => {
+ if (aborted) return;
+ document.getElementById("content").innerHTML = data + " " + (++count);
+ document.getElementById("content").setAttribute("data-done", "");
+ });
+ return () => {
+ aborted = true;
+ };
+ }, [bar]);
+ return (
+
+ Waiting for client hydration....
+
+ )
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(async () => appFixture.close());
+
+ test("deferred response can redirect on document request", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect");
+ await page.waitForURL(/\?redirected/);
+ });
+
+ test("deferred response can redirect on transition", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/redirect");
+ await page.waitForURL(/\?redirected/);
+ });
+
+ test("can directly access result from deferred promise on document request", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/direct-promise-access");
+ let element = await page.waitForSelector("[data-done]");
+ expect(await element.innerText()).toMatch("hamburger 1");
+ });
+});
diff --git a/tests/react-router-framework/integration/defer-test.ts b/tests/react-router-framework/integration/defer-test.ts
new file mode 100644
index 00000000..945683bc
--- /dev/null
+++ b/tests/react-router-framework/integration/defer-test.ts
@@ -0,0 +1,699 @@
+import { test, expect } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+
+const ROOT_ID = "ROOT_ID";
+const INDEX_ID = "INDEX_ID";
+const DEFERRED_ID = "DEFERRED_ID";
+const RESOLVED_DEFERRED_ID = "RESOLVED_DEFERRED_ID";
+const FALLBACK_ID = "FALLBACK_ID";
+const ERROR_ID = "ERROR_ID";
+const ERROR_BOUNDARY_ID = "ERROR_BOUNDARY_ID";
+const MANUAL_RESOLVED_ID = "MANUAL_RESOLVED_ID";
+const MANUAL_FALLBACK_ID = "MANUAL_FALLBACK_ID";
+const MANUAL_ERROR_ID = "MANUAL_ERROR_ID";
+
+let originalConsoleError: typeof console.error;
+
+declare global {
+ var __deferredManualResolveCache: {
+ nextId: number;
+ deferreds: Record<
+ string,
+ { resolve: (value: any) => void; reject: (error: Error) => void }
+ >;
+ };
+}
+
+function counterHtml(id: string, val: number) {
+ return `${val}
`;
+}
+
+const deferredHTMLStartString = " {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/components/counter.tsx": js`
+ import { useState } from "react";
+
+ export default function Counter({ id }) {
+ let [count, setCount] = useState(0);
+ return (
+
+
setCount((c) => c+1)}>Increment
+
{count}
+
+ )
+ }
+ `,
+ "app/components/interactive.tsx": js`
+ import { useEffect, useState } from "react";
+
+ export default function Interactive() {
+ let [interactive, setInteractive] = useState(false);
+ useEffect(() => {
+ setInteractive(true);
+ }, []);
+ return interactive ? (
+
+ ) : null;
+ }
+ `,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+ import Interactive from "~/components/interactive";
+
+ export const meta: MetaFunction = () => {
+ return [{ title: "New Remix App" }];
+ };
+
+ export const loader = () => ({
+ id: "${ROOT_ID}",
+ });
+
+ export default function Root() {
+ let { id } = useLoaderData();
+ return (
+
+
+
+
+
+
+
+
+
+
+ {/* Send arbitrary data so safari renders the initial shell before
+ the document finishes downloading. */}
+ {Array(1000).fill(null).map((_, i)=>YOOOOOOOOOO {i}
)}
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ id: "${INDEX_ID}",
+ };
+ }
+
+ export default function Index() {
+ let { id } = useLoaderData();
+ return (
+
+
{id}
+
+
+
+ deferred-script-resolved
+ deferred-script-unresolved
+ deferred-script-rejected
+ deferred-script-unrejected
+ deferred-script-rejected-no-error-element
+ deferred-script-unrejected-no-error-element
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ deferredUndefined: Promise.resolve(undefined),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ deferredUndefined: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve(undefined);
+ }, 10)
+ ),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId, resolvedUndefined } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-manual-resolve.tsx": js`
+ import { Suspense } from "react";
+ import { Await, Link, useLoaderData } from "react-router";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ global.__deferredManualResolveCache = global.__deferredManualResolveCache || {
+ nextId: 1,
+ deferreds: {},
+ };
+
+ let id = "" + global.__deferredManualResolveCache.nextId++;
+ let promise = new Promise((resolve, reject) => {
+ global.__deferredManualResolveCache.deferreds[id] = { resolve, reject };
+ });
+
+ return {
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ id,
+ manualValue: promise,
+ };
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId, id, manualValue } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+ )}
+ />
+
+ manual fallback}>
+
+ error
+
+
+ }
+ children={(value) => (
+
+
{JSON.stringify(value)}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+ originalConsoleError = console.error;
+ console.error = () => {};
+ });
+
+ test.afterAll(() => {
+ console.error = originalConsoleError;
+ appFixture.close();
+ });
+
+ test("works with critical JSON like data", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(INDEX_ID, 0));
+ expect(deferredHTML.replace("", "")).not.toBe("");
+ expect(deferredHTML).not.toContain(' {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-noscript-resolved",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(criticalHTML).not.toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
+ expect(deferredHTML).toContain(FALLBACK_ID);
+ expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
+ });
+
+ test("slow promises render in subsequent payload", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-noscript-unresolved",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);
+ expect(deferredHTML).toContain(`
`);
+ expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
+ });
+
+ test("resolved promises render in initial payload", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-script-resolved",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(deferredHTML).toContain(FALLBACK_ID);
+ expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
+ });
+
+ test("slow to resolve promises render in subsequent payload", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-script-unresolved",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);
+ expect(deferredHTML).toContain(`
`);
+ expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));
+ });
+
+ test("rejected promises render in initial payload", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-script-rejected",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(deferredHTML).toContain(FALLBACK_ID);
+ expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));
+ });
+
+ test("slow to reject promises render in subsequent payload", async () => {
+ let { status, criticalHTML, deferredHTML } = await getHtmlSections(
+ fixture,
+ "/deferred-script-unrejected",
+ );
+
+ expect(status).toBe(200);
+ expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
+ expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));
+ expect(criticalHTML).not.toContain(ERROR_ID);
+ expect(deferredHTML).toContain(`
`);
+ expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));
+ });
+
+ test("rejected promises bubble to ErrorBoundary on hydrate", async ({
+ page,
+ }) => {
+ let response = await fixture.requestDocument(
+ "/deferred-script-rejected-no-error-element",
+ );
+ let html = await response.text();
+
+ expect(response.status).toBe(200);
+ expect(html).toContain(ROOT_ID);
+ expect(html).toContain(DEFERRED_ID);
+ expect(html).toContain(FALLBACK_ID);
+ expect(html).not.toContain(ERROR_BOUNDARY_ID);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/deferred-script-rejected-no-error-element", true);
+ await page.waitForSelector("#interactive");
+ await page.waitForSelector(`#${ERROR_BOUNDARY_ID}`);
+ });
+
+ test("slow to reject promises bubble to ErrorBoundary on hydrate", async ({
+ page,
+ }) => {
+ let response = await fixture.requestDocument(
+ "/deferred-script-unrejected-no-error-element",
+ );
+ let html = await response.text();
+
+ expect(response.status).toBe(200);
+ expect(html).toContain(ROOT_ID);
+ expect(html).toContain(DEFERRED_ID);
+ expect(html).toContain(FALLBACK_ID);
+ expect(html).not.toContain(ERROR_BOUNDARY_ID);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/deferred-script-unrejected-no-error-element", false);
+ await page.waitForSelector("#interactive");
+ await page.waitForSelector(`#${ERROR_BOUNDARY_ID}`);
+ });
+});
diff --git a/tests/react-router-framework/integration/error-boundary-test.ts b/tests/react-router-framework/integration/error-boundary-test.ts
new file mode 100644
index 00000000..d355b8a3
--- /dev/null
+++ b/tests/react-router-framework/integration/error-boundary-test.ts
@@ -0,0 +1,1020 @@
+import { test, expect } from "@playwright/test";
+
+import { UNSAFE_ServerMode as ServerMode } from "react-router";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test.describe("ErrorBoundary", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+ let _consoleError: any;
+
+ let ROOT_BOUNDARY_TEXT = "ROOT_BOUNDARY_TEXT";
+ let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT";
+
+ let HAS_BOUNDARY_LOADER = "/yes/loader" as const;
+ let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const;
+ let HAS_BOUNDARY_ACTION = "/yes/action" as const;
+ let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const;
+ let HAS_BOUNDARY_RENDER = "/yes/render" as const;
+ let HAS_BOUNDARY_RENDER_FILE = "/yes.render" as const;
+ let HAS_BOUNDARY_NO_LOADER_OR_ACTION = "/yes/no-loader-or-action" as const;
+ let HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE =
+ "/yes.no-loader-or-action" as const;
+
+ let NO_BOUNDARY_ACTION = "/no/action" as const;
+ let NO_BOUNDARY_ACTION_FILE = "/no.action" as const;
+ let NO_BOUNDARY_LOADER = "/no/loader" as const;
+ let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const;
+ let NO_BOUNDARY_RENDER = "/no/render" as const;
+ let NO_BOUNDARY_RENDER_FILE = "/no.render" as const;
+ let NO_BOUNDARY_NO_LOADER_OR_ACTION = "/no/no-loader-or-action" as const;
+ let NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE = "/no.no-loader-or-action" as const;
+
+ let NOT_FOUND_HREF = "/not/found";
+
+ // packages/remix-react/errorBoundaries.tsx
+ let INTERNAL_ERROR_BOUNDARY_HEADING = "Application Error";
+
+ test.beforeAll(async () => {
+ _consoleError = console.error;
+ console.error = () => {};
+ fixture = await createFixture(
+ {
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+
+
+
+ ${ROOT_BOUNDARY_TEXT}
+
+
+
+
+ )
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link, Form } from "react-router";
+ export default function () {
+ return (
+
+ ${NOT_FOUND_HREF}
+
+
+
+
+ ${HAS_BOUNDARY_LOADER}
+
+
+ ${NO_BOUNDARY_LOADER}
+
+
+ ${HAS_BOUNDARY_RENDER}
+
+
+ ${NO_BOUNDARY_RENDER}
+
+
+ )
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js`
+ import { Form } from "react-router";
+ export async function action() {
+ throw new Error("Kaboom!")
+ }
+ export function ErrorBoundary() {
+ return
${OWN_BOUNDARY_TEXT}
+ }
+ export default function () {
+ return (
+
+ );
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js`
+ import { Form } from "react-router";
+ export function action() {
+ throw new Error("Kaboom!")
+ }
+ export default function () {
+ return (
+
+ )
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js`
+ export function loader() {
+ throw new Error("Kaboom!")
+ }
+ export function ErrorBoundary() {
+ return
${OWN_BOUNDARY_TEXT}
+ }
+ export default function () {
+ return
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`
+ export function loader() {
+ throw new Error("Kaboom!")
+ }
+ export default function () {
+ return
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js`
+ export default function () {
+ throw new Error("Kaboom!")
+ return
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js`
+ export default function () {
+ throw new Error("Kaboom!")
+ return
+ }
+
+ export function ErrorBoundary() {
+ return
${OWN_BOUNDARY_TEXT}
+ }
+ `,
+
+ [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js`
+ export function ErrorBoundary() {
+ return
${OWN_BOUNDARY_TEXT}
+ }
+ export default function Index() {
+ return
+ }
+ `,
+
+ [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js`
+ export default function Index() {
+ return
+ }
+ `,
+
+ "app/routes/fetcher-boundary.tsx": js`
+ import { useFetcher } from "react-router";
+ export function ErrorBoundary() {
+ return
${OWN_BOUNDARY_TEXT}
+ }
+ export default function() {
+ let fetcher = useFetcher();
+
+ return (
+
+
+
+
+
+ )
+ }
+ `,
+
+ "app/routes/fetcher-no-boundary.tsx": js`
+ import { useFetcher } from "react-router";
+ export default function() {
+ let fetcher = useFetcher();
+
+ return (
+
+
+
+ No Loader or Action
+
+
+
+ )
+ }
+ `,
+
+ "app/routes/action.tsx": js`
+ import { Outlet, useLoaderData } from "react-router";
+
+ export function loader() {
+ return "PARENT";
+ }
+
+ export default function () {
+ return (
+
+ )
+ }
+ `,
+
+ "app/routes/action.child-error.tsx": js`
+ import { Form, useLoaderData, useRouteError } from "react-router";
+
+ export function loader() {
+ return "CHILD";
+ }
+
+ export function action() {
+ throw new Error("Broken!");
+ }
+
+ export default function () {
+ return (
+ <>
+
{useLoaderData()}
+
+ >
+ )
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return
{error.message}
;
+ }
+ `,
+ },
+ },
+ ServerMode.Development,
+ );
+
+ appFixture = await createAppFixture(fixture, ServerMode.Development);
+ });
+
+ test.afterAll(() => {
+ console.error = _consoleError;
+ appFixture.close();
+ });
+
+ test("invalid request methods", async () => {
+ let res = await fixture.requestDocument("/", { method: "OPTIONS" });
+ expect(res.status).toBe(405);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, action, document request", async () => {
+ let params = new URLSearchParams();
+ let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, action, client transition from other route", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(HAS_BOUNDARY_ACTION);
+ await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, action, client transition from itself", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(HAS_BOUNDARY_ACTION);
+ await app.clickSubmitButton(HAS_BOUNDARY_ACTION);
+ await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in action document requests", async () => {
+ let params = new URLSearchParams();
+ let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in action script transitions from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(NO_BOUNDARY_ACTION);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in action script transitions from self", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(NO_BOUNDARY_ACTION);
+ await app.clickSubmitButton(NO_BOUNDARY_ACTION);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, loader, document request", async () => {
+ let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("own boundary, loader, client transition", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(HAS_BOUNDARY_LOADER);
+ await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in loader document requests", async () => {
+ let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("bubbles to parent in loader script transitions from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(NO_BOUNDARY_LOADER);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("ssr rendering errors with no boundary", async () => {
+ let res = await fixture.requestDocument(NO_BOUNDARY_RENDER);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("script transition rendering errors with no boundary", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(NO_BOUNDARY_RENDER);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("ssr rendering errors with boundary", async () => {
+ let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER);
+ expect(res.status).toBe(500);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("script transition rendering errors with boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink(HAS_BOUNDARY_RENDER);
+ await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);
+ expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("uses correct error boundary on server action errors in nested routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/action/child-error`);
+ expect(await app.getHtml("#parent-data")).toMatch("PARENT");
+ expect(await app.getHtml("#child-data")).toMatch("CHILD");
+ await page.click("button[type=submit]");
+ await page.waitForSelector("#child-error");
+ // Preserves parent loader data
+ expect(await app.getHtml("#parent-data")).toMatch("PARENT");
+ expect(await app.getHtml("#child-error")).toMatch("Broken!");
+ });
+
+ test("renders own boundary in fetcher action submission without action from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-boundary");
+ await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);
+ await page.waitForSelector("#fetcher-boundary");
+ });
+
+ test("renders root boundary in fetcher action submission without action from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-no-boundary");
+ await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ });
+
+ test("renders root boundary in document POST without action requests", async () => {
+ let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, {
+ method: "post",
+ });
+ expect(res.status).toBe(405);
+ expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);
+ });
+
+ test("renders root boundary in action script transitions without action from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);
+ await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);
+ });
+
+ test("renders own boundary in document POST without action requests", async () => {
+ let res = await fixture.requestDocument(HAS_BOUNDARY_NO_LOADER_OR_ACTION, {
+ method: "post",
+ });
+ expect(res.status).toBe(405);
+ expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);
+ });
+
+ test("renders own boundary in action script transitions without action from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION);
+ await page.waitForSelector("#boundary-no-loader-or-action");
+ });
+
+ test.describe("if no error boundary exists in the app", () => {
+ let NO_ROOT_BOUNDARY_LOADER = "/loader-bad" as const;
+ let NO_ROOT_BOUNDARY_ACTION = "/action-bad" as const;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link, Form } from "react-router";
+
+ export default function () {
+ return (
+
+
Home
+
+
+ )
+ }
+ `,
+
+ [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js`
+ export async function loader() {
+ throw Error("BLARGH");
+ }
+
+ export default function () {
+ return (
+
+
Hello
+
+ )
+ }
+ `,
+
+ [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js`
+ export async function action() {
+ throw Error("YOOOOOOOO WHAT ARE YOU DOING");
+ }
+
+ export default function () {
+ return (
+
+
Goodbye
+
+ )
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test("bubbles to internal boundary in loader document requests", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(NO_ROOT_BOUNDARY_LOADER);
+ expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING);
+ });
+
+ test("bubbles to internal boundary in action script transitions from other routes", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION);
+ await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`);
+ expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING);
+ });
+ });
+});
+
+test.describe("Default ErrorBoundary", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+ let _consoleError: any;
+
+ function getFiles({
+ includeRootErrorBoundary = false,
+ rootErrorBoundaryThrows = false,
+ } = {}) {
+ let errorBoundaryCode = !includeRootErrorBoundary
+ ? ""
+ : rootErrorBoundaryThrows
+ ? js`
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+
+
+
+
+ Root Error Boundary
+ {error.message}
+ {oh.no.what.have.i.done}
+
+
+
+
+ )
+ }
+ `
+ : js`
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+
+
+
+
+ Root Error Boundary
+ {error.message}
+
+
+
+
+ )
+ }
+ `;
+
+ return {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts, useRouteError } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ ${errorBoundaryCode}
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function () {
+ return (
+
+
Index
+ Loader Error
+ Render Error
+
+ );
+ }
+ `,
+
+ "app/routes/loader-error.tsx": js`
+ export function loader() {
+ throw new Error('Loader Error');
+ }
+ export default function () {
+ return Loader Error
+ }
+ `,
+
+ "app/routes/render-error.tsx": js`
+ export default function () {
+ throw new Error("Render Error")
+ }
+ `,
+ };
+ }
+
+ test.beforeAll(async () => {
+ _consoleError = console.error;
+ console.error = () => {};
+ });
+
+ test.afterAll(async () => {
+ console.error = _consoleError;
+ appFixture.close();
+ });
+
+ test.describe("When the root route does not have a boundary", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture(
+ {
+ files: getFiles({ includeRootErrorBoundary: false }),
+ },
+ ServerMode.Development,
+ );
+ appFixture = await createAppFixture(fixture, ServerMode.Development);
+ });
+
+ test.afterAll(() => appFixture.close());
+
+ test.describe("document requests", () => {
+ test("renders default boundary on loader errors", async () => {
+ let res = await fixture.requestDocument("/loader-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Application Error");
+ expect(text).toMatch("Loader Error");
+ expect(text).not.toMatch("Root Error Boundary");
+ });
+
+ test("renders default boundary on render errors", async () => {
+ let res = await fixture.requestDocument("/render-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Application Error");
+ expect(text).toMatch("Render Error");
+ expect(text).not.toMatch("Root Error Boundary");
+ });
+ });
+
+ test.describe("SPA navigations", () => {
+ test("renders default boundary on loader errors", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/loader-error");
+ await page.waitForSelector("pre");
+ let html = await app.getHtml();
+ expect(html).toMatch("Application Error");
+ expect(html).toMatch("Loader Error");
+ expect(html).not.toMatch("Root Error Boundary");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+
+ test("renders default boundary on render errors", async ({
+ page,
+ }, workerInfo) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/render-error");
+ await page.waitForSelector("pre");
+ let html = await app.getHtml();
+ expect(html).toMatch("Application Error");
+ // Chromium seems to be the only one that includes the message in the stack
+ if (workerInfo.project.name === "chromium") {
+ expect(html).toMatch("Render Error");
+ }
+ expect(html).not.toMatch("Root Error Boundary");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+ });
+ });
+
+ test.describe("When the root route has a boundary", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture(
+ {
+ files: getFiles({ includeRootErrorBoundary: true }),
+ },
+ ServerMode.Development,
+ );
+ appFixture = await createAppFixture(fixture, ServerMode.Development);
+ });
+
+ test.afterAll(() => appFixture.close());
+
+ test.describe("document requests", () => {
+ test("renders root boundary on loader errors", async () => {
+ let res = await fixture.requestDocument("/loader-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Root Error Boundary");
+ expect(text).toMatch("Loader Error");
+ expect(text).not.toMatch("Application Error");
+ });
+
+ test("renders root boundary on render errors", async () => {
+ let res = await fixture.requestDocument("/render-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Root Error Boundary");
+ expect(text).toMatch("Render Error");
+ expect(text).not.toMatch("Application Error");
+ });
+ });
+
+ test.describe("SPA navigations", () => {
+ test("renders root boundary on loader errors", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/loader-error");
+ await page.waitForSelector("#root-error-boundary");
+ let html = await app.getHtml();
+ expect(html).toMatch("Root Error Boundary");
+ expect(html).toMatch("Loader Error");
+ expect(html).not.toMatch("Application Error");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+
+ test("renders root boundary on render errors", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/render-error");
+ await page.waitForSelector("#root-error-boundary");
+ let html = await app.getHtml();
+ expect(html).toMatch("Root Error Boundary");
+ expect(html).toMatch("Render Error");
+ expect(html).not.toMatch("Application Error");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+ });
+ });
+
+ test.describe("When the root route has a boundary but it also throws 😦", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: getFiles({
+ includeRootErrorBoundary: true,
+ rootErrorBoundaryThrows: true,
+ }),
+ });
+ appFixture = await createAppFixture(fixture, ServerMode.Development);
+ });
+
+ test.afterAll(() => appFixture.close());
+
+ test.describe("document requests", () => {
+ test("tries to render root boundary on loader errors but bubbles to default boundary", async () => {
+ let res = await fixture.requestDocument("/loader-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Unexpected Server Error");
+ expect(text).not.toMatch("Application Error");
+ expect(text).not.toMatch("Loader Error");
+ expect(text).not.toMatch("Root Error Boundary");
+ });
+
+ test("tries to render root boundary on render errors but bubbles to default boundary", async () => {
+ let res = await fixture.requestDocument("/render-error");
+ expect(res.status).toBe(500);
+ let text = await res.text();
+ expect(text).toMatch("Unexpected Server Error");
+ expect(text).not.toMatch("Application Error");
+ expect(text).not.toMatch("Render Error");
+ expect(text).not.toMatch("Root Error Boundary");
+ });
+ });
+
+ test.describe("SPA navigations", () => {
+ test("tries to render root boundary on loader errors but bubbles to default boundary", async ({
+ page,
+ }, workerInfo) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/loader-error");
+ await page.waitForSelector("pre");
+ let html = await app.getHtml();
+ expect(html).toMatch("Application Error");
+ if (workerInfo.project.name === "chromium") {
+ expect(html).toMatch("ReferenceError: oh is not defined");
+ }
+ expect(html).not.toMatch("Loader Error");
+ expect(html).not.toMatch("Root Error Boundary");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+
+ test("tries to render root boundary on render errors but bubbles to default boundary", async ({
+ page,
+ }, workerInfo) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/render-error");
+ await page.waitForSelector("pre");
+ let html = await app.getHtml();
+ expect(html).toMatch("Application Error");
+ if (workerInfo.project.name === "chromium") {
+ expect(html).toMatch("ReferenceError: oh is not defined");
+ }
+ expect(html).not.toMatch("Render Error");
+ expect(html).not.toMatch("Root Error Boundary");
+
+ // Ensure we can click back to our prior page
+ await app.goBack();
+ await page.waitForSelector("h1#index");
+ });
+ });
+ });
+});
+
+test("Allows back-button out of an error boundary after a hard reload", async ({
+ page,
+ browserName,
+}) => {
+ let _consoleError = console.error;
+ console.error = () => {};
+
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts, useRouteError } from "react-router";
+
+ export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+
+
+ Oh no!
+
+
+
+
+ ERROR BOUNDARY
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+
+ export default function Index() {
+ return (
+
+
INDEX
+ This will error
+
+ );
+ }
+ `,
+
+ "app/routes/boom.tsx": js`
+ export function loader() { return boom(); }
+ export default function() { return my page ; }
+ `,
+ },
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/");
+ await page.waitForSelector("#index");
+ expect(app.page.url()).not.toMatch("/boom");
+
+ await app.clickLink("/boom");
+ await page.waitForSelector("#error");
+ expect(app.page.url()).toMatch("/boom");
+
+ await app.reload();
+ await page.waitForSelector("#error");
+ expect(app.page.url()).toMatch("boom");
+
+ await app.goBack();
+
+ // Here be dragons
+ // - Playwright sets the Firefox `fission.webContentIsolationStrategy=0` preference
+ // for reasons having to do with out-of-process iframes:
+ // https://github.com/microsoft/playwright/issues/22640#issuecomment-1543287282
+ // - That preference exposes a bug in firefox where a hard reload adds to the
+ // history stack: https://bugzilla.mozilla.org/show_bug.cgi?id=1832341
+ // - Your can disable this preference via the Playwright `firefoxUserPrefs` config,
+ // but that is broken until 1.34:
+ // https://github.com/microsoft/playwright/issues/22640#issuecomment-1546230104
+ // https://github.com/microsoft/playwright/issues/15405
+ // - We can't yet upgrade to 1.34 because it drops support for Node 14:
+ // https://github.com/microsoft/playwright/releases/tag/v1.34.0
+ //
+ // So for now when in firefox we just navigate back twice to work around the issue
+ if (browserName === "firefox") {
+ await app.goBack();
+ }
+
+ await page.waitForSelector("#index");
+ expect(app.page.url()).not.toContain("boom");
+
+ appFixture.close();
+ console.error = _consoleError;
+});
diff --git a/tests/react-router-framework/integration/error-boundary-v2-test.ts b/tests/react-router-framework/integration/error-boundary-v2-test.ts
new file mode 100644
index 00000000..87d0fcbd
--- /dev/null
+++ b/tests/react-router-framework/integration/error-boundary-v2-test.ts
@@ -0,0 +1,257 @@
+import type { Page } from "@playwright/test";
+import { test, expect } from "@playwright/test";
+
+import { UNSAFE_ServerMode as ServerMode } from "react-router";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test.describe("ErrorBoundary", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+ let oldConsoleError: () => void;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/parent.tsx": js`
+ import {
+ Link,
+ Outlet,
+ isRouteErrorResponse,
+ useLoaderData,
+ useRouteError,
+ } from "react-router";
+
+ export function loader() {
+ return "PARENT LOADER";
+ }
+
+ export default function Component() {
+ return (
+
+
+
+ Link
+ Link
+ Link
+ Link
+ Link
+ Link
+ Link
+
+
+
{useLoaderData()}
+
+
+ )
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return isRouteErrorResponse(error) ?
+ {error.status + ' ' + error.data}
:
+ {error.message}
;
+ }
+ `,
+
+ "app/routes/parent.child-with-boundary.tsx": js`
+ import {
+ isRouteErrorResponse,
+ useLoaderData,
+ useLocation,
+ useRouteError,
+ } from "react-router";
+
+ export function loader({ request }) {
+ let errorType = new URL(request.url).searchParams.get('type');
+ if (errorType === 'response') {
+ throw new Response('Loader Response', { status: 418 });
+ } else if (errorType === 'error') {
+ throw new Error('Loader Error');
+ }
+ return "CHILD LOADER";
+ }
+
+ export default function Component() {;
+ let data = useLoaderData();
+ if (new URLSearchParams(useLocation().search).get('type') === "render") {
+ throw new Error("Render Error");
+ }
+ return {data}
;
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return isRouteErrorResponse(error) ?
+ {error.status + ' ' + error.data}
:
+ {error.message}
;
+ }
+ `,
+
+ "app/routes/parent.child-without-boundary.tsx": js`
+ import { useLoaderData, useLocation } from "react-router";
+
+ export function loader({ request }) {
+ let errorType = new URL(request.url).searchParams.get('type');
+ if (errorType === 'response') {
+ throw new Response('Loader Response', { status: 418 });
+ } else if (errorType === 'error') {
+ throw new Error('Loader Error');
+ }
+ return "CHILD LOADER";
+ }
+
+ export default function Component() {;
+ let data = useLoaderData();
+ if (new URLSearchParams(useLocation().search).get('type') === "render") {
+ throw new Error("Render Error");
+ }
+ return {data}
;
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture, ServerMode.Development);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test.beforeEach(({ page }) => {
+ oldConsoleError = console.error;
+ console.error = () => {};
+ });
+
+ test.afterEach(() => {
+ console.error = oldConsoleError;
+ });
+
+ test.describe("without JavaScript", () => {
+ test.use({ javaScriptEnabled: false });
+ runBoundaryTests();
+ });
+
+ test.describe("with JavaScript", () => {
+ test.use({ javaScriptEnabled: true });
+ runBoundaryTests();
+
+ test("Network errors that never reach the Remix server", async ({
+ page,
+ }) => {
+ // Cause a .data request to trigger an HTTP error that never reaches the
+ // Remix server, and ensure we properly handle it at the ErrorBoundary
+ await page.route(/\/parent\/child-with-boundary\.data$/, (route) => {
+ route.fulfill({ status: 500, body: "CDN Error!" });
+ });
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-with-boundary");
+ await waitForAndAssert(
+ page,
+ app,
+ "#parent-error-response",
+ "500 CDN Error!",
+ );
+ });
+ });
+
+ function runBoundaryTests() {
+ test("No errors", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-with-boundary");
+ await waitForAndAssert(page, app, "#child-data", "CHILD LOADER");
+ });
+
+ test("Throwing a Response to own boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-with-boundary?type=response");
+ await waitForAndAssert(
+ page,
+ app,
+ "#child-error-response",
+ "418 Loader Response",
+ );
+ });
+
+ test("Throwing an Error to own boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-with-boundary?type=error");
+ await waitForAndAssert(page, app, "#child-error", "Loader Error");
+ });
+
+ test("Throwing a render error to own boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-with-boundary?type=render");
+ await waitForAndAssert(page, app, "#child-error", "Render Error");
+ });
+
+ test("Throwing a Response to parent boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-without-boundary?type=response");
+ await waitForAndAssert(
+ page,
+ app,
+ "#parent-error-response",
+ "418 Loader Response",
+ );
+ });
+
+ test("Throwing an Error to parent boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-without-boundary?type=error");
+ await waitForAndAssert(page, app, "#parent-error", "Loader Error");
+ });
+
+ test("Throwing a render error to parent boundary", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await app.clickLink("/parent/child-without-boundary?type=render");
+ await waitForAndAssert(page, app, "#parent-error", "Render Error");
+ });
+ }
+});
+
+// Shorthand util to wait for an element to appear before asserting it
+async function waitForAndAssert(
+ page: Page,
+ app: PlaywrightFixture,
+ selector: string,
+ match: string,
+) {
+ await page.waitForSelector(selector);
+ expect(await app.getHtml(selector)).toMatch(match);
+}
diff --git a/tests/react-router-framework/integration/error-data-request-test.ts b/tests/react-router-framework/integration/error-data-request-test.ts
new file mode 100644
index 00000000..7dddf9ad
--- /dev/null
+++ b/tests/react-router-framework/integration/error-data-request-test.ts
@@ -0,0 +1,168 @@
+import { test, expect } from "@playwright/test";
+import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from "react-router";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+
+test.describe("ErrorBoundary", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+ let _consoleError: any;
+ let errorLogs: any[];
+
+ test.beforeAll(async () => {
+ _consoleError = console.error;
+ console.error = (v) => errorLogs.push(v);
+
+ fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { Link, Form } from "react-router";
+
+ export default function () {
+ return Index
+ }
+ `,
+
+ [`app/routes/loader-throw-error.jsx`]: js`
+ export async function loader() {
+ throw Error("BLARGH");
+ }
+
+ export default function () {
+ return Hello
+ }
+ `,
+
+ [`app/routes/loader-return-json.jsx`]: js`
+ export async function loader() {
+ return { ok: true };
+ }
+
+ export default function () {
+ return Hello
+ }
+ `,
+
+ [`app/routes/action-throw-error.jsx`]: js`
+ export async function action() {
+ throw Error("YOOOOOOOO WHAT ARE YOU DOING");
+ }
+
+ export default function () {
+ return Goodbye ;
+ }
+ `,
+
+ [`app/routes/action-return-json.jsx`]: js`
+ export async function action() {
+ return { ok: true };
+ }
+
+ export default function () {
+ return Hi!
+ }
+ `,
+ },
+ });
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.beforeEach(async () => {
+ errorLogs = [];
+ });
+
+ test.afterAll(() => {
+ console.error = _consoleError;
+ appFixture.close();
+ });
+
+ function assertLoggedErrorInstance(message: string) {
+ let error = errorLogs[0] as Error;
+ expect(error).toBeInstanceOf(Error);
+ expect(error.message).toEqual(message);
+ }
+
+ test("returns a 200 empty response on a data fetch to a path with no loaders", async () => {
+ let { status, headers, data } =
+ await fixture.requestSingleFetchData("/_.data");
+ expect(status).toBe(200);
+ expect(headers.has("X-Remix-Response")).toBe(true);
+ expect(data).toEqual({});
+ });
+
+ test("returns a 405 on a data fetch POST to a path with no action", async () => {
+ let { status, headers, data } = await fixture.requestSingleFetchData(
+ "/_.data?index",
+ {
+ method: "POST",
+ },
+ );
+ expect(status).toBe(405);
+ expect(headers.has("X-Remix-Response")).toBe(true);
+ expect(data).toEqual({
+ error: new ErrorResponseImpl(
+ 405,
+ "Method Not Allowed",
+ 'Error: You made a POST request to "/_.data" but did not provide an `action` for route "routes/_index", so there is no way to handle the request.',
+ ),
+ });
+ assertLoggedErrorInstance(
+ 'You made a POST request to "/_.data" but did not provide an `action` for route "routes/_index", so there is no way to handle the request.',
+ );
+ });
+
+ test("returns a 405 on a data fetch with a bad method", async () => {
+ try {
+ await fixture.requestSingleFetchData("/loader-return-json.data", {
+ method: "TRACE",
+ });
+ expect(false).toBe(true);
+ } catch (e) {
+ expect((e as Error).message).toMatch(
+ "'TRACE' HTTP method is unsupported.",
+ );
+ }
+ });
+
+ test("returns a 404 on a data fetch to a path with no matches", async () => {
+ let { status, headers, data } = await fixture.requestSingleFetchData(
+ "/i/match/nothing.data",
+ );
+ expect(status).toBe(404);
+ expect(headers.has("X-Remix-Response")).toBe(true);
+ expect(data).toEqual({
+ root: {
+ error: new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ 'Error: No route matches URL "/i/match/nothing"',
+ ),
+ },
+ });
+ assertLoggedErrorInstance('No route matches URL "/i/match/nothing"');
+ });
+});
diff --git a/tests/react-router-framework/integration/error-sanitization-test.ts b/tests/react-router-framework/integration/error-sanitization-test.ts
new file mode 100644
index 00000000..7b5a4b7a
--- /dev/null
+++ b/tests/react-router-framework/integration/error-sanitization-test.ts
@@ -0,0 +1,707 @@
+import { test, expect } from "@playwright/test";
+import {
+ UNSAFE_ErrorResponseImpl as ErrorResponseImpl,
+ UNSAFE_ServerMode as ServerMode,
+} from "react-router";
+
+import type { Fixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+const routeFiles = {
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { useLoaderData, useLocation, useRouteError } from "react-router";
+
+ export function loader({ request }) {
+ if (new URL(request.url).searchParams.has('loader')) {
+ throw new Error("Loader Error");
+ }
+ if (new URL(request.url).searchParams.has('subclass')) {
+ // This will throw a ReferenceError
+ console.log(thisisnotathing);
+ }
+ return "LOADER"
+ }
+
+ export default function Component() {
+ let data = useLoaderData();
+ let location = useLocation();
+
+ if (location.search.includes('render')) {
+ throw new Error("Render Error");
+ }
+
+ return (
+ <>
+ Index Route
+ {JSON.stringify(data)}
+ >
+ );
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+ <>
+ Index Error
+ {"MESSAGE:" + error.message}
+ {"NAME:" + error.name}
+ {error.stack ? {"STACK:" + error.stack}
: null}
+ >
+ );
+ }
+ `,
+
+ "app/routes/defer.tsx": js`
+ import * as React from 'react';
+ import { Await, useAsyncError, useLoaderData, useRouteError } from "react-router";
+
+ export function loader({ request }) {
+ if (new URL(request.url).searchParams.has('loader')) {
+ return {
+ lazy: Promise.reject(new Error("REJECTED")),
+ };
+ }
+ return {
+ lazy: Promise.resolve("RESOLVED"),
+ };
+ }
+
+ export default function Component() {
+ let data = useLoaderData();
+
+ return (
+ <>
+ Defer Route
+ Loading...}>
+ }>
+ {(val) => {val}
}
+
+
+ >
+ );
+ }
+
+ function AwaitError() {
+ let error = useAsyncError();
+ return (
+ <>
+ Defer Error
+ {error.message}
+ >
+ );
+ }
+
+ export function ErrorBoundary() {
+ let error = useRouteError();
+ return (
+ <>
+ Defer Error
+ {"MESSAGE:" + error.message}
+ {error.stack ? {"STACK:" + error.stack}
: null}
+ >
+ );
+ }
+ `,
+
+ "app/routes/resource.tsx": js`
+ export function loader({ request }) {
+ if (new URL(request.url).searchParams.has('loader')) {
+ throw new Error("Loader Error");
+ }
+ return "RESOURCE LOADER"
+ }
+ `,
+};
+
+test.describe("Error Sanitization", () => {
+ let fixture: Fixture;
+ let oldConsoleError: () => void;
+ let errorLogs: any[] = [];
+
+ test.beforeEach(() => {
+ oldConsoleError = console.error;
+ errorLogs = [];
+ console.error = (...args) => errorLogs.push(args);
+ });
+
+ test.afterEach(() => {
+ console.error = oldConsoleError;
+ });
+
+ test.describe("serverMode=production", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture(
+ {
+ files: routeFiles,
+ },
+ ServerMode.Production,
+ );
+ });
+
+ test("renders document without errors", async () => {
+ let response = await fixture.requestDocument("/");
+ let html = await response.text();
+ expect(html).toMatch("Index Route");
+ expect(html).toMatch("LOADER");
+ expect(html).not.toMatch("MESSAGE:");
+ expect(html).not.toMatch(/stack/i);
+ });
+
+ test("sanitizes loader errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?loader");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).not.toMatch("LOADER");
+ expect(html).toMatch("MESSAGE:Unexpected Server Error");
+ expect(html).toMatch('\\"SanitizedError\\"');
+ expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
+ expect(html).not.toMatch(/ at /i);
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("sanitizes render errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?render");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).toMatch("MESSAGE:Unexpected Server Error");
+ expect(html).toMatch('\\"SanitizedError\\"');
+ expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
+ expect(html).not.toMatch(/ at /i);
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Render Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("renders deferred document without errors", async () => {
+ let response = await fixture.requestDocument("/defer");
+ let html = await response.text();
+ expect(html).toMatch("Defer Route");
+ expect(html).toMatch("RESOLVED");
+ expect(html).not.toMatch("MESSAGE:");
+ // Defer errors are not part of the JSON blob but rather rejected
+ // against a pending promise and therefore are inlined JS.
+ expect(html).not.toMatch("x.stack=e.stack;");
+ });
+
+ test("sanitizes defer errors in document requests", async () => {
+ let response = await fixture.requestDocument("/defer?loader");
+ let html = await response.text();
+ expect(html).toMatch("Defer Error");
+ expect(html).not.toMatch("RESOLVED");
+ expect(html).toMatch("Unexpected Server Error");
+ expect(html).not.toMatch("stack");
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("returns data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data");
+ expect(data).toEqual({
+ "routes/_index": {
+ data: "LOADER",
+ },
+ });
+ });
+
+ test("sanitizes loader errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data?loader");
+ expect(data).toEqual({
+ "routes/_index": {
+ error: new Error("Unexpected Server Error"),
+ },
+ });
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("returns deferred data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data");
+ // @ts-expect-error
+ expect(await data["routes/defer"].data.lazy).toEqual("RESOLVED");
+ });
+
+ test("sanitizes loader errors in deferred data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data?loader");
+ try {
+ // @ts-expect-error
+ await data["routes/defer"].data.lazy;
+ expect(true).toBe(false);
+ } catch (e) {
+ expect((e as Error).message).toBe("Unexpected Server Error");
+ expect((e as Error).stack).toBeUndefined();
+ }
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("sanitizes loader errors in resource requests", async () => {
+ let response = await fixture.requestResource("/resource?loader");
+ let text = await response.text();
+ expect(text).toBe("Unexpected Server Error");
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("does not sanitize mismatched route errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/not-a-route.data");
+ expect(data).toEqual({
+ root: {
+ error: new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ 'Error: No route matches URL "/not-a-route"',
+ ),
+ },
+ });
+ expect(errorLogs).toEqual([
+ [new Error('No route matches URL "/not-a-route"')],
+ ]);
+ });
+
+ test("does not support hydration of Error subclasses", async ({ page }) => {
+ let response = await fixture.requestDocument("/?subclass");
+ let html = await response.text();
+ expect(html).toMatch("MESSAGE:Unexpected Server Error");
+ expect(html).toMatch("
NAME:Error");
+
+ // Hydration
+ let appFixture = await createAppFixture(fixture);
+ try {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/?subclass", true);
+ html = await app.getHtml();
+ expect(html).toMatch("
MESSAGE:Unexpected Server Error");
+ expect(html).toMatch("
NAME:Error");
+ } finally {
+ appFixture.close();
+ }
+ });
+ });
+
+ test.describe("serverMode=development", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture(
+ {
+ files: routeFiles,
+ },
+ ServerMode.Development,
+ );
+ });
+ let ogEnv = process.env.NODE_ENV;
+ test.beforeEach(() => {
+ ogEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = "development";
+ });
+ test.afterEach(() => {
+ process.env.NODE_ENV = ogEnv;
+ });
+
+ test("renders document without errors", async () => {
+ let response = await fixture.requestDocument("/");
+ let html = await response.text();
+ expect(html).toMatch("Index Route");
+ expect(html).toMatch("LOADER");
+ expect(html).not.toMatch("MESSAGE:");
+ expect(html).not.toMatch(/stack/i);
+ });
+
+ test("does not sanitize loader errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?loader");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).not.toMatch("LOADER");
+ expect(html).toMatch("
MESSAGE:Loader Error");
+ expect(html).toMatch("
STACK:Error: Loader Error");
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("does not sanitize render errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?render");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).toMatch("
MESSAGE:Render Error");
+ expect(html).toMatch("
STACK:Error: Render Error");
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Render Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("renders deferred document without errors", async () => {
+ let response = await fixture.requestDocument("/defer");
+ let html = await response.text();
+ expect(html).toMatch("Defer Route");
+ expect(html).toMatch("RESOLVED");
+ expect(html).not.toMatch("MESSAGE:");
+ expect(html).not.toMatch(/"stack":/i);
+ });
+
+ test("does not sanitize defer errors in document requests", async () => {
+ let response = await fixture.requestDocument("/defer?loader");
+ let html = await response.text();
+ expect(html).toMatch("Defer Error");
+ expect(html).not.toMatch("RESOLVED");
+ expect(html).toMatch("
REJECTED
");
+ expect(html).toMatch("Error: REJECTED\\\\n at ");
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("returns data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data");
+ expect(data).toEqual({
+ "routes/_index": {
+ data: "LOADER",
+ },
+ });
+ });
+
+ test("does not sanitize loader errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data?loader");
+ expect(data).toEqual({
+ "routes/_index": {
+ error: new Error("Loader Error"),
+ },
+ });
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("returns deferred data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data");
+ // @ts-expect-error
+ expect(await data["routes/defer"].data.lazy).toEqual("RESOLVED");
+ });
+
+ test("does not sanitize loader errors in deferred data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data?loader");
+ try {
+ // @ts-expect-error
+ await data["routes/defer"].data.lazy;
+ expect(true).toBe(false);
+ } catch (e) {
+ expect((e as Error).message).toBe("REJECTED");
+ expect((e as Error).stack).not.toBeUndefined();
+ }
+
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("does not sanitize loader errors in resource requests", async () => {
+ let response = await fixture.requestResource("/resource?loader");
+ let text = await response.text();
+ expect(text).toBe("Unexpected Server Error\n\nError: Loader Error");
+ expect(errorLogs.length).toBe(1);
+ expect(errorLogs[0][0].message).toMatch("Loader Error");
+ expect(errorLogs[0][0].stack).toMatch(" at ");
+ });
+
+ test("does not sanitize mismatched route errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/not-a-route.data");
+ expect(data).toEqual({
+ root: {
+ error: new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ 'Error: No route matches URL "/not-a-route"',
+ ),
+ },
+ });
+ expect(errorLogs).toEqual([
+ [new Error('No route matches URL "/not-a-route"')],
+ ]);
+ });
+
+ test("supports hydration of Error subclasses", async ({ page }) => {
+ let response = await fixture.requestDocument("/?subclass");
+ let html = await response.text();
+ expect(html).toMatch("MESSAGE:thisisnotathing is not defined");
+ expect(html).toMatch("
NAME:ReferenceError");
+ expect(html).toMatch(
+ "
STACK:ReferenceError: thisisnotathing is not defined",
+ );
+
+ // Hydration
+ let appFixture = await createAppFixture(fixture, ServerMode.Development);
+ try {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/?subclass", true);
+ html = await app.getHtml();
+ expect(html).toMatch("
MESSAGE:thisisnotathing is not defined");
+ expect(html).toMatch("
NAME:ReferenceError");
+ expect(html).toMatch(
+ "STACK:ReferenceError: thisisnotathing is not defined",
+ );
+ } finally {
+ appFixture.close();
+ }
+ });
+ });
+
+ test.describe("serverMode=production (user-provided handleError)", () => {
+ test.beforeAll(async () => {
+ fixture = await createFixture(
+ {
+ files: {
+ "app/entry.server.tsx": js`
+ import { PassThrough } from "node:stream";
+
+ import { createReadableStreamFromReadable } from "@react-router/node";
+ import { ServerRouter, isRouteErrorResponse } from "react-router";
+ import { renderToPipeableStream } from "react-dom/server";
+
+ export default function handleRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ ) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error) {
+ reject(error);
+ },
+ onError(error) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, 5000);
+ });
+ }
+
+ export function handleError(
+ error: unknown,
+ { request }: { request: Request },
+ ) {
+ console.error("App Specific Error Logging:");
+ console.error(" Request: " + request.method + " " + request.url);
+ if (isRouteErrorResponse(error)) {
+ console.error(" Status: " + error.status + " " + error.statusText);
+ console.error(" Error: " + error.error.message);
+ console.error(" Stack: " + error.error.stack);
+ } else if (error instanceof Error) {
+ console.error(" Error: " + error.message);
+ console.error(" Stack: " + error.stack);
+ } else {
+ console.error("Dunno what this is");
+ }
+ }
+ `,
+ ...routeFiles,
+ },
+ },
+ ServerMode.Production,
+ );
+ });
+
+ test("renders document without errors", async () => {
+ let response = await fixture.requestDocument("/");
+ let html = await response.text();
+ expect(html).toMatch("Index Route");
+ expect(html).toMatch("LOADER");
+ expect(html).not.toMatch("MESSAGE:");
+ expect(html).not.toMatch(/stack/i);
+ });
+
+ test("sanitizes loader errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?loader");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).not.toMatch("LOADER");
+ expect(html).toMatch("MESSAGE:Unexpected Server Error");
+ expect(html).toMatch('\\"SanitizedError\\"');
+ expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
+ expect(html).not.toMatch(/ at /i);
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?loader");
+ expect(errorLogs[2][0]).toEqual(" Error: Loader Error");
+ expect(errorLogs[3][0]).toMatch(" at ");
+ expect(errorLogs.length).toBe(4);
+ });
+
+ test("sanitizes render errors in document requests", async () => {
+ let response = await fixture.requestDocument("/?render");
+ let html = await response.text();
+ expect(html).toMatch("Index Error");
+ expect(html).toMatch("MESSAGE:Unexpected Server Error");
+ expect(html).toMatch('\\"SanitizedError\\"');
+ expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
+ expect(html).not.toMatch(/ at /i);
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?render");
+ expect(errorLogs[2][0]).toEqual(" Error: Render Error");
+ expect(errorLogs[3][0]).toMatch(" at ");
+ expect(errorLogs.length).toBe(4);
+ });
+
+ test("renders deferred document without errors", async () => {
+ let response = await fixture.requestDocument("/defer");
+ let html = await response.text();
+ expect(html).toMatch("Defer Route");
+ expect(html).toMatch("RESOLVED");
+ expect(html).not.toMatch("MESSAGE:");
+ // Defer errors are not part of the JSON blob but rather rejected
+ // against a pending promise and therefore are inlined JS.
+ expect(html).not.toMatch("x.stack=e.stack;");
+ });
+
+ test("sanitizes defer errors in document requests", async () => {
+ let response = await fixture.requestDocument("/defer?loader");
+ let html = await response.text();
+ expect(html).toMatch("Defer Error");
+ expect(html).not.toMatch("RESOLVED");
+ expect(html).toMatch("Unexpected Server Error");
+ expect(html).not.toMatch("stack");
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("returns data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data");
+ expect(data).toEqual({
+ "routes/_index": {
+ data: "LOADER",
+ },
+ });
+ });
+
+ test("sanitizes loader errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/_.data?loader");
+ expect(data).toEqual({
+ "routes/_index": {
+ error: new Error("Unexpected Server Error"),
+ },
+ });
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[1][0]).toEqual(
+ " Request: GET test://test/_.data?loader",
+ );
+ expect(errorLogs[2][0]).toEqual(" Error: Loader Error");
+ expect(errorLogs[3][0]).toMatch(" at ");
+ expect(errorLogs.length).toBe(4);
+ });
+
+ test("returns deferred data without errors", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data");
+ // @ts-expect-error
+ expect(await data["routes/defer"].data.lazy).toBe("RESOLVED");
+ });
+
+ test("sanitizes loader errors in deferred data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/defer.data?loader");
+ try {
+ // @ts-expect-error
+ await data["routes/defer"].data.lazy;
+ expect(true).toBe(false);
+ } catch (e) {
+ expect((e as Error).message).toBe("Unexpected Server Error");
+ expect((e as Error).stack).toBeUndefined();
+ }
+ // defer errors are not logged to the server console since the request
+ // has "succeeded"
+ expect(errorLogs.length).toBe(0);
+ });
+
+ test("sanitizes loader errors in resource requests", async () => {
+ let response = await fixture.requestResource("/resource?loader");
+ let text = await response.text();
+ expect(text).toBe("Unexpected Server Error");
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[1][0]).toEqual(
+ " Request: GET test://test/resource?loader",
+ );
+ expect(errorLogs[2][0]).toEqual(" Error: Loader Error");
+ expect(errorLogs[3][0]).toMatch(" at ");
+ expect(errorLogs.length).toBe(4);
+ });
+
+ test("does not sanitize mismatched route errors in data requests", async () => {
+ let { data } = await fixture.requestSingleFetchData("/not-a-route.data");
+ expect(data).toEqual({
+ root: {
+ error: new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ 'Error: No route matches URL "/not-a-route"',
+ ),
+ },
+ });
+ expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
+ expect(errorLogs[1][0]).toEqual(
+ " Request: GET test://test/not-a-route.data",
+ );
+ expect(errorLogs[2][0]).toEqual(" Status: 404 Not Found");
+ expect(errorLogs[3][0]).toEqual(
+ ' Error: No route matches URL "/not-a-route"',
+ );
+ expect(errorLogs[4][0]).toMatch(" at ");
+ expect(errorLogs.length).toBe(5);
+ });
+ });
+});
diff --git a/tests/react-router-framework/integration/fetch-globals-test.ts b/tests/react-router-framework/integration/fetch-globals-test.ts
new file mode 100644
index 00000000..2d07e9ad
--- /dev/null
+++ b/tests/react-router-framework/integration/fetch-globals-test.ts
@@ -0,0 +1,42 @@
+import { test, expect } from "@playwright/test";
+
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { useLoaderData } from "react-router";
+ export async function loader() {
+ const resp = await fetch('https://reqres.in/api/users?page=2');
+ return (resp instanceof Response) ? 'is an instance of global Response' : 'is not an instance of global Response';
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ return (
+
+ {data}
+
+ )
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(async () => appFixture.close());
+
+test("returned variable from fetch() should be instance of global Response", async () => {
+ let response = await fixture.requestDocument("/");
+ expect(await response.text()).toMatch("is an instance of global Response");
+});
diff --git a/tests/react-router-framework/integration/fetcher-layout-test.ts b/tests/react-router-framework/integration/fetcher-layout-test.ts
new file mode 100644
index 00000000..24ba05d6
--- /dev/null
+++ b/tests/react-router-framework/integration/fetcher-layout-test.ts
@@ -0,0 +1,268 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/layout-action.tsx": js`
+ import { Outlet, useFetcher, useFormAction } from "react-router";
+
+ export let action = ({ params }) => "layout action data";
+
+ export default function ActionLayout() {
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.submit({}, { method: "post", action });
+ };
+
+ return (
+
+
Layout
+
Invoke Fetcher
+ {!!fetcher.data &&
{fetcher.data}
}
+
+
+ );
+ }
+ `,
+
+ "app/routes/layout-action._index.tsx": js`
+ import {
+ useFetcher,
+ useFormAction,
+ useLoaderData,
+ } from "react-router";
+
+ export let loader = ({ params }) => "index data";
+
+ export let action = ({ params }) => "index action data";
+
+ export default function ActionLayoutIndex() {
+ let data = useLoaderData();
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.submit({}, { method: "post", action })
+ };
+
+ return (
+ <>
+ {data}
+ Invoke Index Fetcher
+ {!!fetcher.data && {fetcher.data}
}
+ >
+ );
+ }
+ `,
+
+ "app/routes/layout-action.$param.tsx": js`
+ import {
+ useFetcher,
+ useFormAction,
+ useLoaderData,
+ } from "react-router";
+
+ export let loader = ({ params }) => params.param;
+
+ export let action = ({ params }) => "param action data";
+
+ export default function ActionLayoutChild() {
+ let data = useLoaderData();
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.submit({}, { method: "post", action })
+ };
+
+ return (
+ <>
+ {data}
+ Invoke Param Fetcher
+ {!!fetcher.data && {fetcher.data}
}
+ >
+ );
+ }
+ `,
+
+ "app/routes/layout-loader.tsx": js`
+ import { Outlet, useFetcher, useFormAction } from "react-router";
+
+ export let loader = () => "layout loader data";
+
+ export default function LoaderLayout() {
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.load(action);
+ };
+
+ return (
+
+
Layout
+
Invoke Fetcher
+ {!!fetcher.data &&
{fetcher.data}
}
+
+
+ );
+ }
+ `,
+
+ "app/routes/layout-loader._index.tsx": js`
+ import {
+ useFetcher,
+ useFormAction,
+ useLoaderData,
+ } from "react-router";
+
+ export let loader = ({ params }) => "index data";
+
+ export default function ActionLayoutIndex() {
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.load(action);
+ };
+
+ return (
+ <>
+ Invoke Index Fetcher
+ {!!fetcher.data && {fetcher.data}
}
+ >
+ );
+ }
+ `,
+
+ "app/routes/layout-loader.$param.tsx": js`
+ import {
+ useFetcher,
+ useFormAction,
+ useLoaderData,
+ } from "react-router";
+
+ export let loader = ({ params }) => params.param;
+
+ export default function ActionLayoutChild() {
+ let fetcher = useFetcher();
+ let action = useFormAction();
+
+ let invokeFetcher = () => {
+ fetcher.load(action);
+ };
+
+ return (
+ <>
+ Invoke Param Fetcher
+ {!!fetcher.data && {fetcher.data}
}
+ >
+ );
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+test("fetcher calls layout route action when at index route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-action");
+ await app.clickElement("#layout-fetcher");
+ await expect(page.locator("#layout-fetcher-data")).toHaveText(
+ "layout action data",
+ );
+ await expect(page.locator("#child-data")).toHaveText("index data");
+});
+
+test("fetcher calls layout route loader when at index route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-loader");
+ await app.clickElement("#layout-fetcher");
+ await expect(page.locator("#layout-fetcher-data")).toHaveText(
+ "layout loader data",
+ );
+});
+
+test("fetcher calls index route action when at index route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-action");
+ await app.clickElement("#index-fetcher");
+ await expect(page.locator("#index-fetcher-data")).toHaveText(
+ "index action data",
+ );
+ await expect(page.locator("#child-data")).toHaveText("index data");
+});
+
+test("fetcher calls index route loader when at index route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-loader");
+ await app.clickElement("#index-fetcher");
+ await expect(page.locator("#index-fetcher-data")).toHaveText("index data");
+});
+
+test("fetcher calls layout route action when at parameterized route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-action/foo");
+ await app.clickElement("#layout-fetcher");
+ await expect(page.locator("#layout-fetcher-data")).toHaveText(
+ "layout action data",
+ );
+ await expect(page.locator("#child-data")).toHaveText("foo");
+});
+
+test("fetcher calls layout route loader when at parameterized route", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-loader/foo");
+ await app.clickElement("#layout-fetcher");
+ await expect(page.locator("#layout-fetcher-data")).toHaveText(
+ "layout loader data",
+ );
+});
+
+test("fetcher calls parameterized route action", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-action/foo");
+ await app.clickElement("#param-fetcher");
+ await expect(page.locator("#param-fetcher-data")).toHaveText(
+ "param action data",
+ );
+ await expect(page.locator("#child-data")).toHaveText("foo");
+});
+
+test("fetcher calls parameterized route loader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/layout-loader/foo");
+ await app.clickElement("#param-fetcher");
+ await expect(page.locator("#param-fetcher-data")).toHaveText("foo");
+});
diff --git a/tests/react-router-framework/integration/fetcher-test.ts b/tests/react-router-framework/integration/fetcher-test.ts
new file mode 100644
index 00000000..aa2368b1
--- /dev/null
+++ b/tests/react-router-framework/integration/fetcher-test.ts
@@ -0,0 +1,593 @@
+import { expect, test } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test.describe("useFetcher", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ let CHEESESTEAK = "CHEESESTEAK";
+ let LUNCH = "LUNCH";
+ let PARENT_LAYOUT_LOADER = "parent layout loader";
+ let PARENT_LAYOUT_ACTION = "parent layout action";
+ let PARENT_INDEX_LOADER = "parent index loader";
+ let PARENT_INDEX_ACTION = "parent index action";
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/resource-route-action-only.ts": js`
+ export function action() {
+ return new Response("${CHEESESTEAK}");
+ }
+ `,
+
+ "app/routes/fetcher-action-only-call.tsx": js`
+ import { useFetcher } from "react-router";
+
+ export default function FetcherActionOnlyCall() {
+ let fetcher = useFetcher();
+
+ let executeFetcher = () => {
+ fetcher.submit(new URLSearchParams(), {
+ method: 'post',
+ action: '/resource-route-action-only',
+ });
+ };
+
+ return (
+ <>
+ Click Me
+ {fetcher.data && {fetcher.data} }
+ >
+ );
+ }
+ `,
+
+ "app/routes/resource-route.tsx": js`
+ export function loader() {
+ return new Response("${LUNCH}");
+ }
+ export function action() {
+ return new Response("${CHEESESTEAK}");
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ import { useFetcher } from "react-router";
+ export default function Index() {
+ let fetcher = useFetcher();
+ return (
+ <>
+
+ get
+ post
+
+ {
+ fetcher.load('/resource-route');
+ }}>
+ load
+
+ {
+ fetcher.submit(new URLSearchParams(), {
+ method: 'post',
+ action: '/resource-route'
+ });
+ }}>
+ submit
+
+ {fetcher.data}
+ >
+ );
+ }
+ `,
+
+ "app/routes/parent.tsx": js`
+ import { Outlet } from "react-router";
+
+ export function action() {
+ return new Response("${PARENT_LAYOUT_ACTION}");
+ };
+
+ export function loader() {
+ return new Response("${PARENT_LAYOUT_LOADER}");
+ };
+
+ export default function Parent() {
+ return ;
+ }
+ `,
+
+ "app/routes/parent._index.tsx": js`
+ import { useFetcher } from "react-router";
+
+ export function action() {
+ return new Response("${PARENT_INDEX_ACTION}");
+ };
+
+ export function loader() {
+ return new Response("${PARENT_INDEX_LOADER}");
+ };
+
+ export default function ParentIndex() {
+ let fetcher = useFetcher();
+
+ return (
+ <>
+ {fetcher.data}
+ fetcher.load('/parent')}>
+ Load parent
+
+ fetcher.load('/parent?index')}>
+ Load index
+
+ fetcher.submit({})}>
+ Submit empty
+
+ fetcher.submit({}, { method: 'get', action: '/parent' })}>
+ Submit parent
+
+ fetcher.submit({}, { method: 'get', action: '/parent?index' })}>
+ Submit index
+
+ fetcher.submit({}, { method: 'post', action: '/parent' })}>
+ Submit parent
+
+ fetcher.submit({}, { method: 'post', action: '/parent?index' })}>
+ Submit index
+
+ >
+ );
+ }
+ `,
+
+ "app/routes/fetcher-echo.tsx": js`
+ import { useFetcher } from "react-router";
+
+ export async function action({ request }) {
+ await new Promise(r => setTimeout(r, 1000));
+ let contentType = request.headers.get('Content-Type');
+ let value;
+ if (contentType.includes('application/json')) {
+ let json = await request.json();
+ value = json === null ? json : json.value;
+ } else if (contentType.includes('text/plain')) {
+ value = await request.text();
+ } else {
+ value = (await request.formData()).get('value');
+ }
+ return { data: "ACTION (" + contentType + ") " + value }
+ }
+
+ export async function loader({ request }) {
+ await new Promise(r => setTimeout(r, 1000));
+ let value = new URL(request.url).searchParams.get('value');
+ return { data: "LOADER " + value }
+ }
+
+ export default function Index() {
+ let fetcherValues = [];
+ if (typeof window !== 'undefined') {
+ if (!window.fetcherValues) {
+ window.fetcherValues = [];
+ }
+ fetcherValues = window.fetcherValues
+ }
+
+ let fetcher = useFetcher();
+
+ let currentValue = fetcher.state + '/' + fetcher.data?.data;
+ if (fetcherValues[fetcherValues.length - 1] !== currentValue) {
+ fetcherValues.push(currentValue)
+ }
+
+ return (
+ <>
+
+ {
+ let value = document.getElementById('fetcher-input').value;
+ fetcher.load('/fetcher-echo?value=' + value)
+ }}>Load
+ {
+ let value = document.getElementById('fetcher-input').value;
+ fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo' })
+ }}>Submit
+ {
+ let value = document.getElementById('fetcher-input').value;
+ fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })
+ }}>Submit JSON
+ {
+ fetcher.submit(null, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })
+ }}>Submit Null JSON
+ {
+ let value = document.getElementById('fetcher-input').value;
+ fetcher.submit(value, { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })
+ }}>Submit Text
+ {
+ fetcher.submit("", { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })
+ }}>Submit Empty Text
+
+ {fetcher.state === 'idle' ? IDLE
: null}
+ {JSON.stringify(fetcherValues)}
+ >
+ );
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test.describe("No JavaScript", () => {
+ test.use({ javaScriptEnabled: false });
+
+ test("Form can hit a loader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+
+ await Promise.all([
+ page.waitForNavigation(),
+ app.clickSubmitButton("/resource-route", {
+ wait: false,
+ method: "get",
+ }),
+ ]);
+ // Check full HTML here - Chromium/Firefox/Webkit seem to render this in
+ // a but Edge puts it in some weird code editor markup:
+ //
+ // "LUNCH"
+ await page.getByText(LUNCH);
+ });
+
+ test("Form can hit an action", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: "load" }),
+ app.clickSubmitButton("/resource-route", {
+ wait: false,
+ method: "post",
+ }),
+ ]);
+ // Check full HTML here - Chromium/Firefox/Webkit seem to render this in
+ // a but Edge puts it in some weird code editor markup:
+ //
+ // "LUNCH"
+ await page.getByText(CHEESESTEAK);
+ });
+ });
+
+ test("load can hit a loader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickElement("#fetcher-load");
+ await page.waitForSelector(`pre:has-text("${LUNCH}")`);
+ });
+
+ test("submit can hit an action", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickElement("#fetcher-submit");
+ await page.waitForSelector(`pre:has-text("${CHEESESTEAK}")`);
+ });
+
+ test("submit can hit an action with json", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-echo", true);
+ await page.fill("#fetcher-input", "input value");
+ await app.clickElement("#fetcher-submit-json");
+ await page.waitForSelector(`#fetcher-idle`);
+ await page.getByText('ACTION (application/json) input value"');
+ });
+
+ test("submit can hit an action with null json", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-echo", true);
+ await app.clickElement("#fetcher-submit-json-null");
+ await new Promise((r) => setTimeout(r, 1000));
+ await page.waitForSelector(`#fetcher-idle`);
+ await page.getByText('ACTION (application/json) null"');
+ });
+
+ test("submit can hit an action with text", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-echo", true);
+ await page.fill("#fetcher-input", "input value");
+ await app.clickElement("#fetcher-submit-text");
+ await page.waitForSelector(`#fetcher-idle`);
+ await page.getByText('ACTION (text/plain;charset=UTF-8) input value"');
+ });
+
+ test("submit can hit an action with empty text", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-echo", true);
+ await app.clickElement("#fetcher-submit-text-empty");
+ await new Promise((r) => setTimeout(r, 1000));
+ await page.waitForSelector(`#fetcher-idle`);
+ await page.getByText('ACTION (text/plain;charset=UTF-8) "');
+ });
+
+ test("submit can hit an action only route", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/fetcher-action-only-call");
+ await app.clickElement("#fetcher-submit");
+ await page.waitForSelector(`pre:has-text("${CHEESESTEAK}")`);
+ });
+
+ test("fetchers handle ?index param correctly", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+
+ await app.clickElement("#load-parent");
+ await page.waitForSelector(`pre:has-text("${PARENT_LAYOUT_LOADER}")`);
+
+ await app.clickElement("#load-index");
+ await page.waitForSelector(`pre:has-text("${PARENT_INDEX_LOADER}")`);
+
+ // fetcher.submit({}) defaults to GET for the current Route
+ await app.clickElement("#submit-empty");
+ await page.waitForSelector(`pre:has-text("${PARENT_INDEX_LOADER}")`);
+
+ await app.clickElement("#submit-parent-get");
+ await page.waitForSelector(`pre:has-text("${PARENT_LAYOUT_LOADER}")`);
+
+ await app.clickElement("#submit-index-get");
+ await page.waitForSelector(`pre:has-text("${PARENT_INDEX_LOADER}")`);
+
+ await app.clickElement("#submit-parent-post");
+ await page.waitForSelector(`pre:has-text("${PARENT_LAYOUT_ACTION}")`);
+
+ await app.clickElement("#submit-index-post");
+ await page.waitForSelector(`pre:has-text("${PARENT_INDEX_ACTION}")`);
+ });
+
+ test("fetcher.load persists data through reloads", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/fetcher-echo", true);
+ await page.getByText(JSON.stringify(["idle/undefined"]));
+
+ await page.fill("#fetcher-input", "1");
+ await app.clickElement("#fetcher-load");
+ await page.waitForSelector("#fetcher-idle");
+ await page.getByText(
+ JSON.stringify(["idle/undefined", "loading/undefined", "idle/LOADER 1"]),
+ );
+
+ await page.fill("#fetcher-input", "2");
+ await app.clickElement("#fetcher-load");
+ await page.waitForSelector("#fetcher-idle");
+ await page.getByText(
+ JSON.stringify([
+ "idle/undefined",
+ "loading/undefined",
+ "idle/LOADER 1",
+ "loading/LOADER 1", // Preserves old data during reload
+ "idle/LOADER 2",
+ ]),
+ );
+ });
+
+ test("fetcher.submit persists data through resubmissions", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/fetcher-echo", true);
+ await page.getByText(JSON.stringify(["idle/undefined"]));
+
+ await page.fill("#fetcher-input", "1");
+ await app.clickElement("#fetcher-submit");
+ await page.waitForSelector("#fetcher-idle");
+ await page.getByText(
+ JSON.stringify([
+ "idle/undefined",
+ "submitting/undefined",
+ "loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
+ "idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
+ ]),
+ );
+
+ await page.fill("#fetcher-input", "2");
+ await app.clickElement("#fetcher-submit");
+ await page.waitForSelector("#fetcher-idle");
+ await page.getByText(
+ JSON.stringify([
+ "idle/undefined",
+ "submitting/undefined",
+ "loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
+ "idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
+ // Preserves old data during resubmissions
+ "submitting/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1",
+ "loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2",
+ "idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2",
+ ]),
+ );
+ });
+});
+
+test.describe("fetcher aborts and adjacent forms", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import * as React from "react";
+ import {
+ Form,
+ useFetcher,
+ useLoaderData,
+ useNavigation
+ } from "react-router";
+
+ export async function loader({ request }) {
+ // 1 second timeout on data
+ await new Promise((r) => setTimeout(r, 1000));
+ return { foo: 'bar' };
+ }
+
+ export default function Index() {
+ const [open, setOpen] = React.useState(true);
+ const { data } = useLoaderData();
+ const navigation = useNavigation();
+
+ return (
+
+ {navigation.state === 'idle' &&
Idle
}
+
+
+
setOpen(true)}>Show async form
+ {open &&
setOpen(false)} />}
+
+ );
+ }
+
+ function Child({ onClose }) {
+ const fetcher = useFetcher();
+
+ return (
+
+ Trigger fetcher (shows a message)
+ setTimeout(onClose, 250)}
+ >
+ Submit main form and close async form
+
+
+ );
+ }
+ `,
+
+ "app/routes/api.tsx": js`
+ export async function loader() {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ return { message: 'Hello world!' }
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("Unmounting a fetcher does not cancel the request of an adjacent form", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await page.waitForSelector("#submit-and-close");
+
+ // Works as expected before the fetcher is loaded
+
+ // submit the main form and unmount the fetcher form
+ await app.clickElement("#submit-and-close");
+ // Wait for our navigation state to be "Idle"
+ await page.waitForSelector("#idle", { timeout: 2000 });
+
+ // Breaks after the fetcher is loaded
+
+ // re-mount the fetcher form
+ await app.clickElement("#open");
+ await page.waitForSelector("#submit-and-close");
+ // submit the fetcher form
+ await app.clickElement("#submit-fetcher");
+ // submit the main form and unmount the fetcher form
+ await app.clickElement("#submit-and-close");
+ // Wait for navigation state to be "Idle"
+ await page.waitForSelector("#idle", { timeout: 2000 });
+ });
+});
+
+test.describe("fetcher lazy route discovery", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("skips revalidation of initial load fetchers performing lazy route discovery", async ({
+ page,
+ }) => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/parent.tsx": js`
+ import * as React from "react";
+ import { useFetcher, useNavigate, Outlet } from "react-router";
+
+ export default function Index() {
+ const fetcher = useFetcher();
+ const navigate = useNavigate();
+
+ React.useEffect(() => {
+ fetcher.load('/api');
+ }, []);
+
+ React.useEffect(() => {
+ navigate('/parent/child');
+ }, []);
+
+ return (
+ <>
+ Parent
+ {fetcher.data ?
+ {fetcher.data} :
+ null}
+
+ >
+ );
+ }
+ `,
+ "app/routes/parent.child.tsx": js`
+ export default function Index() {
+ return Child ;
+ }
+ `,
+ "app/routes/api.tsx": js`
+ export async function loader() {
+ return "FETCHED!"
+ }
+ `,
+ },
+ });
+
+ // Slow down the fetcher discovery a tiny bit so it doesn't resolve prior
+ // to the navigation
+ page.route(/\/__manifest/, async (route) => {
+ if (route.request().url().includes(encodeURIComponent("/api"))) {
+ await new Promise((r) => setTimeout(r, 100));
+ }
+ route.continue();
+ });
+
+ appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent");
+ await page.waitForSelector("h2", { timeout: 3000 });
+ await expect(page.locator("h2")).toHaveText("Child");
+ await page.waitForSelector("[data-fetcher]", { timeout: 3000 });
+ await expect(page.locator("[data-fetcher]")).toHaveText("FETCHED!");
+ });
+});
diff --git a/tests/react-router-framework/integration/fog-of-war-test.ts b/tests/react-router-framework/integration/fog-of-war-test.ts
new file mode 100644
index 00000000..03b29b05
--- /dev/null
+++ b/tests/react-router-framework/integration/fog-of-war-test.ts
@@ -0,0 +1,1798 @@
+import { test, expect } from "@playwright/test";
+import { PassThrough } from "node:stream";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import { reactRouterConfig } from "./helpers/vite.js";
+
+function getFiles() {
+ return {
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Link, Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ let [showLink, setShowLink] = React.useState(false);
+ return (
+
+
+
+
+
+
+ Home
+ /a
+ setShowLink(true)}>Show Link
+ {showLink ? /a/b : null}
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+
+ "app/routes/a.tsx": js`
+ import { Link, Outlet, useLoaderData } from "react-router";
+ export function loader({ request }) {
+ return { message: "A LOADER" };
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ return (
+ <>
+ A: {data.message}
+ /a/b
+
+ >
+ )
+ }
+ `,
+ "app/routes/a.b.tsx": js`
+ import { Outlet, useLoaderData } from "react-router";
+ export function loader({ request }) {
+ return { message: "B LOADER" };
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ return (
+ <>
+ B: {data.message}
+
+ >
+ )
+ }
+ `,
+ "app/routes/a.b.c.tsx": js`
+ import { Outlet, useLoaderData } from "react-router";
+ export function loader({ request }) {
+ return { message: "C LOADER" };
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ return C: {data.message}
+ }
+ `,
+ };
+}
+
+test.describe("Fog of War", () => {
+ let oldConsoleError: typeof console.error;
+
+ test.beforeEach(() => {
+ oldConsoleError = console.error;
+ });
+
+ test.afterEach(() => {
+ console.error = oldConsoleError;
+ });
+
+ test("loads minimal manifest on initial load", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/entry.client.tsx": js`
+ import { HydratedRouter } from "react-router/dom";
+ import { startTransition, StrictMode } from "react";
+ import { hydrateRoot } from "react-dom/client";
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+ });
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ let res = await fixture.requestDocument("/");
+ let html = await res.text();
+
+ expect(html).toContain("window.__reactRouterManifest = {");
+ expect(html).not.toContain(
+ ' A: A LOADER`);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toContain("routes/a");
+ });
+
+ test("prefetches initially rendered links", async ({ page }) => {
+ let fixture = await createFixture({
+ files: getFiles(),
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toBe(`A: A LOADER `);
+ });
+
+ test("prefetches links rendered via navigations", async ({ page }) => {
+ let fixture = await createFixture({
+ files: getFiles(),
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+
+ await page.waitForFunction(
+ () => (window as any).__reactRouterManifest.routes["routes/a.b"],
+ );
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
+ });
+
+ test("prefetches links rendered via in-page stateful updates", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: getFiles(),
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickElement("button");
+ await page.waitForFunction(
+ () => (window as any).__reactRouterManifest.routes["routes/a.b"],
+ );
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
+ });
+
+ test("prefetches links who opt-into [data-discover] via an in-page stateful update", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import * as React from 'react';
+ import { Link, Outlet, useLoaderData } from "react-router";
+ export default function Index() {
+ let [discover, setDiscover] = React.useState(false)
+ return (
+ <>
+ /a
+ setDiscover(true)}>Toggle
+ >
+ )
+ }
+ `,
+ "app/routes/a.tsx": js`
+ export default function Index() {
+ return A
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index"]);
+
+ await app.clickElement("button");
+ await page.waitForFunction(
+ () => (window as any).__reactRouterManifest.routes["routes/a"],
+ );
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+ });
+
+ test('does not prefetch links with discover="none"', async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/a.tsx": js`
+ import { Link, Outlet, useLoaderData } from "react-router";
+ export function loader({ request }) {
+ return { message: "A LOADER" };
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ return (
+ <>
+ A: {data.message}
+ /a/b
+
+ >
+ )
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ await new Promise((resolve) => setTimeout(resolve, 250));
+
+ // /a/b is not discovered yet even thought it's rendered
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ // /a/b gets discovered on click
+ await app.clickLink("/a/b");
+ await page.waitForSelector("#b");
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
+ });
+
+ test("prefetches initially rendered forms", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Form, Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ let [showLink, setShowLink] = React.useState(false);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/a.tsx": js`
+ import { useActionData } from "react-router";
+ export function action() {
+ return { message: "A ACTION" };
+ }
+ export default function Index() {
+ let actionData = useActionData();
+ return A: {actionData.message}
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ await page.waitForFunction(
+ () => (window as any).__reactRouterManifest.routes["routes/a"],
+ );
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickSubmitButton("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toBe(`A: A ACTION `);
+ });
+
+ test("prefetches forms rendered via navigations", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/a.tsx": js`
+ import { Form } from "react-router";
+ export default function Component() {
+ return (
+
+ );
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("form");
+
+ await page.waitForFunction(
+ () => (window as any).__reactRouterManifest.routes["routes/a.b"],
+ );
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
+ });
+
+ test("prefetches root index child when SSR-ing a deep route", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/deep.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Deep
+ Home
+ >
+ )
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/deep", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/deep", "routes/_index"]);
+
+ // Without pre-loading the index, we'd "match" `/` to just the root route
+ // client side and never fetch the `routes/_index` route
+ await app.clickLink("/");
+ await page.waitForSelector("#index");
+ expect(await app.getHtml("#index")).toMatch(`Index`);
+
+ expect(manifestRequests.length).toBe(0);
+ });
+
+ test("prefetches ancestor index children when SSR-ing a deep route", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ Parent Index
+ Child Index
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/parent.tsx": js`
+ import { Outlet } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Parent
+
+ >
+ )
+ }
+ `,
+ "app/routes/parent._index.tsx": js`
+ export default function Component() {
+ return Parent Index ;
+ }
+ `,
+ "app/routes/parent.child.tsx": js`
+ import { Outlet } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Child
+
+ >
+ )
+ }
+ `,
+ "app/routes/parent.child._index.tsx": js`
+ export default function Component() {
+ return Child Index ;
+ }
+ `,
+ "app/routes/parent.child.grandchild.tsx": js`
+ export default function Component() {
+ return Grandchild ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/parent/child/grandchild", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual([
+ "root",
+ "routes/parent",
+ "routes/parent.child",
+ "routes/parent.child.grandchild",
+ "routes/_index",
+ "routes/parent.child._index",
+ "routes/parent._index",
+ ]);
+
+ // Without pre-loading the index, we'd "match" `/parent/child` to just the
+ // parent and child routes client side and never fetch the
+ // `routes/parent.child._index` route
+ await app.clickLink("/parent/child");
+ await page.waitForSelector("#child-index");
+ expect(await app.getHtml("#parent")).toMatch("Parent");
+ expect(await app.getHtml("#child")).toMatch("Child");
+ expect(await app.getHtml("#child-index")).toMatch(`Child Index`);
+
+ await app.clickLink("/parent");
+ await page.waitForSelector("#parent-index");
+ expect(await app.getHtml("#parent")).toMatch(`Parent`);
+ expect(await app.getHtml("#parent-index")).toMatch(`Parent Index`);
+
+ expect(manifestRequests.length).toBe(0);
+ });
+
+ test("prefetches ancestor pathless children when SSR-ing a deep route", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ Parent Index
+ Child2
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/parent.tsx": js`
+ import { Outlet } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Parent
+
+ >
+ )
+ }
+ `,
+ "app/routes/parent.child.tsx": js`
+ export default function Component() {
+ return Child ;
+ }
+ `,
+ "app/routes/parent._a.tsx": js`
+ import { Outlet } from 'react-router';
+ export default function Component() {
+ return
;
+ }
+ `,
+ "app/routes/parent._a._b._index.tsx": js`
+ export default function Component() {
+ return Parent Pathless Index ;
+ }
+ `,
+ "app/routes/parent._a._b.tsx": js`
+ import { Outlet } from 'react-router';
+ export default function Component() {
+ return
;
+ }
+ `,
+ "app/routes/parent._a._b.child2.tsx": js`
+ export default function Component() {
+ return Child 2 ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/parent/child", true);
+ expect(await app.getHtml("#child")).toMatch("Child");
+ expect(await page.$("#a")).toBeNull();
+ expect(await page.$("#b")).toBeNull();
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual([
+ "root",
+ "routes/parent",
+ "routes/parent.child",
+ "routes/_index",
+ "routes/parent._a",
+ "routes/parent._a._b",
+ "routes/parent._a._b._index",
+ ]);
+ expect(manifestRequests).toEqual([]);
+
+ // Without pre-loading the index, we'd "match" `/parent` to just the
+ // parent route client side and never fetch the children pathless/index routes
+ await app.clickLink("/parent");
+ await page.waitForSelector("#parent-index");
+ expect(await page.$("#a")).not.toBeNull();
+ expect(await page.$("#b")).not.toBeNull();
+ expect(await app.getHtml("#parent")).toMatch("Parent");
+ expect(await app.getHtml("#parent-index")).toMatch("Parent Pathless Index");
+ expect(manifestRequests.length).toBe(0);
+
+ // This will require a new fetch for the child2 portion
+ await app.clickLink("/parent/child2");
+ await page.waitForSelector("#child2");
+ expect(await app.getHtml("#parent")).toMatch(`Parent`);
+ expect(await app.getHtml("#child2")).toMatch(`Child 2`);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/__manifest\?paths=%2Fparent%2C%2Fparent%2Fchild2&version=/,
+ ),
+ ]);
+ });
+
+ test("detects higher-ranking static routes on the server when a slug match is already known by the client", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ /something
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Slug ;
+ Go to /static
+ >
+ );
+ }
+ `,
+ "app/routes/static.tsx": js`
+ export default function Component() {
+ return Static ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/$slug"]);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fsomething&version=/),
+ ]);
+ manifestRequests = [];
+
+ await app.clickLink("/something");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug");
+ expect(manifestRequests).toEqual([]);
+
+ // This will require a new fetch for the /static route
+ await app.clickLink("/static");
+ await page.waitForSelector("#static");
+ expect(await app.getHtml("#static")).toMatch("Static");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fstatic&version=/),
+ ]);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/$slug", "routes/static"]);
+ });
+
+ test("detects higher-ranking static routes on the server when a splat match is already known by the client", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ /something
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/$.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Splat ;
+ Go to /static
+ >
+ );
+ }
+ `,
+ "app/routes/static.tsx": js`
+ export default function Component() {
+ return Static ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/$"]);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fsomething&version=/),
+ ]);
+ manifestRequests = [];
+
+ await app.clickLink("/something");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat");
+ expect(manifestRequests).toEqual([]);
+
+ // This will require a new fetch for the /static route
+ await app.clickLink("/static");
+ await page.waitForSelector("#static");
+ expect(await app.getHtml("#static")).toMatch("Static");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fstatic&version=/),
+ ]);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/$", "routes/static"]);
+ });
+
+ test("does not re-request for previously discovered slug routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ Go to /
+ Go to /a
+ Go to /b
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index ;
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Slug: {params.slug} ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click /a which will discover via a manifest request
+ await app.clickLink("/a");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: a");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fa&version=/),
+ ]);
+ manifestRequests = [];
+
+ // Go back home
+ await app.clickLink("/");
+ await page.waitForSelector("#index");
+ expect(manifestRequests).toEqual([]);
+
+ // Click /a again which will not re-discover
+ await app.clickLink("/a");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: a");
+ expect(manifestRequests).toEqual([]);
+ manifestRequests = [];
+
+ // Click /b which will need to discover
+ await app.clickLink("/b");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: b");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fb&version=/),
+ ]);
+ });
+
+ test("does not re-request for previously discovered splat routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+ Go to /
+ Go to /a
+ Go to /b/c
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index ;
+ }
+ `,
+ "app/routes/$.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Splat: {params["*"]} ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click /a which will discover via a manifest request
+ await app.clickLink("/a");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: a");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fa&version=/),
+ ]);
+ manifestRequests = [];
+
+ // Go back home
+ await app.clickLink("/");
+ await page.waitForSelector("#index");
+ expect(manifestRequests).toEqual([]);
+
+ // Click /a again which will not re-discover
+ await app.clickLink("/a");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: a");
+ expect(manifestRequests).toEqual([]);
+ manifestRequests = [];
+
+ // Click /b which will need to discover
+ await app.clickLink("/b/c");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: b/c");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fb%2C%2Fb%2Fc&version=/),
+ ]);
+ });
+
+ test("does not re-request for previously navigated 404 routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export function Layout({ children }) {
+ return (
+
+
+
+
+ Go to /
+ Go to /something
+ Go to /not/a/path
+
+ {children}
+
+
+
+ );
+ }
+ export default function Root() {
+ return ;
+ }
+ export function ErrorBoundary() {
+ return Error ;
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index ;
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Slug: {params.slug} ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click a 404 link which will try to discover via a manifest request
+ await app.clickLink("/not/a/path");
+ await page.waitForSelector("#error");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/__manifest\?paths=%2Fnot%2C%2Fnot%2Fa%2C%2Fnot%2Fa%2Fpath&version=/,
+ ),
+ ]);
+ manifestRequests = [];
+
+ // Go to a valid slug route
+ await app.clickLink("/something");
+ await page.waitForSelector("#slug");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?paths=%2Fsomething&version=/),
+ ]);
+ manifestRequests = [];
+
+ // Click the same 404 link again which will not re-discover
+ await app.clickLink("/not/a/path");
+ await page.waitForSelector("#error");
+ expect(manifestRequests).toEqual([]);
+ });
+
+ test("skips prefetching if the URL gets too large", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return (
+ <>
+ Index
+ {/* 400 links * ~19 chars per link > our 7198 char URL limit */}
+ {...new Array(400).fill(null).map((el, i) => (
+ {i}
+ ))}
+ >
+ );
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ expect(manifestRequests.length).toBe(0);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toMatch("A LOADER");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+ });
+
+ test("includes a version query parameter as a cachebuster", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return (
+ <>
+ Index
+ /a
+ /b
+ >
+ );
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/__manifest\?paths=%2F%2C%2Fa%2C%2Fb&version=[a-z0-9]{8}/,
+ ),
+ ]);
+ });
+
+ test("sorts url parameters", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export default function Index() {
+ return (
+ <>
+ Index
+ /a
+ /c
+ /e
+ /g
+ /f
+ /d
+ /b
+ >
+ );
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/__manifest\?paths=%2F%2C%2Fa%2C%2Fb%2C%2Fc%2C%2Fd%2C%2Fe%2C%2Ff%2C%2Fg/,
+ ),
+ ]);
+ });
+
+ test("handles interruptions from back to back navigations", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/routes/a.tsx": js`
+ import { Link, Outlet, useLoaderData, useNavigate } from "react-router";
+ export function loader({ request }) {
+ return { message: "A LOADER" };
+ }
+ export default function Index() {
+ let data = useLoaderData();
+ let navigate = useNavigate();
+ return (
+ <>
+ A: {data.message}
+ {
+ navigate('/a/b');
+ setTimeout(() => navigate('/a/b'), 0)
+ }}>
+ /a/b
+
+
+ >
+ )
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/a", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/a", "routes/_index"]);
+
+ // /a/b gets discovered on click
+ await app.clickElement("[data-link]");
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ expect(await (await page.$("body"))?.textContent()).not.toContain(
+ "Not Found",
+ );
+ await page.waitForSelector("#b");
+
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/a", "routes/_index", "routes/a.b"]);
+ });
+
+ test("loads ancestor index routes on navigations", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Link, Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ let [showLink, setShowLink] = React.useState(false);
+ return (
+
+
+
+
+
+
+ Home
+ /a
+ /a/b
+ /a/b/c
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/a._index.tsx": js`
+ export default function Index() {
+ return A INDEX ;
+ }
+ `,
+ "app/routes/a.b._index.tsx": js`
+ export default function Index() {
+ return B INDEX ;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index"]);
+
+ await app.clickLink("/a/b/c");
+ await page.waitForSelector("#c");
+
+ // /a/b is not discovered yet even thought it's rendered
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual([
+ "root",
+ "routes/_index",
+ "routes/a",
+ "routes/a._index",
+ "routes/a.b",
+ "routes/a.b._index",
+ "routes/a.b.c",
+ ]);
+
+ await app.clickLink("/a/b");
+ await page.waitForSelector("#b-index");
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a-index");
+ });
+
+ test("allows configuration of the manifest path", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "react-router.config.ts": reactRouterConfig({
+ routeDiscovery: { mode: "lazy", manifestPath: "/custom-manifest" },
+ }),
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let wrongManifestRequests: string[] = [];
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ wrongManifestRequests.push(req.url());
+ }
+ if (req.url().includes("/custom-manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual(["root", "routes/_index", "routes/a"]);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/custom-manifest\?paths=%2F%2C%2Fa&version=/),
+ ]);
+ manifestRequests = [];
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toBe(`A: A LOADER `);
+ // Wait for eager discovery to kick off
+ await new Promise((r) => setTimeout(r, 500));
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/custom-manifest\?paths=%2Fa%2C%2Fa%2Fb&version=/,
+ ),
+ ]);
+
+ expect(wrongManifestRequests).toEqual([]);
+ });
+
+ test("manifest version mismatch reload should preserve query parameters and hash", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ import { Link, useLocation } from "react-router";
+
+ export default function Index() {
+ const location = useLocation();
+ return (
+
+
Home
+
Location: {location.pathname + location.search + location.hash}
+
Go to Other
+
+ );
+ }
+ `,
+ "app/routes/other.tsx": js`
+ import { useLocation } from "react-router";
+
+ export default function Other() {
+ const location = useLocation();
+ return (
+
+
Other Page
+
Location: {location.pathname + location.search + location.hash}
+
+ );
+ }
+ `,
+ },
+ });
+
+ // Trigger mismatch + hard reload when trying to patch the /other route
+ await page.route(/\/__manifest/, async (route) => {
+ if (route.request().url().includes(encodeURIComponent("/other"))) {
+ await route.fulfill({
+ status: 204,
+ headers: {
+ "X-Remix-Reload-Document": "true",
+ },
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // Start on home page
+ await app.goto("/");
+ await page.waitForSelector("h1");
+ await expect(page.locator("[data-location]")).toHaveText("Location: /");
+
+ // Click link to /other with query params and hash
+ // This should trigger manifest fetch -> version mismatch -> hard reload
+ await app.clickLink("/other?token=abc123&ref=campaign#section1");
+
+ // Wait for the page to reload and render
+ await page.waitForSelector("[data-location2]", { timeout: 5000 });
+
+ // Query parameters and hash should be preserved after reload
+ await expect(page.locator("[data-location2]")).toHaveText(
+ "Location: /other?token=abc123&ref=campaign#section1",
+ );
+
+ // Also verify the URL in the browser
+ const currentUrl = page.url();
+ expect(currentUrl).toContain("token=abc123");
+ expect(currentUrl).toContain("ref=campaign");
+ expect(currentUrl).toContain("#section1");
+ });
+
+ test("Preserves meta tags on hash links in splat routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/routes.ts": js`
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
+ export default [
+ index("routes/_index.tsx"),
+ route("*", "routes/catchall.tsx"),
+ ] satisfies RouteConfig;
+ `,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export function meta() {
+ return [{ title: "Home" }];
+ }
+ export default function Index() {
+ return (
+
+
Home
+
+ Go to catchall
+
+
+ );
+ }
+ `,
+ "app/routes/catchall.tsx": js`
+ import { Link } from "react-router";
+ export function meta() {
+ return [{ title: "Catchall" }];
+ }
+ export default function Catchall() {
+ return (
+
+
Catchall route
+ Hash link
+
+ );
+ }
+ `,
+ },
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // / => /catch-all => /catch-all#hash
+ await app.goto("/");
+ expect(await page.title()).toBe("Home");
+ await page.waitForSelector("[data-testid='go-catchall']");
+ await page.click("[data-testid='go-catchall']");
+ await page.waitForSelector("[data-testid='catchall-heading']");
+ expect(await page.title()).toBe("Catchall");
+
+ await page.click("[data-testid='hash-link']");
+ // Hash navigation doesn't trigger a load event; waitForFunction polls the DOM directly
+ await page.waitForFunction(() => window.location.hash === "#hash");
+ expect(await page.title()).toBe("Catchall");
+
+ // /catch-all => /catch-all#hash
+ await app.goto("/catchall");
+ await page.waitForSelector("[data-testid='catchall-heading']");
+ expect(await page.title()).toBe("Catchall");
+
+ await page.click("[data-testid='hash-link']");
+ // Hash navigation doesn't trigger a load event; waitForFunction polls the DOM directly
+ await page.waitForFunction(() => window.location.hash === "#hash");
+ expect(await page.title()).toBe("Catchall");
+
+ appFixture.close();
+ });
+
+ test.describe("routeDiscovery=initial", () => {
+ test("loads full manifest on initial load", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...getFiles(),
+ "react-router.config.ts": reactRouterConfig({
+ routeDiscovery: { mode: "initial" },
+ }),
+ "app/entry.client.tsx": js`
+ import { HydratedRouter } from "react-router/dom";
+ import { startTransition, StrictMode } from "react";
+ import { hydrateRoot } from "react-dom/client";
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+ });
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ let app = new PlaywrightFixture(appFixture, page);
+ let res = await fixture.requestDocument("/");
+ let html = await res.text();
+
+ expect(html).not.toContain("window.__reactRouterManifest = {");
+ expect(html).toContain(
+ '
+ Object.keys((window as any).__reactRouterManifest.routes),
+ ),
+ ).toEqual([
+ "root",
+ "routes/_index",
+ "routes/a",
+ "routes/a.b",
+ "routes/a.b.c",
+ ]);
+
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toBe(`A: A LOADER `);
+ expect(manifestRequests).toEqual([]);
+ });
+
+ test("defaults to `routeDiscovery=initial` when `ssr:false` is set", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ spaMode: true,
+ files: {
+ "react-router.config.ts": reactRouterConfig({
+ ssr: false,
+ }),
+ "app/root.tsx": js`
+ import * as React from "react";
+ import { Link, Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ let [showLink, setShowLink] = React.useState(false);
+ return (
+
+
+
+
+
+
+ Home
+ /a
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+
+ "app/routes/a.tsx": js`
+ export function clientLoader({ request }) {
+ return { message: "A LOADER" };
+ }
+ export default function Index({ loaderData }) {
+ return A: {loaderData.message}
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ let app = new PlaywrightFixture(appFixture, page);
+ let res = await fixture.requestDocument("/");
+ let html = await res.text();
+
+ expect(html).toContain('"routeDiscovery":{"mode":"initial"}');
+
+ await app.goto("/", true);
+ await page.waitForSelector("#index");
+ await app.clickLink("/a");
+ await page.waitForSelector("#a");
+ expect(await app.getHtml("#a")).toBe(`A: A LOADER `);
+ expect(manifestRequests).toEqual([]);
+ });
+
+ test("Errors if you try to set routeDiscovery=lazy and ssr:false", async () => {
+ let ogConsole = console.error;
+ console.error = () => {};
+ let buildStdio = new PassThrough();
+ let err;
+ try {
+ await createFixture({
+ buildStdio,
+ spaMode: true,
+ files: {
+ ...getFiles(),
+ "react-router.config.ts": reactRouterConfig({
+ ssr: false,
+ routeDiscovery: { mode: "lazy" },
+ }),
+ },
+ });
+ } catch (e) {
+ err = e;
+ }
+
+ let chunks: Buffer[] = [];
+ let buildOutput = await new Promise((resolve, reject) => {
+ buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
+ buildStdio.on("error", (err) => reject(err));
+ buildStdio.on("end", () =>
+ resolve(Buffer.concat(chunks).toString("utf8")),
+ );
+ });
+
+ expect(err).toEqual(new Error("Build failed, check the output above"));
+ expect(buildOutput).toContain(
+ 'Error: The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`',
+ );
+ console.error = ogConsole;
+ });
+ });
+});
diff --git a/tests/react-router-framework/integration/form-data-test.ts b/tests/react-router-framework/integration/form-data-test.ts
new file mode 100644
index 00000000..3653f928
--- /dev/null
+++ b/tests/react-router-framework/integration/form-data-test.ts
@@ -0,0 +1,58 @@
+import { test, expect } from "@playwright/test";
+
+import { createFixture, js } from "./helpers/create-fixture.js";
+import type { Fixture } from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/_index.tsx": js`
+ export async function action({ request }) {
+ try {
+ await request.formData()
+ } catch {
+ return new Response("no pizza");
+ }
+ return new Response("pizza");
+ }
+ `,
+ },
+ });
+});
+
+test("invalid content-type does not crash server", async () => {
+ let response = await fixture.requestDocument("/", {
+ method: "post",
+ headers: { "content-type": "application/json" },
+ });
+ expect(await response.text()).toMatch("no pizza");
+});
+
+test("invalid urlencoded body does not crash server", async () => {
+ let response = await fixture.requestDocument("/", {
+ method: "post",
+ headers: { "content-type": "application/x-www-form-urlencoded" },
+ body: "$rofl this is totally invalid$",
+ });
+ expect(await response.text()).toMatch("pizza");
+});
+
+test("invalid multipart content-type does not crash server", async () => {
+ let response = await fixture.requestDocument("/", {
+ method: "post",
+ headers: { "content-type": "multipart/form-data" },
+ body: "$rofl this is totally invalid$",
+ });
+ expect(await response.text()).toMatch("pizza");
+});
+
+test("invalid multipart body does not crash server", async () => {
+ let response = await fixture.requestDocument("/", {
+ method: "post",
+ headers: { "content-type": "multipart/form-data; boundary=abc" },
+ body: "$rofl this is totally invalid$",
+ });
+ expect(await response.text()).toMatch("pizza");
+});
diff --git a/tests/react-router-framework/integration/form-test.ts b/tests/react-router-framework/integration/form-test.ts
new file mode 100644
index 00000000..62766272
--- /dev/null
+++ b/tests/react-router-framework/integration/form-test.ts
@@ -0,0 +1,1142 @@
+import { test, expect } from "@playwright/test";
+
+import {
+ createAppFixture,
+ createFixture,
+ js,
+} from "./helpers/create-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import { getElement, PlaywrightFixture } from "./helpers/playwright-fixture.js";
+
+test.describe("Forms", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ let KEYBOARD_INPUT = "KEYBOARD_INPUT";
+ let CHECKBOX_BUTTON = "CHECKBOX_BUTTON";
+ let ORPHAN_BUTTON = "ORPHAN_BUTTON";
+ let FORM_WITH_ACTION_INPUT = "FORM_WITH_ACTION_INPUT";
+ let FORM_WITH_ORPHAN = "FORM_WITH_ORPHAN";
+ let LUNCH = "LUNCH";
+ let CHEESESTEAK = "CHEESESTEAK";
+ let LAKSA = "LAKSA";
+ let SQUID_INK_HOTDOG = "SQUID_INK_HOTDOG";
+ let ACTION = "action";
+ let EAT = "EAT";
+
+ let STATIC_ROUTE_NO_ACTION = "static-route-none";
+ let STATIC_ROUTE_ABSOLUTE_ACTION = "static-route-abs";
+ let STATIC_ROUTE_CURRENT_ACTION = "static-route-cur";
+ let STATIC_ROUTE_PARENT_ACTION = "static-route-parent";
+ let STATIC_ROUTE_TOO_MANY_DOTS_ACTION = "static-route-too-many-dots";
+ let INDEX_ROUTE_NO_ACTION = "index-route-none";
+ let INDEX_ROUTE_NO_ACTION_POST = "index-route-none-post";
+ let INDEX_ROUTE_ABSOLUTE_ACTION = "index-route-abs";
+ let INDEX_ROUTE_CURRENT_ACTION = "index-route-cur";
+ let INDEX_ROUTE_PARENT_ACTION = "index-route-parent";
+ let INDEX_ROUTE_TOO_MANY_DOTS_ACTION = "index-route-too-many-dots";
+ let DYNAMIC_ROUTE_NO_ACTION = "dynamic-route-none";
+ let DYNAMIC_ROUTE_ABSOLUTE_ACTION = "dynamic-route-abs";
+ let DYNAMIC_ROUTE_CURRENT_ACTION = "dynamic-route-cur";
+ let DYNAMIC_ROUTE_PARENT_ACTION = "dynamic-route-parent";
+ let DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION = "dynamic-route-too-many-dots";
+ let LAYOUT_ROUTE_NO_ACTION = "layout-route-none";
+ let LAYOUT_ROUTE_ABSOLUTE_ACTION = "layout-route-abs";
+ let LAYOUT_ROUTE_CURRENT_ACTION = "layout-route-cur";
+ let LAYOUT_ROUTE_PARENT_ACTION = "layout-route-parent";
+ let LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION = "layout-route-too-many-dots";
+ let SPLAT_ROUTE_NO_ACTION = "splat-route-none";
+ let SPLAT_ROUTE_ABSOLUTE_ACTION = "splat-route-abs";
+ let SPLAT_ROUTE_CURRENT_ACTION = "splat-route-cur";
+ let SPLAT_ROUTE_PARENT_ACTION = "splat-route-parent";
+ let SPLAT_ROUTE_TOO_MANY_DOTS_ACTION = "splat-route-too-many-dots";
+
+ test.beforeEach(async ({ context }) => {
+ await context.route(/\.data$/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ route.continue();
+ });
+ });
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/get-submission.tsx": js`
+ import { useLoaderData, Form } from "react-router";
+
+ export function loader({ request }) {
+ let url = new URL(request.url);
+ return url.searchParams.toString()
+ }
+
+ export default function() {
+ let data = useLoaderData();
+ return (
+ <>
+
+
+
+
+
+
+ Orphan
+
+
+
+ {data}
+ >
+ )
+ }
+ `,
+
+ "app/routes/about.tsx": js`
+ export async function action({ request }) {
+ return { submitted: true };
+ }
+ export default function () {
+ return About ;
+ }
+ `,
+
+ "app/routes/inbox.tsx": js`
+ import { Form } from "react-router";
+ export default function() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/blog.tsx": js`
+ import { Form, Outlet } from "react-router";
+ export default function() {
+ return (
+ <>
+ Blog
+
+
+
+
+
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/blog._index.tsx": js`
+ import { Form } from "react-router";
+
+ export function loader() {
+ return { timestamp: Date.now() }
+ }
+
+ export function action() {
+ return { ok: true };
+ }
+
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ {loaderData.timestamp}
+
+
+
+
+
+
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/blog.$postId.tsx": js`
+ import { Form } from "react-router";
+ export default function() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/projects.tsx": js`
+ import { Form, Outlet } from "react-router";
+ export default function() {
+ return (
+ <>
+ Projects
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/projects._index.tsx": js`
+ export default function() {
+ return All projects
+ }
+ `,
+
+ "app/routes/projects.$.tsx": js`
+ import { Form } from "react-router";
+ export default function() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/stop-propagation.tsx": js`
+ import { Form, useActionData } from "react-router";
+
+ export async function action({ request }) {
+ let formData = await request.formData();
+ return Object.fromEntries(formData);
+ }
+
+ export default function Index() {
+ let actionData = useActionData();
+ return (
+ event.stopPropagation()}>
+ {actionData ?
{JSON.stringify(actionData)} : null}
+
+
+ )
+ }
+ `,
+
+ "app/routes/form-method.tsx": js`
+ import { Form, useActionData, useLoaderData, useSearchParams } from "react-router";
+
+ export function action({ request }) {
+ return request.method
+ }
+
+ export function loader({ request }) {
+ return request.method
+ }
+
+ export default function() {
+ let actionData = useActionData();
+ let loaderData = useLoaderData();
+ let [searchParams] = useSearchParams();
+ let formMethod = searchParams.get('method') || 'GET';
+ let submitterFormMethod = searchParams.get('submitterFormMethod') || 'GET';
+ return (
+ <>
+
+ {actionData ? {actionData} : null}
+ {loaderData}
+ >
+ )
+ }
+ `,
+
+ "app/routes/submitter.tsx": js`
+ import { Form } from "react-router";
+
+ export default function() {
+ return (
+ <>
+ Outside
+
+ >
+ )
+ }
+ `,
+
+ "app/routes/file-upload.tsx": js`
+ import { Form, useSearchParams } from "react-router";
+
+ export default function() {
+ const [params] = useSearchParams();
+ return (
+
+ )
+ }
+ `,
+
+ "app/routes/empty-file-upload.tsx": js`
+ import { Form, useActionData } from "react-router";
+
+ export async function action({ request }) {
+ let formData = await request.formData();
+ return {
+ text: formData.get('text'),
+ file: {
+ name: formData.get('file').name,
+ size: formData.get('file').size,
+ },
+ fileMultiple: formData.getAll('fileMultiple').map(f => ({
+ name: f.name,
+ size: f.size,
+ })),
+ }
+ }
+
+ export default function() {
+ const actionData = useActionData();
+ return (
+
+ )
+ }
+ `,
+
+ // Generic route for outputting url-encoded form data (either from the request body or search params)
+ //
+ // TODO: refactor other tests to use this
+ "app/routes/outputFormData.tsx": js`
+ import { useActionData, useSearchParams } from "react-router";
+
+ export async function action({ request }) {
+ const formData = await request.formData();
+ const body = new URLSearchParams();
+ for (let [key, value] of formData) {
+ body.append(
+ key,
+ value instanceof File ? await streamToString(value.stream()) : value
+ );
+ }
+ return body.toString();
+ }
+
+ export default function OutputFormData() {
+ const requestBody = useActionData();
+ const searchParams = useSearchParams()[0];
+ return ;
+ }
+ `,
+
+ "myfile.txt": "stuff",
+
+ "app/routes/pathless-layout-parent.tsx": js`
+ import { Form, Outlet, useActionData } from "react-router"
+
+ export async function action({ request }) {
+ return { submitted: true };
+ }
+ export default function () {
+ let data = useActionData();
+ return (
+ <>
+
+
+ {data?.submitted === true ? 'Submitted - Yes' : 'Submitted - No'}
+ >
+ );
+ }
+ `,
+
+ "app/routes/pathless-layout-parent._pathless.nested.tsx": js`
+ import { Outlet } from "react-router";
+
+ export default function () {
+ return (
+ <>
+ Pathless Layout
+
+ >
+ );
+ }
+ `,
+
+ "app/routes/pathless-layout-parent._pathless.nested._index.tsx": js`
+ export default function () {
+ return Pathless Layout Index
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test.describe("without JavaScript", () => {
+ test.use({ javaScriptEnabled: false });
+
+ runFormTests();
+ });
+
+ test.describe("with JavaScript", () => {
+ test.use({ javaScriptEnabled: true }); // explicitly set so we don't have to check against undefined
+
+ runFormTests();
+ });
+
+ function runFormTests() {
+ test("posts to a loader", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ // this indirectly tests that clicking SVG children in buttons works
+ await app.goto("/get-submission");
+ await app.clickSubmitButton("/get-submission", { wait: true });
+ await page.waitForSelector(`pre:has-text("${CHEESESTEAK}")`);
+ });
+
+ test("posts to a loader with an ", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/get-submission");
+ await page.locator(`#${FORM_WITH_ACTION_INPUT} button`).click();
+ await page.locator(`pre:has-text("${EAT}")`).waitFor();
+ });
+
+ test("posts to a loader with button data with click", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/get-submission");
+ await page.locator("#buttonWithValue").click();
+ await page.locator(`pre:has-text("${LAKSA}")`).waitFor();
+ });
+
+ test("posts to a loader with button data with keyboard", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/get-submission");
+ await page.focus(`#${KEYBOARD_INPUT}`);
+ await app.waitForNetworkAfter(async () => {
+ await page.keyboard.press("Enter");
+ // there can be a delay before the request gets kicked off (worse with JS disabled)
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ });
+ await page.waitForSelector(`pre:has-text("${LAKSA}")`);
+ });
+
+ test("posts with the correct checkbox data", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/get-submission");
+ await page.locator(`#${CHECKBOX_BUTTON}`).click();
+ await page.locator(`pre:has-text("${LAKSA}")`).waitFor();
+ await page.locator(`pre:has-text("${CHEESESTEAK}")`).waitFor();
+ });
+
+ test("posts button data from outside the form", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/get-submission");
+ await page.locator(`#${ORPHAN_BUTTON}`).click();
+ await page.locator(`pre:has-text("${SQUID_INK_HOTDOG}")`).waitFor();
+ });
+
+ test(
+ "when clicking on a submit button as a descendant of an element that " +
+ "stops propagation on click, still passes the clicked submit button's " +
+ "`name` and `value` props to the request payload",
+ async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/stop-propagation");
+ await app.clickSubmitButton("/stop-propagation", { wait: true });
+ await page.waitForSelector("#action-data");
+ expect(await app.getHtml()).toMatch('{"intent":"add"}');
+ },
+ );
+
+ test.describe("