diff --git a/.changeset/react-router-8-default-template.md b/.changeset/react-router-8-default-template.md new file mode 100644 index 00000000..03e007b2 --- /dev/null +++ b/.changeset/react-router-8-default-template.md @@ -0,0 +1,10 @@ +--- +'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, supports +React Router RSC mode, analyzes transformed MDX route modules for manifest +generation, and includes React Router 8/RSC examples plus framework integration +coverage. diff --git a/README.md b/README.md index 8f51f7b8..42b4d21b 100644 --- a/README.md +++ b/README.md @@ -780,6 +780,7 @@ The repository includes several examples demonstrating different use cases: | [custom-node-server](./examples/custom-node-server) | Custom Express server with SSR | 3003 | `pnpm dev` | | [cloudflare](./examples/cloudflare) | Cloudflare Workers deployment | 3004 | `pnpm dev` | | [client-only](./examples/client-only) | `.client` modules with SSR hydration | 3010 | `pnpm dev` | +| [react-router-8](./examples/react-router-8) | React Router 8 framework-mode SSR | 3020 | `pnpm dev` | | [epic-stack](./examples/epic-stack) | Full-featured Epic Stack example | 3005 | `pnpm dev` | | [federation/epic-stack](./examples/federation/epic-stack) | Module Federation host | 3006 | `pnpm dev` | | [federation/epic-stack-remote](./examples/federation/epic-stack-remote) | Module Federation remote | 3007 | `pnpm dev` | diff --git a/examples/react-router-8/.gitignore b/examples/react-router-8/.gitignore new file mode 100644 index 00000000..c08251ce --- /dev/null +++ b/examples/react-router-8/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +.env +.react-router diff --git a/examples/react-router-8/README.md b/examples/react-router-8/README.md new file mode 100644 index 00000000..d6107db5 --- /dev/null +++ b/examples/react-router-8/README.md @@ -0,0 +1,5 @@ +# React Router 8 Default Template + +This example starts as a direct copy of React Router's `integration/helpers/vite-8-template` and swaps the Vite config/scripts for `rsbuild-plugin-react-router`. + +Run `pnpm --filter react-router-8-default-template test:e2e` to exercise development and production browser flows. diff --git a/examples/react-router-8/app/root.tsx b/examples/react-router-8/app/root.tsx new file mode 100644 index 00000000..b36392b4 --- /dev/null +++ b/examples/react-router-8/app/root.tsx @@ -0,0 +1,19 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; + +export default function App() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/examples/react-router-8/app/routes.ts b/examples/react-router-8/app/routes.ts new file mode 100644 index 00000000..4c05936c --- /dev/null +++ b/examples/react-router-8/app/routes.ts @@ -0,0 +1,4 @@ +import { type RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; diff --git a/examples/react-router-8/app/routes/_index.tsx b/examples/react-router-8/app/routes/_index.tsx new file mode 100644 index 00000000..ecfc25c6 --- /dev/null +++ b/examples/react-router-8/app/routes/_index.tsx @@ -0,0 +1,16 @@ +import type { MetaFunction } from "react-router"; + +export const meta: MetaFunction = () => { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +}; + +export default function Index() { + return ( +
+

Welcome to React Router

+
+ ); +} diff --git a/examples/react-router-8/env.d.ts b/examples/react-router-8/env.d.ts new file mode 100644 index 00000000..5e7dfe5d --- /dev/null +++ b/examples/react-router-8/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/react-router-8/package.json b/examples/react-router-8/package.json new file mode 100644 index 00000000..52999308 --- /dev/null +++ b/examples/react-router-8/package.json @@ -0,0 +1,42 @@ +{ + "name": "react-router-8-default-template", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-global-webcrypto\" rsbuild dev --port 3020 --host 127.0.0.1", + "build": "rsbuild build", + "start": "HOST=127.0.0.1 PORT=3020 react-router-serve ./build/server/static/js/app.js", + "test:e2e": "pnpm run test:e2e:dev && pnpm run build && pnpm run test:e2e:prod", + "test:e2e:dev": "playwright test", + "test:e2e:prod": "cross-env RR8_E2E_MODE=production playwright test", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@react-router/fs-routes": "^8.0.1", + "@react-router/node": "^8.0.1", + "@react-router/serve": "^8.0.1", + "@vanilla-extract/css": "^1.20.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^8.0.1", + "serialize-javascript": "^6.0.1" + }, + "devDependencies": { + "@playwright/test": "^1.58.0", + "@react-router/dev": "^8.0.1", + "@rsbuild/core": "2.1.0", + "@rsbuild/plugin-react": "2.1.0", + "@types/node": "^25.0.10", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "cross-env": "^10.1.0", + "rsbuild-plugin-react-router": "workspace:*", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-env-only": "^3.0.3" + }, + "engines": { + "node": ">=22.22.0" + } +} diff --git a/examples/react-router-8/playwright.config.ts b/examples/react-router-8/playwright.config.ts new file mode 100644 index 00000000..304b899b --- /dev/null +++ b/examples/react-router-8/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +const isProduction = process.env.RR8_E2E_MODE === 'production'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { + timeout: 10_000, + }, + use: { + baseURL: 'http://127.0.0.1:3020', + trace: 'on-first-retry', + }, + webServer: { + command: isProduction ? 'pnpm run start' : 'pnpm run dev', + url: 'http://127.0.0.1:3020', + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/examples/react-router-8/public/favicon.ico b/examples/react-router-8/public/favicon.ico new file mode 100644 index 00000000..5dbdfcdd Binary files /dev/null and b/examples/react-router-8/public/favicon.ico differ 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/examples/rsc-mode/.gitignore b/examples/rsc-mode/.gitignore new file mode 100644 index 00000000..74818a80 --- /dev/null +++ b/examples/rsc-mode/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ +test-results +.rspack-profile-* diff --git a/examples/rsc-mode/README.md b/examples/rsc-mode/README.md new file mode 100644 index 00000000..ee1365af --- /dev/null +++ b/examples/rsc-mode/README.md @@ -0,0 +1,25 @@ +# RSC Mode Example + +This example is a small React Router RSC Framework Mode app wired for `rsbuild-plugin-react-router`. + +It includes: + +- `pluginReactRouter({ rsc: true })` in `rsbuild.config.ts` +- RSC-safe React Router config without `splitRouteModules` or `subResourceIntegrity` +- A server-first index route using `ServerComponent` +- A loader that returns a React element rendered on the server +- A `"use client"` island mounted inside the server-first route +- A client-first route for soft navigation coverage +- A Playwright smoke test for dev and production mode + +## Commands + +```sh +pnpm run dev +pnpm run build +pnpm run test:e2e +``` + +## Notes + +This example uses `react-server-dom-rspack` and `rsbuild-plugin-rsc`; it does not use the Vite RSC runtime packages from the upstream React Router template. diff --git a/examples/rsc-mode/app/client-counter.tsx b/examples/rsc-mode/app/client-counter.tsx new file mode 100644 index 00000000..d14946b6 --- /dev/null +++ b/examples/rsc-mode/app/client-counter.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useState } from 'react'; + +export function ClientCounter() { + const [count, setCount] = useState(0); + + return ( + + ); +} diff --git a/examples/rsc-mode/app/root.tsx b/examples/rsc-mode/app/root.tsx new file mode 100644 index 00000000..ac47312f --- /dev/null +++ b/examples/rsc-mode/app/root.tsx @@ -0,0 +1,64 @@ +import type { Route } from './+types/root'; +import { + isRouteErrorResponse, + Link, + Links, + Meta, + Outlet, +} from 'react-router'; + +import './styles.css'; + +export function meta() { + return [ + { title: 'Rsbuild RSC example' }, + { + name: 'description', + content: 'React Router RSC Framework Mode with Rsbuild', + }, + ]; +} + +export function ServerLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + +
+ + RSC Mode + + +
+
{children}
+ + + ); +} + +export function ServerComponent() { + return ; +} + +export function ServerErrorBoundary({ error }: Route.ServerErrorBoundaryProps) { + const message = isRouteErrorResponse(error) + ? `${error.status} ${error.statusText}` + : error instanceof Error + ? error.message + : 'Unknown error'; + + return ( +
+

Route error

+

{message}

+
+ ); +} diff --git a/examples/rsc-mode/app/routes.ts b/examples/rsc-mode/app/routes.ts new file mode 100644 index 00000000..9f976145 --- /dev/null +++ b/examples/rsc-mode/app/routes.ts @@ -0,0 +1,6 @@ +import { type RouteConfig, index, route } from '@react-router/dev/routes'; + +export default [ + index('routes/_index.tsx'), + route('client', 'routes/client.tsx'), +] satisfies RouteConfig; diff --git a/examples/rsc-mode/app/routes/_index.tsx b/examples/rsc-mode/app/routes/_index.tsx new file mode 100644 index 00000000..555f533c --- /dev/null +++ b/examples/rsc-mode/app/routes/_index.tsx @@ -0,0 +1,32 @@ +import type { Route } from './+types/_index'; +import { Link } from 'react-router'; + +import { ClientCounter } from '~/client-counter'; +import { getRscShowcase } from '~/rsc-data'; + +export async function loader() { + return getRscShowcase(); +} + +export function ServerComponent({ loaderData }: Route.ServerComponentProps) { + return ( +
+
+

Rsbuild React Router RSC

+

{loaderData.message}

+

{loaderData.element}

+
+ +
+

+ This route renders through React Router RSC Framework Mode and mounts a + small client island inside the server-first route. +

+ + + Visit the client-first route + +
+
+ ); +} diff --git a/examples/rsc-mode/app/routes/client.tsx b/examples/rsc-mode/app/routes/client.tsx new file mode 100644 index 00000000..a570d7ef --- /dev/null +++ b/examples/rsc-mode/app/routes/client.tsx @@ -0,0 +1,16 @@ +export function meta() { + return [{ title: 'Rsbuild RSC example' }]; +} + +export default function ClientRoute() { + return ( +
+

Client-first route

+

+ This route uses the conventional default route component export so the + example exercises navigation between server-first and client-first + routes. +

+
+ ); +} diff --git a/examples/rsc-mode/app/rsc-data.tsx b/examples/rsc-mode/app/rsc-data.tsx new file mode 100644 index 00000000..4cb9e0c8 --- /dev/null +++ b/examples/rsc-mode/app/rsc-data.tsx @@ -0,0 +1,10 @@ +export async function getRscShowcase() { + return { + message: 'Message rendered by an RSC loader', + element: ( + + React element returned from the server loader + + ), + }; +} diff --git a/examples/rsc-mode/app/styles.css b/examples/rsc-mode/app/styles.css new file mode 100644 index 00000000..66e37d6c --- /dev/null +++ b/examples/rsc-mode/app/styles.css @@ -0,0 +1,150 @@ +:root { + color: #172033; + background: #f7f9fc; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + radial-gradient(circle at 20% 10%, rgba(69, 125, 255, 0.16), transparent 28rem), + linear-gradient(135deg, #f7f9fc 0%, #ffffff 46%, #eef6f1 100%); +} + +a { + color: inherit; +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1.25rem clamp(1rem, 4vw, 3rem); + border-bottom: 1px solid rgba(23, 32, 51, 0.12); + background: rgba(255, 255, 255, 0.82); +} + +.brand { + color: #0f5132; + font-size: 1.125rem; + font-weight: 800; + text-decoration: none; +} + +nav { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.75rem; +} + +nav a, +.text-link { + color: #274c77; + font-weight: 700; + text-decoration: none; +} + +nav a:hover, +.text-link:hover { + text-decoration: underline; +} + +.page-shell { + width: min(100% - 2rem, 1040px); + margin: 0 auto; + padding: clamp(2rem, 8vw, 5rem) 0; +} + +.hero-panel { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(18rem, 0.85fr); + gap: clamp(1.5rem, 5vw, 4rem); + align-items: center; +} + +.hero-copy h1, +.client-route h1 { + max-width: 12ch; + margin: 0 0 1rem; + color: #172033; + font-size: clamp(2.5rem, 8vw, 5.25rem); + line-height: 0.96; +} + +.hero-copy p, +.client-route p, +.interaction-panel p { + color: #475569; + font-size: 1.05rem; + line-height: 1.7; +} + +.server-element { + margin-top: 1.5rem; +} + +.server-element strong { + display: inline-block; + border-left: 4px solid #0f766e; + padding: 0.5rem 0 0.5rem 1rem; + color: #0f5132; +} + +.interaction-panel { + display: grid; + gap: 1rem; + padding: 1.5rem; + border: 1px solid rgba(23, 32, 51, 0.12); + border-radius: 8px; + background: rgba(255, 255, 255, 0.88); + box-shadow: 0 24px 60px rgba(36, 54, 92, 0.12); +} + +.counter-button { + min-height: 2.75rem; + border: 0; + border-radius: 6px; + padding: 0.7rem 1rem; + background: #172033; + color: white; + font: inherit; + font-weight: 800; + cursor: pointer; +} + +.counter-button:hover { + background: #274c77; +} + +.client-route { + max-width: 720px; +} + +@media (max-width: 720px) { + .site-header, + .hero-panel { + align-items: flex-start; + } + + .hero-panel { + grid-template-columns: 1fr; + } + + .site-header { + flex-direction: column; + } + + nav { + justify-content: flex-start; + } +} diff --git a/examples/rsc-mode/package.json b/examples/rsc-mode/package.json new file mode 100644 index 00000000..77bec69d --- /dev/null +++ b/examples/rsc-mode/package.json @@ -0,0 +1,41 @@ +{ + "name": "rsc-mode-example", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "NODE_OPTIONS=\"--experimental-vm-modules --experimental-global-webcrypto\" rsbuild dev --port 3021 --host 127.0.0.1", + "build": "rsbuild build", + "start": "HOST=127.0.0.1 PORT=3021 node start.js", + "typecheck": "react-router typegen && tsc", + "test:e2e": "pnpm run test:e2e:dev && pnpm run build && pnpm run test:e2e:prod", + "test:e2e:dev": "playwright test", + "test:e2e:prod": "cross-env RSC_E2E_MODE=production playwright test" + }, + "dependencies": { + "@react-router/node": "^8.0.1", + "@react-router/serve": "^8.0.1", + "@remix-run/node-fetch-server": "^0.13.3", + "express": "^4.22.2", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^8.0.1", + "react-server-dom-rspack": "0.0.2" + }, + "devDependencies": { + "@playwright/test": "^1.58.0", + "@react-router/dev": "^8.0.1", + "@rsbuild/core": "2.1.0", + "@rsbuild/plugin-react": "2.1.0", + "@types/node": "^25.0.10", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "cross-env": "^10.1.0", + "rsbuild-plugin-react-router": "workspace:*", + "rsbuild-plugin-rsc": "^0.1.1", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.22.0" + } +} diff --git a/examples/rsc-mode/playwright.config.ts b/examples/rsc-mode/playwright.config.ts new file mode 100644 index 00000000..923c5bc8 --- /dev/null +++ b/examples/rsc-mode/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +const isProduction = process.env.RSC_E2E_MODE === 'production'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { + timeout: 10_000, + }, + use: { + baseURL: 'http://127.0.0.1:3021', + trace: 'on-first-retry', + }, + webServer: { + command: isProduction ? 'pnpm run start' : 'pnpm run dev', + url: 'http://127.0.0.1:3021', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/examples/rsc-mode/react-router.config.ts b/examples/rsc-mode/react-router.config.ts new file mode 100644 index 00000000..564b76a0 --- /dev/null +++ b/examples/rsc-mode/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + routeDiscovery: { mode: 'initial' }, +} satisfies Config; diff --git a/examples/rsc-mode/rsbuild.config.ts b/examples/rsc-mode/rsbuild.config.ts new file mode 100644 index 00000000..ebb03455 --- /dev/null +++ b/examples/rsc-mode/rsbuild.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginReactRouter } from 'rsbuild-plugin-react-router'; + +type ReactRouterRscOptions = NonNullable< + Parameters[0] +> & { + rsc: true; +}; + +const reactRouterRscOptions = { + rsc: true, +} satisfies ReactRouterRscOptions; + +export default defineConfig({ + plugins: [pluginReact(), pluginReactRouter(reactRouterRscOptions)], +}); diff --git a/examples/rsc-mode/start.js b/examples/rsc-mode/start.js new file mode 100644 index 00000000..e1ef7adb --- /dev/null +++ b/examples/rsc-mode/start.js @@ -0,0 +1,23 @@ +import { createRequestListener } from '@remix-run/node-fetch-server'; +import express from 'express'; +import build from './build/server/index.js'; + +const app = express(); +const port = Number(process.env.PORT ?? 3021); +const host = process.env.HOST ?? '127.0.0.1'; + +app.use('/', express.static('build/client', { index: false })); + +app.get('/.well-known/appspecific/com.chrome.devtools.json', (_req, res) => { + res.status(404).send('Not Found'); +}); + +if (typeof build?.fetch !== 'function') { + throw new Error('Expected build/server/index.js to export default.fetch'); +} + +app.use(createRequestListener(build.fetch)); + +app.listen(port, host, () => { + console.log(`Server listening on http://${host}:${port}`); +}); diff --git a/examples/rsc-mode/tests/e2e/rsc-mode.test.ts b/examples/rsc-mode/tests/e2e/rsc-mode.test.ts new file mode 100644 index 00000000..1653a9f0 --- /dev/null +++ b/examples/rsc-mode/tests/e2e/rsc-mode.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; + +test('renders server-first RSC routes and hydrates the client island', 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: 'Rsbuild React Router RSC' }) + ).toBeVisible(); + await expect(page.getByTestId('server-message')).toHaveText( + 'Message rendered by an RSC loader' + ); + await expect(page.getByTestId('server-element')).toHaveText( + 'React element returned from the server loader' + ); + expect(await page.content()).toMatch( + /\(self\.__FLIGHT_DATA\|\|=\[\]\)\.push\(/ + ); + + const counter = page.getByRole('button', { name: /Client island count:/ }); + await expect(counter).toHaveText('Client island count: 0'); + await counter.click(); + await expect(counter).toHaveText('Client island count: 1'); + + await page.getByRole('link', { name: 'Client route' }).click(); + await expect( + page.getByRole('heading', { name: 'Client-first route' }) + ).toBeVisible(); + await expect(page).toHaveTitle('Rsbuild RSC example'); + + await page.waitForTimeout(250); + expect(browserProblems).toEqual([]); +}); diff --git a/examples/rsc-mode/tsconfig.json b/examples/rsc-mode/tsconfig.json new file mode 100644 index 00000000..2e0923ff --- /dev/null +++ b/examples/rsc-mode/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["**/*.ts", "**/*.tsx", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "@react-router/dev/rsc-types"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/package.json b/package.json index 83d26f76..7adf20ae 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,18 @@ "types": "./dist/templates/entry.server.d.ts", "import": "./dist/templates/entry.server.js", "require": "./dist/templates/entry.server.cjs" + }, + "./templates/entry.rsc": { + "types": "./dist/templates/entry.rsc.d.ts", + "import": "./dist/templates/entry.rsc.js" + }, + "./templates/entry.rsc.client": { + "types": "./dist/templates/entry.rsc.client.d.ts", + "import": "./dist/templates/entry.rsc.client.js" + }, + "./templates/entry.rsc.ssr": { + "types": "./dist/templates/entry.rsc.ssr.d.ts", + "import": "./dist/templates/entry.rsc.ssr.js" } }, "main": "./dist/index.cjs", @@ -57,7 +69,7 @@ "bench:baseline": "node scripts/bench-builds.mts --profile default --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/baseline", "bench:full": "node scripts/bench-builds.mts --profile full --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/full", "bench:large": "node scripts/bench-builds.mts --profile large --iterations 1 --warmup 0 --clean cold --format both --out .benchmark/results/large", - "e2e": "pnpm build && pnpm test:package-interop && 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,rsc-mode}' test:e2e", "dev": "rslib build --watch", "test": "rstest run", "test:watch": "rstest watch", @@ -66,6 +78,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.mts", + "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,8 +88,8 @@ "release:local": "pnpm build && changeset version && changeset publish && git add . && git commit -m \"chore: version packages\" && git push && git push --tags" }, "dependencies": { - "@react-router/node": "^7.13.0", - "@remix-run/node-fetch-server": "^0.13.0", + "@react-router/node": "^7.0.0 || ^8.0.0", + "@remix-run/node-fetch-server": "^0.13.3", "@rspack/plugin-react-refresh": "^2.0.2", "effect": "^3.21.4", "execa": "^9.6.1", @@ -91,9 +105,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", @@ -105,19 +125,47 @@ "@types/node": "^25.0.10", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", + "@types/webpack-env": "^1.18.8", + "@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" + "react-server-dom-rspack": "0.0.2", + "rsbuild-plugin-rsc": "^0.1.1", + "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": { - "@rsbuild/core": "^2.0.0" + "@react-router/dev": "^7.0.0 || ^8.0.0", + "@rsbuild/core": "^2.0.0", + "react-router": "^7.0.0 || ^8.0.0", + "react-server-dom-rspack": "0.0.2", + "rsbuild-plugin-rsc": "^0.1.1" + }, + "peerDependenciesMeta": { + "react-server-dom-rspack": { + "optional": true + }, + "rsbuild-plugin-rsc": { + "optional": true + } }, "publishConfig": { "access": "public", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33ca5adb..b3707822 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,10 +12,10 @@ importers: .: dependencies: '@react-router/node': - specifier: ^7.13.0 + 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 @@ -57,15 +57,33 @@ 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@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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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/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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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': + 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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/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)(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)) @@ -99,9 +117,33 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.10) + '@types/webpack-env': + specifier: ^1.18.8 + version: 1.18.8 + '@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 @@ -109,8 +151,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 @@ -126,9 +168,33 @@ 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) + react-server-dom-rspack: + specifier: 0.0.2 + version: 0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + rsbuild-plugin-rsc: + specifier: ^0.1.1 + version: 0.1.1(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0))(react-server-dom-rspack@0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(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: @@ -174,7 +240,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -232,7 +298,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -293,7 +359,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -357,7 +423,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -483,7 +549,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(xhvfyiezg3y7kimmwytekynbn4))(typescript@5.9.3) '@remix-run/server-runtime': specifier: 2.17.4 version: 2.17.4(typescript@5.9.3) @@ -600,7 +666,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:../.. @@ -646,7 +712,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(xhvfyiezg3y7kimmwytekynbn4) '@rstest/core': specifier: 0.8.1 version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0)) @@ -865,7 +931,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(xhvfyiezg3y7kimmwytekynbn4))(typescript@5.9.3) '@remix-run/server-runtime': specifier: 2.17.4 version: 2.17.4(typescript@5.9.3) @@ -982,7 +1048,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:../../.. @@ -1025,7 +1091,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(xhvfyiezg3y7kimmwytekynbn4) '@rstest/core': specifier: 0.8.1 version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0)) @@ -1229,7 +1295,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(xhvfyiezg3y7kimmwytekynbn4))(typescript@5.9.3) '@remix-run/server-runtime': specifier: 2.17.4 version: 2.17.4(typescript@5.9.3) @@ -1346,7 +1412,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:../../.. @@ -1389,7 +1455,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(xhvfyiezg3y7kimmwytekynbn4) '@rstest/core': specifier: 0.8.1 version: 0.8.1(jsdom@27.4.0(@noble/hashes@2.2.0)) @@ -1537,7 +1603,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -1587,6 +1653,131 @@ 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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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/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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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) + '@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/rsc-mode: + dependencies: + '@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) + '@remix-run/node-fetch-server': + specifier: ^0.13.3 + version: 0.13.3 + express: + specifier: ^4.22.2 + version: 4.22.2 + 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) + react-server-dom-rspack: + specifier: 0.0.2 + version: 0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + devDependencies: + '@playwright/test': + specifier: ^1.58.0 + version: 1.61.1 + '@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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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) + '@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:../.. + rsbuild-plugin-rsc: + specifier: ^0.1.1 + version: 0.1.1(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0))(react-server-dom-rspack@0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + examples/spa-mode: dependencies: '@react-router/express': @@ -1616,7 +1807,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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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) @@ -2087,6 +2278,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==} @@ -2484,6 +2678,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'} @@ -2717,6 +2931,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==} @@ -3230,6 +3455,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==} @@ -4040,6 +4270,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'} @@ -4080,6 +4331,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'} @@ -4087,6 +4348,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==} @@ -4130,6 +4398,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] @@ -4296,6 +4567,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: @@ -5056,12 +5335,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==} @@ -5081,6 +5366,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==} @@ -5093,9 +5381,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==} @@ -5155,6 +5452,15 @@ 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/webpack-env@1.18.8': + resolution: {integrity: sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -5217,6 +5523,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] @@ -5327,12 +5636,29 @@ 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} 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'} @@ -5598,6 +5924,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-loose@8.5.2: + resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} + engines: {node: '>=0.4.0'} + acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -5745,10 +6075,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} @@ -5760,9 +6097,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==} @@ -5923,6 +6266,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==} @@ -5946,9 +6292,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'} @@ -6019,6 +6384,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==} @@ -6035,6 +6403,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'} @@ -6091,6 +6466,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==} @@ -6271,6 +6649,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'} @@ -6294,6 +6675,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'} @@ -6309,6 +6693,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'} @@ -6332,6 +6720,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==} @@ -6406,6 +6797,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==} @@ -6443,6 +6837,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'} @@ -6510,6 +6908,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'} @@ -6646,6 +7050,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'} @@ -6703,6 +7128,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==} @@ -6788,6 +7216,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'} @@ -6796,6 +7233,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==} @@ -6891,6 +7332,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'} @@ -7011,6 +7456,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 @@ -7043,6 +7497,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==} @@ -7134,6 +7591,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: @@ -7156,6 +7616,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'} @@ -7195,6 +7661,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'} @@ -7224,6 +7693,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'} @@ -7387,9 +7859,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 @@ -7620,6 +8099,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 @@ -7661,6 +8143,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'} @@ -7670,9 +8156,39 @@ 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==} + 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'} @@ -7703,12 +8219,96 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - mime-db@1.33.0: - resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + 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'} + + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} engines: {node: '>= 0.6'} mime-db@1.52.0: @@ -7821,6 +8421,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==} @@ -8077,6 +8680,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'} @@ -8097,6 +8703,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==} @@ -8216,11 +8831,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + playwright@1.58.0: resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} engines: {node: '>=18'} hasBin: true + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -8441,6 +9066,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'} @@ -8448,6 +9076,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==} @@ -8483,6 +9115,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'} @@ -8601,6 +9236,32 @@ 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-server-dom-rspack@0.0.2: + resolution: {integrity: sha512-PowIm+Z65LkvC3yk4t3YrH2nMejwL361jiBT0BMW2bQCDUaNQknDHMPH0aTgMtJ9ZZ1Gc7KFgBvwB0itSzzELQ==} + engines: {node: '>=0.10.0'} + peerDependencies: + '@rspack/core': ^2.0.0-0 + react: ^19.1.0 + react-dom: ^19.1.0 + + react-server-dom-webpack@19.2.7: + resolution: {integrity: sha512-bYfuvqPJnHB4CHo2Ze7fQyJAO77TUu1W/aKFt81ICXbLiOTg5jHaO/NPDlpvGWUALoEGaGb7Oi1uXAaO+Bw6jw==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.7 + react-dom: ^19.2.7 + webpack: ^5.59.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -8645,6 +9306,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'} @@ -8667,6 +9342,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} @@ -8810,6 +9497,12 @@ packages: typescript: optional: true + rsbuild-plugin-rsc@0.1.1: + resolution: {integrity: sha512-aDITXfKR7D6LudDZeErVIzftgVRK6qoA77Mt/Lnp90qXgRRT3X63yOq4bx8bRPeFXIp1Rp+KS3VJq1K9oWmkMQ==} + peerDependencies: + '@rsbuild/core': ^2.0.0 + react-server-dom-rspack: '*' + rslog@2.1.3: resolution: {integrity: sha512-DCUkRKUBR1lSpHKRcxNvHaYwGrUVf9MsoE1u6gd0CF37I8vwwtWc4b+FA9OwYZ4QA/shslzAYorD3MMfd+Rs/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9003,6 +9696,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'} @@ -9084,6 +9780,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'} @@ -9158,6 +9858,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==} @@ -9185,6 +9888,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'} @@ -9244,6 +9952,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'} @@ -9272,6 +9983,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'} @@ -9280,6 +9995,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'} @@ -9407,6 +10131,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'} @@ -9438,6 +10168,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'} @@ -9516,6 +10249,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'} @@ -9603,6 +10357,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: @@ -9658,10 +10418,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==} @@ -9696,6 +10469,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'} @@ -9860,6 +10638,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': {} @@ -9884,7 +10665,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: @@ -10395,6 +11176,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': {} @@ -10676,6 +11459,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 @@ -10877,6 +11676,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 @@ -11531,6 +12369,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 @@ -12174,7 +13016,60 @@ 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(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)(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 + '@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': 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': 0.13.3 + arg: 5.0.2 + babel-dead-code-elimination: 1.0.12 + chokidar: 4.0.3 + dedent: 1.7.2 + es-module-lexer: 1.7.0 + exit-hook: 2.2.1 + isbot: 5.1.34 + jsesc: 3.0.2 + lodash: 4.18.1 + p-map: 7.0.4 + pathe: 1.1.2 + picocolors: 1.1.1 + pkg-types: 2.3.1 + prettier: 3.8.1 + react-refresh: 0.14.2 + react-router: 7.18.0(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) + 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)) + typescript: 5.9.3 + wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@react-router/dev@7.18.0(xhvfyiezg3y7kimmwytekynbn4)': dependencies: '@babel/core': 7.29.7 '@babel/generator': 7.29.7 @@ -12208,6 +13103,8 @@ 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16)))(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-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16)) typescript: 5.9.3 wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1) transitivePeerDependencies: @@ -12225,7 +13122,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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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 @@ -12257,6 +13154,50 @@ 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) + '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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 + '@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) + '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)) typescript: 5.9.3 wrangler: 4.105.0(@cloudflare/workers-types@4.20260628.1) transitivePeerDependencies: @@ -12287,6 +13228,44 @@ 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) + 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 + + '@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@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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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': 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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)) + minimatch: 10.2.5 + 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))(@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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': 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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)) + 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 @@ -12308,18 +13287,31 @@ 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/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: - '@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)) + '@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/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)': + '@react-router/remix-routes-option-adapter@7.13.0(@react-router/dev@7.18.0(xhvfyiezg3y7kimmwytekynbn4))(typescript@5.9.3)': dependencies: - '@mjackson/node-fetch-server': 0.2.0 - '@react-router/express': 7.18.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': 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) - compression: 1.8.1 + '@react-router/dev': 7.18.0(xhvfyiezg3y7kimmwytekynbn4) + 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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': 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-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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/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)': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + '@react-router/express': 7.18.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': 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) + compression: 1.8.1 express: 4.22.2 get-port: 5.1.1 morgan: 1.10.1 @@ -12329,6 +13321,36 @@ 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 + + '@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)': @@ -12373,6 +13395,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 @@ -12496,6 +13521,15 @@ snapshots: - '@rspack/core' - webpack + '@rsbuild/plugin-mdx@1.1.3(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1)(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) @@ -13382,6 +14416,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': @@ -13389,6 +14427,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': {} @@ -13415,6 +14457,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': {} @@ -13425,10 +14471,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 @@ -13496,6 +14550,12 @@ snapshots: dependencies: '@types/node': 25.0.10 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/webpack-env@1.18.8': {} + '@types/ws@8.18.1': dependencies: '@types/node': 25.0.10 @@ -13591,6 +14651,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 @@ -13661,6 +14723,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 @@ -13673,6 +14753,40 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16)))(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)) + optionalDependencies: + react-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16)) + optional: true + + '@vitejs/plugin-rsc@0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)))(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)) + optionalDependencies: + react-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.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 @@ -13891,6 +15005,11 @@ snapshots: dependencies: acorn: 8.17.0 + acorn-loose@8.5.2: + dependencies: + acorn: 8.17.0 + optional: true + acorn-walk@8.3.5: dependencies: acorn: 8.17.0 @@ -14055,8 +15174,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 @@ -14070,6 +15193,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 @@ -14079,6 +15212,8 @@ snapshots: transitivePeerDependencies: - supports-color + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -14258,6 +15393,8 @@ snapshots: caniuse-lite@1.0.30001799: {} + ccount@2.0.1: {} + chain-function@1.0.1: {} chalk-template@0.4.0: @@ -14279,8 +15416,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 @@ -14349,6 +15517,8 @@ snapshots: clsx@2.1.1: {} + collapse-white-space@2.1.0: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -14363,6 +15533,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: {} @@ -14414,6 +15590,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@3.1.1: {} + cookie-signature@1.0.7: {} cookie-signature@1.2.2: {} @@ -14489,7 +15667,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: {} @@ -14580,6 +15758,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 @@ -14594,6 +15776,8 @@ snapshots: deep-is@0.1.4: {} + deep-object-diff@1.1.9: {} + deepmerge@4.3.1: {} defaults@1.0.4: @@ -14612,6 +15796,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: {} @@ -14624,6 +15810,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: @@ -14691,6 +15881,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 @@ -14739,6 +15934,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + entities@8.0.0: {} envinfo@7.21.0: {} @@ -14873,6 +16070,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 @@ -15088,6 +16299,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: {} @@ -15219,6 +16463,8 @@ snapshots: exsolve@1.1.0: {} + extend@3.0.2: {} + extendable-error@0.1.7: {} fast-check@3.23.2: @@ -15313,6 +16559,8 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -15322,6 +16570,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: {} @@ -15408,6 +16664,8 @@ snapshots: get-port@7.1.0: {} + get-port@7.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -15527,6 +16785,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: {} @@ -15562,6 +16865,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 @@ -15656,6 +16966,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 @@ -15673,6 +16985,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 @@ -15719,6 +17038,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: @@ -15745,6 +17066,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-interactive@2.0.0: {} is-map@2.0.3: {} @@ -15887,8 +17210,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 @@ -16105,6 +17441,8 @@ snapshots: long-timeout@0.1.1: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -16138,12 +17476,117 @@ 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: + dependencies: + '@babel/runtime': 7.29.7 + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -16160,6 +17603,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 @@ -16246,6 +17895,8 @@ snapshots: mkdirp-classic@0.5.3: {} + modern-ahocorasick@1.1.0: {} + module-details-from-path@1.0.4: {} moo@0.5.3: {} @@ -16531,6 +18182,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 @@ -16549,6 +18210,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 @@ -16638,12 +18312,20 @@ snapshots: playwright-core@1.58.0: {} + playwright-core@1.61.1: {} + playwright@1.58.0: dependencies: playwright-core: 1.58.0 optionalDependencies: fsevents: 2.3.2 + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} possible-typed-array-names@1.1.0: {} @@ -16752,6 +18434,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 @@ -16759,6 +18443,8 @@ snapshots: proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + prr@1.0.1: optional: true @@ -16793,6 +18479,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: {} @@ -16936,6 +18626,39 @@ 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-server-dom-rspack@0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@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: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + webpack: 5.108.1(esbuild@0.27.2)(lightningcss@1.32.0)(postcss@8.5.16) + webpack-sources: 3.5.0 + optional: true + + react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.108.1(lightningcss@1.32.0)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + webpack: 5.108.1(lightningcss@1.32.0) + webpack-sources: 3.5.0 + optional: true + react-style-singleton@2.2.3(@types/react@19.2.10)(react@19.2.7): dependencies: get-nonce: 1.0.1 @@ -16980,6 +18703,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 @@ -17016,6 +18768,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 @@ -17032,12 +18816,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) @@ -17153,6 +18938,11 @@ snapshots: optionalDependencies: typescript: 5.9.3 + rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.1.0(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0))(react-server-dom-rspack@0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)): + dependencies: + '@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) + react-server-dom-rspack: 0.0.2(@rspack/core@2.1.0(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + rslog@2.1.3: {} run-parallel@1.2.0: @@ -17353,6 +19143,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 @@ -17482,6 +19276,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 @@ -17580,6 +19379,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -17610,6 +19411,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: {} @@ -17698,6 +19502,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 @@ -17718,10 +19527,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: @@ -17827,6 +19651,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 @@ -17850,6 +19678,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 @@ -17940,6 +19771,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: {} @@ -18033,6 +19901,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 @@ -18096,10 +19974,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 @@ -18197,6 +20091,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: {} @@ -18436,3 +20334,5 @@ snapshots: '@yuku-parser/binding-win32-x64': 0.5.39 zod@3.25.76: {} + + zwitch@2.0.4: {} diff --git a/rslib.config.ts b/rslib.config.ts index 566bf25c..7dab55e6 100644 --- a/rslib.config.ts +++ b/rslib.config.ts @@ -13,8 +13,12 @@ const config = defineConfig({ index: './src/index.ts', 'parallel-route-transform-worker': './src/parallel-route-transform-worker.ts', + 'rsc-route-transform-loader': './src/rsc-route-transform-loader.ts', 'templates/entry.server': './src/templates/entry.server.tsx', 'templates/entry.client': './src/templates/entry.client.tsx', + 'templates/entry.rsc': './src/templates/entry.rsc.tsx', + 'templates/entry.rsc.client': './src/templates/entry.rsc.client.tsx', + 'templates/entry.rsc.ssr': './src/templates/entry.rsc.ssr.tsx', }, }, }, @@ -23,6 +27,7 @@ const config = defineConfig({ source: { entry: { index: './src/index.ts', + 'rsc-route-transform-loader': './src/rsc-route-transform-loader.ts', 'templates/entry.server': './src/templates/entry.server.tsx', 'templates/entry.client': './src/templates/entry.client.tsx', }, @@ -34,11 +39,15 @@ const config = defineConfig({ externals: [ ...commonExternals, 'user-routes', + /^virtual\/react-router\//, + /^virtual:react-router\//, /^react-router-dom/, /^react-router/, /^@react-router/, 'react', /^react-dom/, + /^react-server-dom-rspack/, + /^rsbuild-plugin-rsc/, ], }, }, diff --git a/scripts/test-package-interop.mts b/scripts/test-package-interop.mts index 973da721..d9f7d293 100644 --- a/scripts/test-package-interop.mts +++ b/scripts/test-package-interop.mts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; +import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; @@ -112,9 +113,49 @@ const verifyPackIncludesOriginalSourceEffect = () => ); }); +const verifyRscPublicSurfaceEffect = (esm, commonjs) => + tryScriptSync(() => { + assert.equal(typeof esm.pluginReactRouterRSC, 'function'); + assert.equal(typeof commonjs.pluginReactRouterRSC, 'function'); + assert.match( + readFileSync(new URL('../dist/index.js', import.meta.url), 'utf8'), + /rsc-route-transform-loader\.js/ + ); + assert.match( + readFileSync(new URL('../dist/index.cjs', import.meta.url), 'utf8'), + /rsc-route-transform-loader\.cjs/ + ); + + for (const specifier of [ + 'rsbuild-plugin-react-router/templates/entry.rsc', + 'rsbuild-plugin-react-router/templates/entry.rsc.client', + 'rsbuild-plugin-react-router/templates/entry.rsc.ssr', + ]) { + const resolved = import.meta.resolve(specifier); + assert.match( + resolved, + /\/dist\/templates\/entry\.rsc(?:\.client|\.ssr)?\.js$/ + ); + assert.throws( + () => require.resolve(specifier), + error => { + const code = + error && typeof error === 'object' && 'code' in error + ? error.code + : undefined; + return ( + code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || + code === 'ERR_PACKAGE_IMPORT_NOT_DEFINED' + ); + } + ); + } + }); + const mainEffect = Effect.gen(function* () { const [esm, commonjs] = yield* loadEntryPointsEffect(); yield* verifyPackIncludesOriginalSourceEffect(); + yield* verifyRscPublicSurfaceEffect(esm, commonjs); yield* tryScriptSync(() => process.chdir( fileURLToPath(new URL('../tests/fixtures/dev-runtime/', import.meta.url)) diff --git a/src/build-output-transforms.ts b/src/build-output-transforms.ts index 93be1494..aa87422c 100644 --- a/src/build-output-transforms.ts +++ b/src/build-output-transforms.ts @@ -12,6 +12,10 @@ import { createBundlerRouteExportResolver } from './route-export-resolution.js'; import type { RouteChunkConfig } from './route-chunks.js'; import type { PluginOptions, Route } from './types.js'; import { isSourceMapEnabled } from './warnings/warn-on-client-source-maps.js'; +import { + analyzeRouteModuleCode, + type RouteModuleAnalysis, +} from './export-utils.js'; type ReactRouterManifest = Awaited< ReturnType @@ -39,6 +43,10 @@ type RegisterBuildOutputTransformsOptions = { ssr: boolean; isSpaMode: boolean; rootRoutePath: string; + onRouteModuleAnalysis?: ( + resourcePath: string, + analysis: RouteModuleAnalysis + ) => void; }; export const registerBuildOutputTransforms = ({ @@ -61,9 +69,25 @@ export const registerBuildOutputTransforms = ({ ssr, isSpaMode, rootRoutePath, + onRouteModuleAnalysis, }: RegisterBuildOutputTransformsOptions): void => { - const transformRouteModule = async (args: Parameters[0]) => - performanceProfiler.record( + const rememberRouteModuleAnalysis = ( + args: Parameters[0] + ): void => { + if (!routeByFilePath.has(args.resourcePath)) { + return; + } + onRouteModuleAnalysis?.( + args.resourcePath, + analyzeRouteModuleCode(args.code) + ); + }; + + const transformRouteModule = async ( + args: Parameters[0] + ) => { + rememberRouteModuleAnalysis(args); + return performanceProfiler.record( args.environment?.name, 'route:module', args.resource, @@ -83,6 +107,7 @@ export const registerBuildOutputTransforms = ({ rootRoutePath, }) ); + }; api.processAssets( { stage: 'additional', targets: ['node'] }, @@ -150,9 +175,11 @@ export const registerBuildOutputTransforms = ({ api.transform( { resourceQuery: /__react-router-build-client-route/, + order: 'post', }, - async args => - performanceProfiler.record( + async args => { + rememberRouteModuleAnalysis(args); + return performanceProfiler.record( args.environment?.name, 'route:client-entry', args.resource, @@ -165,16 +192,18 @@ export const registerBuildOutputTransforms = ({ isBuild, routeChunkConfig, }) - ) + ); + } ); api.transform( { resourceQuery: /route-chunk=/, environments: ['web'], + order: 'post', }, - async args => - performanceProfiler.record( + async args => { + return performanceProfiler.record( args.environment?.name, 'route:chunk', args.resource, @@ -187,7 +216,8 @@ export const registerBuildOutputTransforms = ({ isBuild, routeChunkConfig, }) - ) + ); + } ); if (isBuild && splitRouteModules) { @@ -198,9 +228,10 @@ export const registerBuildOutputTransforms = ({ not: /__react-router-build-client-route|react-router-route|route-chunk=/, }, environments: ['web'], + order: 'post', }, - async args => - performanceProfiler.record( + async args => { + return performanceProfiler.record( args.environment?.name, 'route:split-exports', args.resource, @@ -211,7 +242,8 @@ export const registerBuildOutputTransforms = ({ resourcePath: args.resourcePath, routeChunkConfig, }) - ) + ); + } ); } diff --git a/src/classic-mode.ts b/src/classic-mode.ts new file mode 100644 index 00000000..28592fb2 --- /dev/null +++ b/src/classic-mode.ts @@ -0,0 +1,246 @@ +import { readFileSync } from 'node:fs'; +import type { RsbuildEntryDescription, RsbuildPluginAPI } from '@rsbuild/core'; +import type { RouteConfigEntry } from '@react-router/dev/routes'; +import { resolve } from 'pathe'; +import { + getBuildManifest, + getRoutesByServerBundleId, +} from './build-manifest.js'; +import { BUILD_CLIENT_ROUTE_QUERY_STRING } from './constants.js'; +import { + createReactRouterDevRuntimeController, + type ReactRouterDevRuntimeController, +} from './dev-runtime-controller.js'; +import { generateWithProps } from './plugin-utils.js'; +import { resolvePrerenderPaths } from './prerender.js'; +import { generateServerBuild } from './server-utils.js'; +import { + createReactRouterServerBuildPlan, + type ReactRouterServerBundleEntry, +} from './server-build-plan.js'; +import { + getRouteChunkEntryName, + getRouteChunkModuleId, + routeChunkExportNames, +} from './route-chunks.js'; +import type { Config } from './react-router-config.js'; +import type { Route } from './types.js'; + +type CreateClassicWebRouteEntriesOptions = { + appDirectory: string; + isBuild: boolean; + routes: Record; + splitRouteModules: boolean; +}; + +type CreateClassicVirtualModulesOptions = { + allowedActionOrigins: string[] | undefined; + appDirectory: string; + assetsBuildDirectory: string; + basename: string; + entryServerPath: string; + federation?: boolean; + future: Config['future']; + prerenderPaths: string[]; + publicPath: string; + routeDiscovery: Config['routeDiscovery']; + routes: Record; + routesByServerBundleId: Record>; + ssr: boolean; +}; + +type CreateClassicBuildArtifactsOptions = { + api: RsbuildPluginAPI; + defaultEntryName: string; + isBuild: boolean; + prerenderConfig: Config['prerender']; + reactRouterConfig: Required< + Pick< + Config, + 'appDirectory' | 'buildDirectory' | 'serverBuildFile' | 'future' + > + > & + Pick; + routeConfig: RouteConfigEntry[]; + routes: Record; + rootDirectory: string; + ssr: boolean; +}; + +export type ClassicBuildArtifacts = { + buildManifest: Awaited>; + devRuntime: ReactRouterDevRuntimeController; + prerenderPaths: string[]; + routesByServerBundleId: Record>; + serverBundleEntries: ReactRouterServerBundleEntry[]; +}; + +export const createClassicBuildArtifacts = async ({ + api, + defaultEntryName, + isBuild, + prerenderConfig, + reactRouterConfig, + routeConfig, + routes, + rootDirectory, + ssr, +}: CreateClassicBuildArtifactsOptions): Promise => { + const buildManifest = await getBuildManifest({ + reactRouterConfig, + routes, + rootDirectory, + }); + const routesByServerBundleId = getRoutesByServerBundleId( + buildManifest, + routes + ); + const serverBuildPlan = createReactRouterServerBuildPlan({ + routesByServerBundleId, + serverBuildFile: reactRouterConfig.serverBuildFile, + defaultEntryName, + }); + const prerenderPaths = await resolvePrerenderPaths( + prerenderConfig, + ssr, + routeConfig, + { + logWarning: true, + warn: message => api.logger.warn(message), + } + ); + + return { + buildManifest, + devRuntime: createReactRouterDevRuntimeController({ + api, + isBuild, + buildPlan: serverBuildPlan, + }), + prerenderPaths, + routesByServerBundleId, + serverBundleEntries: serverBuildPlan.serverBundleEntries, + }; +}; + +export const createClassicWebRouteEntries = ({ + appDirectory, + isBuild, + routes, + splitRouteModules, +}: CreateClassicWebRouteEntriesOptions): { + manifestChunkNames: Set; + webRouteEntries: Record; +} => { + const manifestChunkNames = new Set(['entry.client']); + const webRouteEntries = Object.values(routes).reduce( + (acc, route) => { + const entryName = route.file.slice(0, route.file.lastIndexOf('.')); + const routeFilePath = resolve(appDirectory, route.file); + manifestChunkNames.add(entryName); + acc[entryName] = { + import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`, + html: false, + }; + + if (isBuild && splitRouteModules && route.id !== 'root') { + let source: string; + try { + source = readFileSync(routeFilePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return acc; + } + throw error; + } + for (const exportName of routeChunkExportNames) { + if (!source.includes(exportName)) { + continue; + } + const chunkEntryName = getRouteChunkEntryName(route.id, exportName); + manifestChunkNames.add(chunkEntryName); + acc[chunkEntryName] = { + import: getRouteChunkModuleId(routeFilePath, exportName), + html: false, + }; + } + } + + return acc; + }, + {} as Record + ); + + return { + manifestChunkNames, + webRouteEntries, + }; +}; + +export const createClassicVirtualModules = ({ + allowedActionOrigins, + appDirectory, + assetsBuildDirectory, + basename, + entryServerPath, + federation, + future, + prerenderPaths, + publicPath, + routeDiscovery, + routes, + routesByServerBundleId, + ssr, +}: CreateClassicVirtualModulesOptions): Record => { + const bundleVirtualModules = Object.fromEntries( + Object.entries(routesByServerBundleId).map(([bundleId, bundleRoutes]) => [ + `virtual/react-router/server-build-${bundleId}`, + generateServerBuild(bundleRoutes, { + entryServerPath, + assetsBuildDirectory, + basename, + appDirectory, + ssr, + federation, + future, + allowedActionOrigins, + prerender: prerenderPaths, + routeDiscovery, + publicPath, + serverManifestId: `virtual/react-router/server-manifest-${bundleId}`, + }), + ]) + ); + const bundleManifestModules = Object.fromEntries( + Object.entries(routesByServerBundleId) + .filter( + ([, bundleRoutes]) => + bundleRoutes && Object.keys(bundleRoutes).length > 0 + ) + .map(([bundleId]) => [ + `virtual/react-router/server-manifest-${bundleId}`, + 'export default {};', + ]) + ); + + return { + 'virtual/react-router/browser-manifest': 'export default {};', + 'virtual/react-router/server-manifest': 'export default {};', + 'virtual/react-router/server-build': generateServerBuild(routes, { + entryServerPath, + assetsBuildDirectory, + basename, + appDirectory, + ssr, + federation, + future, + allowedActionOrigins, + prerender: prerenderPaths, + routeDiscovery, + publicPath, + }), + ...bundleVirtualModules, + ...bundleManifestModules, + 'virtual/react-router/with-props': generateWithProps(), + }; +}; diff --git a/src/entry-paths.ts b/src/entry-paths.ts new file mode 100644 index 00000000..b0744e2a --- /dev/null +++ b/src/entry-paths.ts @@ -0,0 +1,80 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'pathe'; +import { findEntryFile } from './plugin-utils.js'; + +type ReactRouterEntryPathsOptions = { + appDirectory: string; + templatesDirectory: string; +}; + +export type ReactRouterEntryPaths = { + devServerBuildEntryName: string; + finalEntryClientPath: string; + finalEntryRscClientPath: string; + finalEntryRscPath: string; + finalEntryRscSsrPath: string; + finalEntryServerPath: string; + hasServerApp: boolean; + serverAppPath: string; +}; + +const resolveEntryWithTemplate = ({ + appDirectory, + entryName, + templateName, + templatesDirectory, +}: ReactRouterEntryPathsOptions & { + entryName: string; + templateName: string; +}): string => { + const userEntryPath = findEntryFile(resolve(appDirectory, entryName)); + return existsSync(userEntryPath) + ? userEntryPath + : resolve(templatesDirectory, templateName); +}; + +export const resolveReactRouterEntryPaths = ({ + appDirectory, + templatesDirectory, +}: ReactRouterEntryPathsOptions): ReactRouterEntryPaths => { + const serverAppPath = findEntryFile(resolve(appDirectory, '../server/index')); + const hasServerApp = existsSync(serverAppPath); + + return { + devServerBuildEntryName: hasServerApp + ? 'static/js/react-router-server-build' + : 'static/js/app', + finalEntryClientPath: resolveEntryWithTemplate({ + appDirectory, + entryName: 'entry.client', + templateName: 'entry.client.js', + templatesDirectory, + }), + finalEntryServerPath: resolveEntryWithTemplate({ + appDirectory, + entryName: 'entry.server', + templateName: 'entry.server.js', + templatesDirectory, + }), + finalEntryRscClientPath: resolveEntryWithTemplate({ + appDirectory, + entryName: 'entry.rsc.client', + templateName: 'entry.rsc.client.js', + templatesDirectory, + }), + finalEntryRscPath: resolveEntryWithTemplate({ + appDirectory, + entryName: 'entry.rsc', + templateName: 'entry.rsc.js', + templatesDirectory, + }), + finalEntryRscSsrPath: resolveEntryWithTemplate({ + appDirectory, + entryName: 'entry.ssr', + templateName: 'entry.rsc.ssr.js', + templatesDirectory, + }), + hasServerApp, + serverAppPath, + }; +}; diff --git a/src/environment-output.ts b/src/environment-output.ts new file mode 100644 index 00000000..fc46e407 --- /dev/null +++ b/src/environment-output.ts @@ -0,0 +1,90 @@ +import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core'; + +type ModuleFederationPluginLike = { + name?: string; + _options?: { experiments?: { asyncStartup?: boolean } }; + options?: { experiments?: { asyncStartup?: boolean } }; +}; + +const ensureFederationAsyncStartup = ( + rspackConfig: Rspack.Configuration | undefined +): void => { + if (!rspackConfig?.plugins?.length) { + return; + } + + for (const plugin of rspackConfig.plugins) { + if (!plugin || typeof plugin !== 'object') { + continue; + } + const pluginName = (plugin as ModuleFederationPluginLike).name; + if (pluginName !== 'ModuleFederationPlugin') { + continue; + } + + const pluginOptions = + (plugin as ModuleFederationPluginLike)._options ?? + (plugin as ModuleFederationPluginLike).options; + if (!pluginOptions) { + continue; + } + + pluginOptions.experiments = { + ...pluginOptions.experiments, + asyncStartup: true, + }; + } +}; + +export const registerReactRouterEnvironmentOutput = ({ + api, + federation, + resolvedServerOutput, +}: { + api: RsbuildPluginAPI; + federation: boolean | undefined; + resolvedServerOutput: 'commonjs' | 'module'; +}): void => { + api.modifyEnvironmentConfig( + async (config, { name, mergeEnvironmentConfig }) => { + if (name !== 'web' && name !== 'node') { + return config; + } + + return mergeEnvironmentConfig(config, { + tools: { + rspack: rspackConfig => { + if (federation) { + ensureFederationAsyncStartup(rspackConfig); + } + + if (name === 'node') { + const output = rspackConfig.output; + if (output) { + const library = output.library; + const libraryOptions = + library && + typeof library === 'object' && + !Array.isArray(library) + ? library + : {}; + rspackConfig.output = { + ...output, + library: { + ...libraryOptions, + type: + resolvedServerOutput === 'module' + ? 'module' + : 'commonjs2', + }, + }; + } + } + + return rspackConfig; + }, + }, + }); + } + ); +}; diff --git a/src/export-utils.ts b/src/export-utils.ts index 6a4c5265..a2ee119f 100644 --- a/src/export-utils.ts +++ b/src/export-utils.ts @@ -14,7 +14,7 @@ type ExportInfo = { readonly exportAllModules: readonly string[]; }; -type RouteModuleAnalysis = { +export type RouteModuleAnalysis = { readonly code: string; readonly exports: readonly string[]; readonly exportAllModules: readonly string[]; @@ -177,6 +177,18 @@ export const getExportNamesAndExportAll = async ( return trackedExportInfo; }; +export const analyzeRouteModuleCode = ( + code: string, + resourcePath?: string +): RouteModuleAnalysis => { + const program = parseProgram(code, resourcePath); + return { + code, + exports: collectProgramExportNames(program), + exportAllModules: collectExportAllModules(program), + }; +}; + export const getRouteModuleAnalysis = async ( resourcePath: string ): Promise => { @@ -188,12 +200,7 @@ export const getRouteModuleAnalysis = async ( const analysis = (async () => { const source = await readFile(resourcePath, 'utf8'); - const program = parseProgram(source, resourcePath); - return { - code: source, - exports: collectProgramExportNames(program), - exportAllModules: collectExportAllModules(program), - }; + return analyzeRouteModuleCode(source, resourcePath); })(); let trackedAnalysis: Promise; diff --git a/src/index.ts b/src/index.ts index e22cc42b..edde1d9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,76 +1,46 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync } 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'; -import { - rspack, - type RsbuildEntryDescription, - type RsbuildPlugin, - type Rspack, -} from '@rsbuild/core'; -import { Effect } from 'effect'; +import { rspack, type RsbuildPlugin, type Rspack } from '@rsbuild/core'; import { createJiti } from 'jiti'; import { relative, resolve } from 'pathe'; import { getDefaultConcurrency } from './concurrency.js'; -import { - BUILD_CLIENT_ROUTE_QUERY_STRING, - JS_EXTENSIONS, - PLUGIN_NAME, -} from './constants.js'; +import { JS_EXTENSIONS, PLUGIN_NAME } from './constants.js'; import { guardReactRouterLazyCompilation } from './lazy-compilation.js'; import { createDevServerMiddleware } from './dev-server.js'; -import { - generateWithProps, - findEntryFile, - normalizeAssetPrefix, -} from './plugin-utils.js'; -import type { PluginOptions } from './types.js'; -import { - generateServerBuild, - resolveReactRouterServerBuild, -} from './server-utils.js'; -import { resolvePrerenderPaths, validatePrerenderConfig } from './prerender.js'; +import { findEntryFile, normalizeAssetPrefix } from './plugin-utils.js'; +import { resolveReactRouterEntryPaths } from './entry-paths.js'; +import { registerReactRouterEnvironmentOutput } from './environment-output.js'; +import type { PluginOptions, ReactRouterRSCPluginOptions } from './types.js'; +import { resolveReactRouterServerBuild } from './server-utils.js'; +import { validatePrerenderConfig } from './prerender.js'; import { runReactRouterPrerenderBuild } from './prerender-build.js'; import { resolveReactRouterConfig, + resolveRouteDiscoveryConfig, type ResolvedReactRouterConfig, } from './react-router-config.js'; import { getReactRouterManifestForDev, configRoutesToRouteManifest, - configRoutesToRouteManifestEntries, createReactRouterManifestStats, type ReactRouterManifestStats, type RouteManifestModuleExports, } from './manifest.js'; +import type { RouteModuleAnalysis } from './export-utils.js'; import { registerModifyBrowserManifestAssets } from './modify-browser-manifest.js'; import { registerBuildOutputTransforms } from './build-output-transforms.js'; -import { - getRouteChunkEntryName, - getRouteChunkModuleId, - routeChunkExportNames, - type RouteChunkCache, - type RouteChunkConfig, -} from './route-chunks.js'; +import { type RouteChunkCache, type RouteChunkConfig } from './route-chunks.js'; import { createRouteTransformExecutor } from './parallel-route-transforms.js'; import { - createRouteTopologyWatcher, - createRouteManifestSnapshot, - ensureDevRestartMarker, - getRouteRestartMarkerPath, mergeWatchFiles, - type WatchFileConfig, + registerRouteTopologyDevWatch, } from './route-watch.js'; import { validateRouteConfig } from './route-config.js'; -import { - getBuildManifestEffect, - getRoutesByServerBundleId, -} from './build-manifest.js'; -import { - createReactRouterNodeEntries, - createReactRouterServerBuildPlan, -} from './server-build-plan.js'; +import { createReactRouterNodeEntries } from './server-build-plan.js'; import { warnOnClientSourceMaps } from './warnings/warn-on-client-source-maps.js'; import { validatePluginOrderFromConfig } from './validation/validate-plugin-order.js'; import { getSsrExternals } from './ssr-externals.js'; @@ -79,20 +49,60 @@ import { roundMs, } from './performance.js'; import { mapVirtualModules } from './virtual-modules.js'; -import { createReactRouterDevRuntimeController } from './dev-runtime-controller.js'; -import { - createDelayedPluginTask, - DEV_BACKGROUND_STARTUP_DELAY_MS, - runPluginEffect, - tryPluginPromise, -} from './effect-runtime.js'; +import { runPluginEffect, tryPluginPromise } from './effect-runtime.js'; import { registerReactRouterTypegen } from './typegen.js'; import { importConfigWithWatchPaths } from './config-imports.js'; +import { + createClassicBuildArtifacts, + createClassicVirtualModules, + createClassicWebRouteEntries, +} from './classic-mode.js'; +import { + assertReactRouterRscSupport, + createReactRouterRscDevServerSetup, + createReactRouterRscResolveAliases, + createReactRouterRscVirtualModules, + registerReactRouterRscRouteTransforms, + setupReactRouterRscPlugin, +} from './rsc-support.js'; export { loadReactRouterServerBuild } from './dev-generation.js'; export { resolveReactRouterServerBuild }; +export type { PluginOptions, ReactRouterRSCPluginOptions } from './types.js'; 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 = ({ + preserveReactRouterExports = false, +}: { + preserveReactRouterExports?: boolean; +} = {}): Record => { + if (preserveReactRouterExports) { + return {}; + } + + 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< + NonNullable[number]['reactRouterConfigResolved'] + > +>[0]['reactRouterConfig']; export const shouldParallelizeEnvironmentBuilds = ({ isBuild, @@ -103,48 +113,17 @@ export const shouldParallelizeEnvironmentBuilds = ({ }): boolean => !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES; -type ModuleFederationPluginLike = { - name?: string; - _options?: { experiments?: { asyncStartup?: boolean } }; - options?: { experiments?: { asyncStartup?: boolean } }; -}; - -const ensureFederationAsyncStartup = ( - rspackConfig: Rspack.Configuration | undefined -): void => { - if (!rspackConfig?.plugins?.length) { - return; - } - - for (const plugin of rspackConfig.plugins) { - if (!plugin || typeof plugin !== 'object') { - continue; - } - const pluginName = (plugin as ModuleFederationPluginLike).name; - if (pluginName !== 'ModuleFederationPlugin') { - continue; - } - - const pluginOptions = - (plugin as ModuleFederationPluginLike)._options ?? - (plugin as ModuleFederationPluginLike).options; - if (!pluginOptions) { - continue; - } - - pluginOptions.experiments = { - ...pluginOptions.experiments, - asyncStartup: true, - }; - } -}; - const cssUrlAssetExtensions = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/; const urlAssetResourceQuery = /^(?=.*(?:\?|&)url(?:&|$))(?!.*(?:\?|&)(?:raw|inline)(?:&|$))/; -export const pluginReactRouter = ( +export const pluginReactRouter = (options: PluginOptions = {}): RsbuildPlugin => + createReactRouterPlugin(options); + +const RSC_LAYERS = rspack.experiments.rsc.Layers; + +const createReactRouterPlugin = ( options: PluginOptions = {} ): RsbuildPlugin => ({ name: PLUGIN_NAME, @@ -152,6 +131,7 @@ export const pluginReactRouter = ( async setup(api) { const defaultOptions = { customServer: false, + rsc: false, serverOutput: 'module' as const, }; @@ -159,6 +139,7 @@ export const pluginReactRouter = ( ...defaultOptions, ...options, }; + const isRscMode = Boolean(pluginOptions.rsc); const logPerformance = pluginOptions.logPerformance === true; const setupStartMs = logPerformance ? performance.now() : 0; const performanceProfiler = createReactRouterPerformanceProfiler({ @@ -286,30 +267,10 @@ export const pluginReactRouter = ( throw new Error(prerenderConfigError); } - // React Router defaults to "lazy" route discovery, but "ssr:false" builds - // have no runtime server to serve manifest patch requests, so we force - // `mode:"initial"` in SPA mode to avoid any `/__manifest` fetches. - let routeDiscovery: Config['routeDiscovery']; - if (!userRouteDiscovery) { - routeDiscovery = ssr - ? ({ mode: 'lazy', manifestPath: '/__manifest' } as const) - : ({ mode: 'initial' } as const); - } else if (userRouteDiscovery.mode === 'initial') { - routeDiscovery = userRouteDiscovery; - } else if (userRouteDiscovery.mode === 'lazy') { - if (!ssr) { - throw new Error( - 'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`' - ); - } - const manifestPath = userRouteDiscovery.manifestPath; - if (manifestPath && !manifestPath.startsWith('/')) { - throw new Error( - 'The `routeDiscovery.manifestPath` config must be a root-relative pathname beginning with a slash (i.e., "/__manifest")' - ); - } - routeDiscovery = userRouteDiscovery; - } + const routeDiscovery = resolveRouteDiscoveryConfig({ + ssr, + userRouteDiscovery, + }); (globalThis as any).__reactRouterAppDirectory = resolve(appDirectory); const routesPath = findEntryFile(resolve(appDirectory, 'routes')); @@ -348,53 +309,42 @@ export const pluginReactRouter = ( const { value: routeConfig, watchPaths: routeConfigWatchPaths } = await importConfigWithWatchPaths(routesPath, importRouteConfig); - const entryClientPath = findEntryFile( - resolve(appDirectory, 'entry.client') - ); - const entryServerPath = findEntryFile( - resolve(appDirectory, 'entry.server') - ); - - const serverAppPath = findEntryFile( - resolve(appDirectory, '../server/index') - ); - const hasServerApp = existsSync(serverAppPath); - const devServerBuildEntryName = hasServerApp - ? 'static/js/react-router-server-build' - : 'static/js/app'; - - const templateDir = resolve(__dirname, 'templates'); - const templateClientPath = resolve(templateDir, 'entry.client.js'); - const templateServerPath = resolve(templateDir, 'entry.server.js'); + const { + devServerBuildEntryName, + finalEntryClientPath, + finalEntryRscClientPath, + finalEntryRscPath, + finalEntryRscSsrPath, + finalEntryServerPath, + hasServerApp, + serverAppPath, + } = resolveReactRouterEntryPaths({ + appDirectory, + templatesDirectory: resolve(__dirname, 'templates'), + }); - const finalEntryClientPath = existsSync(entryClientPath) - ? entryClientPath - : templateClientPath; - const finalEntryServerPath = existsSync(entryServerPath) - ? entryServerPath - : templateServerPath; + if (isRscMode) { + assertReactRouterRscSupport({ + pluginName: PLUGIN_NAME, + resolvePackagePath: resolveAppPackagePath, + }); + await setupReactRouterRscPlugin({ + api, + entryRscPath: finalEntryRscPath, + entrySsrPath: finalEntryRscSsrPath, + pluginName: PLUGIN_NAME, + rsc: + pluginOptions.rsc && pluginOptions.rsc !== true + ? pluginOptions.rsc + : true, + }); + } const getRootRoutePath = () => findEntryFile(resolve(appDirectory, 'root')); const rootRoutePath = getRootRoutePath(); // React Router's server build expects route files relative to `appDirectory` // so it can resolve them correctly during compilation. const rootRouteFile = relative(appDirectory, rootRoutePath); - const createRouteTopologySnapshot = ( - routeFile: string, - routeConfig: RouteConfigEntry[] - ) => - createRouteManifestSnapshot([ - ['root', { path: '', id: 'root', file: routeFile }], - ...configRoutesToRouteManifestEntries(appDirectory, routeConfig), - ]); - const getWatchedRouteTopology = async (): Promise> => { - const latestRouteConfig = await loadRouteConfig(); - const latestRootRouteFile = relative(appDirectory, getRootRoutePath()); - return createRouteTopologySnapshot( - latestRootRouteFile, - latestRouteConfig - ); - }; const routes = { root: { path: '', id: 'root', file: rootRouteFile }, @@ -416,7 +366,8 @@ export const pluginReactRouter = ( resolvedConfigWithRoutes; for (const preset of configPresets) { await preset.reactRouterConfigResolved?.({ - reactRouterConfig: resolvedConfigForPreset, + reactRouterConfig: + resolvedConfigForPreset as ReactRouterPresetResolvedConfig, }); } @@ -429,151 +380,65 @@ export const pluginReactRouter = ( const isSpaMode = !ssr && !isPrerenderEnabled; const routeCount = Object.keys(routes).length; const routeChunkConfig: RouteChunkConfig = { - splitRouteModules, + splitRouteModules: isRscMode ? false : splitRouteModules, appDirectory, rootRouteFile, }; const routeChunkCache: RouteChunkCache = new Map(); - const routeTransformExecutor = createRouteTransformExecutor({ - parallelTransforms: pluginOptions.parallelTransforms, - routeChunkCache, - splitRouteModules: Boolean(splitRouteModules), - }); - const routeChunkOptions = { - splitRouteModules, - rootRouteFile, - isBuild, - cache: routeChunkCache, + const routeTransformExecutor = isRscMode + ? undefined + : createRouteTransformExecutor({ + parallelTransforms: pluginOptions.parallelTransforms, + routeChunkCache, + splitRouteModules: Boolean(splitRouteModules), + }); + const transformedRouteModuleAnalyses = new Map< + string, + RouteModuleAnalysis + >(); + const rememberRouteModuleAnalysis = ( + resourcePath: string, + analysis: RouteModuleAnalysis + ) => { + transformedRouteModuleAnalyses.set(resolve(resourcePath), analysis); }; + const routeChunkOptions = isRscMode + ? undefined + : { + splitRouteModules, + rootRouteFile, + isBuild, + cache: routeChunkCache, + analyzeRouteModule: async (routeFilePath: string) => + transformedRouteModuleAnalyses.get(resolve(routeFilePath)), + }; const outputClientPath = resolve(buildDirectory, 'client'); const assetsBuildDirectory = relative(process.cwd(), outputClientPath); - const watchDirectory = resolve(appDirectory); - const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath); - const routeTopologyWatchFiles: WatchFileConfig[] = - pluginOptions.onRouteTopologyChange - ? [] - : [ - { - paths: routeConfigWatchPaths, - type: 'reload-server', - }, - { - paths: routeRestartMarkerPath, - type: 'reload-server', - }, - ]; - const routeWatchFiles: WatchFileConfig[] = [ - { - paths: configWatchPaths, - type: 'reload-server', - }, - ...routeTopologyWatchFiles, - ]; - let closeRouteTopologyWatcher: (() => Promise) | undefined; - let routeTopologyWatcherClosed = false; - - const reportRouteTopologyWatcherError = (error: unknown): void => { - api.logger.warn( - `[${PLUGIN_NAME}] Failed to watch route topology changes: ${error}` - ); - }; - - const routeTopologyWatcherTask = createDelayedPluginTask({ - delayMs: DEV_BACKGROUND_STARTUP_DELAY_MS, - run: () => - Effect.gen(function* () { - yield* tryPluginPromise(() => - ensureDevRestartMarker(routeRestartMarkerPath) - ); - const closeWatcher = yield* tryPluginPromise(() => - createRouteTopologyWatcher({ - watchDirectory, - getRouteTopology: getWatchedRouteTopology, - initialRouteTopology: createRouteTopologySnapshot( - rootRouteFile, - routeConfig - ), - restartMarkerPath: routeRestartMarkerPath, - onRouteTopologyChange: pluginOptions.onRouteTopologyChange, - onError: reportRouteTopologyWatcherError, - }) - ); - if (routeTopologyWatcherClosed) { - yield* tryPluginPromise(() => closeWatcher()); - return; - } - closeRouteTopologyWatcher = closeWatcher; - }), - onError: reportRouteTopologyWatcherError, - }); - - const scheduleRouteTopologyWatcher = (): void => { - if (routeTopologyWatcherClosed || closeRouteTopologyWatcher) { - return; - } - routeTopologyWatcherTask.schedule(); - }; + const routeWatchFiles = isBuild + ? [] + : registerRouteTopologyDevWatch({ + api, + appDirectory, + configWatchPaths, + getRootRouteFile: () => relative(appDirectory, getRootRoutePath()), + loadRouteConfig, + onRouteTopologyChange: pluginOptions.onRouteTopologyChange, + outputClientPath, + pluginName: PLUGIN_NAME, + routeConfig, + routeConfigWatchPaths, + }); if (!isBuild) { - api.onBeforeStartDevServer(() => { - routeTopologyWatcherClosed = false; - }); - - api.onAfterDevCompile(() => { - scheduleRouteTopologyWatcher(); - }); - api.onAfterCreateCompiler(() => { - routeTransformExecutor.prewarm(); + routeTransformExecutor?.prewarm(); }); } - const closeRouteTopologyWatcherEffect = (): Effect.Effect< - void, - Error, - never - > => - Effect.gen(function* () { - routeTopologyWatcherClosed = true; - yield* routeTopologyWatcherTask.cancelEffect(); - yield* tryPluginPromise(() => closeRouteTopologyWatcher?.()); - closeRouteTopologyWatcher = undefined; - }); - const closeRouteTransformExecutorEffect = (): Effect.Effect< - void, - Error, - never - > => tryPluginPromise(() => routeTransformExecutor.close()); - const closeDevServerResourcesEffect = (): Effect.Effect< - void, - Error, - never - > => - closeRouteTopologyWatcherEffect().pipe( - Effect.matchEffect({ - onFailure: topologyError => - closeRouteTransformExecutorEffect().pipe( - Effect.matchEffect({ - onFailure: executorError => - Effect.fail( - new AggregateError( - [topologyError, executorError], - '[rsbuild-plugin-react-router] Failed to close dev server resources.' - ) - ), - onSuccess: () => Effect.fail(topologyError), - }) - ), - onSuccess: () => closeRouteTransformExecutorEffect(), - }) - ); - - api.onCloseDevServer(() => - runPluginEffect(closeDevServerResourcesEffect()) - ); - api.onCloseBuild(() => - runPluginEffect(closeRouteTransformExecutorEffect()) - ); + const closeRouteTransformExecutor = () => + runPluginEffect(tryPluginPromise(() => routeTransformExecutor?.close())); + api.onCloseBuild(closeRouteTransformExecutor); + api.onCloseDevServer(closeRouteTransformExecutor); type ReactRouterManifest = Awaited< ReturnType @@ -606,8 +471,16 @@ export const pluginReactRouter = ( [devServerBuildEntryName]: baseServerManifest, }; - for (const { bundleId, entryName } of serverBundleEntries) { - const bundleRoutes = routesByServerBundleId[bundleId]; + if (!classicBuildArtifacts) { + return; + } + + for (const { + bundleId, + entryName, + } of classicBuildArtifacts.serverBundleEntries) { + const bundleRoutes = + classicBuildArtifacts.routesByServerBundleId[bundleId]; if (!bundleRoutes) { continue; } @@ -627,7 +500,10 @@ export const pluginReactRouter = ( } if (!isBuild) { - devRuntime.captureWeb(compilation, manifestsByEntryName); + classicBuildArtifacts.devRuntime.captureWeb( + compilation, + manifestsByEntryName + ); } } ); @@ -640,66 +516,34 @@ export const pluginReactRouter = ( ]) ); - const manifestChunkNames = new Set(['entry.client']); - const webRouteEntries = Object.values(routes).reduce( - (acc, route) => { - const entryName = route.file.slice(0, route.file.lastIndexOf('.')); - const routeFilePath = resolve(appDirectory, route.file); - manifestChunkNames.add(entryName); - acc[entryName] = { - import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`, - html: false, - }; - - if (isBuild && splitRouteModules && route.id !== 'root') { - let source: string; - try { - source = readFileSync(routeFilePath, 'utf8'); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return acc; - } - throw error; - } - for (const exportName of routeChunkExportNames) { - if (!source.includes(exportName)) { - continue; - } - const chunkEntryName = getRouteChunkEntryName(route.id, exportName); - manifestChunkNames.add(chunkEntryName); - acc[chunkEntryName] = { - import: getRouteChunkModuleId(routeFilePath, exportName), - html: false, - }; - } - } - - return acc; - }, - {} as Record - ); - const buildManifest = await runPluginEffect( - getBuildManifestEffect({ - reactRouterConfig: resolvedConfigWithRoutes, - routes, - rootDirectory: process.cwd(), - }) - ); - const routesByServerBundleId = getRoutesByServerBundleId( - buildManifest, - routes + const classicWebRouteEntries = isRscMode + ? undefined + : createClassicWebRouteEntries({ + appDirectory, + isBuild, + routes, + splitRouteModules: Boolean(splitRouteModules), + }); + const manifestChunkNames = + classicWebRouteEntries?.manifestChunkNames ?? new Set(['index']); + const webRouteEntries = classicWebRouteEntries?.webRouteEntries ?? {}; + const classicBuildArtifacts = isRscMode + ? undefined + : await createClassicBuildArtifacts({ + api, + defaultEntryName: devServerBuildEntryName, + isBuild, + prerenderConfig, + reactRouterConfig: resolvedConfigWithRoutes, + routeConfig, + routes, + rootDirectory: process.cwd(), + ssr, + }); + const rscServerEntryName = (serverBuildFile || 'index.js').replace( + /\.js$/, + '' ); - const serverBuildPlan = createReactRouterServerBuildPlan({ - routesByServerBundleId, - serverBuildFile, - defaultEntryName: devServerBuildEntryName, - }); - const { serverBundleEntries } = serverBuildPlan; - const devRuntime = createReactRouterDevRuntimeController({ - api, - isBuild, - buildPlan: serverBuildPlan, - }); let clientStats: ReactRouterManifestStats | undefined; api.onAfterEnvironmentCompile(({ stats, environment }) => { @@ -724,104 +568,79 @@ export const pluginReactRouter = ( } }); - const prerenderPaths = await resolvePrerenderPaths( - prerenderConfig, - ssr, - routeConfig, - { - logWarning: true, - warn: message => api.logger.warn(message), - } - ); - - api.onAfterBuild(({ environments }) => - runPluginEffect( - tryPluginPromise(() => - runReactRouterPrerenderBuild({ - api, - hasWebEnvironment: Boolean(environments.web), - buildDirectory, - serverBuildFile, - ssr, - isPrerenderEnabled, - prerenderConfig, - prerenderPaths, - basename, - future, - routes, - latestBrowserManifest, - latestBrowserManifestModuleExports, - clientStats, - pluginOptions, - appDirectory, - assetPrefix, - routeChunkOptions, - buildManifest, - resolvedConfigWithRoutes, - buildEnd, - }) + if (classicBuildArtifacts && routeChunkOptions) { + api.onAfterBuild(({ environments }) => + runPluginEffect( + tryPluginPromise(() => + runReactRouterPrerenderBuild({ + api, + hasWebEnvironment: Boolean(environments.web), + buildDirectory, + serverBuildFile, + ssr, + isPrerenderEnabled, + prerenderConfig, + prerenderPaths: classicBuildArtifacts.prerenderPaths, + basename, + future, + routes, + latestBrowserManifest, + latestBrowserManifestModuleExports, + clientStats, + pluginOptions, + appDirectory, + assetPrefix, + routeChunkOptions, + buildManifest: classicBuildArtifacts.buildManifest, + resolvedConfigWithRoutes, + buildEnd, + }) + ) ) - ) - ); + ); + } const allowedActionOriginsForBuild = allowedActionOrigins === false ? undefined : allowedActionOrigins; // Public requests stay bare while Rspack resolves seeded virtual files. const createVirtualModulePlugin = (publicPath: string) => { - const bundleVirtualModules = Object.fromEntries( - Object.entries(routesByServerBundleId).map( - ([bundleId, bundleRoutes]) => [ - `virtual/react-router/server-build-${bundleId}`, - generateServerBuild(bundleRoutes, { - entryServerPath: finalEntryServerPath, - assetsBuildDirectory, - basename, - appDirectory, - ssr, - federation: options.federation, - future, - allowedActionOrigins: allowedActionOriginsForBuild, - prerender: prerenderPaths, - routeDiscovery, - publicPath, - serverManifestId: `virtual/react-router/server-manifest-${bundleId}`, - }), - ] - ) - ); - const bundleManifestModules = Object.fromEntries( - Object.entries(routesByServerBundleId) - .filter( - ([, bundleRoutes]) => - bundleRoutes && Object.keys(bundleRoutes).length > 0 - ) - .map(([bundleId]) => [ - `virtual/react-router/server-manifest-${bundleId}`, - 'export default {};', - ]) - ); - - return new rspack.experiments.VirtualModulesPlugin( - mapVirtualModules({ - 'virtual/react-router/browser-manifest': 'export default {};', - 'virtual/react-router/server-manifest': 'export default {};', - 'virtual/react-router/server-build': generateServerBuild(routes, { - entryServerPath: finalEntryServerPath, - assetsBuildDirectory, - basename, + const rscVirtualModules: Record = isRscMode + ? createReactRouterRscVirtualModules({ appDirectory, + basename, + buildDirectory, + isBuild, + outputClientPath, + publicPath, + routeDiscovery, + routes, ssr, + }) + : {}; + const classicVirtualModules = classicBuildArtifacts + ? createClassicVirtualModules({ + allowedActionOrigins: allowedActionOriginsForBuild, + appDirectory, + assetsBuildDirectory, + basename, + entryServerPath: finalEntryServerPath, federation: options.federation, future, - allowedActionOrigins: allowedActionOriginsForBuild, - prerender: prerenderPaths, - routeDiscovery, + prerenderPaths: classicBuildArtifacts.prerenderPaths, publicPath, - }), - ...bundleVirtualModules, - ...bundleManifestModules, - 'virtual/react-router/with-props': generateWithProps(), + routeDiscovery, + routes, + routesByServerBundleId: + classicBuildArtifacts.routesByServerBundleId, + ssr, + }) + : {}; + + return new rspack.experiments.VirtualModulesPlugin( + mapVirtualModules({ + ...classicVirtualModules, + ...rscVirtualModules, }) ); }; @@ -837,14 +656,16 @@ export const pluginReactRouter = ( } else if (useAsyncNodeChunkLoading) { nodeChunkLoading = 'async-node'; } - const nodeEntries = createReactRouterNodeEntries({ - hasServerApp, - isBuild, - serverAppPath, - entryServerPath: finalEntryServerPath, - defaultEntryName: devServerBuildEntryName, - serverBundleEntries, - }); + const nodeEntries = classicBuildArtifacts + ? createReactRouterNodeEntries({ + hasServerApp, + isBuild, + serverAppPath, + entryServerPath: finalEntryServerPath, + defaultEntryName: devServerBuildEntryName, + serverBundleEntries: classicBuildArtifacts.serverBundleEntries, + }) + : {}; const configuredLazyCompilation = pluginOptions.lazyCompilation === undefined @@ -863,6 +684,16 @@ export const pluginReactRouter = ( routeCount >= 256 && (config.performance?.printFileSize === undefined || config.performance.printFileSize === true); + const reactRouterAliases = isRscMode + ? {} + : createReactRouterPackageAliases(); + const reactRouterRscAliases: Record = isRscMode + ? createReactRouterRscResolveAliases(api.context.rootPath) + : {}; + const resolveAliases: Record = { + ...reactRouterAliases, + ...reactRouterRscAliases, + }; return mergeRsbuildConfig(config, { ...(shouldCompactFileSizeReport @@ -879,18 +710,28 @@ export const pluginReactRouter = ( output: { assetPrefix: config.output?.assetPrefix || '/', }, + server: + isRscMode && !pluginOptions.customServer && ssr + ? { + setup: createReactRouterRscDevServerSetup({ + entryName: rscServerEntryName, + pluginName: PLUGIN_NAME, + }), + } + : undefined, dev: { writeToDisk: true, ...lazyCompilation, watchFiles: mergeWatchFiles(config.dev?.watchFiles, routeWatchFiles), setupMiddlewares: - pluginOptions.customServer || !ssr + !classicBuildArtifacts || pluginOptions.customServer || !ssr ? [] : [ middlewares => { middlewares.push( createDevServerMiddleware({ - loadBuild: devRuntime.createBuildLoader(), + loadBuild: + classicBuildArtifacts.devRuntime.createBuildLoader(), }) ); }, @@ -898,6 +739,22 @@ export const pluginReactRouter = ( }, tools: { rspack: { + resolve: + isRscMode || Object.keys(resolveAliases).length > 0 + ? { + ...(isRscMode + ? { + modules: [ + resolve(api.context.rootPath, 'node_modules'), + 'node_modules', + ], + } + : {}), + ...(Object.keys(resolveAliases).length > 0 + ? { alias: resolveAliases } + : {}), + } + : undefined, plugins: [vmodPlugin], }, }, @@ -913,15 +770,22 @@ export const pluginReactRouter = ( } : {}), source: { - entry: { - // no query needed when federation is disabled - 'entry.client': finalEntryClientPath, - 'virtual/react-router/browser-manifest': { - import: 'virtual/react-router/browser-manifest', - html: false, - }, - ...webRouteEntries, - }, + entry: isRscMode + ? { + index: { + import: finalEntryRscClientPath, + html: false, + }, + } + : { + // no query needed when federation is disabled + 'entry.client': finalEntryClientPath, + 'virtual/react-router/browser-manifest': { + import: 'virtual/react-router/browser-manifest', + html: false, + }, + ...webRouteEntries, + }, }, output: { filename: { @@ -950,18 +814,32 @@ export const pluginReactRouter = ( }, } : {}), - externalsType: 'module', - output: { - chunkFormat: 'module', - chunkLoading: 'import', - workerChunkLoading: 'import', - wasmLoading: 'fetch', - library: { type: 'module' }, - module: true, - }, + externalsType: isRscMode ? undefined : 'module', + output: isRscMode + ? { + chunkFormat: 'array-push', + chunkLoading: 'jsonp', + workerChunkLoading: 'import-scripts', + wasmLoading: 'fetch', + module: false, + } + : { + chunkFormat: 'module', + chunkLoading: 'import', + workerChunkLoading: 'import', + wasmLoading: 'fetch', + library: { type: 'module' }, + module: true, + }, optimization: { - avoidEntryIife: true, - runtimeChunk: 'single', + ...(isRscMode + ? { + mangleExports: false, + splitChunks: false, + usedExports: false, + } + : { avoidEntryIife: true }), + runtimeChunk: isRscMode ? false : 'single', }, }, }, @@ -971,7 +849,14 @@ export const pluginReactRouter = ( // root route into a hydratable `index.html` at build time. node: { source: { - entry: nodeEntries, + entry: isRscMode + ? { + [rscServerEntryName]: { + import: finalEntryRscPath, + layer: RSC_LAYERS.rsc, + }, + } + : nodeEntries, }, output: { distPath: { @@ -994,8 +879,10 @@ export const pluginReactRouter = ( }, ], }, - externals: nodeExternals, - ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}), + externals: isRscMode ? undefined : nodeExternals, + ...(shouldDependOnWebCompiler && !isRscMode + ? { dependencies: ['web'] } + : {}), externalsType: resolvedServerOutput, output: { chunkFormat: resolvedServerOutput, @@ -1011,91 +898,80 @@ export const pluginReactRouter = ( }); }); - api.modifyEnvironmentConfig( - async (config, { name, mergeEnvironmentConfig }) => { - if (name !== 'web' && name !== 'node') { - return config; - } - - return mergeEnvironmentConfig(config, { - tools: { - rspack: rspackConfig => { - if (pluginOptions.federation) { - ensureFederationAsyncStartup(rspackConfig); - } - - if (name === 'node') { - const output = rspackConfig.output; - if (output) { - const library = output.library; - const libraryOptions = - library && - typeof library === 'object' && - !Array.isArray(library) - ? library - : {}; - rspackConfig.output = { - ...output, - library: { - ...libraryOptions, - type: - resolvedServerOutput === 'module' - ? 'module' - : 'commonjs2', - }, - }; - } - } + registerReactRouterEnvironmentOutput({ + api, + federation: pluginOptions.federation, + resolvedServerOutput, + }); - return rspackConfig; - }, - }, - }); + if (isRscMode) { + registerReactRouterRscRouteTransforms({ + api, + isBuild, + performanceProfiler, + routeByFilePath, + routeChunkCache, + routeChunkConfig, + }); + } else { + if (!routeChunkOptions || !routeTransformExecutor) { + throw new Error( + `[${PLUGIN_NAME}] Classic React Router mode was initialized without route transform support.` + ); } - ); - registerModifyBrowserManifestAssets( - api, - routes, - pluginOptions, - appDirectory, - () => assetPrefix, - routeChunkOptions, - { - subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity, - future, - manifestChunkNames, - onManifest: (manifest, sri, moduleExportsByRouteId, context) => - stageLatestManifests( - manifest, - sri, - moduleExportsByRouteId, - context.compilation - ), - } - ); + registerModifyBrowserManifestAssets( + api, + routes, + pluginOptions, + appDirectory, + () => assetPrefix, + routeChunkOptions, + { + subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity, + future, + manifestChunkNames, + onManifest: (manifest, sri, moduleExportsByRouteId, context) => + stageLatestManifests( + manifest, + sri, + moduleExportsByRouteId, + context.compilation + ), + } + ); - registerBuildOutputTransforms({ - api, - resolvedServerOutput, - performanceProfiler, - getLatestServerManifest: () => latestServerManifest, - getLatestServerManifestByBundleId: bundleId => - latestServerManifestsByBundleId[bundleId], - routes, - pluginOptions, - getClientStats: () => clientStats, - appDirectory, - getAssetPrefix: () => assetPrefix, - routeChunkOptions, - routeTransformExecutor, - routeByFilePath, - routeChunkConfig, - isBuild, - splitRouteModules: Boolean(splitRouteModules), - ssr, - isSpaMode, - rootRoutePath, - }); + registerBuildOutputTransforms({ + api, + resolvedServerOutput, + performanceProfiler, + getLatestServerManifest: () => latestServerManifest, + getLatestServerManifestByBundleId: bundleId => + latestServerManifestsByBundleId[bundleId], + routes, + pluginOptions, + getClientStats: () => clientStats, + appDirectory, + getAssetPrefix: () => assetPrefix, + routeChunkOptions, + routeTransformExecutor, + routeByFilePath, + routeChunkConfig, + isBuild, + splitRouteModules: Boolean(splitRouteModules), + ssr, + isSpaMode, + rootRoutePath, + onRouteModuleAnalysis: rememberRouteModuleAnalysis, + }); + } }, }); + +export const pluginReactRouterRSC = ( + options: ReactRouterRSCPluginOptions = {} +): RsbuildPlugin | RsbuildPlugin[] => + pluginReactRouter({ + ...options, + rsc: options.rsc ?? true, + }); diff --git a/src/manifest.ts b/src/manifest.ts index 3d6ad53c..b63bcab1 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -15,7 +15,10 @@ import { type RouteChunkCache, type RouteChunkConfig, } from './route-chunks.js'; -import { getRouteModuleAnalysis } from './export-utils.js'; +import { + getRouteModuleAnalysis, + type RouteModuleAnalysis, +} from './export-utils.js'; import { getCappedPluginConcurrency } from './concurrency.js'; import { runPluginEffect, tryPluginPromise } from './effect-runtime.js'; @@ -79,6 +82,10 @@ type RouteChunkManifestOptions = { rootRouteFile?: string; isBuild?: boolean; cache?: RouteChunkCache; + analyzeRouteModule?: ( + routeFilePath: string, + route: Route + ) => Promise; }; export type ReactRouterManifestForDev = { @@ -278,6 +285,8 @@ const analyzeRouteForManifestEffect = ({ routeChunkConfig, routeEntryName, routeFilePath, + route, + analyzeRouteModule, }: { discoveredCssAssets: string[]; isBuild: boolean; @@ -285,10 +294,16 @@ const analyzeRouteForManifestEffect = ({ routeChunkConfig: RouteChunkConfig | null; routeEntryName: string; routeFilePath: string; + route: Route; + analyzeRouteModule?: ( + routeFilePath: string, + route: Route + ) => Promise; }): Effect.Effect => tryPluginPromise(async () => { const { code, exports: exportNames } = - await getRouteModuleAnalysis(routeFilePath); + (await analyzeRouteModule?.(routeFilePath, route)) ?? + (await getRouteModuleAnalysis(routeFilePath)); const cssAssets = !isBuild && discoveredCssAssets.length === 0 && CSS_IMPORT_RE.test(code) ? [ @@ -430,6 +445,8 @@ export function generateReactRouterManifestForDevEffect( routeChunkConfig, routeEntryName, routeFilePath, + route, + analyzeRouteModule: routeChunkOptions?.analyzeRouteModule, }); const hasClientAction = routeAnalysis.exports.has( diff --git a/src/modify-browser-manifest.ts b/src/modify-browser-manifest.ts index 76ad4689..ca1a79c8 100644 --- a/src/modify-browser-manifest.ts +++ b/src/modify-browser-manifest.ts @@ -33,8 +33,8 @@ type CompilationWithIntegrityAssets = | Pick; type ModifyBrowserManifestOptions = { - future?: { unstable_subResourceIntegrity?: boolean }; subResourceIntegrity?: boolean; + future?: { unstable_subResourceIntegrity?: boolean }; manifestChunkNames?: ReadonlySet; onManifest?: ( manifest: Awaited>, @@ -132,8 +132,8 @@ export function registerModifyBrowserManifestAssets( ); const finalizeSri = Boolean( routeChunkOptions?.isBuild && - (options?.subResourceIntegrity ?? - options?.future?.unstable_subResourceIntegrity) + (options?.subResourceIntegrity ?? + options?.future?.unstable_subResourceIntegrity) ); const generatedManifests = finalizeSri ? new WeakMap() 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/prerender.ts b/src/prerender.ts index a01fa8af..1c60860b 100644 --- a/src/prerender.ts +++ b/src/prerender.ts @@ -13,12 +13,11 @@ type PrerenderPathsConfig = getStaticPaths: () => string[]; }) => boolean | string[] | Promise); -type PrerenderConfigObject = Extract< - NonNullable, - { paths: unknown } -> & { +type PrerenderConfigObject = { + paths?: PrerenderPathsConfig; + concurrency?: number; unstable_concurrency?: number; -}; +} & Record; type PrerenderConfig = ReactRouterPrerenderConfig | PrerenderConfigObject; type PrerenderConcurrencyConfig = @@ -78,7 +77,11 @@ export const createPrerenderRoutes = ( grouped: Record = groupRoutesByParentId(manifest) ): MatchRouteObject[] => { return (grouped[parentId] || []).map(route => { - const common = { id: route.id, path: route.path }; + const common = { + id: route.id, + path: route.path, + caseSensitive: route.caseSensitive, + }; if (route.index) { return { index: true, ...common } as MatchRouteObject; } @@ -120,10 +123,9 @@ export const getSsrFalsePrerenderExportErrors = ({ const errors: string[] = []; for (const [routeId, route] of Object.entries(manifestRoutes)) { const exports = routeExports[routeId] ?? []; - const invalidApis: string[] = []; - - if (exports.includes('headers')) invalidApis.push('headers'); - if (exports.includes('action')) invalidApis.push('action'); + const invalidApis = ['headers', 'action', 'middleware'].filter(api => + exports.includes(api) + ); if (invalidApis.length > 0) { errors.push( diff --git a/src/react-router-config.ts b/src/react-router-config.ts index 3ddf4da4..59477ae6 100644 --- a/src/react-router-config.ts +++ b/src/react-router-config.ts @@ -2,11 +2,14 @@ import type { BuildManifest as ReactRouterBuildManifest, Config as ReactRouterConfig, } from '@react-router/dev/config'; +import { createRequire } from 'node:module'; import type { NormalizedConfig } from '@rsbuild/core'; import type { RouteConfigEntry } from '@react-router/dev/routes'; import { Effect } from 'effect'; import { runPluginEffect, tryPluginPromise } from './effect-runtime.js'; +const require = createRequire(import.meta.url); + export type BuildEndHook = { bivarianceHack(args: { buildManifest: ReactRouterBuildManifest | undefined; @@ -19,10 +22,15 @@ type SplitRouteModulesConfig = boolean | 'enforce'; export type Config = Omit< ReactRouterConfig, - 'buildEnd' | 'future' | 'splitRouteModules' | 'subResourceIntegrity' + | 'buildEnd' + | 'future' + | 'prerender' + | 'splitRouteModules' + | 'subResourceIntegrity' > & { buildEnd?: BuildEndHook; future?: Partial; + prerender?: PrerenderConfig; splitRouteModules?: SplitRouteModulesConfig; subResourceIntegrity?: boolean; }; @@ -35,6 +43,15 @@ type FutureConfig = { v8_splitRouteModules: boolean | 'enforce'; }; +type PrerenderConfig = + | ReactRouterConfig['prerender'] + | ({ + paths?: ReactRouterConfig['prerender']; + concurrency?: number; + unstable_concurrency?: number; + } & Record) + | undefined; + type RouteManifestEntry = { id: string; parentId?: string; @@ -52,6 +69,53 @@ type ResolveReactRouterConfigResult = { hasConfiguredServerModuleFormat: boolean; }; +const getInstalledReactRouterVersion = (): string | undefined => { + try { + return ( + require('react-router/package.json') as { version?: string | undefined } + ).version; + } catch { + return undefined; + } +}; + +export const getDefaultTrailingSlashAwareDataRequests = ( + reactRouterVersion: string | undefined = getInstalledReactRouterVersion() +): boolean => { + const major = Number(reactRouterVersion?.split('.')[0]); + return Number.isInteger(major) && major >= 8; +}; + +export const resolveRouteDiscoveryConfig = ({ + ssr, + userRouteDiscovery, +}: { + ssr: boolean; + userRouteDiscovery: Config['routeDiscovery']; +}): Config['routeDiscovery'] => { + if (!userRouteDiscovery) { + return ssr + ? ({ mode: 'lazy', manifestPath: '/__manifest' } as const) + : ({ mode: 'initial' } as const); + } + if (userRouteDiscovery.mode === 'initial') { + return userRouteDiscovery; + } + + if (!ssr) { + throw new Error( + 'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`' + ); + } + const manifestPath = userRouteDiscovery.manifestPath; + if (manifestPath && !manifestPath.startsWith('/')) { + throw new Error( + 'The `routeDiscovery.manifestPath` config must be a root-relative pathname beginning with a slash (i.e., "/__manifest")' + ); + } + return userRouteDiscovery; +}; + export type ResolvedReactRouterConfig = Readonly<{ appDirectory: string; basename: string; @@ -71,6 +135,15 @@ export type ResolvedReactRouterConfig = Readonly<{ unstable_routeConfig: RouteConfigEntry[]; }>; +const createDefaultFutureConfig = (): FutureConfig => ({ + unstable_optimizeDeps: false, + unstable_subResourceIntegrity: false, + unstable_trailingSlashAwareDataRequests: + getDefaultTrailingSlashAwareDataRequests(), + v8_middleware: false, + v8_splitRouteModules: false, +}); + const DEFAULT_CONFIG = { appDirectory: 'app', basename: '/', @@ -80,13 +153,7 @@ const DEFAULT_CONFIG = { splitRouteModules: true, subResourceIntegrity: false, ssr: true, - future: { - unstable_optimizeDeps: false, - unstable_subResourceIntegrity: false, - unstable_trailingSlashAwareDataRequests: false, - v8_middleware: false, - v8_splitRouteModules: false, - } satisfies FutureConfig, + future: createDefaultFutureConfig(), routeDiscovery: undefined, prerender: undefined, serverBundles: undefined, @@ -178,13 +245,14 @@ export const resolveReactRouterConfigEffect = ( reactRouterUserConfig; const presetConfig = yield* tryPluginPromise(() => preset.reactRouterConfig?.({ - reactRouterUserConfig: reactRouterUserConfigForPreset, + reactRouterUserConfig: + reactRouterUserConfigForPreset as ReactRouterConfig, }) ); if (!presetConfig) return null; const { presets: _presets, ...rest } = presetConfig as Config; return rest; - }), + }), { concurrency: 'unbounded' } ); 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/src/route-watch.ts b/src/route-watch.ts index 7a1211d7..ec775ba6 100644 --- a/src/route-watch.ts +++ b/src/route-watch.ts @@ -1,10 +1,17 @@ import { watch, type FSWatcher } from 'node:fs'; import { access, mkdir, readdir, writeFile } from 'node:fs/promises'; -import type { RsbuildConfig } from '@rsbuild/core'; +import type { RsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core'; +import type { RouteConfigEntry } from '@react-router/dev/routes'; import { Duration, Effect, Fiber } from 'effect'; import { dirname, resolve } from 'pathe'; import { getCappedPluginConcurrency } from './concurrency.js'; -import { runPluginEffect, tryPluginPromise } from './effect-runtime.js'; +import { + createDelayedPluginTask, + DEV_BACKGROUND_STARTUP_DELAY_MS, + runPluginEffect, + tryPluginPromise, +} from './effect-runtime.js'; +import { configRoutesToRouteManifestEntries } from './manifest.js'; import type { Route } from './types.js'; const ROUTE_RESTART_MARKER_ASSET = '.react-router/route-watch'; @@ -91,6 +98,20 @@ export const createRouteManifestSnapshot = ( ) ); +export const createRouteTopologySnapshot = ({ + appDirectory, + rootRouteFile, + routeConfig, +}: { + appDirectory: string; + rootRouteFile: string; + routeConfig: RouteConfigEntry[]; +}): Set => + createRouteManifestSnapshot([ + ['root', { path: '', id: 'root', file: rootRouteFile }], + ...configRoutesToRouteManifestEntries(appDirectory, routeConfig), + ]); + export const ensureDevRestartMarker = async ( restartMarkerPath: string ): Promise => { @@ -384,3 +405,116 @@ export const createRouteTopologyWatcher = async ({ directoryWatchers.clear(); }; }; + +export const registerRouteTopologyDevWatch = ({ + api, + appDirectory, + configWatchPaths, + getRootRouteFile, + loadRouteConfig, + onRouteTopologyChange, + outputClientPath, + pluginName, + routeConfig, + routeConfigWatchPaths, +}: { + api: RsbuildPluginAPI; + appDirectory: string; + configWatchPaths: string | string[]; + getRootRouteFile: () => string; + loadRouteConfig: () => Promise; + onRouteTopologyChange?: () => void | Promise; + outputClientPath: string; + pluginName: string; + routeConfig: RouteConfigEntry[]; + routeConfigWatchPaths: string | string[]; +}): WatchFileConfig[] => { + const watchDirectory = resolve(appDirectory); + const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath); + const getWatchedRouteTopology = async (): Promise> => + createRouteTopologySnapshot({ + appDirectory, + rootRouteFile: getRootRouteFile(), + routeConfig: await loadRouteConfig(), + }); + const routeTopologyWatchFiles: WatchFileConfig[] = onRouteTopologyChange + ? [] + : [ + { + paths: routeConfigWatchPaths, + type: 'reload-server', + }, + { + paths: routeRestartMarkerPath, + type: 'reload-server', + }, + ]; + let closeRouteTopologyWatcher: (() => Promise) | undefined; + let routeTopologyWatcherClosed = false; + + const reportRouteTopologyWatcherError = (error: unknown): void => { + api.logger.warn( + `[${pluginName}] Failed to watch route topology changes: ${error}` + ); + }; + + const routeTopologyWatcherTask = createDelayedPluginTask({ + delayMs: DEV_BACKGROUND_STARTUP_DELAY_MS, + run: () => + Effect.gen(function* () { + yield* tryPluginPromise(() => + ensureDevRestartMarker(routeRestartMarkerPath) + ); + const closeWatcher = yield* tryPluginPromise(() => + createRouteTopologyWatcher({ + watchDirectory, + getRouteTopology: getWatchedRouteTopology, + initialRouteTopology: createRouteTopologySnapshot({ + appDirectory, + rootRouteFile: getRootRouteFile(), + routeConfig, + }), + restartMarkerPath: routeRestartMarkerPath, + onRouteTopologyChange, + onError: reportRouteTopologyWatcherError, + }) + ); + if (routeTopologyWatcherClosed) { + yield* tryPluginPromise(() => closeWatcher()); + return; + } + closeRouteTopologyWatcher = closeWatcher; + }), + onError: reportRouteTopologyWatcherError, + }); + + const scheduleRouteTopologyWatcher = (): void => { + if (routeTopologyWatcherClosed || closeRouteTopologyWatcher) { + return; + } + routeTopologyWatcherTask.schedule(); + }; + + api.onBeforeStartDevServer(() => { + routeTopologyWatcherClosed = false; + }); + + api.onAfterDevCompile(() => { + scheduleRouteTopologyWatcher(); + }); + + api.onCloseDevServer(async () => { + routeTopologyWatcherClosed = true; + await routeTopologyWatcherTask.cancel(); + await closeRouteTopologyWatcher?.(); + closeRouteTopologyWatcher = undefined; + }); + + return [ + { + paths: configWatchPaths, + type: 'reload-server', + }, + ...routeTopologyWatchFiles, + ]; +}; diff --git a/src/rsc-route-config.ts b/src/rsc-route-config.ts new file mode 100644 index 00000000..ae746364 --- /dev/null +++ b/src/rsc-route-config.ts @@ -0,0 +1,189 @@ +import { resolve } from 'pathe'; +import type { Route } from './types.js'; + +const js = String.raw; + +type RouteNode = Route & { + children?: RouteNode[]; +}; + +const sortRoutes = (routes: RouteNode[]): RouteNode[] => + [...routes].sort((a, b) => a.id.localeCompare(b.id)); + +const createRouteTree = (routes: Record): RouteNode[] => { + const nodes = new Map(); + for (const route of Object.values(routes)) { + nodes.set(route.id, { ...route, children: [] }); + } + + const roots: RouteNode[] = []; + for (const route of nodes.values()) { + if (route.parentId) { + const parent = nodes.get(route.parentId); + if (parent) { + parent.children?.push(route); + continue; + } + } + roots.push(route); + } + + const sortChildren = (route: RouteNode): RouteNode => { + if (route.children?.length) { + route.children = sortRoutes(route.children).map(sortChildren); + } else { + delete route.children; + } + return route; + }; + + return sortRoutes(roots).map(sortChildren); +}; + +const appendRoute = ( + code: string, + route: RouteNode, + appDirectory: string +): string => { + const routeFile = resolve(appDirectory, route.file); + code += '{'; + code += `lazy: frameworkRoute(() => import(${JSON.stringify(routeFile)})),`; + code += `id: ${JSON.stringify(route.id)},`; + if (typeof route.path === 'string') { + code += `path: ${JSON.stringify(route.path)},`; + } + if (route.index) { + code += 'index: true,'; + } + if (route.caseSensitive) { + code += 'caseSensitive: true,'; + } + if (route.children?.length) { + code += 'children:['; + for (const child of route.children) { + code = appendRoute(code, child, appDirectory); + } + code += ']'; + } + code += '},'; + return code; +}; + +export const createRscRouteConfig = ({ + appDirectory, + routes, +}: { + appDirectory: string; + routes: Record; +}): string => { + let code = js` +function frameworkRoute(lazy) { + return async () => { + const mod = await lazy(); + let Component; + let Layout; + let ErrorBoundary; + let HydrateFallback; + if ("default" in mod && mod.default) { + if ("ServerComponent" in mod && mod.ServerComponent) { + throw new Error("Module cannot have both a default export and a ServerComponent export"); + } + Component = mod.default; + } else if ("ServerComponent" in mod && mod.ServerComponent) { + Component = mod.ServerComponent; + } + if ("Layout" in mod && mod.Layout) { + if ("ServerLayout" in mod && mod.ServerLayout) { + throw new Error("Module cannot have both a Layout export and a ServerLayout export"); + } + Layout = mod.Layout; + } else if ("ServerLayout" in mod && mod.ServerLayout) { + Layout = mod.ServerLayout; + } + if ("ErrorBoundary" in mod && mod.ErrorBoundary) { + if ("ServerErrorBoundary" in mod && mod.ServerErrorBoundary) { + throw new Error("Module cannot have both an ErrorBoundary export and a ServerErrorBoundary export"); + } + ErrorBoundary = mod.ErrorBoundary; + } else if ("ServerErrorBoundary" in mod && mod.ServerErrorBoundary) { + ErrorBoundary = mod.ServerErrorBoundary; + } + if ("HydrateFallback" in mod && mod.HydrateFallback) { + if ("ServerHydrateFallback" in mod && mod.ServerHydrateFallback) { + throw new Error("Module cannot have both a HydrateFallback export and a ServerHydrateFallback export"); + } + HydrateFallback = mod.HydrateFallback; + } else if ("ServerHydrateFallback" in mod && mod.ServerHydrateFallback) { + HydrateFallback = mod.ServerHydrateFallback; + } + + const { + action, + clientAction, + clientLoader, + clientMiddleware, + handle, + headers, + links, + loader, + meta, + middleware, + shouldRevalidate, + } = mod; + + return { + Component, + ErrorBoundary, + HydrateFallback, + Layout, + action, + clientAction, + clientLoader, + clientMiddleware, + handle, + headers, + links, + loader, + meta, + middleware, + shouldRevalidate, + }; + }; +} +export default [`; + + for (const route of createRouteTree(routes)) { + code = appendRoute(code, route, appDirectory); + } + + return `${code}];\n`; +}; + +export const createRscInternalClientModule = (): string => js` +"use client"; + +export { + BrowserRouter, + Form, + HashRouter, + Link, + Links, + MemoryRouter, + Meta, + Navigate, + NavLink, + Outlet, + Route, + Router, + RouterProvider, + Routes, + ScrollRestoration, + StaticRouter, + StaticRouterProvider, + UNSAFE_AwaitContextProvider, + UNSAFE_WithComponentProps, + UNSAFE_WithErrorBoundaryProps, + UNSAFE_WithHydrateFallbackProps, + unstable_HistoryRouter, +} from "react-router"; +`; diff --git a/src/rsc-route-transform-loader.ts b/src/rsc-route-transform-loader.ts new file mode 100644 index 00000000..df638b94 --- /dev/null +++ b/src/rsc-route-transform-loader.ts @@ -0,0 +1,65 @@ +type LoaderCallback = ( + error: Error | null, + code?: string | Buffer, + map?: unknown +) => void; + +type TransformArgs = { + code: string; + resource: string; + resourcePath: string; + resourceQuery?: string; + environment: { name: string }; +}; + +type TransformResult = { + code: string; + map?: unknown; +}; + +type ReactRouterRscTransform = ( + args: TransformArgs +) => Promise; + +type LoaderCompiler = { + __reactRouterRscRouteTransform?: ReactRouterRscTransform; +}; + +type RscRouteTransformLoaderContext = { + _compiler?: LoaderCompiler; + async(): LoaderCallback; + getOptions(): { + environmentName: string; + }; + resource: string; + resourcePath: string; + resourceQuery?: string; +}; + +export default function rscRouteTransformLoader( + this: RscRouteTransformLoaderContext, + source: string | Buffer, + map: unknown +): void { + const callback = this.async(); + const transform = this._compiler?.__reactRouterRscRouteTransform; + + if (!transform) { + callback(null, source, map); + return; + } + + transform({ + code: source.toString(), + resource: this.resource, + resourcePath: this.resourcePath, + resourceQuery: this.resourceQuery, + environment: { name: this.getOptions().environmentName }, + }) + .then(result => { + callback(null, result.code, result.map ?? map); + }) + .catch(error => { + callback(error instanceof Error ? error : new Error(String(error))); + }); +} diff --git a/src/rsc-route-transforms.ts b/src/rsc-route-transforms.ts new file mode 100644 index 00000000..7a283cbe --- /dev/null +++ b/src/rsc-route-transforms.ts @@ -0,0 +1,503 @@ +import { generate, parse } from './yuku.js'; +import { getExportNames } from './export-utils.js'; +import { removeExports, removeUnusedImports } from './route-export-pruning.js'; +import { + createEmptyRouteChunkByExportName, + detectRouteChunks, + routeChunkExportNames, + validateRouteChunks, + type RouteChunkCache, + type RouteChunkConfig, + type RouteChunkExportName, +} from './route-chunks.js'; + +const ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR = ` +import * as ___EnsureClientRouteModuleForHMR_REACT___ from "react"; +export function EnsureClientRouteModuleForHMR___() { return ___EnsureClientRouteModuleForHMR_REACT___.createElement(___EnsureClientRouteModuleForHMR_REACT___.Fragment, null) } +`; + +const CLIENT_NON_COMPONENT_EXPORTS = [ + 'clientAction', + 'clientLoader', + 'clientMiddleware', + 'handle', + 'meta', + 'links', + 'shouldRevalidate', +] as const; + +const CLIENT_ROUTE_EXPORTS = [ + ...CLIENT_NON_COMPONENT_EXPORTS, + 'default', + 'ErrorBoundary', + 'HydrateFallback', + 'Layout', +] as const; + +const SERVER_COMPONENT_EXPORTS = [ + 'ServerComponent', + 'ServerLayout', + 'ServerHydrateFallback', + 'ServerErrorBoundary', +] as const; + +const SERVER_ROUTE_EXPORTS = [ + ...SERVER_COMPONENT_EXPORTS, + 'loader', + 'action', + 'middleware', + 'headers', +] as const; + +const CLIENT_ROUTE_EXPORTS_SET = new Set(CLIENT_ROUTE_EXPORTS); +const SERVER_COMPONENT_EXPORTS_SET = new Set(SERVER_COMPONENT_EXPORTS); +const SERVER_ROUTE_EXPORTS_SET = new Set(SERVER_ROUTE_EXPORTS); + +const MUTUALLY_EXCLUSIVE_ROUTE_EXPORTS = new Map([ + ['ErrorBoundary', 'ServerErrorBoundary'], + ['HydrateFallback', 'ServerHydrateFallback'], + ['Layout', 'ServerLayout'], + ['default', 'ServerComponent'], +]); + +const CLIENT_CHUNK_QUERY = 'client-route-module'; +const SERVER_MODULE_QUERY = 'server-route-module'; + +type RscRouteTransformOptions = { + code: string; + resourcePath: string; + resourceQuery?: string; + isRootRoute: boolean; + routeId: string; + routeChunkCache: RouteChunkCache; + routeChunkConfig: RouteChunkConfig; + isServerEnvironment: boolean; + isDev: boolean; +}; + +type RscRouteTransformResult = { + code: string; + map: null | ReturnType['map']; +}; + +type RscRouteTransformTarget = + | { kind: 'client-route-module'; chunk: string } + | { kind: 'server-route-module' } + | { kind: 'server-route-entry' } + | { kind: 'client-route-entry' }; + +const hasQuery = (resourceQuery: string | undefined, key: string): boolean => + createResourceQueryParams(resourceQuery).has(key); + +const getQueryValue = ( + resourceQuery: string | undefined, + key: string +): string | null => createResourceQueryParams(resourceQuery).get(key); + +const createResourceQueryParams = ( + resourceQuery: string | undefined +): URLSearchParams => + new URLSearchParams((resourceQuery ?? '').replace(/^\?/, '')); + +const createClientRouteModuleId = ( + resourcePath: string, + resourceQuery: string | undefined, + value: string +): string => { + const params = createResourceQueryParams(resourceQuery); + params.delete(CLIENT_CHUNK_QUERY); + params.delete(SERVER_MODULE_QUERY); + params.set(CLIENT_CHUNK_QUERY, value); + return `${resourcePath}?${params.toString()}`; +}; + +const createServerRouteModuleId = ( + resourcePath: string, + resourceQuery: string | undefined +): string => { + const params = createResourceQueryParams(resourceQuery); + params.delete(CLIENT_CHUNK_QUERY); + params.delete(SERVER_MODULE_QUERY); + params.set(SERVER_MODULE_QUERY, ''); + return `${resourcePath}?${params.toString()}`; +}; + +const isClientRouteExport = (name: string): boolean => + CLIENT_ROUTE_EXPORTS_SET.has(name); + +const isServerRouteExport = (name: string): boolean => + SERVER_ROUTE_EXPORTS_SET.has(name); + +const isServerComponentExport = (name: string): boolean => + SERVER_COMPONENT_EXPORTS_SET.has(name); + +const shouldSplitRouteModules = ({ + isRootRoute, + routeChunkConfig, +}: Pick< + RscRouteTransformOptions, + 'isRootRoute' | 'routeChunkConfig' +>): boolean => routeChunkConfig.splitRouteModules === 'enforce' && !isRootRoute; + +const resolveRscRouteTransformTarget = ({ + resourceQuery, + isServerEnvironment, +}: Pick< + RscRouteTransformOptions, + 'resourceQuery' | 'isServerEnvironment' +>): RscRouteTransformTarget => { + const clientRouteChunk = getQueryValue(resourceQuery, CLIENT_CHUNK_QUERY); + if (clientRouteChunk) { + return { + kind: 'client-route-module', + chunk: clientRouteChunk, + }; + } + + if (hasQuery(resourceQuery, SERVER_MODULE_QUERY)) { + return { kind: 'server-route-module' }; + } + + return { + kind: isServerEnvironment ? 'server-route-entry' : 'client-route-entry', + }; +}; + +const getRouteChunks = ({ + code, + resourcePath, + isRootRoute, + routeChunkCache, +}: Pick< + RscRouteTransformOptions, + 'code' | 'resourcePath' | 'isRootRoute' | 'routeChunkCache' +>) => { + if (isRootRoute) { + return { + chunkedExports: [] as RouteChunkExportName[], + hasRouteChunks: false, + hasRouteChunkByExportName: createEmptyRouteChunkByExportName(), + }; + } + + if (!routeChunkExportNames.some(exportName => code.includes(exportName))) { + return { + chunkedExports: [] as RouteChunkExportName[], + hasRouteChunks: false, + hasRouteChunkByExportName: createEmptyRouteChunkByExportName(), + }; + } + + return detectRouteChunks(code, routeChunkCache, resourcePath); +}; + +const getRouteChunkValidation = ( + exportNames: readonly string[], + routeChunks: ReturnType +): Record => { + const exports = new Set(exportNames); + return { + clientAction: + !exports.has('clientAction') || + routeChunks.hasRouteChunkByExportName.clientAction, + clientLoader: + !exports.has('clientLoader') || + routeChunks.hasRouteChunkByExportName.clientLoader, + clientMiddleware: + !exports.has('clientMiddleware') || + routeChunks.hasRouteChunkByExportName.clientMiddleware, + HydrateFallback: + !exports.has('HydrateFallback') || + routeChunks.hasRouteChunkByExportName.HydrateFallback, + }; +}; + +const validateRouteModuleExports = (exports: readonly string[]): void => { + const errors: string[] = []; + for (const [clientExport, serverExport] of MUTUALLY_EXCLUSIVE_ROUTE_EXPORTS) { + if (exports.includes(clientExport) && exports.includes(serverExport)) { + errors.push(`- ${clientExport} and ${serverExport}`); + } + } + if (errors.length > 0) { + throw new Error( + 'Invalid route module exports. The following pairs of exports are ' + + `mutually exclusive and cannot be exported from the same module:\n${errors.join( + '\n' + )}` + ); + } +}; + +const createClientRouteEntry = async ({ + code, + resourcePath, + resourceQuery, + isRootRoute, + routeId, + routeChunkCache, + routeChunkConfig, +}: RscRouteTransformOptions): Promise => { + const exportNames = await getExportNames(code); + validateRouteModuleExports(exportNames); + const routeChunks = getRouteChunks({ + code, + resourcePath, + isRootRoute, + routeChunkCache, + }); + let needsReactImport = false; + + const reexports = exportNames + .filter(exportName => !isServerRouteExport(exportName)) + .map(exportName => { + const chunkName = routeChunks.hasRouteChunkByExportName[ + exportName as RouteChunkExportName + ] + ? exportName + : 'shared'; + const target = createClientRouteModuleId( + resourcePath, + resourceQuery, + chunkName + ); + if (exportName === 'default') { + return `export { default } from ${JSON.stringify(target)};`; + } + if (exportName === 'HydrateFallback') { + needsReactImport = true; + return `export const HydrateFallback = React.lazy(() => import(${JSON.stringify( + target + )}).then(mod => ({ default: mod.HydrateFallback })));`; + } + if ( + exportName === 'clientAction' || + exportName === 'clientLoader' || + exportName === 'clientMiddleware' + ) { + return `export const ${exportName} = async (...args) => import(${JSON.stringify( + target + )}).then(mod => mod.${exportName}(...args));`; + } + return `export { ${exportName} } from ${JSON.stringify(target)};`; + }); + + if (shouldSplitRouteModules({ isRootRoute, routeChunkConfig })) { + validateRouteChunks({ + config: routeChunkConfig, + id: routeId, + valid: getRouteChunkValidation(exportNames, routeChunks), + }); + } + + return { + code: `"use client";\n${ + needsReactImport ? 'import * as React from "react";\n' : '' + }${reexports.join('\n')}\n`, + map: null, + }; +}; + +const createServerRouteEntry = async ({ + code, + resourcePath, + resourceQuery, + isRootRoute, + routeId, + routeChunkCache, + routeChunkConfig, +}: RscRouteTransformOptions): Promise => { + const exportNames = await getExportNames(code); + validateRouteModuleExports(exportNames); + const routeChunks = getRouteChunks({ + code, + resourcePath, + isRootRoute, + routeChunkCache, + }); + + const clientTarget = createClientRouteModuleId( + resourcePath, + resourceQuery, + 'shared' + ); + const serverTarget = createServerRouteModuleId(resourcePath, resourceQuery); + const lines: string[] = []; + let needsReactImport = false; + const needsDefaultRootErrorBoundary = + isRootRoute && + !exportNames.includes('ErrorBoundary') && + !exportNames.includes('ServerErrorBoundary'); + + for (const exportName of exportNames) { + if (isClientRouteExport(exportName)) { + const chunkName = routeChunks.hasRouteChunkByExportName[ + exportName as RouteChunkExportName + ] + ? exportName + : 'shared'; + const target = createClientRouteModuleId( + resourcePath, + resourceQuery, + chunkName + ); + lines.push( + exportName === 'default' + ? `export { default } from ${JSON.stringify(target)};` + : `export { ${exportName} } from ${JSON.stringify(target)};` + ); + continue; + } + if (isServerComponentExport(exportName)) { + needsReactImport = true; + lines.push( + `import { ${exportName} as ${exportName}WithoutClientChunk } from ${JSON.stringify( + serverTarget + )};` + ); + lines.push(`export function ${exportName}(props) {`); + lines.push(' return React.createElement(React.Fragment, null,'); + lines.push( + ' React.createElement(EnsureClientRouteModuleForHMR___, null),' + ); + lines.push( + ` React.createElement(${exportName}WithoutClientChunk, props),` + ); + lines.push(' );'); + lines.push('}'); + continue; + } + if (isServerRouteExport(exportName)) { + lines.push( + `export { ${exportName} } from ${JSON.stringify(serverTarget)};` + ); + continue; + } + lines.push( + `export { ${exportName} } from ${JSON.stringify(serverTarget)};` + ); + } + + if (needsDefaultRootErrorBoundary) { + lines.push( + `export { ErrorBoundary } from ${JSON.stringify(clientTarget)};` + ); + } + + if (shouldSplitRouteModules({ isRootRoute, routeChunkConfig })) { + validateRouteChunks({ + config: routeChunkConfig, + id: routeId, + valid: getRouteChunkValidation(exportNames, routeChunks), + }); + } + + const prefix = needsReactImport + ? `import * as React from "react";\nimport { EnsureClientRouteModuleForHMR___ } from ${JSON.stringify( + clientTarget + )};\n` + : ''; + return { + code: `${prefix}${lines.join('\n')}\n`, + map: null, + }; +}; + +const createClientRouteModule = async ( + code: string, + sourceFileName: string, + clientRouteChunk: string, + options: RscRouteTransformOptions +): Promise => { + const ast = parse(code, { sourceType: 'module' }); + const exportNames = new Set(await getExportNames(code)); + const routeChunks = getRouteChunks(options); + const exportsToRemove = + clientRouteChunk === 'shared' + ? [...SERVER_ROUTE_EXPORTS, ...routeChunks.chunkedExports] + : [ + ...SERVER_ROUTE_EXPORTS, + ...Array.from(exportNames).filter( + exportName => exportName !== clientRouteChunk + ), + ]; + const removed = removeExports(ast, exportsToRemove); + if (removed) { + removeUnusedImports(ast); + } + const generated = generate(ast, { + sourceMaps: false, + filename: sourceFileName, + sourceFileName, + }); + let clientModuleCode = `"use client";\n${generated.code}`; + + if ( + clientRouteChunk === 'shared' && + options.isRootRoute && + !exportNames.has('ErrorBoundary') && + !exportNames.has('ServerErrorBoundary') + ) { + const hasRootLayout = + exportNames.has('Layout') || exportNames.has('ServerLayout'); + clientModuleCode += `\nimport { createElement as __rr_createElement } from "react";\n`; + clientModuleCode += `export function ErrorBoundary() {\n`; + clientModuleCode += ` return __rr_createElement(${JSON.stringify( + hasRootLayout ? 'main' : 'div' + )}, null, "Unexpected Server Error");\n`; + clientModuleCode += `}\n`; + } + + return { + code: + clientModuleCode + + (clientRouteChunk === 'shared' + ? `\n${ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR}` + : '') + + (options.isDev + ? `\nif (import.meta.webpackHot) { import.meta.webpackHot.accept(); }\n` + : ''), + map: null, + }; +}; + +const createServerRouteModule = ( + code: string, + sourceFileName: string +): RscRouteTransformResult => { + const ast = parse(code, { sourceType: 'module' }); + const removed = removeExports(ast, CLIENT_ROUTE_EXPORTS); + if (removed) { + removeUnusedImports(ast); + } + const generated = generate(ast, { + sourceMaps: false, + filename: sourceFileName, + sourceFileName, + }); + return { + code: generated.code, + map: null, + }; +}; + +export const transformRscRouteModule = async ( + options: RscRouteTransformOptions +): Promise => { + const target = resolveRscRouteTransformTarget(options); + switch (target.kind) { + case 'client-route-module': + return createClientRouteModule( + options.code, + options.resourcePath, + target.chunk, + options + ); + case 'server-route-module': + return createServerRouteModule(options.code, options.resourcePath); + case 'server-route-entry': + return createServerRouteEntry(options); + case 'client-route-entry': + return createClientRouteEntry(options); + } +}; diff --git a/src/rsc-runtime.d.ts b/src/rsc-runtime.d.ts new file mode 100644 index 00000000..cac987d1 --- /dev/null +++ b/src/rsc-runtime.d.ts @@ -0,0 +1,108 @@ +declare module 'react-server-dom-rspack/client.browser' { + export function createFromReadableStream( + stream: ReadableStream, + options?: { temporaryReferences?: unknown } + ): Promise; + + export function createTemporaryReferenceSet(): unknown; + + export function encodeReply( + value: unknown, + options?: { temporaryReferences?: unknown } + ): Promise; + + export function setServerCallback( + fn: (id: string, args: unknown[]) => Promise + ): void; +} + +declare module 'react-server-dom-rspack/client.node' { + export * from 'react-server-dom-rspack/client.browser'; +} + +declare module 'react-server-dom-rspack/server.node' { + export function createTemporaryReferenceSet(): unknown; + + export function decodeAction( + body: FormData, + serverManifest?: unknown + ): Promise<() => Promise>; + + export function decodeFormState( + actionResult: unknown, + body: FormData, + serverManifest?: unknown + ): unknown; + + export function decodeReply( + body: FormData | string, + options?: { temporaryReferences?: unknown } + ): Promise; + + export function loadServerAction( + actionId: string + ): (...args: unknown[]) => unknown; + + export function renderToReadableStream( + model: unknown, + options?: { + onError?: (error: unknown) => string | undefined; + temporaryReferences?: unknown; + } + ): ReadableStream; +} + +declare module 'virtual/react-router/unstable_rsc/routes' { + import type { unstable_RSCRouteConfig as RSCRouteConfig } from 'react-router'; + const routes: RSCRouteConfig; + export default routes; +} + +declare module 'virtual/react-router/unstable_rsc/route-discovery' { + const routeDiscovery: + | { mode: 'initial' } + | { mode: 'lazy'; manifestPath?: string }; + export default routeDiscovery; +} + +declare module 'virtual/react-router/unstable_rsc/basename' { + const basename: string; + export default basename; +} + +declare module 'virtual/react-router/unstable_rsc/react-router-serve-config' { + const config: { + assetsBuildDirectory: string; + publicPath: string; + }; + export default config; +} + +declare module 'virtual/react-router/unstable_rsc/inject-hmr-runtime' {} + +declare module 'virtual/react-router/unstable_rsc/bootstrap-scripts' { + const bootstrapScripts: string[]; + export default bootstrapScripts; +} + +declare module 'virtual:react-router/unstable_rsc/routes' { + export { default } from 'virtual/react-router/unstable_rsc/routes'; +} + +declare module 'virtual:react-router/unstable_rsc/route-discovery' { + export { default } from 'virtual/react-router/unstable_rsc/route-discovery'; +} + +declare module 'virtual:react-router/unstable_rsc/basename' { + export { default } from 'virtual/react-router/unstable_rsc/basename'; +} + +declare module 'virtual:react-router/unstable_rsc/react-router-serve-config' { + export { default } from 'virtual/react-router/unstable_rsc/react-router-serve-config'; +} + +declare module 'virtual:react-router/unstable_rsc/inject-hmr-runtime' {} + +declare module 'virtual:react-router/unstable_rsc/bootstrap-scripts' { + export { default } from 'virtual/react-router/unstable_rsc/bootstrap-scripts'; +} diff --git a/src/rsc-support.ts b/src/rsc-support.ts new file mode 100644 index 00000000..942bcc57 --- /dev/null +++ b/src/rsc-support.ts @@ -0,0 +1,426 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequestListener } from '@remix-run/node-fetch-server'; +import type { RsbuildConfig, RsbuildPlugin } from '@rsbuild/core'; +import { relative, resolve } from 'pathe'; +import type { Config } from './react-router-config.js'; +import type { RouteChunkCache, RouteChunkConfig } from './route-chunks.js'; +import type { PluginOptions, Route } from './types.js'; +import { getVirtualModuleFilePath } from './virtual-modules.js'; +import { + createRscInternalClientModule, + createRscRouteConfig, +} from './rsc-route-config.js'; +import { transformRscRouteModule } from './rsc-route-transforms.js'; + +const RSC_VIRTUAL_ALIAS_IDS = [ + 'routes', + 'route-discovery', + 'inject-hmr-runtime', + 'basename', + 'react-router-serve-config', + 'bootstrap-scripts', +] as const; + +type RscDevServer = { + environments: { + node: { + loadBundle(entryName: string): Promise; + }; + }; +}; + +type RscServerBuild = { + default?: { + fetch?: (request: Request) => Promise; + }; +}; + +type RscVirtualModulesOptions = { + appDirectory: string; + basename: string; + buildDirectory: string; + isBuild: boolean; + outputClientPath: string; + publicPath: string; + routeDiscovery: Config['routeDiscovery']; + routes: Record; + ssr: boolean; +}; + +type RscPluginOptions = Exclude, boolean>; + +type RscDevServerSetup = NonNullable< + NonNullable['setup'] +>; + +type RscRouteTransformProfiler = { + record( + environmentName: string | undefined, + label: string, + resource: string, + task: () => Promise + ): Promise; +}; + +const mdxRoutePattern = /\.mdx?$/i; +const RSC_ROUTE_TRANSFORM_LOADER = 'react-router-rsc-route-transform'; + +const getRscRouteTransformLoaderPath = (): string => + join( + dirname(fileURLToPath(import.meta.url)), + import.meta.url.endsWith('.cjs') + ? 'rsc-route-transform-loader.cjs' + : 'rsc-route-transform-loader.js' + ); + +const getPackageVersion = ( + packageName: string, + resolvePackagePath: (specifier: string) => string | undefined +): string | undefined => { + const packageJsonPath = resolvePackagePath(`${packageName}/package.json`); + if (!packageJsonPath) { + return undefined; + } + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { + version?: unknown; + }; + return typeof packageJson.version === 'string' + ? packageJson.version + : undefined; + } catch { + return undefined; + } +}; + +const supportsReactRouterRsc = (version: string | undefined): boolean => { + const match = version?.match(/^(\d+)\.(\d+)\./); + if (!match) { + return false; + } + const major = Number(match[1]); + const minor = Number(match[2]); + return major >= 8 || (major === 7 && minor >= 18); +}; + +export const assertReactRouterRscSupport = ({ + pluginName, + resolvePackagePath, +}: { + pluginName: string; + resolvePackagePath: (specifier: string) => string | undefined; +}): void => { + const reactRouterVersion = getPackageVersion( + 'react-router', + resolvePackagePath + ); + if (!supportsReactRouterRsc(reactRouterVersion)) { + throw new Error( + `[${pluginName}] React Router RSC mode requires react-router >=7.18.0 or >=8.0.0.` + ); + } + + for (const specifier of [ + 'react-server-dom-rspack/client.browser', + 'rsbuild-plugin-rsc', + ]) { + if (!resolvePackagePath(specifier)) { + throw new Error( + `[${pluginName}] React Router RSC mode requires \`${specifier}\` to be installed.` + ); + } + } +}; + +export const setupReactRouterRscPlugin = async ({ + api, + entryRscPath, + entrySsrPath, + pluginName, + rsc, +}: { + api: Parameters[0]; + entryRscPath: string; + entrySsrPath: string; + pluginName: string; + rsc: true | RscPluginOptions; +}): Promise => { + const { PLUGIN_RSC_NAME, pluginRSC } = await import('rsbuild-plugin-rsc'); + if (api.isPluginExists(PLUGIN_RSC_NAME)) { + api.logger.warn( + `[${pluginName}] The "${PLUGIN_RSC_NAME}" plugin is already registered. ` + + 'Skipping built-in RSC setup.' + ); + return; + } + + const userRscOptions: RscPluginOptions = rsc === true ? {} : rsc; + await pluginRSC({ + ...userRscOptions, + environments: { + server: 'node', + client: 'web', + }, + layers: { + rsc: [entryRscPath], + ssr: [entrySsrPath], + ...userRscOptions.layers, + }, + }).setup(api); +}; + +const shouldBypassRscDevRequest = (request: { + method?: string; + url?: string; +}): boolean => { + if (!request.url) { + return true; + } + if (request.method !== 'GET' && request.method !== 'POST') { + return true; + } + + const url = new URL(request.url, 'http://localhost'); + const pathname = url.pathname; + if (pathname.startsWith('/__rsbuild_')) { + return true; + } + if (pathname.endsWith('.rsc') || pathname.endsWith('.manifest')) { + return false; + } + return pathname !== '/' && /\.[a-z0-9]+$/i.test(pathname); +}; + +export const createReactRouterRscResolveAliases = ( + rootPath: string +): Record => ({ + ...Object.fromEntries( + RSC_VIRTUAL_ALIAS_IDS.flatMap(id => { + const moduleId = `virtual/react-router/unstable_rsc/${id}`; + const modulePath = resolve(rootPath, getVirtualModuleFilePath(moduleId)); + return [ + [`virtual:react-router/unstable_rsc/${id}`, modulePath], + [moduleId, modulePath], + ]; + }) + ), + 'react-router/internal/react-server-client': resolve( + rootPath, + getVirtualModuleFilePath('virtual/react-router/rsc-internal-client') + ), +}); + +export const createReactRouterRscVirtualModules = ({ + appDirectory, + basename, + buildDirectory, + isBuild, + outputClientPath, + publicPath, + routeDiscovery, + routes, + ssr, +}: RscVirtualModulesOptions): Record => { + const rscAssetsBuildDirectory = relative( + resolve(buildDirectory, 'server'), + outputClientPath + ); + const bootstrapPublicPath = publicPath.endsWith('/') + ? publicPath + : `${publicPath}/`; + + return { + 'virtual/react-router/unstable_rsc/routes': createRscRouteConfig({ + appDirectory, + routes, + }), + 'virtual/react-router/unstable_rsc/route-discovery': `export default ${JSON.stringify( + ssr === false ? { mode: 'initial' } : (routeDiscovery ?? { mode: 'lazy' }) + )};`, + 'virtual/react-router/unstable_rsc/inject-hmr-runtime': !isBuild + ? `if (import.meta.webpackHot) { + import.meta.webpackHot.accept(); + import.meta.webpackHot.on("rsc:update", () => { + requestAnimationFrame(() => { + globalThis.__reactRouterDataRouter?.revalidate?.(); + }); + }); +}` + : '', + 'virtual/react-router/unstable_rsc/basename': `export default ${JSON.stringify( + basename + )};`, + 'virtual/react-router/unstable_rsc/react-router-serve-config': `export default ${JSON.stringify( + { + assetsBuildDirectory: rscAssetsBuildDirectory, + publicPath, + } + )};`, + 'virtual/react-router/unstable_rsc/bootstrap-scripts': `export default ${JSON.stringify( + [`${bootstrapPublicPath}static/js/index.js`] + )};`, + 'virtual/react-router/rsc-internal-client': createRscInternalClientModule(), + }; +}; + +export const createReactRouterRscDevServerSetup = ({ + entryName, + pluginName, +}: { + entryName: string; + pluginName: string; +}): RscDevServerSetup => { + return ({ server }) => { + const devServer = server as unknown as RscDevServer; + const listener = createRequestListener(async request => { + const build = + await devServer.environments.node.loadBundle(entryName); + const handler = build.default?.fetch; + if (typeof handler !== 'function') { + throw new Error( + `[${pluginName}] RSC server build must default-export an object with a fetch function.` + ); + } + return handler(request); + }); + + server.middlewares.use((req, res, next) => { + if (shouldBypassRscDevRequest(req)) { + next(); + return; + } + Promise.resolve(listener(req, res)).catch(next); + }); + }; +}; + +export const registerReactRouterRscRouteTransforms = ({ + api, + isBuild, + performanceProfiler, + routeByFilePath, + routeChunkCache, + routeChunkConfig, +}: { + api: Parameters[0]; + isBuild: boolean; + performanceProfiler: RscRouteTransformProfiler; + routeByFilePath: Map; + routeChunkCache: RouteChunkCache; + routeChunkConfig: RouteChunkConfig; +}): void => { + const transformRoute = async ( + args: Parameters[1]>[0] + ) => { + const route = routeByFilePath.get(resolve(args.resourcePath)); + if (!route) { + return { code: args.code }; + } + + return performanceProfiler.record( + args.environment?.name, + 'rsc:route', + args.resource, + () => + transformRscRouteModule({ + code: args.code, + resourcePath: args.resourcePath, + resourceQuery: args.resourceQuery, + isRootRoute: route.id === 'root', + routeId: route.id, + routeChunkCache, + routeChunkConfig, + isServerEnvironment: args.environment.name === 'node', + isDev: !isBuild, + }) + ); + }; + + api.transform( + { + test: path => + routeByFilePath.has(resolve(path)) && !mdxRoutePattern.test(path), + order: 'pre', + }, + transformRoute + ); + + api.modifyBundlerChain((chain, { CHAIN_ID, environment }) => { + if (!chain.module.rules.has('mdx')) { + return; + } + + chain.plugin(`${RSC_ROUTE_TRANSFORM_LOADER}-${environment.name}`).use( + class ReactRouterRscRouteTransformPlugin { + apply(compiler: { + __reactRouterRscRouteTransform?: typeof transformRoute; + hooks: { + thisCompilation: { + tap( + name: string, + handler: (compilation: { + hooks: { + childCompiler: { + tap( + name: string, + handler: (childCompiler: { + __reactRouterRscRouteTransform?: typeof transformRoute; + }) => void + ): void; + }; + }; + }) => void + ): void; + }; + }; + }) { + compiler.__reactRouterRscRouteTransform = transformRoute; + compiler.hooks.thisCompilation.tap( + RSC_ROUTE_TRANSFORM_LOADER, + compilation => { + compilation.hooks.childCompiler.tap( + RSC_ROUTE_TRANSFORM_LOADER, + childCompiler => { + childCompiler.__reactRouterRscRouteTransform = transformRoute; + } + ); + } + ); + } + } + ); + + const mdxRule = chain.module.rules.get('mdx'); + const rscRouteTransformLoaderPath = getRscRouteTransformLoaderPath(); + const use = mdxRule + .use(RSC_ROUTE_TRANSFORM_LOADER) + .loader(rscRouteTransformLoaderPath) + .options({ + environmentName: environment.name, + }); + + if (mdxRule.uses.has('mdx')) { + use.before('mdx'); + } + + if (!mdxRule.uses.has(CHAIN_ID.USE.SWC)) { + const jsRule = chain.module.rules.get(CHAIN_ID.RULE.JS); + const jsMainRule = jsRule?.oneOfs.get(CHAIN_ID.ONE_OF.JS_MAIN); + const jsSwcUse = jsMainRule?.uses.get(CHAIN_ID.USE.SWC); + if (jsSwcUse) { + const mdxSwcUse = mdxRule.use(CHAIN_ID.USE.SWC); + const swcLoader = jsSwcUse.get('loader'); + const swcOptions = jsSwcUse.get('options'); + if (swcLoader) { + mdxSwcUse.loader(swcLoader); + } + if (swcOptions) { + mdxSwcUse.options(swcOptions); + } + mdxSwcUse.before(RSC_ROUTE_TRANSFORM_LOADER); + } + } + }); +}; diff --git a/src/templates/entry.rsc.client.tsx b/src/templates/entry.rsc.client.tsx new file mode 100644 index 00000000..5c451943 --- /dev/null +++ b/src/templates/entry.rsc.client.tsx @@ -0,0 +1,80 @@ +import 'virtual/react-router/unstable_rsc/inject-hmr-runtime'; + +import * as React from 'react'; +import { startTransition } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import type { DataRouter } from 'react-router'; +import { + unstable_createCallServer as createCallServer, + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, + type unstable_RSCPayload as RSCPayload, +} from 'react-router/dom'; +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from 'react-server-dom-rspack/client.browser'; + +setServerCallback( + createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + }) +); + +const hydrate = () => { + createFromReadableStream(getRSCStream()).then( + payload => { + startTransition(async () => { + const formState = + payload.type === 'render' ? await payload.formState : undefined; + + hydrateRoot( + document, + React.createElement( + React.StrictMode, + null, + React.createElement(RSCHydratedRouter, { + createFromReadableStream, + payload, + }) + ), + { + // @ts-expect-error React Router RSC formState is not typed yet. + formState, + } + ); + }); + }, + error => { + setTimeout(() => { + throw error; + }); + } + ); +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', hydrate, { once: true }); +} else { + hydrate(); +} + +const hot = ( + import.meta as unknown as { + webpackHot?: { + on(event: string, handler: () => void): void; + }; + } +).webpackHot; + +hot?.on('rsc:update', () => { + requestAnimationFrame(() => { + ( + window as typeof window & { __reactRouterDataRouter?: DataRouter } + ).__reactRouterDataRouter?.revalidate(); + }); +}); diff --git a/src/templates/entry.rsc.ssr.tsx b/src/templates/entry.rsc.ssr.tsx new file mode 100644 index 00000000..7dfe514f --- /dev/null +++ b/src/templates/entry.rsc.ssr.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { renderToReadableStream as renderHTMLToReadableStream } from 'react-dom/server'; +import { + unstable_routeRSCServerRequest as routeRSCServerRequest, + unstable_RSCStaticRouter as RSCStaticRouter, +} from 'react-router'; +import type { unstable_RSCPayload as RSCPayload } from 'react-router/dom'; +import { createFromReadableStream } from 'react-server-dom-rspack/client.node'; + +type PayloadPromise = Promise & { + _deepestRenderedBoundaryId?: string | null; + formState?: Promise; +}; + +export async function generateHTML( + request: Request, + serverResponse: Response, + options: { + bootstrapScripts?: string[]; + bootstrapModules?: string[]; + } = {} +): Promise { + return routeRSCServerRequest({ + request, + serverResponse, + createFromReadableStream, + async renderHTML(getPayload, renderOptions) { + const payloadPromise = getPayload() as PayloadPromise; + payloadPromise.formState ??= payloadPromise.then(payload => + payload.type === 'render' ? payload.formState : undefined + ); + + return renderHTMLToReadableStream( + , + { + ...renderOptions, + bootstrapModules: options.bootstrapModules, + bootstrapScripts: options.bootstrapScripts, + formState: (await payloadPromise.formState) as never, + signal: request.signal, + } + ); + }, + }); +} diff --git a/src/templates/entry.rsc.tsx b/src/templates/entry.rsc.tsx new file mode 100644 index 00000000..0ed3d3b8 --- /dev/null +++ b/src/templates/entry.rsc.tsx @@ -0,0 +1,79 @@ +import { + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction as loadServerActionSync, + renderToReadableStream, +} from 'react-server-dom-rspack/server.node'; +import { + RouterContextProvider, + unstable_matchRSCServerRequest as matchRSCServerRequest, +} from 'react-router'; + +import routes from 'virtual/react-router/unstable_rsc/routes'; +import routeDiscovery from 'virtual/react-router/unstable_rsc/route-discovery'; +import basename from 'virtual/react-router/unstable_rsc/basename'; +import unstable_reactRouterServeConfig from 'virtual/react-router/unstable_rsc/react-router-serve-config'; +import bootstrapScripts from 'virtual/react-router/unstable_rsc/bootstrap-scripts'; +import { generateHTML } from './entry.rsc.ssr.js'; + +export { unstable_reactRouterServeConfig }; + +type RscRequestHandler = { + fetch( + request: Request, + requestContext?: RouterContextProvider + ): Promise; +}; + +export function fetchServer( + request: Request, + requestContext?: RouterContextProvider +): Promise { + return matchRSCServerRequest({ + basename, + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction: (id: string) => Promise.resolve(loadServerActionSync(id)), + request, + requestContext, + routes, + routeDiscovery, + generateResponse(match, options) { + return new Response(renderToReadableStream(match.payload, options), { + status: match.statusCode, + headers: match.headers, + }); + }, + }); +} + +const handler: RscRequestHandler = { + async fetch( + request: Request, + requestContext?: RouterContextProvider + ): Promise { + if (requestContext && !(requestContext instanceof RouterContextProvider)) { + requestContext = undefined; + } + + return generateHTML(request, await fetchServer(request, requestContext), { + bootstrapScripts, + }); + }, +}; + +export default handler; + +const hot = ( + import.meta as unknown as { + webpackHot?: { + accept(): void; + }; + } +).webpackHot; + +hot?.accept(); diff --git a/src/types.ts b/src/types.ts index 49c5739d..641c27dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { RsbuildConfig } from '@rsbuild/core'; +import type { RsbuildConfig, Rspack } from '@rsbuild/core'; export type Route = { id: string; @@ -30,6 +30,23 @@ export type PluginOptions = { */ federation?: boolean; + /** + * Enable experimental React Router RSC framework mode. + * This composes `rsbuild-plugin-rsc` with React Router's Rsbuild + * environments. Environment names are managed by this plugin. + * Requires `react-router >=7.18.0 || >=8.0.0`, `rsbuild-plugin-rsc`, + * and `react-server-dom-rspack`. + * @default false + */ + rsc?: + | boolean + | { + layers?: { + rsc?: Rspack.RuleSetCondition; + ssr?: Rspack.RuleSetCondition; + }; + }; + /** * Opt in to Rsbuild's dev-only lazy compilation behavior. * @@ -63,6 +80,14 @@ export type PluginOptions = { onRouteTopologyChange?: () => void | Promise; }; +export type ReactRouterRSCPluginOptions = Omit & { + /** + * Optional overrides forwarded to `rsbuild-plugin-rsc`. + * Environment names are managed by this plugin. + */ + rsc?: Exclude, boolean>; +}; + export type RouteManifestItem = Omit & { module: string; clientActionModule?: string; diff --git a/tests/build-output-transforms.test.ts b/tests/build-output-transforms.test.ts index b080e5b4..06926d79 100644 --- a/tests/build-output-transforms.test.ts +++ b/tests/build-output-transforms.test.ts @@ -87,28 +87,40 @@ describe('build output transforms', () => { registerBuildOutputTransforms(options); - const routeModuleTransforms = harness.transforms.filter( - transform => transform.descriptor.order === 'post' + const explicitRouteModuleTransform = harness.transforms.find( + transform => + String(transform.descriptor.resourceQuery) === + String(/\?react-router-route/) + ); + const querylessRouteModuleTransform = harness.transforms.find( + transform => + transform.descriptor.order === 'post' && + transform.descriptor.environments === undefined && + typeof transform.descriptor.test === 'function' && + (transform.descriptor.test as (path: string) => boolean)( + options.routePath + ) ); - expect(routeModuleTransforms).toHaveLength(2); - expect(routeModuleTransforms[0].descriptor).toMatchObject({ + expect(explicitRouteModuleTransform?.descriptor).toMatchObject({ resourceQuery: /\?react-router-route/, order: 'post', }); - expect(routeModuleTransforms[1].descriptor).toMatchObject({ + expect(querylessRouteModuleTransform?.descriptor).toMatchObject({ order: 'post', }); expect( - (routeModuleTransforms[1].descriptor.test as (path: string) => boolean)( - options.routePath - ) + ( + querylessRouteModuleTransform!.descriptor.test as ( + path: string + ) => boolean + )(options.routePath) ).toBe(true); - await routeModuleTransforms[0].handler( + await explicitRouteModuleTransform!.handler( createTransformArgs(options.routePath, '?react-router-route') ); - await routeModuleTransforms[1].handler( + await querylessRouteModuleTransform!.handler( createTransformArgs(options.routePath) ); @@ -119,6 +131,44 @@ describe('build output transforms', () => { expect(run).toHaveBeenCalledTimes(2); }); + it('captures post-loader route exports for manifest generation', async () => { + const harness = createTransformHarness(); + const options = createBaseOptions(harness); + const onRouteModuleAnalysis = rstest.fn(); + + registerBuildOutputTransforms({ + ...options, + onRouteModuleAnalysis, + }); + + const clientRouteTransform = harness.transforms.find( + transform => + String(transform.descriptor.resourceQuery) === + String(/__react-router-build-client-route/) + ); + expect(clientRouteTransform?.descriptor).toMatchObject({ + order: 'post', + }); + + await clientRouteTransform!.handler( + createTransformArgs( + options.routePath, + '?__react-router-build-client-route', + ` + export const loader = () => null; + export default function MDXContent() { return null; } + ` + ) + ); + + expect(onRouteModuleAnalysis).toHaveBeenCalledWith( + options.routePath, + expect.objectContaining({ + exports: expect.arrayContaining(['loader', 'default']), + }) + ); + }); + it('does not match queryless route-module transforms for internal route requests', () => { const harness = createTransformHarness(); const options = createBaseOptions(harness); @@ -128,6 +178,7 @@ describe('build output transforms', () => { const querylessRouteModuleTransform = harness.transforms.find( transform => transform.descriptor.order === 'post' && + transform.descriptor.environments === undefined && typeof transform.descriptor.test === 'function' ); diff --git a/tests/index.test.ts b/tests/index.test.ts index 2d396abf..5b5e0275 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,15 @@ import { createStubRsbuild } from '@scripts/test-helper'; import { describe, expect, it, rstest } from '@rstest/core'; +import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; -import { pluginReactRouter, shouldParallelizeEnvironmentBuilds } from '../src'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + pluginReactRouter, + pluginReactRouterRSC, + shouldParallelizeEnvironmentBuilds, +} from '../src'; type ReactRouterTestGlobal = typeof globalThis & { __reactRouterTestConfig?: unknown; @@ -33,6 +41,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 +74,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 })]); @@ -322,6 +346,90 @@ describe('pluginReactRouter', () => { ).toBe(true); }); + it('composes RSC bundler plumbing with the React Router environments', async () => { + const rsbuild = await createStubRsbuild({ + rsbuildConfig: {}, + }); + + rsbuild.addPlugins([pluginReactRouter({ rsc: true })]); + const config = await rsbuild.unwrapConfig(); + + expect(config.tools.swc.rspackExperiments.reactServerComponents).toBe( + true + ); + expect(config.environments.node.source.include).toEqual([ + { + not: /[\\/]core-js[\\/]/, + }, + ]); + expect(config.environments.node.tools.rspack.dependencies).toBeUndefined(); + expect(config.environments.web.output.target).toBe('web'); + expect( + config.environments.web.tools.rspack.output.workerChunkLoading + ).toBe('import-scripts'); + expect(config.environments.web.tools.rspack.optimization.usedExports).toBe( + false + ); + expect( + config.environments.web.tools.rspack.optimization.mangleExports + ).toBe(false); + }); + + it('exposes an explicit RSC plugin helper', async () => { + const rsbuild = await createStubRsbuild({ + rsbuildConfig: {}, + }); + + rsbuild.addPlugins([pluginReactRouterRSC()]); + const config = await rsbuild.unwrapConfig(); + + expect(config.environments.web.source.entry).toEqual({ + index: { + import: expect.stringMatching(/entry\.rsc\.client/), + html: false, + }, + }); + expect(config.environments.node.source.entry.index).toMatchObject({ + import: expect.stringMatching(/entry\.rsc/), + layer: 'react-server-components', + }); + }); + + it('publishes option types from the package root declarations', () => { + const outDir = fs.mkdtempSync(join(tmpdir(), 'rr-plugin-dts-')); + + try { + execFileSync( + 'pnpm', + [ + 'exec', + 'tsc', + '-p', + 'tsconfig.json', + '--emitDeclarationOnly', + '--outDir', + outDir, + '--tsBuildInfoFile', + join(outDir, 'tsconfig.tsbuildinfo'), + '--isolatedDeclarations', + 'false', + ], + { cwd: process.cwd(), stdio: 'pipe' } + ); + + const rootDeclarations = fs.readFileSync( + join(outDir, 'index.d.ts'), + 'utf8' + ); + + expect(rootDeclarations).toContain( + "export type { PluginOptions, ReactRouterRSCPluginOptions } from './types.js';" + ); + } finally { + fs.rmSync(outDir, { force: true, recursive: true }); + } + }); + it('reduces file size reporting overhead for medium split route builds by default', async () => { const restoreEnv = captureEnv([ 'RR_TEST_SPLIT_ROUTE_MODULES', diff --git a/tests/manifest.test.ts b/tests/manifest.test.ts index 738f45a2..1d08b75f 100644 --- a/tests/manifest.test.ts +++ b/tests/manifest.test.ts @@ -415,6 +415,60 @@ describe('manifest', () => { } }); + it('uses transformed route analysis for non-JavaScript route modules', async () => { + const { root, appDir } = createTempApp(` + import { MdxComponent } from '../components/mdx'; + + export const loader = () => ({ content: 'from mdx' }); + + ## MDX Route + + `); + try { + const { manifest, moduleExportsByRouteId } = + await generateReactRouterManifestForDev( + { + ...routes, + 'routes/page': { + ...routes['routes/page'], + file: 'routes/page.mdx', + }, + }, + {}, + clientStats, + appDir, + '/', + { + isBuild: true, + rootRouteFile: 'root.tsx', + splitRouteModules: false, + analyzeRouteModule: async routeFilePath => + routeFilePath.endsWith('page.mdx') + ? { + code: ` + export const loader = () => ({ content: 'from mdx' }); + export default function MDXContent() { return

MDX

; } + `, + exports: ['loader', 'default'], + exportAllModules: [], + } + : undefined, + } + ); + + expect(manifest.routes['routes/page']).toMatchObject({ + hasLoader: true, + hasDefaultExport: true, + }); + expect(moduleExportsByRouteId['routes/page']).toEqual([ + 'loader', + 'default', + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it('preserves dev css fallback when route analysis uses transformed code', async () => { const { root, appDir } = createTempApp(` import './page.css'; diff --git a/tests/modify-browser-manifest.test.ts b/tests/modify-browser-manifest.test.ts index eb7a3270..ef8df780 100644 --- a/tests/modify-browser-manifest.test.ts +++ b/tests/modify-browser-manifest.test.ts @@ -328,6 +328,55 @@ describe('modify browser manifest plugin', () => { } }); + it('enables SRI from the stable subResourceIntegrity config field', async () => { + const { root, appDir } = createTempApp(); + const harness = createProcessAssetsHarness(); + const optimizedEntrySource = 'console.log("stable sri");'; + const assets = { + ...createBrowserManifestAssets(), + 'static/js/entry.client.js': createAsset( + optimizedEntrySource, + 'sha384-stable-entry' + ), + }; + const compilation = createCompilation( + [['entry.client', { files: new Set(['static/js/entry.client.js']) }]], + assets + ); + let reportedSri: Record | undefined; + + try { + registerModifyBrowserManifestAssets( + harness.api as never, + { root: rootRoute }, + {}, + appDir, + '/', + { isBuild: true }, + { + subResourceIntegrity: true, + onManifest(_manifest, sri) { + reportedSri = sri; + }, + } + ); + + expect(harness.getDescriptors()).toEqual([ + { stage: 'additions', environments: ['web'] }, + { stage: 'report', environments: ['web'] }, + ]); + + await harness.runStage('additions', { assets, compilation }); + await harness.runStage('report', { assets, compilation }); + + expect(reportedSri?.['/static/js/entry.client.js']).toBe( + 'sha384-stable-entry' + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it('rejects the promise hook when build route analysis fails', async () => { const { root, appDir } = createTempApp(); const harness = createProcessAssetsHarness(); 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'); diff --git a/tests/prerender.test.ts b/tests/prerender.test.ts index 0ff5dc62..d2bb4e9c 100644 --- a/tests/prerender.test.ts +++ b/tests/prerender.test.ts @@ -116,6 +116,17 @@ describe('prerender helpers', () => { expect(getPrerenderConcurrency({ paths: ['/'] }, 2)).toBe(1); }); + it('validates stable prerender concurrency config', () => { + expect( + validatePrerenderConfig({ paths: ['/'], concurrency: 1 } as any) + ).toBeNull(); + expect( + validatePrerenderConfig({ paths: ['/'], concurrency: 1.5 } as any) + ).toBe( + 'The `prerender.concurrency` config must be a positive integer if specified.' + ); + }); + it('creates React Router match routes from a route manifest', () => { expect( createPrerenderRoutes({ @@ -259,6 +270,51 @@ describe('prerender helpers', () => { ]); }); + it('reports invalid ssr:false prerender middleware exports', () => { + const manifestRoutes = { + root: { id: 'root', file: 'root.tsx', path: '' }, + dashboard: { + id: 'dashboard', + parentId: 'root', + file: 'routes/dashboard.tsx', + path: 'dashboard', + }, + }; + + expect( + getSsrFalsePrerenderExportErrors({ + routes: manifestRoutes, + manifestRoutes, + routeExports: { + dashboard: ['middleware'], + }, + prerenderPaths: ['/dashboard'], + }) + ).toEqual([ + expect.stringContaining( + '`dashboard` when pre-rendering with `ssr:false`: `middleware`' + ), + ]); + }); + + it('preserves case sensitivity when matching prerender paths', () => { + const prerenderRoutes = createPrerenderRoutes({ + root: { id: 'root', file: 'root.tsx', path: '' }, + settings: { + id: 'settings', + parentId: 'root', + file: 'routes/settings.tsx', + path: 'Settings', + caseSensitive: true, + }, + }); + + expect(prerenderRoutes[0]?.children?.[0]).toMatchObject({ + id: 'settings', + caseSensitive: true, + }); + }); + it('reports loader exports on routes outside the ssr:false prerender set', () => { const manifestRoutes = { root: { id: 'root', file: 'root.tsx', path: '' }, diff --git a/tests/react-router-config.test.ts b/tests/react-router-config.test.ts index 2f87e70f..118518b6 100644 --- a/tests/react-router-config.test.ts +++ b/tests/react-router-config.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from '@rstest/core'; import { + getDefaultTrailingSlashAwareDataRequests, resolveReactRouterConfig, resolveReactRouterConfigEffect, } from '../src/react-router-config'; @@ -91,10 +92,10 @@ describe('resolveReactRouterConfig', () => { const defaultResult = await resolveReactRouterConfig({}); const disabledResult = await resolveReactRouterConfig({ splitRouteModules: false, - } as any); + }); const enforcedResult = await resolveReactRouterConfig({ splitRouteModules: 'enforce', - } as any); + }); expect(defaultResult.resolved.splitRouteModules).toBe(true); expect(disabledResult.resolved.splitRouteModules).toBe(false); @@ -177,4 +178,34 @@ describe('resolveReactRouterConfig', () => { disabledByTopLevel.resolved.future.unstable_subResourceIntegrity ).toBe(false); }); + + it('keeps React Router 7 future aliases while exposing React Router 8 stable fields', async () => { + const result = await resolveReactRouterConfig({ + subResourceIntegrity: true, + future: { + unstable_subResourceIntegrity: true, + unstable_trailingSlashAwareDataRequests: true, + }, + } as any); + + expect(result.resolved.subResourceIntegrity).toBe(true); + expect(result.resolved.future.unstable_subResourceIntegrity).toBe(true); + expect( + result.resolved.future.unstable_trailingSlashAwareDataRequests + ).toBe(true); + }); + + it('uses the legacy subresource integrity future flag when the stable field is absent', async () => { + const result = await resolveReactRouterConfig({ + future: { unstable_subResourceIntegrity: true }, + } as any); + + expect(result.resolved.subResourceIntegrity).toBe(true); + }); + + it('defaults trailing slash-aware data requests for React Router 8 and newer', () => { + expect(getDefaultTrailingSlashAwareDataRequests('7.13.0')).toBe(false); + expect(getDefaultTrailingSlashAwareDataRequests('8.0.1')).toBe(true); + expect(getDefaultTrailingSlashAwareDataRequests('9.0.0')).toBe(true); + }); }); 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 ( +
+

+ {data ? {data} : "${WAITING_VALUE}"} +

+

+ + +

+
+ ); + } + `, + + "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 ( +
+

+ {data ? {data} : "${WAITING_VALUE}"} +

+

+ + + +

+
+ ); + } + `, + + [`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 ( +
+
pizza
+ burger link +
+ ) + } + `, + + "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.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 ( +
+
{loaderData}
+ +
+ ); + } + `, + + [`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} + +
+
+ ) + } + `, + + [`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 ( +
+

{useLoaderData()}

+ +
+ ) + } + `, + + "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}

+
+ + {actionData ?

{actionData.message}

: null} +
+ + ); + } + `, + }; + } + + 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}

+ + + ); + } + 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}

+ + + ); + } + 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 = "