diff --git a/.changeset/deferred-hydration-start.md b/.changeset/deferred-hydration-start.md new file mode 100644 index 00000000000..90d76803998 --- /dev/null +++ b/.changeset/deferred-hydration-start.md @@ -0,0 +1,16 @@ +--- +'@tanstack/react-start-client': minor +'@tanstack/solid-start-client': minor +'@tanstack/start-client-core': minor +'@tanstack/start-plugin-core': minor +'@tanstack/start-server-core': minor +'@tanstack/router-core': patch +'@tanstack/router-plugin': patch +'@tanstack/router-utils': patch +--- + +Add deferred Hydrate boundary support for TanStack Start. + +Hydrate boundaries can now be code-split by the Start compiler, preload their generated client chunks, preserve server-rendered fallback HTML, and replay interaction-triggered events after hydration. The compiler integration now uses a Start-owned compiler plugin for Hydrate virtual modules across Vite and Rsbuild, with dev invalidation for generated virtual modules. + +Shared AST utilities used by the router code-splitter and Hydrate virtual modules were moved into `@tanstack/router-utils` so both pipelines can retain referenced top-level declarations, unwrap local exports, and let dead-code elimination remove unused route module code. diff --git a/benchmarks/bundle-size/README.md b/benchmarks/bundle-size/README.md index ba3095f0ae9..f222b8088b5 100644 --- a/benchmarks/bundle-size/README.md +++ b/benchmarks/bundle-size/README.md @@ -13,6 +13,7 @@ Each package has `minimal` and `full` scenarios: - `minimal`: Small route app with `__root` + index route that renders `hello world` - `full`: Same route shape plus a broad root-level harness that imports/uses the full hooks/components surface - Start `full` scenarios also exercise `createServerFn`, `createMiddleware`, and `useServerFn` +- Start `deferred-hydration` scenarios match the minimal route shape and wrap the index route content in `Hydrate` ## Design Notes diff --git a/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/router.tsx b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/router.tsx new file mode 100644 index 00000000000..9d87d8748b5 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/router.tsx @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/__root.tsx b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/__root.tsx new file mode 100644 index 00000000000..ff1da4c3046 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/index.tsx b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/index.tsx new file mode 100644 index 00000000000..a462508abd6 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/src/routes/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Hydrate } from '@tanstack/react-start' +import { visible } from '@tanstack/react-start/hydration' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( + +
hello world
+
+ ) +} diff --git a/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/vite.config.ts b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/vite.config.ts new file mode 100644 index 00000000000..d4e4cd980d7 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/react-start-deferred-hydration/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import viteReact from '@vitejs/plugin-react' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' + +export default defineConfig({ + plugins: [tanstackStart(), viteReact()], +}) diff --git a/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/router.tsx b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/router.tsx new file mode 100644 index 00000000000..aa7ead67524 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/router.tsx @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/__root.tsx b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/__root.tsx new file mode 100644 index 00000000000..e59de722362 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/__root.tsx @@ -0,0 +1,24 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/index.tsx b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/index.tsx new file mode 100644 index 00000000000..80362386b24 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/src/routes/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { Hydrate } from '@tanstack/solid-start' +import { visible } from '@tanstack/solid-start/hydration' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( + +
hello world
+
+ ) +} diff --git a/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/vite.config.ts b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/vite.config.ts new file mode 100644 index 00000000000..0bd21e64f44 --- /dev/null +++ b/benchmarks/bundle-size/scenarios/solid-start-deferred-hydration/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' + +export default defineConfig({ + plugins: [tanstackStart(), solid({ ssr: true })], +}) diff --git a/docs/start/config.json b/docs/start/config.json index 25d1a7b060d..95840588f3a 100644 --- a/docs/start/config.json +++ b/docs/start/config.json @@ -117,6 +117,10 @@ "label": "Hydration Errors", "to": "framework/react/guide/hydration-errors" }, + { + "label": "Deferred Hydration", + "to": "framework/react/guide/deferred-hydration" + }, { "label": "Selective SSR", "to": "framework/react/guide/selective-ssr" @@ -242,6 +246,10 @@ "label": "Hydration Errors", "to": "framework/solid/guide/hydration-errors" }, + { + "label": "Deferred Hydration", + "to": "framework/solid/guide/deferred-hydration" + }, { "label": "Selective SSR", "to": "framework/solid/guide/selective-ssr" diff --git a/docs/start/framework/react/guide/deferred-hydration.md b/docs/start/framework/react/guide/deferred-hydration.md new file mode 100644 index 00000000000..98792618059 --- /dev/null +++ b/docs/start/framework/react/guide/deferred-hydration.md @@ -0,0 +1,319 @@ +--- +id: deferred-hydration +title: Deferred Hydration +--- + +> Deferred hydration is experimental + +Server rendering gives users useful HTML quickly. Hydration is the client-side work that turns that HTML into an interactive app. That work includes loading the JavaScript for client components, executing that JavaScript, running the components, attaching event handlers, and reconnecting the rendered DOM to the app tree. + +On small pages this cost is usually fine. On large pages, especially on mobile devices, interactivity can be delayed by both parts of the cost: waiting for JavaScript to load and waiting for the browser to execute that JavaScript and hydrate the components. Hydrating the whole page immediately can also keep the main thread busy during the first interactions. Deferred hydration lets you keep the SSR HTML while delaying selected JavaScript loading and hydration work until that part of the page is likely to matter. + +This is useful when a page has content that should be visible and indexable immediately, but does not need to be interactive immediately. + +## A Motivating Example + +Imagine a product detail page: + +```tsx +import { Hydrate } from '@tanstack/react-start' +import { + idle, + interaction, + never, + visible, +} from '@tanstack/react-start/hydration' + +export function ProductPage() { + return ( + <> + + + + + + + + } + > + + + + + + + + ) +} +``` + +In this page, the hero and buy box are critical. They should be interactive immediately, so they are not deferred. + +The reviews are useful SSR content, but they are below the initial viewport. `visible()` keeps their HTML in the document and hydrates them when the user scrolls near them. `prefetch={idle()}` lets TanStack Start start loading the generated child chunk during browser idle time so the reviews are more likely to be ready by the time they enter view. + +The recommendation carousel is expensive and only matters if the user interacts with it. `interaction()` delays hydration until there is user intent, while `prefetch={visible(...)}` can start downloading the chunk before the first interaction. + +The trust badges are static SSR HTML. `never()` keeps them non-interactive during initial hydration and avoids client work for that boundary. + +## Why This Requires Application Knowledge + +TanStack Start cannot know which parts of your app are safe to delay. The right boundary depends on your layout, your product priorities, and real user behavior. + +Good candidates are usually parts of the page that are visible in SSR HTML but not needed for immediate interaction: + +- Below-the-fold reviews, comments, related content, product details, or long marketing sections. +- Rich widgets such as maps, charts, carousels, video players, editors, or embeds. +- Panels that are visible later or activated by intent, such as filters, preview panes, or contextual tools. +- Responsive UI that only matters for a matching media query. +- Static server-rendered content that should never hydrate on the initial document. + +Poor candidates are parts of the page users expect to use immediately: + +- Primary navigation, route chrome, search boxes, and login/account controls. +- Above-the-fold forms, add-to-cart buttons, checkout actions, or consent controls. +- The interactive part of the LCP/hero area when users may click it immediately. +- Accessibility-critical controls that must be keyboard-ready as soon as the page appears. +- Components whose props or shared state are expected to update immediately after app startup. + +Use measurements to validate each boundary. Deferred hydration is a performance tool, not a blanket rule. A good boundary reduces startup JavaScript and main-thread work without making expected interactions feel late. + +## What Deferred Hydration Does + +Deferred hydration is different from `ssr: false` and `ssr: 'data-only'`. Those route options change whether a route renders HTML on the server. Deferred hydration still renders real SSR HTML, then delays selected client JavaScript work. + +TanStack Start keeps the normal app model: + +- One app root. +- Router context, loaders, links, head management, and SPA navigation. +- Server-rendered HTML for the deferred boundary. +- Client hydration when the chosen strategy resolves. + +By default, the compiler extracts each split `Hydrate` boundary's children into a separate client chunk. The server still renders the children normally, but the browser does not load and execute that child chunk until the boundary is ready or prefetched. + +## Basic Usage + +Use `Hydrate` with strategy factories from `@tanstack/react-start/hydration`: + +```tsx +import { Hydrate } from '@tanstack/react-start' +import { visible } from '@tanstack/react-start/hydration' + +export function ProductPage() { + return ( + + + + ) +} +``` + +`Hydrate` only preserves server HTML for boundaries that are present in the initial server-rendered document. When a boundary first mounts after the app has already hydrated, such as after client-side navigation, TanStack Start renders it on the client because no server HTML exists to preserve. + +Use `fallback` for that no-SSR-DOM case only. It is shown if the boundary first mounts after the app is hydrated and the transformed child chunk, or another child `Suspense`, is still loading: + +```tsx +}> + + +``` + +`fallback` does not replace server-rendered HTML in the initial document. During initial hydration, TanStack Start preserves the existing server HTML until the boundary can hydrate. With `never()`, the initial server HTML remains static and `fallback` is not used. + +The compiler removes statically visible `fallback` props from the server bundle. Prefer passing `fallback` directly, in an inline object spread, or through a single-use `const` object spread so server builds can strip that UI completely. + +## Splitting And Prefetching + +By default, `Hydrate` splits the children into a generated child chunk. This delays both hydration work and child JavaScript loading. + +Set `split={false}` when you only want to delay hydration work without splitting the child code: + +```tsx +import { idle } from '@tanstack/react-start/hydration' + +export function ProductPage() { + return ( + + + + ) +} +``` + +Because `prefetch` only loads the compiler-generated child chunk, it is only valid on split boundaries. TypeScript rejects `prefetch` when `split={false}`. + +Use `prefetch` when the child chunk should load before the boundary hydrates. `when` controls when the boundary becomes interactive. `prefetch` controls when TanStack Start calls the generated child chunk's lazy preload function: + +```tsx +import { idle, interaction, visible } from '@tanstack/react-start/hydration' + + + + + + + + +``` + +Common pairings: + +| Boundary goal | `when` | `prefetch` | +| ------------------------------------------ | -------------------- | ----------------------------------- | +| Hydrate below-the-fold content on scroll | `visible()` | `idle()` or none | +| Prepare content before it reaches viewport | `visible()` | `visible({ rootMargin: '1200px' })` | +| Keep a widget cold until user intent | `interaction()` | `visible(...)` or `idle()` | +| Hydrate non-critical work after startup | `idle()` | none | +| Hydrate only when app state says it is OK | `condition(isReady)` | `idle()`, `visible(...)`, or none | +| Keep initial SSR HTML static | `never()` | not supported | + +## Strategies + +`when` accepts a hydration strategy object: + +| Strategy | Behavior | +| --------------- | ------------------------------------------------------------------------------------------------------- | +| `load()` | Hydrates as soon as the app hydrates. | +| `idle()` | Hydrates in `requestIdleCallback`, or after `timeout` when idle callbacks are unavailable. | +| `visible()` | Hydrates when the boundary marker enters the viewport. | +| `media()` | Hydrates when the media query matches. | +| `interaction()` | Hydrates on the configured interaction intent events. Defaults to hover, focus, pointer down, or click. | +| `condition()` | Hydrates once the condition is truthy. | +| `never()` | Never hydrates the initial server-rendered boundary. | + +Use `never()` for intentionally static initial SSR HTML: + +```tsx +import { never } from '@tanstack/react-start/hydration' + +export function MarketingPage() { + return ( + + + + ) +} +``` + +`never()` keeps the existing server HTML static during initial hydration. If the same boundary mounts later during client-side navigation, it renders normally because no server HTML exists for TanStack Start to preserve. `never()` cannot be used as a `prefetch` strategy. + +Use `condition()` for app-specific one-time hydration conditions: + +```tsx +import { condition } from '@tanstack/react-start/hydration' + +export function CartPage() { + return ( + + + + ) +} +``` + +After a condition boundary hydrates, it stays hydrated even if `condition` later becomes false. + +For `interaction`, TanStack Start installs lightweight native intent listeners on the boundary marker, or on the nearest unresolved ancestor marker when a nested interaction boundary has not mounted yet. Those listeners open hydration gates and start deferred chunk loading. For bubbling intent events, TanStack Start queues a same-type event and redispatches it after the boundary hydrates so the first click-like interaction can reach React handlers. Native listener payload details such as pointer coordinates are not guaranteed to be preserved. + +The default interaction event list is `pointerenter`, `focusin`, `pointerdown`, and `click`. Use `events` when a boundary should listen to a different event or a smaller set: + +```tsx + + + + + + + +``` + +Nested boundaries use parent-first hydration. A child boundary can only hydrate after its ancestor boundaries have hydrated, so non-interaction child triggers such as `visible`, `media`, `idle`, or `condition` cannot fire while their parent boundary is still dehydrated. When a user shows interaction intent inside a nested unhydrated boundary, TanStack Start resolves the unresolved ancestor chain and marks the target boundary as intended. A `never()` ancestor still wins during initial hydration, so descendants under it remain non-interactive. + +## Settings + +`Hydrate` accepts these settings: + +| Option | Type | Notes | +| ------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `when` | `HydrationStrategy` | Required. Use strategy factories from `@tanstack/react-start/hydration`, including `never()` for static initial SSR HTML. | +| `prefetch` | `HydrationPrefetchStrategy` | Optional split-boundary strategy for preloading the child chunk before hydration. Accepts `load`, `idle`, `visible`, `media`, and `interaction`. `never` and `condition` are not valid prefetch strategies. | +| `split` | `boolean` | Defaults to `true`. Set to literal `false` to disable compiler extraction. | +| `fallback` | `ReactNode` | Client-only loading UI used when the boundary mounts after the app has already hydrated and the child chunk or child `Suspense` is still loading. | +| `onHydrated` | `() => void` | Fires once after the boundary has actually hydrated on the client. | + +Strategy options: + +| Strategy | Options | +| ------------- | --------------------------------------------------------------------------------------- | +| `idle` | `{ timeout?: number }`, defaults to `2000`. | +| `visible` | `{ rootMargin?: string; threshold?: number \| Array }`, default margin `600px`. | +| `media` | Query string, for example `media('(min-width: 800px)')`. | +| `interaction` | `{ events?: supported event or readonly array of supported events }`. | +| `condition` | Boolean or boolean-returning function. | + +Supported interaction events are `auxclick`, `click`, `contextmenu`, `dblclick`, `focusin`, `keydown`, `keyup`, `mousedown`, `mouseenter`, `mouseover`, `mouseup`, `pointerdown`, `pointerenter`, `pointerover`, and `pointerup`. + +## Correctness And Updates + +Deferred hydration is a performance hint for React's initial hydration work. React may hydrate a deferred boundary earlier than its strategy would normally allow if state, props, context, or store updates outside the boundary require React to reconcile inside it before the gate opens. This preserves correctness and avoids showing stale server HTML after the surrounding app has changed. + +`never()` is the exception for initial document hydration. Treat it as intentionally static SSR HTML. Do not rely on parent updates to make a `never()` boundary interactive. If the same boundary mounts later during client-side navigation, it renders normally because no server HTML exists for TanStack Start to keep static. + +## Preloading And CSS + +TanStack Start does not modulepreload transformed `Hydrate` JavaScript chunks by default. Without `prefetch`, the child chunk loads when the split boundary is ready to render. If that import suspends during client-side navigation or another client-only mount, the boundary's `fallback` is shown. + +CSS from split, deferred, and `never()` boundaries remains attached to the route assets because the server-rendered HTML may need those styles before any JavaScript runs. CSS is separate from the JavaScript chunk loaded when a deferred split boundary renders. + +## Extraction Limits + +Compiler-backed `Hydrate` splitting works by moving the boundary's children into a generated virtual module and rendering them through a lazy component. That gives TanStack Start a separate child chunk to load later, but it also means the compiler must be able to move the JSX safely. + +The split boundary must use a statically imported `Hydrate` component from `@tanstack/react-start`. Renaming the import is supported, but dynamic component aliases are not analyzed. + +Use the literal prop `split={false}` to opt out of extraction. Dynamic values such as `split={shouldSplit}` cannot be used to opt out at compile time. + +These patterns cannot be split: + +| Pattern | Why it is rejected | What to do instead | +| ---------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| Function-as-children | The compiler cannot move a render function and preserve the expected call pattern. | Use `split={false}` or move the rendered UI into a child component. | +| Hook calls directly inside extracted JSX | Moving that JSX would move where the hook executes. | Move the hook call into a component inside the boundary, then render that component. | +| `this` captures | Extracted function components cannot safely preserve class instance context. | Wrap the UI in a function component or use `split={false}`. | +| `super` captures | Extracted function components cannot preserve superclass access. | Wrap the UI in a function component or use `split={false}`. | + +This fails because `useThing()` would be moved into the generated component: + +```tsx + +

{useThing()}

+
+``` + +Move the hook into a component instead: + +```tsx +function ThingText() { + const thing = useThing() + return

{thing}

+} + +export function ProductPage() { + return ( + + + + ) +} +``` + +Values captured from the surrounding component can be passed into the generated child component, but keep the boundary simple. If extraction starts forcing complicated data flow, prefer a named child component and put the logic there. + +`fallback` stripping is also intentionally conservative. The server build can strip directly passed fallback UI, inline object-spread fallback UI, and single-use `const` object-spread fallback UI. If fallback props are hidden behind dynamic spreads or shared objects, the compiler may keep them. + +For browser-only rendering with no SSR HTML, use `ClientOnly` instead. For route-level SSR control, use [Selective SSR](./selective-ssr.md). diff --git a/docs/start/framework/solid/guide/deferred-hydration.md b/docs/start/framework/solid/guide/deferred-hydration.md new file mode 100644 index 00000000000..99ffc57a86a --- /dev/null +++ b/docs/start/framework/solid/guide/deferred-hydration.md @@ -0,0 +1,12 @@ +--- +ref: docs/start/framework/react/guide/deferred-hydration.md +replace: + '@tanstack/react-start': '@tanstack/solid-start' + 'React handlers': 'Solid handlers' + 'ReactNode': 'JSX.Element' + "Deferred hydration is a performance hint for React's initial hydration work. React may hydrate a deferred boundary earlier than its strategy would normally allow if state, props, context, or store updates outside the boundary require React to reconcile inside it before the gate opens. This preserves correctness and avoids showing stale server HTML after the surrounding app has changed.": "Deferred hydration is a performance hint for Solid's initial hydration work. Once a boundary gate opens, TanStack Start clears the preserved server DOM inside the marker and mounts the live Solid subtree in its place." + 'Hook calls directly inside extracted JSX': 'Render-time `use*` calls directly inside extracted JSX' + 'Moving that JSX would move where the hook executes.': 'Moving that JSX would move where the call executes.' + 'Move the hook call into a component inside the boundary, then render that component.': 'Move the call into a component inside the boundary, then render that component.' + 'Move the hook into a component instead:': 'Move the call into a component instead:' +--- diff --git a/e2e/e2e-utils/src/hmrFileEditor.ts b/e2e/e2e-utils/src/hmrFileEditor.ts new file mode 100644 index 00000000000..309940fbfe8 --- /dev/null +++ b/e2e/e2e-utils/src/hmrFileEditor.ts @@ -0,0 +1,127 @@ +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' + +export function replaceAll(source: string, from: string, to: string) { + return source.split(from).join(to) +} + +export type HmrFileEditorOptions = { + rootDir?: string + files: Record + normalizeSource?: (fileKey: TFileKey, source: string) => string +} + +export function createHmrFileEditor( + options: HmrFileEditorOptions, +) { + const files = Object.fromEntries( + Object.entries(options.files).map(([key, filePath]) => [ + key, + options.rootDir && !path.isAbsolute(filePath as string) + ? path.join(options.rootDir, filePath as string) + : (filePath as string), + ]), + ) as Record + const originalContents: Partial> = {} + const pendingRestoreKeys = new Set() + const normalizeSource = + options.normalizeSource ?? ((_fileKey: TFileKey, source: string) => source) + + async function captureOriginals() { + for (const [key, filePath] of Object.entries(files) as Array< + [TFileKey, string] + >) { + const current = await readFile(filePath, 'utf8') + const normalized = normalizeSource(key, current) + + if (normalized !== current) { + await writeFile(filePath, normalized) + pendingRestoreKeys.add(key) + } + + originalContents[key] = normalized + } + } + + const capturePromise = captureOriginals() + + async function restoreFiles(forceFileKeys: Iterable = []) { + const forceRestoreKeys = new Set(forceFileKeys) + const restoredFileKeys: Array = [] + + for (const [key, filePath] of Object.entries(files) as Array< + [TFileKey, string] + >) { + const content = originalContents[key] + if (content === undefined) continue + + const current = await readFile(filePath, 'utf8') + + if (current !== content || forceRestoreKeys.has(key)) { + await writeFile(filePath, content) + restoredFileKeys.push(key) + } + } + + return restoredFileKeys + } + + async function replaceText(fileKey: TFileKey, from: string, to: string) { + const filePath = files[fileKey] + const source = await readFile(filePath, 'utf8') + + if (!source.includes(from)) { + throw new Error(`Expected file to include ${JSON.stringify(from)}`) + } + + await writeFile(filePath, source.replace(from, to)) + } + + async function rewriteFile( + fileKey: TFileKey, + updater: (source: string) => string, + options: { allowNoop?: boolean } = {}, + ) { + const filePath = files[fileKey] + const source = await readFile(filePath, 'utf8') + const updated = updater(source) + + if (updated === source && !options.allowNoop) { + throw new Error(`Expected ${filePath} to change during rewrite`) + } + + await writeFile(filePath, updated) + } + + async function replaceTextAndWait( + fileKey: TFileKey, + from: string, + to: string, + assertion: () => Promise, + ) { + await replaceText(fileKey, from, to) + await assertion() + } + + async function rewriteFileAndWait( + fileKey: TFileKey, + updater: (source: string) => string, + assertion: () => Promise, + options: { allowNoop?: boolean } = {}, + ) { + await rewriteFile(fileKey, updater, options) + await assertion() + } + + return { + files, + pendingRestoreKeys, + capturePromise, + captureOriginals, + restoreFiles, + replaceText, + replaceTextAndWait, + rewriteFile, + rewriteFileAndWait, + } +} diff --git a/e2e/e2e-utils/src/index.ts b/e2e/e2e-utils/src/index.ts index ec6affdb56c..17e58f82e59 100644 --- a/e2e/e2e-utils/src/index.ts +++ b/e2e/e2e-utils/src/index.ts @@ -4,5 +4,7 @@ export { toRuntimePath } from './to-runtime-path' export { resolveRuntimeSuffix } from './resolve-runtime-suffix' export { e2eStartDummyServer, e2eStopDummyServer } from './e2eSetupTeardown' export { preOptimizeDevServer, waitForServer } from './devServerWarmup' +export { createHmrFileEditor, replaceAll } from './hmrFileEditor' +export type { HmrFileEditorOptions } from './hmrFileEditor' export type { Post } from './posts' export { collectBrowserErrors, test } from './fixture' diff --git a/e2e/react-start/deferred-hydration/.gitignore b/e2e/react-start/deferred-hydration/.gitignore new file mode 100644 index 00000000000..1b3a07ede12 --- /dev/null +++ b/e2e/react-start/deferred-hydration/.gitignore @@ -0,0 +1,15 @@ +node_modules +package-lock.json +yarn.lock +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/react-start/deferred-hydration/package.json b/e2e/react-start/deferred-hydration/package.json new file mode 100644 index 00000000000..db76b2fecfd --- /dev/null +++ b/e2e/react-start/deferred-hydration/package.json @@ -0,0 +1,60 @@ +{ + "name": "tanstack-react-start-e2e-deferred-hydration", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "pnpm dev:vite --port 3000", + "dev:e2e": "pnpm dev:vite", + "dev:vite": "vite dev", + "dev:rsbuild": "rsbuild dev", + "build": "pnpm build:vite", + "build:vite": "vite build && tsc --noEmit", + "build:rsbuild": "rsbuild build && tsc --noEmit", + "preview": "vite preview", + "start": "pnpm start:vite", + "start:vite": "srvx --prod --dir=. -s dist-vite-ssr/client --entry dist-vite-ssr/server/server.js", + "start:rsbuild": "srvx --prod --dir=. -s dist-rsbuild-ssr/client --entry dist-rsbuild-ssr/server/index.js", + "test:e2e": "pnpm test:e2e:dev && pnpm test:e2e:prod", + "test:e2e:dev": "MODE=dev playwright test --project=chromium", + "test:e2e:prod": "MODE=prod playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^8.0.0" + }, + "devDependencies": { + "@rsbuild/core": "^2.0.1", + "@rsbuild/plugin-react": "^2.0.0", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^6.0.1", + "rolldown": "1.0.0-rc.18", + "srvx": "^0.11.9", + "typescript": "^6.0.2" + }, + "nx": { + "targets": { + "test:e2e": { + "parallelism": false + } + }, + "metadata": { + "playwrightModes": [ + { + "toolchain": "vite", + "mode": "ssr" + }, + { + "toolchain": "rsbuild", + "mode": "ssr" + } + ] + } + } +} diff --git a/e2e/react-start/deferred-hydration/playwright.config.ts b/e2e/react-start/deferred-hydration/playwright.config.ts new file mode 100644 index 00000000000..0125792d4b3 --- /dev/null +++ b/e2e/react-start/deferred-hydration/playwright.config.ts @@ -0,0 +1,52 @@ +import fs from 'node:fs' +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' +const distDir = process.env.E2E_DIST_DIR ?? `dist-${toolchain}-ssr` +const e2ePortKey = + process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +const mode = process.env.MODE ?? 'prod' +const isDev = mode === 'dev' +const serverEntryFile = toolchain === 'rsbuild' ? 'index.js' : 'server.js' +const startCommand = `pnpm exec srvx --prod --dir=. -s ${distDir}/client --entry ${distDir}/server/${serverEntryFile}` +const devCommand = + toolchain === 'rsbuild' ? 'pnpm dev:rsbuild' : 'pnpm dev:vite' + +if (process.env.TEST_WORKER_INDEX === undefined) { + fs.rmSync(`port-${e2ePortKey}.txt`, { force: true }) +} + +const PORT = await getTestServerPort(e2ePortKey) +const baseURL = `http://localhost:${PORT}` + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + use: { baseURL }, + webServer: { + command: isDev ? `${devCommand} --port ${PORT}` : startCommand, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: { + E2E_DIST_DIR: distDir, + NODE_ENV: isDev ? 'development' : 'production', + VITE_NODE_ENV: 'test', + PORT: String(PORT), + VITE_SERVER_PORT: String(PORT), + E2E_TOOLCHAIN: toolchain, + E2E_PORT_KEY: e2ePortKey, + }, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/deferred-hydration/rsbuild.config.ts b/e2e/react-start/deferred-hydration/rsbuild.config.ts new file mode 100644 index 00000000000..6279705adb2 --- /dev/null +++ b/e2e/react-start/deferred-hydration/rsbuild.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' +import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist-rsbuild-ssr' + +export default defineConfig({ + plugins: [pluginReact({ splitChunks: false }), tanstackStart()], + output: { + distPath: { + root: outDir, + }, + }, +}) diff --git a/e2e/react-start/deferred-hydration/src/routeTree.gen.ts b/e2e/react-start/deferred-hydration/src/routeTree.gen.ts new file mode 100644 index 00000000000..700c7c65909 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routeTree.gen.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ImportedRouteImport } from './routes/imported' +import { Route as CssRouteImport } from './routes/css' +import { Route as ComponentsRouteImport } from './routes/components' +import { Route as IndexRouteImport } from './routes/index' + +const ImportedRoute = ImportedRouteImport.update({ + id: '/imported', + path: '/imported', + getParentRoute: () => rootRouteImport, +} as any) +const CssRoute = CssRouteImport.update({ + id: '/css', + path: '/css', + getParentRoute: () => rootRouteImport, +} as any) +const ComponentsRoute = ComponentsRouteImport.update({ + id: '/components', + path: '/components', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/components' | '/css' | '/imported' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/components' | '/css' | '/imported' + id: '__root__' | '/' | '/components' | '/css' | '/imported' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ComponentsRoute: typeof ComponentsRoute + CssRoute: typeof CssRoute + ImportedRoute: typeof ImportedRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/imported': { + id: '/imported' + path: '/imported' + fullPath: '/imported' + preLoaderRoute: typeof ImportedRouteImport + parentRoute: typeof rootRouteImport + } + '/css': { + id: '/css' + path: '/css' + fullPath: '/css' + preLoaderRoute: typeof CssRouteImport + parentRoute: typeof rootRouteImport + } + '/components': { + id: '/components' + path: '/components' + fullPath: '/components' + preLoaderRoute: typeof ComponentsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ComponentsRoute: ComponentsRoute, + CssRoute: CssRoute, + ImportedRoute: ImportedRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/deferred-hydration/src/router.tsx b/e2e/react-start/deferred-hydration/src/router.tsx new file mode 100644 index 00000000000..9d87d8748b5 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/router.tsx @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/e2e/react-start/deferred-hydration/src/routes/__root.tsx b/e2e/react-start/deferred-hydration/src/routes/__root.tsx new file mode 100644 index 00000000000..719223992e3 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/__root.tsx @@ -0,0 +1,163 @@ +/// +import * as React from 'react' +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'Deferred Hydration E2E' }, + ], + }), + shellComponent: RootDocument, + component: () => ( +
+ +
+ ), +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + {children} + + + + ) +} diff --git a/e2e/react-start/deferred-hydration/src/routes/components.tsx b/e2e/react-start/deferred-hydration/src/routes/components.tsx new file mode 100644 index 00000000000..95fa7531a47 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/components.tsx @@ -0,0 +1,179 @@ +import { createFileRoute } from '@tanstack/react-router' +import * as React from 'react' +import { Hydrate } from '@tanstack/react-start' +import { + condition, + idle, + interaction, + load, + media, + never, + visible, +} from '@tanstack/react-start/hydration' + +export const Route = createFileRoute('/components')({ + component: ComponentHydrationPage, +}) + +function InteractiveBox(props: { id: string; label: string }) { + const [count, setCount] = React.useState(0) + const [hydrated, setHydrated] = React.useState(false) + + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( + + ) +} + +type HydrationFallbackWindow = Window & { + __componentFallbackReady?: boolean + __componentFallbackPromise?: Promise +} + +function DelayedFallbackBox() { + if (typeof window !== 'undefined') { + const win = window as HydrationFallbackWindow + + if (!win.__componentFallbackReady) { + win.__componentFallbackPromise ??= new Promise((resolve) => { + win.setTimeout(() => { + win.__componentFallbackReady = true + resolve() + }, 1000) + }) + + throw win.__componentFallbackPromise + } + } + + return
fallback child
+} + +function ComponentHydrationPage() { + const [hydratedCallbacks, setHydratedCallbacks] = React.useState(0) + const [conditionReady, setConditionReady] = React.useState(false) + const [showClientFallbackBoundary, setShowClientFallbackBoundary] = + React.useState(false) + + return ( +
+

Component Deferred Hydration

+
+ Manual test guide + + Pink buttons are server HTML that has not hydrated yet. Green buttons + have hydrated and should increment when clicked. Follow the notes + below to trigger each strategy intentionally. + +
+

{hydratedCallbacks}

+

+ load and idle should become green + without interaction shortly after the page loads. +

+ + + + + + +
+ Scroll down to reveal the visible boundary +
+

+ visible hydrates only after this button enters the + viewport. +

+ + + +

+ media hydrates when (min-width: 1px) + matches. interaction hydrates on hover, focus, pointer + down, or click intent. +

+ + + + + + +

+ Custom interaction boundaries below hydrate only for their configured + events: double-click for the single-event example, and right-click or + double-click for the multi-event example. The prefetch example should + download code on hover but hydrate on click. +

+ setHydratedCallbacks((count) => count + 1)} + > + + + + + + + + + + + + + + + + + + + + + + + +

+ never stays as server HTML forever on the initial page, + so clicking should not increment it. +

+ + {showClientFallbackBoundary ? ( + client fallback + } + > + + + ) : null} +
+ ) +} diff --git a/e2e/react-start/deferred-hydration/src/routes/css.tsx b/e2e/react-start/deferred-hydration/src/routes/css.tsx new file mode 100644 index 00000000000..2c01aa9e578 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/css.tsx @@ -0,0 +1,57 @@ +import { createFileRoute } from '@tanstack/react-router' +import { Hydrate } from '@tanstack/react-start' +import { media, never, visible } from '@tanstack/react-start/hydration' +import outerStyles from './css/outer.module.css' +import deferredStyles from './css/deferred-only.module.css' +import sharedStyles from './css/shared.module.css' + +export const Route = createFileRoute('/css')({ + component: CssHydrationPage, +}) + +function CssHydrationPage() { + return ( +
+
+

+ CSS Deferred Hydration +

+

+ CSS from deferred, never, shared, and nested Hydrate boundaries should + be available even before the client JavaScript hydrates those islands. +

+
+
+
+ Outer CSS +
+
+ Shared outer CSS +
+
+ +
+ Deferred CSS +
+
+ +
+ Never CSS +
+
+ + +
+ Nested CSS +
+
+
+
+ ) +} diff --git a/e2e/react-start/deferred-hydration/src/routes/css/deferred-only.module.css b/e2e/react-start/deferred-hydration/src/routes/css/deferred-only.module.css new file mode 100644 index 00000000000..05eafda03cf --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/css/deferred-only.module.css @@ -0,0 +1,15 @@ +.deferredBox { + background-color: rgb(23, 45, 67); + color: rgb(255, 255, 255); + padding: 12px; +} + +.neverBox { + color: rgb(45, 67, 89); + padding: 12px; +} + +.nestedBox { + border-left: 5px solid rgb(67, 89, 123); + padding-left: 12px; +} diff --git a/e2e/react-start/deferred-hydration/src/routes/css/outer.module.css b/e2e/react-start/deferred-hydration/src/routes/css/outer.module.css new file mode 100644 index 00000000000..98ca5e0934e --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/css/outer.module.css @@ -0,0 +1,9 @@ +.heading { + color: rgb(11, 31, 53); +} + +.outerBox { + background-color: rgb(242, 250, 255); + color: rgb(12, 34, 56); + padding: 12px; +} diff --git a/e2e/react-start/deferred-hydration/src/routes/css/shared.module.css b/e2e/react-start/deferred-hydration/src/routes/css/shared.module.css new file mode 100644 index 00000000000..020da5d7adc --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/css/shared.module.css @@ -0,0 +1,4 @@ +.sharedBox { + border-top: 4px solid rgb(98, 76, 54); + margin-top: 8px; +} diff --git a/e2e/react-start/deferred-hydration/src/routes/imported.tsx b/e2e/react-start/deferred-hydration/src/routes/imported.tsx new file mode 100644 index 00000000000..bf0f8b74bdf --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/imported.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { ImportedHydrateWidget } from '../shared/ImportedHydrateWidget' + +export const Route = createFileRoute('/imported')({ + component: ImportedHydrationPage, +}) + +function ImportedHydrationPage() { + return ( +
+

Imported Hydrate

+ +
+ ) +} diff --git a/e2e/react-start/deferred-hydration/src/routes/index.tsx b/e2e/react-start/deferred-hydration/src/routes/index.tsx new file mode 100644 index 00000000000..492148887dc --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/routes/index.tsx @@ -0,0 +1,21 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Deferred Hydration

+

Component strategies

+ component strategies +

CSS

+ CSS deferred hydration +

Imported component

+ + imported Hydrate + +
+ ) +} diff --git a/e2e/react-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx b/e2e/react-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx new file mode 100644 index 00000000000..4ee8c5e2f44 --- /dev/null +++ b/e2e/react-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx @@ -0,0 +1,33 @@ +import * as React from 'react' +import { Hydrate } from '@tanstack/react-start' +import { media } from '@tanstack/react-start/hydration' + +function ImportedHydrateChild() { + const [count, setCount] = React.useState(0) + + return ( + + ) +} + +export function ImportedHydrateWidget() { + return ( + + imported hydrate fallback + + } + > + + + ) +} diff --git a/e2e/react-start/deferred-hydration/tests/hydration.spec.ts b/e2e/react-start/deferred-hydration/tests/hydration.spec.ts new file mode 100644 index 00000000000..c285a7e1597 --- /dev/null +++ b/e2e/react-start/deferred-hydration/tests/hydration.spec.ts @@ -0,0 +1,681 @@ +import { expect } from '@playwright/test' +import { createHmrFileEditor, test } from '@tanstack/router-e2e-utils' +import crypto from 'node:crypto' +import path from 'node:path' +import type { APIRequestContext, Page } from '@playwright/test' + +const isDev = process.env.MODE === 'dev' +const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' +const isVite = toolchain === 'vite' +const hmrExpect = expect.configure({ timeout: 20_000 }) +const componentsRouteFile = path.join( + process.cwd(), + 'src/routes/components.tsx', +) +const interactiveBoxLabelSource = + ' {props.label}: {count}' +const interactiveBoxHmrLabelSource = + ' hmr {props.label}: {count}' + +function normalizeComponentsRouteSource(source: string) { + return source + .split(interactiveBoxHmrLabelSource) + .join(interactiveBoxLabelSource) +} + +const componentsRouteEditor = createHmrFileEditor({ + files: { + componentsRoute: componentsRouteFile, + }, + normalizeSource: (_fileKey, source) => normalizeComponentsRouteSource(source), +}) + +function getVisibleHydrateVirtualPath() { + const normalizedSourcePath = path + .relative(process.cwd(), componentsRouteFile) + .replaceAll('\\', '/') + const sourceHash = crypto + .createHash('sha1') + .update(normalizedSourcePath) + .digest('hex') + .slice(0, 10) + const params = new URLSearchParams() + params.set('tss-hydrate', `0_${sourceHash}`) + + return `${componentsRouteFile}?${params.toString()}` +} + +async function waitForVisibleHydrateVirtualModule(page: Page, marker: string) { + const virtualPath = getVisibleHydrateVirtualPath() + + await expect + .poll( + async () => { + try { + const response = await page.request.get(virtualPath) + const text = await response.text() + + if (response.ok() && text.includes(marker)) { + return 'ready' + } + + return `${response.status()} ${text.slice(0, 240)}` + } catch (error) { + return String(error) + } + }, + { timeout: 20_000 }, + ) + .toBe('ready') +} + +async function clickAndExpectCount( + page: Page, + buttonTestId: string, + countTestId: string, + count: string, +) { + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await page.getByTestId(buttonTestId).click() + await expect(page.getByTestId(countTestId)).toHaveText(count) +} + +async function hoverIntentAndExpectCount( + page: Page, + buttonTestId: string, + countTestId: string, + count: string, +) { + await expectRouteToStayUnhydrated(page, buttonTestId) + await page.mouse.move(0, 0) + await page.getByTestId(buttonTestId).hover() + await clickAndExpectCount(page, buttonTestId, countTestId, count) +} + +async function dispatchHydrationIntent( + page: Page, + buttonTestId: string, + eventName: string, +) { + await page.getByTestId(buttonTestId).evaluate((element, eventName) => { + const marker = element.closest('[data-ts-hydrate-id]') + + if (!marker) { + throw new Error('Expected Hydrate marker to exist') + } + + marker.dispatchEvent( + new Event(eventName, { bubbles: true, cancelable: true }), + ) + }, eventName) +} + +async function expectRouteToStayUnhydrated( + page: Page, + buttonTestId: string, + duration = 250, +) { + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await page.waitForTimeout(duration) + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'false', + ) +} + +async function scrollToBoundary(page: Page, buttonTestId: string) { + const button = page.getByTestId(buttonTestId) + for (let attempt = 0; attempt < 3; attempt++) { + await button.evaluate((element) => { + element.scrollIntoView({ block: 'center', inline: 'nearest' }) + }) + + await page.waitForTimeout(100) + const isVisible = await button.evaluate((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > 0 && rect.top < window.innerHeight + }) + + if (isVisible) return + } + + await expect(button).toBeInViewport() +} + +async function expectCssProperty( + page: Page, + testId: string, + property: string, + value: string, +) { + await expect + .poll(() => + page.getByTestId(testId).evaluate((element, propertyName) => { + return getComputedStyle(element).getPropertyValue(propertyName) + }, property), + ) + .toBe(value) +} + +function htmlContainsText(html: string, text: string) { + const pattern = text.split(' ').join('(?:\\s|)+') + expect(html).toMatch(new RegExp(pattern)) +} + +async function waitForComponentsServerHtmlText(page: Page, text: string) { + await expect + .poll( + async () => { + const response = await page.request.get('/components') + const html = await response.text() + + if (!response.ok()) { + return `${response.status()} ${html.slice(0, 240)}` + } + + try { + htmlContainsText(html, text) + return 'ready' + } catch { + return html.slice(0, 240) + } + }, + { timeout: 20_000 }, + ) + .toBe('ready') +} + +function getModulePreloadHrefs(html: string) { + return Array.from(html.matchAll(/]*>/g), (match) => match[0]) + .filter((tag) => /\brel="modulepreload"/.test(tag)) + .map((tag) => tag.match(/\bhref="([^"]+)"/)?.[1]) + .filter((href): href is string => !!href) +} + +async function modulePreloadContentsContain( + request: APIRequestContext, + hrefs: Array, + marker: string, +) { + for (const href of hrefs) { + const response = await request.get(href) + if (!response.ok()) continue + + const text = await response.text() + if (text.includes(marker)) return true + } + + return false +} + +async function resourceContentsContain( + page: Page, + request: APIRequestContext, + marker: string, + filter: (url: string) => boolean, +) { + const resourceUrls = await page.evaluate(() => + performance.getEntriesByType('resource').map((entry) => entry.name), + ) + + return modulePreloadContentsContain( + request, + resourceUrls.filter(filter), + marker, + ) +} + +async function documentModulePreloadHrefs(page: Page) { + return page.evaluate(() => + Array.from( + document.querySelectorAll('link[rel~="modulepreload"]'), + (link) => link.href, + ), + ) +} + +function isHydrateBoundaryResource(url: string) { + return ( + url.includes('/assets/components-') || url.includes('/static/js/async/') + ) +} + +function isClientJavaScriptResource(url: string) { + return ( + url.includes('/assets/') || + url.includes('/static/js/') || + url.includes('/static/js/async/') + ) +} + +async function expectClientRouterReady(page: Page) { + await expect + .poll(() => + page.evaluate(() => + Boolean( + ( + globalThis as typeof globalThis & { + __TSR_ROUTER__?: unknown + } + ).__TSR_ROUTER__, + ), + ), + ) + .toBe(true) +} + +test.describe('Hydrate HMR', () => { + test.skip(!isDev, 'HMR regression coverage runs against the dev server only') + + test.beforeAll(async () => { + await componentsRouteEditor.capturePromise + }) + + test.afterEach(async () => { + await componentsRouteEditor.capturePromise + await componentsRouteEditor.restoreFiles() + }) + + test.afterAll(async () => { + await componentsRouteEditor.capturePromise + await componentsRouteEditor.restoreFiles() + }) + + test('updates deferred child chunks after the parent route is edited', async ({ + page, + }) => { + const pageErrors: Array = [] + page.on('pageerror', (error) => { + pageErrors.push(error.message) + }) + + await page.goto('/components') + await expectClientRouterReady(page) + await expectRouteToStayUnhydrated(page, 'component-visible-button') + + await componentsRouteEditor.replaceText( + 'componentsRoute', + interactiveBoxLabelSource, + interactiveBoxHmrLabelSource, + ) + + await waitForComponentsServerHtmlText(page, 'hmr visible') + if (isVite) { + await waitForVisibleHydrateVirtualModule(page, 'hmr ') + } + + await page.goto('/components') + await expectClientRouterReady(page) + await expectRouteToStayUnhydrated(page, 'component-visible-button') + await scrollToBoundary(page, 'component-visible-button') + await hmrExpect(page.getByTestId('component-visible-button')).toContainText( + 'hmr visible', + ) + await clickAndExpectCount( + page, + 'component-visible-button', + 'component-visible-count', + '1', + ) + expect(pageErrors).toEqual([]) + }) +}) + +test.describe('component-level Hydrate runtime strategies', () => { + test.skip( + isDev, + 'production hydration coverage runs against the preview server', + ) + + test('renders SSR HTML and hydrates each runtime when appropriately', async ({ + page, + request, + }) => { + await page.goto('/components') + + await expect(page.getByTestId('component-heading')).toHaveText( + 'Component Deferred Hydration', + ) + + await clickAndExpectCount( + page, + 'component-load-button', + 'component-load-count', + '1', + ) + await clickAndExpectCount( + page, + 'component-idle-button', + 'component-idle-count', + '1', + ) + await expect( + resourceContentsContain(page, request, 'component-visible', (url) => + isHydrateBoundaryResource(url), + ), + ).resolves.toBe(false) + await expectRouteToStayUnhydrated(page, 'component-visible-button') + await scrollToBoundary(page, 'component-visible-button') + await clickAndExpectCount( + page, + 'component-visible-button', + 'component-visible-count', + '1', + ) + await expect + .poll(() => + resourceContentsContain(page, request, 'component-visible', (url) => + isHydrateBoundaryResource(url), + ), + ) + .toBe(true) + await clickAndExpectCount( + page, + 'component-media-button', + 'component-media-count', + '1', + ) + await hoverIntentAndExpectCount( + page, + 'component-interaction-button', + 'component-interaction-count', + '1', + ) + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '0', + ) + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await page.getByTestId('component-custom-single-button').hover() + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await page.getByTestId('component-custom-single-button').click() + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await dispatchHydrationIntent( + page, + 'component-custom-single-button', + 'dblclick', + ) + await expect( + page.getByTestId('component-custom-single-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '1', + ) + await clickAndExpectCount( + page, + 'component-custom-single-button', + 'component-custom-single-count', + '1', + ) + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-custom-multi-button') + await dispatchHydrationIntent( + page, + 'component-custom-multi-button', + 'contextmenu', + ) + await clickAndExpectCount( + page, + 'component-custom-multi-button', + 'component-custom-multi-count', + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-condition-button') + await page.getByTestId('component-enable-condition').click() + await clickAndExpectCount( + page, + 'component-condition-button', + 'component-condition-count', + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-click-replay-button') + await page.getByTestId('component-click-replay-button').click() + await expect( + page.getByTestId('component-click-replay-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expect(page.getByTestId('component-click-replay-count')).toHaveText( + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-prefetch-button') + await expect( + resourceContentsContain(page, request, 'component-prefetch', (url) => + isHydrateBoundaryResource(url), + ), + ).resolves.toBe(false) + await page.mouse.move(0, 0) + await page.getByTestId('component-prefetch-button').hover() + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await expect + .poll(() => + resourceContentsContain(page, request, 'component-prefetch', (url) => + isHydrateBoundaryResource(url), + ), + ) + .toBe(true) + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await page.getByTestId('component-prefetch-button').click() + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await expect(page.getByTestId('component-prefetch-count')).toHaveText('1') + await hoverIntentAndExpectCount( + page, + 'component-nested-child-button', + 'component-nested-child-count', + '1', + ) + + await page.getByTestId('component-never-button').click() + await expect(page.getByTestId('component-never-count')).toHaveText('0') + }) + + test('replays click after another interaction boundary hydrates first', async ({ + page, + }) => { + await page.goto('/components') + await expectClientRouterReady(page) + + await scrollToBoundary(page, 'component-custom-multi-button') + await expectRouteToStayUnhydrated(page, 'component-custom-multi-button') + await page.getByTestId('component-custom-multi-button').click({ + button: 'right', + }) + await expect( + page.getByTestId('component-custom-multi-button'), + ).toHaveAttribute('data-hydrated', 'true') + + await scrollToBoundary(page, 'component-click-replay-button') + await expectRouteToStayUnhydrated(page, 'component-click-replay-button') + await page.getByTestId('component-click-replay-button').click() + await expect( + page.getByTestId('component-click-replay-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expect(page.getByTestId('component-click-replay-count')).toHaveText( + '1', + ) + }) + + test('shows fallback during a client-only mount while the child suspends', async ({ + page, + }) => { + await page.goto('/components') + await expect(page.getByTestId('component-load-button')).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await page.getByTestId('component-show-client-fallback').click() + + await expect(page.getByTestId('component-client-fallback')).toHaveText( + 'client fallback', + ) + await expect(page.getByTestId('component-fallback-child')).toHaveText( + 'fallback child', + ) + await expect(page.getByTestId('component-client-fallback')).toHaveCount(0) + }) +}) + +test.describe('Hydrate CSS delivery', () => { + test.skip( + isDev, + 'production hydration coverage runs against the preview server', + ) + + test('ships CSS for deferred, never, shared, and nested boundaries without JavaScript', async ({ + browser, + request, + }) => { + const response = await request.get('/css') + const html = await response.text() + + htmlContainsText(html, 'CSS Deferred Hydration') + htmlContainsText(html, 'Outer CSS') + htmlContainsText(html, 'Deferred CSS') + htmlContainsText(html, 'Never CSS') + htmlContainsText(html, 'Nested CSS') + + const context = await browser.newContext({ javaScriptEnabled: false }) + const page = await context.newPage() + + try { + await page.goto('/css') + + await expect(page.getByTestId('css-heading')).toHaveText( + 'CSS Deferred Hydration', + ) + await expect(page.getByTestId('css-deferred')).toHaveText('Deferred CSS') + await expect(page.getByTestId('css-never')).toHaveText('Never CSS') + await expect(page.getByTestId('css-nested')).toHaveText('Nested CSS') + + await expectCssProperty(page, 'css-outer', 'color', 'rgb(12, 34, 56)') + await expectCssProperty( + page, + 'css-deferred', + 'background-color', + 'rgb(23, 45, 67)', + ) + await expectCssProperty(page, 'css-never', 'color', 'rgb(45, 67, 89)') + await expectCssProperty( + page, + 'css-shared-outer', + 'border-top-color', + 'rgb(98, 76, 54)', + ) + await expectCssProperty( + page, + 'css-deferred', + 'border-top-color', + 'rgb(98, 76, 54)', + ) + await expectCssProperty( + page, + 'css-nested', + 'border-left-color', + 'rgb(67, 89, 123)', + ) + await expectCssProperty(page, 'css-nested', 'border-left-width', '5px') + } finally { + await context.close() + } + }) + + test('renders deferred content and omits never content after client-side navigation', async ({ + page, + }) => { + await page.goto('/') + await expectClientRouterReady(page) + await page.getByRole('link', { name: 'CSS', exact: true }).click() + await expect(page).toHaveURL(/\/css$/) + + await expect(page.getByTestId('css-heading')).toHaveText( + 'CSS Deferred Hydration', + ) + await expect(page.getByTestId('css-deferred')).toHaveText('Deferred CSS') + await expect(page.getByTestId('css-never')).toHaveCount(0) + await expect(page.getByTestId('css-nested')).toHaveCount(0) + + await expectCssProperty( + page, + 'css-deferred', + 'background-color', + 'rgb(23, 45, 67)', + ) + }) +}) + +test.describe('imported Hydrate boundaries', () => { + test.skip( + isDev, + 'production hydration coverage runs against the preview server', + ) + + test('does not emit filtered shared Hydrate child JS on the initial document', async ({ + request, + }) => { + const response = await request.get('/imported') + const html = await response.text() + + htmlContainsText(html, 'Imported Hydrate') + htmlContainsText(html, 'Imported Hydrate Child') + + await expect( + modulePreloadContentsContain( + request, + getModulePreloadHrefs(html), + 'imported-hydrate-child', + ), + ).resolves.toBe(false) + }) + + test('does not preload Hydrate child chunks before client navigation', async ({ + page, + request, + }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toHaveText( + 'Deferred Hydration', + ) + await expectClientRouterReady(page) + + const link = page.getByRole('link', { name: 'imported Hydrate' }) + await page.mouse.move(0, 0) + await link.hover() + await link.focus() + + await expect( + modulePreloadContentsContain( + request, + await documentModulePreloadHrefs(page), + 'imported-hydrate-child', + ), + ).resolves.toBe(false) + await expect( + resourceContentsContain(page, request, 'imported-hydrate-child', (url) => + isClientJavaScriptResource(url), + ), + ).resolves.toBe(false) + + await page.getByRole('link', { name: 'imported Hydrate' }).click() + await expect(page).toHaveURL(/\/imported$/) + await expect(page.getByTestId('imported-hydrate-fallback')).toHaveCount(0) + await expect(page.getByTestId('imported-hydrate-child')).toContainText( + 'Imported Hydrate Child', + ) + await page.getByTestId('imported-hydrate-child').click() + await expect(page.getByTestId('imported-hydrate-count')).toHaveText('1') + }) +}) diff --git a/e2e/react-start/deferred-hydration/tests/setup/global.setup.ts b/e2e/react-start/deferred-hydration/tests/setup/global.setup.ts new file mode 100644 index 00000000000..1117664a14e --- /dev/null +++ b/e2e/react-start/deferred-hydration/tests/setup/global.setup.ts @@ -0,0 +1,38 @@ +import { + e2eStartDummyServer, + getTestServerPort, + preOptimizeDevServer, + waitForServer, +} from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +function getE2EPortKey() { + const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' + return process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +} + +export default async function setup() { + if (process.env.MODE !== 'dev') return + + const e2ePortKey = getE2EPortKey() + + await e2eStartDummyServer(e2ePortKey) + + const port = await getTestServerPort(e2ePortKey) + const baseURL = `http://localhost:${port}` + + await waitForServer(baseURL) + await preOptimizeDevServer({ + baseURL, + readyTestId: 'home-heading', + warmup: async (page) => { + await page.goto(`${baseURL}/components`, { + waitUntil: 'domcontentloaded', + }) + await page.getByTestId('component-heading').waitFor({ + state: 'visible', + }) + await page.waitForLoadState('networkidle') + }, + }) +} diff --git a/e2e/react-start/deferred-hydration/tests/setup/global.teardown.ts b/e2e/react-start/deferred-hydration/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..df79f50e828 --- /dev/null +++ b/e2e/react-start/deferred-hydration/tests/setup/global.teardown.ts @@ -0,0 +1,13 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +function getE2EPortKey() { + const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' + return process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +} + +export default async function teardown() { + if (process.env.MODE !== 'dev') return + + await e2eStopDummyServer(getE2EPortKey()) +} diff --git a/e2e/react-start/deferred-hydration/tsconfig.json b/e2e/react-start/deferred-hydration/tsconfig.json new file mode 100644 index 00000000000..cef9369516a --- /dev/null +++ b/e2e/react-start/deferred-hydration/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/deferred-hydration/vite.config.ts b/e2e/react-start/deferred-hydration/vite.config.ts new file mode 100644 index 00000000000..1289bc0be78 --- /dev/null +++ b/e2e/react-start/deferred-hydration/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist-vite-ssr' + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + build: { + outDir, + }, + server: { port: 3000 }, + plugins: [tanstackStart(), viteReact()], +}) diff --git a/e2e/react-start/hmr/tests/app.spec.ts b/e2e/react-start/hmr/tests/app.spec.ts index bbaff95bc13..9dfae25b320 100644 --- a/e2e/react-start/hmr/tests/app.spec.ts +++ b/e2e/react-start/hmr/tests/app.spec.ts @@ -1,6 +1,9 @@ import { expect } from '@playwright/test' -import { test } from '@tanstack/router-e2e-utils' -import { readFile, writeFile } from 'node:fs/promises' +import { + createHmrFileEditor, + replaceAll, + test, +} from '@tanstack/router-e2e-utils' import path from 'node:path' import type { Page } from '@playwright/test' @@ -114,14 +117,6 @@ const routeFileRestoreChecks: Partial< }, } -// Capture original file contents once so beforeEach can restore them -const originalContents: Partial> = {} -const routeKeysPendingRestoreCheck = new Set() - -function replaceAll(source: string, from: string, to: string) { - return source.split(from).join(to) -} - function normalizeRouteSource(routeFileKey: RouteFileKey, source: string) { let next = source @@ -253,59 +248,14 @@ function normalizeRouteSource(routeFileKey: RouteFileKey, source: string) { return next } -async function captureOriginals() { - for (const [key, filePath] of Object.entries(routeFiles) as Array< - [RouteFileKey, string] - >) { - const current = await readFile(filePath, 'utf8') - const normalized = normalizeRouteSource(key, current) - if (normalized !== current) { - await writeFile(filePath, normalized) - routeKeysPendingRestoreCheck.add(key) - } - originalContents[key] = normalized - } -} - -const capturePromise = captureOriginals() - -async function restoreRouteFiles( - forceRouteFileKeys: Iterable = [], -) { - const forceRestoreKeys = new Set(forceRouteFileKeys) - const restoredRouteKeys: Array = [] - - for (const [key, filePath] of Object.entries(routeFiles) as Array< - [RouteFileKey, string] - >) { - const content = originalContents[key] - if (content === undefined) continue - const current = await readFile(filePath, 'utf8') - // Re-emit pending restores in case the watcher coalesced the previous - // restore write and the dev server is still serving stale route options. - if (current !== content || forceRestoreKeys.has(key)) { - await writeFile(filePath, content) - restoredRouteKeys.push(key) - } - } - - return restoredRouteKeys -} - -async function replaceRouteText( - routeFileKey: RouteFileKey, - from: string, - to: string, -) { - const filePath = routeFiles[routeFileKey] - const source = await readFile(filePath, 'utf8') - - if (!source.includes(from)) { - throw new Error(`Expected route file to include ${JSON.stringify(from)}`) - } - - await writeFile(filePath, source.replace(from, to)) -} +const routeFileEditor = createHmrFileEditor({ + files: routeFiles, + normalizeSource: normalizeRouteSource, +}) +const capturePromise = routeFileEditor.capturePromise +const routeKeysPendingRestoreCheck = routeFileEditor.pendingRestoreKeys +const restoreRouteFiles = routeFileEditor.restoreFiles +const replaceRouteText = routeFileEditor.replaceText async function replaceRouteTextAndWait( page: Page, @@ -314,7 +264,7 @@ async function replaceRouteTextAndWait( to: string, assertion: () => Promise, ) { - await replaceRouteText(routeFileKey, from, to) + await routeFileEditor.replaceText(routeFileKey, from, to) await assertion() } @@ -325,17 +275,7 @@ async function rewriteRouteFile( assertion: () => Promise, options: { allowNoop?: boolean } = {}, ) { - const filePath = routeFiles[routeFileKey] - const source = await readFile(filePath, 'utf8') - const updated = updater(source) - - if (updated === source && !options.allowNoop) { - throw new Error(`Expected ${filePath} to change during rewrite`) - } - - // Even a no-op write is useful for tests that need to force the dev server - // to reconcile a stale in-memory module with the current file contents. - await writeFile(filePath, updated) + await routeFileEditor.rewriteFile(routeFileKey, updater, options) await assertion() } diff --git a/e2e/react-start/rsc-deferred-hydration/.gitignore b/e2e/react-start/rsc-deferred-hydration/.gitignore new file mode 100644 index 00000000000..cc99170fbbf --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/.gitignore @@ -0,0 +1,5 @@ +dist +node_modules +port*.txt +test-results +playwright-report diff --git a/e2e/react-start/rsc-deferred-hydration/package.json b/e2e/react-start/rsc-deferred-hydration/package.json new file mode 100644 index 00000000000..7944b815527 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/package.json @@ -0,0 +1,45 @@ +{ + "name": "tanstack-react-start-e2e-rsc-deferred-hydration", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "preview": "vite preview", + "start": "node server.js", + "test:e2e": "pnpm test:e2e:dev && pnpm test:e2e:prod", + "test:e2e:dev": "MODE=dev playwright test --project=chromium", + "test:e2e:prod": "MODE=prod playwright test --project=chromium" + }, + "nx": { + "metadata": { + "playwrightModes": [ + { + "toolchain": "vite", + "mode": "ssr", + "shards": 1 + } + ] + } + }, + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^8.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-rsc": "^0.5.20", + "srvx": "^0.11.9", + "typescript": "^6.0.2" + } +} diff --git a/e2e/react-start/rsc-deferred-hydration/playwright.config.ts b/e2e/react-start/rsc-deferred-hydration/playwright.config.ts new file mode 100644 index 00000000000..10f41cd911b --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` +const mode = process.env.MODE ?? 'prod' +const isDev = mode === 'dev' + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + globalSetup: './tests/setup/global.setup.ts', + globalTeardown: './tests/setup/global.teardown.ts', + use: { baseURL }, + webServer: { + command: isDev ? 'pnpm dev:e2e' : 'pnpm build && pnpm start', + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: { + VITE_NODE_ENV: 'test', + NODE_ENV: isDev ? 'development' : 'production', + PORT: String(PORT), + VITE_SERVER_PORT: String(PORT), + }, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/react-start/rsc-deferred-hydration/server.js b/e2e/react-start/rsc-deferred-hydration/server.js new file mode 100644 index 00000000000..6365c568d44 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/server.js @@ -0,0 +1,47 @@ +import fs from 'node:fs' +import path from 'node:path' +import { spawn } from 'node:child_process' +import { pathToFileURL } from 'node:url' + +const distDir = process.env.E2E_DIST_DIR || 'dist' + +function resolveDistClientDir() { + return path.resolve(distDir, 'client') +} + +function resolveDistServerEntryPath() { + const serverJsPath = path.resolve(distDir, 'server', 'server.js') + if (fs.existsSync(serverJsPath)) return serverJsPath + + const indexJsPath = path.resolve(distDir, 'server', 'index.js') + if (fs.existsSync(indexJsPath)) return indexJsPath + + return serverJsPath +} + +export function start() { + const child = spawn( + 'srvx', + ['--prod', '-s', resolveDistClientDir(), resolveDistServerEntryPath()], + { + stdio: 'inherit', + shell: process.platform === 'win32', + }, + ) + + child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + return + } + + process.exit(code ?? 0) + }) +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + start() +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.module.css b/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.module.css new file mode 100644 index 00000000000..d80e17aebe9 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.module.css @@ -0,0 +1,42 @@ +.cssIsland { + border-color: rgba(16, 185, 129, 0.38); + background: + linear-gradient( + 135deg, + rgba(236, 253, 245, 0.96), + rgba(255, 255, 255, 0.9) + ), + repeating-linear-gradient( + 45deg, + rgba(16, 185, 129, 0.12) 0 8px, + transparent 8px 16px + ); + color: rgb(6, 78, 59); +} + +.cssIsland h2 { + color: rgb(6, 95, 70); +} + +.cssMarker { + width: fit-content; + padding: 0.45rem 0.7rem; + border-radius: 999px; + font-weight: 900; + transition: + background 180ms ease, + color 180ms ease, + box-shadow 180ms ease; +} + +.cssMarkerPending { + background: rgb(252, 231, 243); + color: rgb(157, 23, 77); + box-shadow: 0 8px 26px rgba(219, 39, 119, 0.18); +} + +.cssMarkerHydrated { + background: rgb(209, 250, 229); + color: rgb(6, 95, 70); + box-shadow: 0 8px 26px rgba(5, 150, 105, 0.18); +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.tsx b/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.tsx new file mode 100644 index 00000000000..cf49269f12f --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/components/CssHydrateIsland.tsx @@ -0,0 +1,45 @@ +'use client' + +import * as React from 'react' +import { Hydrate } from '@tanstack/react-start/client' +import { media } from '@tanstack/react-start/hydration' +import { DeferredHydrateIsland } from './DeferredHydrateIsland' +import styles from './CssHydrateIsland.module.css' + +function CssHydratePanel() { + const [hydrated, setHydrated] = React.useState(false) + + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( +
+ CSS module Hydrate island +

CSS modules survive the RSC to client boundary

+

+ {hydrated + ? 'Hydrated module-styled client content' + : 'Pending module-styled client content'} +

+ +
+ ) +} + +export function CssHydrateIsland() { + return ( +
+ + + +
+ ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/components/DeferredHydrateIsland.tsx b/e2e/react-start/rsc-deferred-hydration/src/components/DeferredHydrateIsland.tsx new file mode 100644 index 00000000000..5444fef057d --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/components/DeferredHydrateIsland.tsx @@ -0,0 +1,67 @@ +'use client' + +import * as React from 'react' +import { Hydrate } from '@tanstack/react-start/client' +import { interaction, media, visible } from '@tanstack/react-start/hydration' + +type Strategy = 'interaction' | 'visible' | 'media' + +const strategyCopy: Record = { + interaction: 'Hydrates after pointer or focus intent reaches this island.', + visible: 'Hydrates only after the island scrolls into the viewport.', + media: 'Hydrates immediately when the matching media query is true.', +} + +function getStrategy(strategy: Strategy) { + if (strategy === 'interaction') return interaction() + if (strategy === 'visible') return visible({ rootMargin: '0px' }) + return media('(min-width: 1px)') +} + +function CounterButton(props: { id: string; label: string }) { + const [count, setCount] = React.useState(0) + const [hydrated, setHydrated] = React.useState(false) + + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( + + ) +} + +export function DeferredHydrateIsland(props: { + id: string + title: string + strategy: Strategy + className?: string +}) { + return ( +
+ Client Hydrate island +

{props.title}

+

{strategyCopy[props.strategy]}

+ + + +
+ ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routeTree.gen.ts b/e2e/react-start/rsc-deferred-hydration/src/routeTree.gen.ts new file mode 100644 index 00000000000..246737b5153 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routeTree.gen.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ServerClientRouteImport } from './routes/server-client' +import { Route as CssRouteImport } from './routes/css' +import { Route as CompositeRouteImport } from './routes/composite' +import { Route as IndexRouteImport } from './routes/index' + +const ServerClientRoute = ServerClientRouteImport.update({ + id: '/server-client', + path: '/server-client', + getParentRoute: () => rootRouteImport, +} as any) +const CssRoute = CssRouteImport.update({ + id: '/css', + path: '/css', + getParentRoute: () => rootRouteImport, +} as any) +const CompositeRoute = CompositeRouteImport.update({ + id: '/composite', + path: '/composite', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/composite': typeof CompositeRoute + '/css': typeof CssRoute + '/server-client': typeof ServerClientRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/composite': typeof CompositeRoute + '/css': typeof CssRoute + '/server-client': typeof ServerClientRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/composite': typeof CompositeRoute + '/css': typeof CssRoute + '/server-client': typeof ServerClientRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/composite' | '/css' | '/server-client' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/composite' | '/css' | '/server-client' + id: '__root__' | '/' | '/composite' | '/css' | '/server-client' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + CompositeRoute: typeof CompositeRoute + CssRoute: typeof CssRoute + ServerClientRoute: typeof ServerClientRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/server-client': { + id: '/server-client' + path: '/server-client' + fullPath: '/server-client' + preLoaderRoute: typeof ServerClientRouteImport + parentRoute: typeof rootRouteImport + } + '/css': { + id: '/css' + path: '/css' + fullPath: '/css' + preLoaderRoute: typeof CssRouteImport + parentRoute: typeof rootRouteImport + } + '/composite': { + id: '/composite' + path: '/composite' + fullPath: '/composite' + preLoaderRoute: typeof CompositeRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + CompositeRoute: CompositeRoute, + CssRoute: CssRoute, + ServerClientRoute: ServerClientRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/router.tsx b/e2e/react-start/rsc-deferred-hydration/src/router.tsx new file mode 100644 index 00000000000..9d87d8748b5 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/router.tsx @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routes/__root.tsx b/e2e/react-start/rsc-deferred-hydration/src/routes/__root.tsx new file mode 100644 index 00000000000..7ce2e7bdb13 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routes/__root.tsx @@ -0,0 +1,122 @@ +/// +import * as React from 'react' +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'RSC Deferred Hydration E2E' }, + ], + }), + shellComponent: RootDocument, + component: () => ( +
+ +
+ ), +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + {children} + + + + ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routes/composite.tsx b/e2e/react-start/rsc-deferred-hydration/src/routes/composite.tsx new file mode 100644 index 00000000000..3241ee487d6 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routes/composite.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/react-router' +import { CompositeComponent } from '@tanstack/react-start/rsc' +import { getCompositeHydrate } from '~/server/serverHydrateComponents' +import { DeferredHydrateIsland } from '~/components/DeferredHydrateIsland' + +export const Route = createFileRoute('/composite')({ + loader: async () => ({ + Composite: await getCompositeHydrate(), + }), + component: CompositeRoute, +}) + +function CompositeRoute() { + const { Composite } = Route.useLoaderData() + return ( + + + + ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routes/css.tsx b/e2e/react-start/rsc-deferred-hydration/src/routes/css.tsx new file mode 100644 index 00000000000..ddb22f318a3 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routes/css.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { getCssModuleHydrate } from '~/server/serverHydrateComponents' + +export const Route = createFileRoute('/css')({ + loader: async () => ({ + Server: await getCssModuleHydrate(), + }), + component: CssRoute, +}) + +function CssRoute() { + const { Server } = Route.useLoaderData() + return Server +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routes/index.tsx b/e2e/react-start/rsc-deferred-hydration/src/routes/index.tsx new file mode 100644 index 00000000000..ebe57e70281 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routes/index.tsx @@ -0,0 +1,35 @@ +import { createFileRoute, Link } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

RSC meets Deferred Hydration

+

+ These routes render React Server Components that cross into client + components using Hydrate. Each card explains when its + client island should hydrate and keeps server-rendered HTML visible + first. +

+
+ + Server component to client Hydrate + The RSC renders a separate "use client" component that + defers hydration until interaction. + + + Composite server shellA server component owns the + visual frame while a client child hydrates only after it becomes + visible. + + + CSS module client islandA server component renders a + client Hydrate boundary whose child uses CSS modules. + +
+
+ ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/routes/server-client.tsx b/e2e/react-start/rsc-deferred-hydration/src/routes/server-client.tsx new file mode 100644 index 00000000000..f39b635241a --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/routes/server-client.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { getServerClientHydrate } from '~/server/serverHydrateComponents' + +export const Route = createFileRoute('/server-client')({ + loader: async () => ({ + Server: await getServerClientHydrate(), + }), + component: ServerClientRoute, +}) + +function ServerClientRoute() { + const { Server } = Route.useLoaderData() + return Server +} diff --git a/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateComponents.tsx b/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateComponents.tsx new file mode 100644 index 00000000000..d4e431061a7 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateComponents.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' +import { createServerFn } from '@tanstack/react-start' +import { + createCompositeComponent, + renderServerComponent, +} from '@tanstack/react-start/rsc' +import { + CompositeHydrateContent, + CssModuleHydrateContent, + ServerClientHydrateContent, +} from './serverHydrateContent' + +export const getServerClientHydrate = createServerFn({ method: 'GET' }).handler( + async () => { + const renderedAt = new Date().toISOString() + + return renderServerComponent( + , + ) + }, +) + +export const getCompositeHydrate = createServerFn({ method: 'GET' }).handler( + async () => { + return createCompositeComponent((props: { children?: React.ReactNode }) => ( + {props.children} + )) + }, +) + +export const getCssModuleHydrate = createServerFn({ method: 'GET' }).handler( + async () => { + return renderServerComponent() + }, +) diff --git a/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateContent.tsx b/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateContent.tsx new file mode 100644 index 00000000000..dc785a16ce5 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/src/server/serverHydrateContent.tsx @@ -0,0 +1,63 @@ +import * as React from 'react' +import { DeferredHydrateIsland } from '../components/DeferredHydrateIsland' +import { CssHydrateIsland } from '../components/CssHydrateIsland' + +export function ServerClientHydrateContent({ + renderedAt, +}: { + renderedAt: string +}) { + return ( +
+ React Server Component +

Server component renders a deferred client island

+

+ Server rendered at . The button below is + present in HTML but stays unhydrated until interaction. +

+ +
+ ) +} + +export function CompositeHydrateContent({ + children, +}: { + children?: React.ReactNode +}) { + return ( +
+ Composite Server Component +

Server shell, client Hydrate slot

+

+ The server owns this descriptive shell. The client slot below remains + server HTML until an interaction reaches it. +

+
+ {children} +
+ ) +} + +export function CssModuleHydrateContent() { + return ( +
+ + Server Component plus CSS module client island + +

CSS module Hydrate boundary

+

+ This server component renders a separate client component that uses + Hydrate and CSS modules. +

+ +
+ ) +} diff --git a/e2e/react-start/rsc-deferred-hydration/tests/hydration.spec.ts b/e2e/react-start/rsc-deferred-hydration/tests/hydration.spec.ts new file mode 100644 index 00000000000..2d543c5406b --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/tests/hydration.spec.ts @@ -0,0 +1,82 @@ +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' +import type { Page } from '@playwright/test' + +async function expectUnhydrated(page: Page, id: string) { + await expect(page.getByTestId(`${id}-button`)).toHaveAttribute( + 'data-hydrated', + 'false', + ) +} + +async function clickAndExpectCount(page: Page, id: string, count: string) { + await expect(page.getByTestId(`${id}-button`)).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await page.getByTestId(`${id}-button`).click() + await expect(page.getByTestId(`${id}-count`)).toHaveText(count) +} + +async function waitForHydrateMarkerToMount(page: Page, id: string) { + await page.waitForFunction((testId) => { + const button = document.querySelector(`[data-testid="${testId}-button"]`) + const marker = button?.closest('[data-ts-hydrate-id]') + return Object.keys(marker ?? {}).some((key) => key.startsWith('__react')) + }, id) +} + +test.describe('RSC deferred hydration', () => { + test('server component renders a client Hydrate island that hydrates on interaction', async ({ + page, + }) => { + await page.goto('/server-client') + + await expect(page.getByTestId('server-client-rsc')).toContainText( + 'Server component renders a deferred client island', + ) + await expect(page.getByTestId('server-client-island')).toContainText( + 'Interaction strategy inside RSC output', + ) + await expectUnhydrated(page, 'server-client') + + await page.getByTestId('server-client-button').hover() + await clickAndExpectCount(page, 'server-client', '1') + }) + + test('composite server component can wrap an interaction Hydrate client island', async ({ + page, + }) => { + await page.goto('/composite') + + await expect(page.getByTestId('composite-rsc')).toContainText( + 'Server shell, client Hydrate slot', + ) + await expect( + page.getByTestId('composite-interaction-island'), + ).toContainText('Interaction strategy inside a composite server component') + await expectUnhydrated(page, 'composite-interaction') + + await waitForHydrateMarkerToMount(page, 'composite-interaction') + await page.getByTestId('composite-interaction-button').hover() + await clickAndExpectCount(page, 'composite-interaction', '1') + }) + + test('server component can render a CSS module Hydrate client island', async ({ + page, + }) => { + await page.goto('/css') + + await expect(page.getByTestId('css-rsc')).toContainText( + 'CSS module Hydrate boundary', + ) + await expect(page.getByTestId('css-module-marker')).toHaveCSS( + 'font-weight', + '900', + ) + await expectUnhydrated(page, 'css-nested') + await waitForHydrateMarkerToMount(page, 'css-nested') + await page.getByTestId('css-nested-button').hover() + await clickAndExpectCount(page, 'css-nested', '1') + }) +}) diff --git a/e2e/react-start/rsc-deferred-hydration/tests/setup/global.setup.ts b/e2e/react-start/rsc-deferred-hydration/tests/setup/global.setup.ts new file mode 100644 index 00000000000..3d1579ee87c --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/tests/setup/global.setup.ts @@ -0,0 +1,28 @@ +import { + e2eStartDummyServer, + getTestServerPort, + preOptimizeDevServer, + waitForServer, +} from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function setup() { + await e2eStartDummyServer(packageJson.name) + + if (process.env.MODE !== 'dev') return + + const port = await getTestServerPort(packageJson.name) + const baseURL = `http://localhost:${port}` + + await waitForServer(baseURL) + await preOptimizeDevServer({ + baseURL, + readyTestId: 'home-heading', + warmup: async (page) => { + for (const route of ['/server-client', '/composite', '/css']) { + await page.goto(`${baseURL}${route}`, { waitUntil: 'domcontentloaded' }) + await page.waitForLoadState('networkidle') + } + }, + }) +} diff --git a/e2e/react-start/rsc-deferred-hydration/tests/setup/global.teardown.ts b/e2e/react-start/rsc-deferred-hydration/tests/setup/global.teardown.ts new file mode 100644 index 00000000000..62fd79911cc --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/tests/setup/global.teardown.ts @@ -0,0 +1,6 @@ +import { e2eStopDummyServer } from '@tanstack/router-e2e-utils' +import packageJson from '../../package.json' with { type: 'json' } + +export default async function teardown() { + await e2eStopDummyServer(packageJson.name) +} diff --git a/e2e/react-start/rsc-deferred-hydration/tsconfig.json b/e2e/react-start/rsc-deferred-hydration/tsconfig.json new file mode 100644 index 00000000000..cef9369516a --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/react-start/rsc-deferred-hydration/vite.config.ts b/e2e/react-start/rsc-deferred-hydration/vite.config.ts new file mode 100644 index 00000000000..1919761bc31 --- /dev/null +++ b/e2e/react-start/rsc-deferred-hydration/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' +import rsc from '@vitejs/plugin-rsc' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist' + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + build: { + outDir, + }, + server: { + port: Number(process.env.VITE_SERVER_PORT ?? 3000), + }, + plugins: [ + tanstackStart({ + rsc: { + enabled: true, + }, + }), + rsc(), + viteReact(), + ], +}) diff --git a/e2e/react-start/server-routes/src/routeTree.gen.ts b/e2e/react-start/server-routes/src/routeTree.gen.ts index 1c32040cef7..2b8bac6e096 100644 --- a/e2e/react-start/server-routes/src/routeTree.gen.ts +++ b/e2e/react-start/server-routes/src/routeTree.gen.ts @@ -16,9 +16,9 @@ import { Route as MethodsIndexRouteImport } from './routes/methods/index' import { Route as MethodsOnlyAnyRouteImport } from './routes/methods/only-any' import { Route as ApiOnlyAnyRouteImport } from './routes/api/only-any' import { Route as ApiMiddlewareContextRouteImport } from './routes/api/middleware-context' +import { Route as ApiHeadRedirectFallbackRouteImport } from './routes/api/head-redirect-fallback' import { Route as ApiHeadFallbackRouteImport } from './routes/api/head-fallback' import { Route as ApiGetAndAnyRouteImport } from './routes/api/get-and-any' -import { Route as ApiHeadRedirectFallbackRouteImport } from './routes/api/head-redirect-fallback' import { Route as ApiParamsFooRouteRouteImport } from './routes/api/params/$foo/route' import { Route as ApiParamsFooBarRouteImport } from './routes/api/params/$foo/$bar' @@ -52,6 +52,16 @@ const ApiOnlyAnyRoute = ApiOnlyAnyRouteImport.update({ path: '/api/only-any', getParentRoute: () => rootRouteImport, } as any) +const ApiMiddlewareContextRoute = ApiMiddlewareContextRouteImport.update({ + id: '/api/middleware-context', + path: '/api/middleware-context', + getParentRoute: () => rootRouteImport, +} as any) +const ApiHeadRedirectFallbackRoute = ApiHeadRedirectFallbackRouteImport.update({ + id: '/api/head-redirect-fallback', + path: '/api/head-redirect-fallback', + getParentRoute: () => rootRouteImport, +} as any) const ApiHeadFallbackRoute = ApiHeadFallbackRouteImport.update({ id: '/api/head-fallback', path: '/api/head-fallback', @@ -62,16 +72,6 @@ const ApiGetAndAnyRoute = ApiGetAndAnyRouteImport.update({ path: '/api/get-and-any', getParentRoute: () => rootRouteImport, } as any) -const ApiHeadRedirectFallbackRoute = ApiHeadRedirectFallbackRouteImport.update({ - id: '/api/head-redirect-fallback', - path: '/api/head-redirect-fallback', - getParentRoute: () => rootRouteImport, -} as any) -const ApiMiddlewareContextRoute = ApiMiddlewareContextRouteImport.update({ - id: '/api/middleware-context', - path: '/api/middleware-context', - getParentRoute: () => rootRouteImport, -} as any) const ApiParamsFooRouteRoute = ApiParamsFooRouteRouteImport.update({ id: '/api/params/$foo', path: '/api/params/$foo', @@ -87,11 +87,11 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/methods': typeof MethodsRouteRouteWithChildren '/merge-middleware-context': typeof MergeMiddlewareContextRoute - '/api/middleware-context': typeof ApiMiddlewareContextRoute - '/api/only-any': typeof ApiOnlyAnyRoute - '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -100,11 +100,11 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/merge-middleware-context': typeof MergeMiddlewareContextRoute - '/api/middleware-context': typeof ApiMiddlewareContextRoute - '/api/only-any': typeof ApiOnlyAnyRoute - '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -115,11 +115,11 @@ export interface FileRoutesById { '/': typeof IndexRoute '/methods': typeof MethodsRouteRouteWithChildren '/merge-middleware-context': typeof MergeMiddlewareContextRoute - '/api/middleware-context': typeof ApiMiddlewareContextRoute - '/api/only-any': typeof ApiOnlyAnyRoute - '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/get-and-any': typeof ApiGetAndAnyRoute + '/api/head-fallback': typeof ApiHeadFallbackRoute '/api/head-redirect-fallback': typeof ApiHeadRedirectFallbackRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute '/methods/only-any': typeof MethodsOnlyAnyRoute '/methods/': typeof MethodsIndexRoute '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren @@ -131,11 +131,11 @@ export interface FileRouteTypes { | '/' | '/methods' | '/merge-middleware-context' - | '/api/middleware-context' - | '/api/only-any' - | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-fallback' | '/api/head-redirect-fallback' + | '/api/middleware-context' + | '/api/only-any' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -144,11 +144,11 @@ export interface FileRouteTypes { to: | '/' | '/merge-middleware-context' - | '/api/middleware-context' - | '/api/only-any' - | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-fallback' | '/api/head-redirect-fallback' + | '/api/middleware-context' + | '/api/only-any' | '/methods/only-any' | '/methods' | '/api/params/$foo' @@ -158,11 +158,11 @@ export interface FileRouteTypes { | '/' | '/methods' | '/merge-middleware-context' - | '/api/middleware-context' - | '/api/only-any' - | '/api/head-fallback' | '/api/get-and-any' + | '/api/head-fallback' | '/api/head-redirect-fallback' + | '/api/middleware-context' + | '/api/only-any' | '/methods/only-any' | '/methods/' | '/api/params/$foo' @@ -173,11 +173,11 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute MethodsRouteRoute: typeof MethodsRouteRouteWithChildren MergeMiddlewareContextRoute: typeof MergeMiddlewareContextRoute - ApiMiddlewareContextRoute: typeof ApiMiddlewareContextRoute - ApiOnlyAnyRoute: typeof ApiOnlyAnyRoute - ApiHeadFallbackRoute: typeof ApiHeadFallbackRoute ApiGetAndAnyRoute: typeof ApiGetAndAnyRoute + ApiHeadFallbackRoute: typeof ApiHeadFallbackRoute ApiHeadRedirectFallbackRoute: typeof ApiHeadRedirectFallbackRoute + ApiMiddlewareContextRoute: typeof ApiMiddlewareContextRoute + ApiOnlyAnyRoute: typeof ApiOnlyAnyRoute ApiParamsFooRouteRoute: typeof ApiParamsFooRouteRouteWithChildren } @@ -225,6 +225,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiOnlyAnyRouteImport parentRoute: typeof rootRouteImport } + '/api/middleware-context': { + id: '/api/middleware-context' + path: '/api/middleware-context' + fullPath: '/api/middleware-context' + preLoaderRoute: typeof ApiMiddlewareContextRouteImport + parentRoute: typeof rootRouteImport + } + '/api/head-redirect-fallback': { + id: '/api/head-redirect-fallback' + path: '/api/head-redirect-fallback' + fullPath: '/api/head-redirect-fallback' + preLoaderRoute: typeof ApiHeadRedirectFallbackRouteImport + parentRoute: typeof rootRouteImport + } '/api/head-fallback': { id: '/api/head-fallback' path: '/api/head-fallback' @@ -239,20 +253,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiGetAndAnyRouteImport parentRoute: typeof rootRouteImport } - '/api/head-redirect-fallback': { - id: '/api/head-redirect-fallback' - path: '/api/head-redirect-fallback' - fullPath: '/api/head-redirect-fallback' - preLoaderRoute: typeof ApiHeadRedirectFallbackRouteImport - parentRoute: typeof rootRouteImport - } - '/api/middleware-context': { - id: '/api/middleware-context' - path: '/api/middleware-context' - fullPath: '/api/middleware-context' - preLoaderRoute: typeof ApiMiddlewareContextRouteImport - parentRoute: typeof rootRouteImport - } '/api/params/$foo': { id: '/api/params/$foo' path: '/api/params/$foo' @@ -299,11 +299,11 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, MethodsRouteRoute: MethodsRouteRouteWithChildren, MergeMiddlewareContextRoute: MergeMiddlewareContextRoute, - ApiMiddlewareContextRoute: ApiMiddlewareContextRoute, - ApiOnlyAnyRoute: ApiOnlyAnyRoute, - ApiHeadFallbackRoute: ApiHeadFallbackRoute, ApiGetAndAnyRoute: ApiGetAndAnyRoute, + ApiHeadFallbackRoute: ApiHeadFallbackRoute, ApiHeadRedirectFallbackRoute: ApiHeadRedirectFallbackRoute, + ApiMiddlewareContextRoute: ApiMiddlewareContextRoute, + ApiOnlyAnyRoute: ApiOnlyAnyRoute, ApiParamsFooRouteRoute: ApiParamsFooRouteRouteWithChildren, } export const routeTree = rootRouteImport diff --git a/e2e/solid-start/deferred-hydration/.gitignore b/e2e/solid-start/deferred-hydration/.gitignore new file mode 100644 index 00000000000..1b3a07ede12 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/.gitignore @@ -0,0 +1,15 @@ +node_modules +package-lock.json +yarn.lock +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/solid-start/deferred-hydration/package.json b/e2e/solid-start/deferred-hydration/package.json new file mode 100644 index 00000000000..77f55458fe4 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/package.json @@ -0,0 +1,56 @@ +{ + "name": "tanstack-solid-start-e2e-deferred-hydration", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "pnpm dev:vite --port 3000", + "dev:e2e": "pnpm dev:vite", + "dev:vite": "vite dev", + "dev:rsbuild": "rsbuild dev", + "build": "pnpm build:vite", + "build:vite": "vite build && tsc --noEmit", + "build:rsbuild": "rsbuild build && tsc --noEmit", + "preview": "vite preview", + "start": "pnpm start:vite", + "start:vite": "srvx --prod --dir=. -s dist-vite-ssr/client --entry dist-vite-ssr/server/server.js", + "start:rsbuild": "srvx --prod --dir=. -s dist-rsbuild-ssr/client --entry dist-rsbuild-ssr/server/index.js", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/solid-router": "workspace:^", + "@tanstack/solid-start": "workspace:^", + "solid-js": "^1.9.10", + "vite": "^8.0.0" + }, + "devDependencies": { + "@rsbuild/core": "^2.0.1", + "@rsbuild/plugin-babel": "^1.1.2", + "@rsbuild/plugin-solid": "^1.1.1", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "rolldown": "1.0.0-rc.18", + "srvx": "^0.11.9", + "typescript": "^6.0.2", + "vite-plugin-solid": "^2.11.11" + }, + "nx": { + "targets": { + "test:e2e": { + "parallelism": false + } + }, + "metadata": { + "playwrightModes": [ + { + "toolchain": "vite", + "mode": "ssr" + }, + { + "toolchain": "rsbuild", + "mode": "ssr" + } + ] + } + } +} diff --git a/e2e/solid-start/deferred-hydration/playwright.config.ts b/e2e/solid-start/deferred-hydration/playwright.config.ts new file mode 100644 index 00000000000..ea9e73a8564 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/playwright.config.ts @@ -0,0 +1,43 @@ +import fs from 'node:fs' +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +const toolchain = process.env.E2E_TOOLCHAIN ?? 'vite' +const distDir = process.env.E2E_DIST_DIR ?? `dist-${toolchain}-ssr` +const e2ePortKey = + process.env.E2E_PORT_KEY ?? `${packageJson.name}-${toolchain}` +const serverEntryFile = toolchain === 'rsbuild' ? 'index.js' : 'server.js' +const startCommand = `pnpm exec srvx --prod --dir=. -s ${distDir}/client --entry ${distDir}/server/${serverEntryFile}` + +if (process.env.TEST_WORKER_INDEX === undefined) { + fs.rmSync(`port-${e2ePortKey}.txt`, { force: true }) +} + +const PORT = await getTestServerPort(e2ePortKey) +const baseURL = `http://localhost:${PORT}` + +export default defineConfig({ + testDir: './tests', + workers: 1, + reporter: [['line']], + use: { baseURL }, + webServer: { + command: startCommand, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: { + E2E_DIST_DIR: distDir, + NODE_ENV: 'production', + PORT: String(PORT), + VITE_SERVER_PORT: String(PORT), + }, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/solid-start/deferred-hydration/rsbuild.config.ts b/e2e/solid-start/deferred-hydration/rsbuild.config.ts new file mode 100644 index 00000000000..7ab152bb589 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/rsbuild.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginBabel } from '@rsbuild/plugin-babel' +import { pluginSolid } from '@rsbuild/plugin-solid' +import { tanstackStart } from '@tanstack/solid-start/plugin/rsbuild' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist-rsbuild-ssr' + +export default defineConfig({ + plugins: [ + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + }), + pluginSolid(), + tanstackStart(), + ], + output: { + distPath: { + root: outDir, + }, + }, +}) diff --git a/e2e/solid-start/deferred-hydration/src/routeTree.gen.ts b/e2e/solid-start/deferred-hydration/src/routeTree.gen.ts new file mode 100644 index 00000000000..6b59f5c1ec3 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routeTree.gen.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ImportedRouteImport } from './routes/imported' +import { Route as CssRouteImport } from './routes/css' +import { Route as ComponentsRouteImport } from './routes/components' +import { Route as IndexRouteImport } from './routes/index' + +const ImportedRoute = ImportedRouteImport.update({ + id: '/imported', + path: '/imported', + getParentRoute: () => rootRouteImport, +} as any) +const CssRoute = CssRouteImport.update({ + id: '/css', + path: '/css', + getParentRoute: () => rootRouteImport, +} as any) +const ComponentsRoute = ComponentsRouteImport.update({ + id: '/components', + path: '/components', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/components': typeof ComponentsRoute + '/css': typeof CssRoute + '/imported': typeof ImportedRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/components' | '/css' | '/imported' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/components' | '/css' | '/imported' + id: '__root__' | '/' | '/components' | '/css' | '/imported' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ComponentsRoute: typeof ComponentsRoute + CssRoute: typeof CssRoute + ImportedRoute: typeof ImportedRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/imported': { + id: '/imported' + path: '/imported' + fullPath: '/imported' + preLoaderRoute: typeof ImportedRouteImport + parentRoute: typeof rootRouteImport + } + '/css': { + id: '/css' + path: '/css' + fullPath: '/css' + preLoaderRoute: typeof CssRouteImport + parentRoute: typeof rootRouteImport + } + '/components': { + id: '/components' + path: '/components' + fullPath: '/components' + preLoaderRoute: typeof ComponentsRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ComponentsRoute: ComponentsRoute, + CssRoute: CssRoute, + ImportedRoute: ImportedRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/solid-start/deferred-hydration/src/router.tsx b/e2e/solid-start/deferred-hydration/src/router.tsx new file mode 100644 index 00000000000..aa7ead67524 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/router.tsx @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/__root.tsx b/e2e/solid-start/deferred-hydration/src/routes/__root.tsx new file mode 100644 index 00000000000..f021dbedbc6 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/__root.tsx @@ -0,0 +1,92 @@ +/// +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'Deferred Hydration E2E' }, + ], + }), + component: RootDocument, +}) + +function RootDocument() { + return ( + + + + + + + + +
+ +
+ + + + ) +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/components.tsx b/e2e/solid-start/deferred-hydration/src/routes/components.tsx new file mode 100644 index 00000000000..9dd6387f092 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/components.tsx @@ -0,0 +1,171 @@ +import * as Solid from 'solid-js' + +import { createFileRoute } from '@tanstack/solid-router' +import { Hydrate } from '@tanstack/solid-start' +import { + condition, + idle, + interaction, + load, + media, + never, + visible, +} from '@tanstack/solid-start/hydration' + +export const Route = createFileRoute('/components')({ + component: ComponentHydrationPage, +}) + +function InteractiveBox(props: { id: string; label: string }) { + const [count, setCount] = Solid.createSignal(0) + const [hydrated, setHydrated] = Solid.createSignal(false) + + Solid.onMount(() => { + setHydrated(true) + }) + + return ( + + ) +} + +function DelayedFallbackBox() { + if (typeof window !== 'undefined') { + const [ready] = Solid.createResource(async () => { + await new Promise((resolve) => window.setTimeout(resolve, 1000)) + return true + }) + + return ( + +
fallback child
+
+ ) + } + + return
fallback child
+} + +function ComponentHydrationPage() { + const [hydratedCallbacks, setHydratedCallbacks] = Solid.createSignal(0) + const [conditionReady, setConditionReady] = Solid.createSignal(false) + const [showClientFallbackBoundary, setShowClientFallbackBoundary] = + Solid.createSignal(false) + + return ( +
+

Component Deferred Hydration

+
+ Manual test guide + + Pink buttons are server HTML that has not hydrated yet. Green buttons + have hydrated and should increment when clicked. Follow the notes + below to trigger each strategy intentionally. + +
+

{hydratedCallbacks()}

+

+ load and idle should become green + without interaction shortly after the page loads. +

+ + + + + + +
Scroll down to reveal the visible boundary
+

+ visible hydrates only after this button enters the + viewport. +

+ + + +

+ media hydrates when (min-width: 1px) + matches. interaction hydrates on hover, focus, pointer + down, or click intent. +

+ + + + + + +

+ Custom interaction boundaries below hydrate only for their configured + events: double-click for the single-event example, and right-click or + double-click for the multi-event example. The prefetch example should + download code on hover but hydrate on click. +

+ setHydratedCallbacks((count) => count + 1)} + > + + + + + + + + + + + + + + + + + + + + + + + +

+ never stays as server HTML forever on the initial page, + so clicking should not increment it. +

+ + + client fallback + } + > + + + +
+ ) +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/css.tsx b/e2e/solid-start/deferred-hydration/src/routes/css.tsx new file mode 100644 index 00000000000..c1a80d492b9 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/css.tsx @@ -0,0 +1,57 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { Hydrate } from '@tanstack/solid-start' +import { media, never, visible } from '@tanstack/solid-start/hydration' +import outerStyles from './css/outer.module.css' +import deferredStyles from './css/deferred-only.module.css' +import sharedStyles from './css/shared.module.css' + +export const Route = createFileRoute('/css')({ + component: CssHydrationPage, +}) + +function CssHydrationPage() { + return ( +
+
+

+ CSS Deferred Hydration +

+

+ CSS from deferred, never, shared, and nested Hydrate boundaries should + be available even before the client JavaScript hydrates those islands. +

+
+
+
+ Outer CSS +
+
+ Shared outer CSS +
+
+ +
+ Deferred CSS +
+
+ +
+ Never CSS +
+
+ + +
+ Nested CSS +
+
+
+
+ ) +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/css/deferred-only.module.css b/e2e/solid-start/deferred-hydration/src/routes/css/deferred-only.module.css new file mode 100644 index 00000000000..05eafda03cf --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/css/deferred-only.module.css @@ -0,0 +1,15 @@ +.deferredBox { + background-color: rgb(23, 45, 67); + color: rgb(255, 255, 255); + padding: 12px; +} + +.neverBox { + color: rgb(45, 67, 89); + padding: 12px; +} + +.nestedBox { + border-left: 5px solid rgb(67, 89, 123); + padding-left: 12px; +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/css/outer.module.css b/e2e/solid-start/deferred-hydration/src/routes/css/outer.module.css new file mode 100644 index 00000000000..98ca5e0934e --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/css/outer.module.css @@ -0,0 +1,9 @@ +.heading { + color: rgb(11, 31, 53); +} + +.outerBox { + background-color: rgb(242, 250, 255); + color: rgb(12, 34, 56); + padding: 12px; +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/css/shared.module.css b/e2e/solid-start/deferred-hydration/src/routes/css/shared.module.css new file mode 100644 index 00000000000..020da5d7adc --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/css/shared.module.css @@ -0,0 +1,4 @@ +.sharedBox { + border-top: 4px solid rgb(98, 76, 54); + margin-top: 8px; +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/imported.tsx b/e2e/solid-start/deferred-hydration/src/routes/imported.tsx new file mode 100644 index 00000000000..abfb827c91a --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/imported.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { ImportedHydrateWidget } from '../shared/ImportedHydrateWidget' + +export const Route = createFileRoute('/imported')({ + component: ImportedHydrationPage, +}) + +function ImportedHydrationPage() { + return ( +
+

Imported Hydrate

+ +
+ ) +} diff --git a/e2e/solid-start/deferred-hydration/src/routes/index.tsx b/e2e/solid-start/deferred-hydration/src/routes/index.tsx new file mode 100644 index 00000000000..144afed4c61 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/routes/index.tsx @@ -0,0 +1,21 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Deferred Hydration

+

Component strategies

+ component strategies +

CSS

+ CSS deferred hydration +

Imported component

+ + imported Hydrate + +
+ ) +} diff --git a/e2e/solid-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx b/e2e/solid-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx new file mode 100644 index 00000000000..859b7417559 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/src/shared/ImportedHydrateWidget.tsx @@ -0,0 +1,34 @@ +import { createSignal } from 'solid-js' + +import { Hydrate } from '@tanstack/solid-start' +import { media } from '@tanstack/solid-start/hydration' + +function ImportedHydrateChild() { + const [count, setCount] = createSignal(0) + + return ( + + ) +} + +export function ImportedHydrateWidget() { + return ( + + imported hydrate fallback + + } + > + + + ) +} diff --git a/e2e/solid-start/deferred-hydration/tests/hydration.spec.ts b/e2e/solid-start/deferred-hydration/tests/hydration.spec.ts new file mode 100644 index 00000000000..a7845b44158 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/tests/hydration.spec.ts @@ -0,0 +1,653 @@ +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' +import type { APIRequestContext, Page } from '@playwright/test' + +async function clickAndExpectCount( + page: Page, + buttonTestId: string, + countTestId: string, + count: string, +) { + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await page.getByTestId(buttonTestId).click() + await expect(page.getByTestId(countTestId)).toHaveText(count) +} + +async function hoverIntentAndExpectCount( + page: Page, + buttonTestId: string, + countTestId: string, + count: string, +) { + await expectRouteToStayUnhydrated(page, buttonTestId) + await page.mouse.move(0, 0) + await page.getByTestId(buttonTestId).hover() + await clickAndExpectCount(page, buttonTestId, countTestId, count) +} + +async function dispatchHydrationIntent( + page: Page, + buttonTestId: string, + eventName: string, +) { + await page.getByTestId(buttonTestId).evaluate((element, eventName) => { + const marker = element.closest('[data-ts-hydrate-id]') + + if (!marker) { + throw new Error('Expected Hydrate marker to exist') + } + + marker.dispatchEvent( + new Event(eventName, { bubbles: true, cancelable: true }), + ) + }, eventName) +} + +async function trackBoundaryStability(page: Page, buttonTestId: string) { + await page.getByTestId(buttonTestId).evaluate((element, testId) => { + const marker = element.closest('[data-ts-hydrate-id]') + + if (!(marker instanceof HTMLElement)) { + throw new Error('Expected Hydrate marker to exist') + } + + const win = window as typeof window & { + __hydrateBoundaryVisualGapCount?: number + __hydrateBoundaryMaxScrollDelta?: number + __hydrateBoundaryStabilityDone?: boolean + } + const hasBoundaryButton = () => + Array.from(marker.querySelectorAll('[data-testid]')).some( + (child) => child.getAttribute('data-testid') === testId, + ) + const hasHydratedBoundaryButton = () => + Array.from(marker.querySelectorAll('[data-testid]')).some( + (child) => + child.getAttribute('data-testid') === testId && + child.getAttribute('data-hydrated') === 'true', + ) + const initialScrollY = window.scrollY + + win.__hydrateBoundaryVisualGapCount = 0 + win.__hydrateBoundaryMaxScrollDelta = 0 + win.__hydrateBoundaryStabilityDone = false + + let frameCount = 0 + let hydratedFrameCount = 0 + let observer: MutationObserver | undefined + const recordScroll = () => { + win.__hydrateBoundaryMaxScrollDelta = Math.max( + win.__hydrateBoundaryMaxScrollDelta ?? 0, + Math.abs(window.scrollY - initialScrollY), + ) + + if (hasHydratedBoundaryButton()) hydratedFrameCount++ + + frameCount++ + if (hydratedFrameCount >= 10 || frameCount >= 300) { + observer?.disconnect() + win.__hydrateBoundaryStabilityDone = true + return + } + + requestAnimationFrame(recordScroll) + } + + observer = new MutationObserver(() => { + if (!hasBoundaryButton()) { + win.__hydrateBoundaryVisualGapCount!++ + } + }) + observer.observe(marker, { childList: true }) + + requestAnimationFrame(recordScroll) + }, buttonTestId) +} + +async function expectBoundaryStable(page: Page) { + await expect + .poll(() => + page.evaluate( + () => + ( + window as typeof window & { + __hydrateBoundaryStabilityDone?: boolean + } + ).__hydrateBoundaryStabilityDone ?? false, + ), + ) + .toBe(true) + await expect + .poll(() => + page.evaluate( + () => + ( + window as typeof window & { + __hydrateBoundaryVisualGapCount?: number + } + ).__hydrateBoundaryVisualGapCount ?? 0, + ), + ) + .toBe(0) + await expect + .poll(() => + page.evaluate( + () => + ( + window as typeof window & { + __hydrateBoundaryMaxScrollDelta?: number + } + ).__hydrateBoundaryMaxScrollDelta ?? 0, + ), + ) + .toBeLessThanOrEqual(1) +} + +async function expectRouteToStayUnhydrated( + page: Page, + buttonTestId: string, + duration = 250, +) { + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await page.waitForTimeout(duration) + await expect(page.getByTestId(buttonTestId)).toHaveAttribute( + 'data-hydrated', + 'false', + ) +} + +async function scrollToBoundary(page: Page, buttonTestId: string) { + const button = page.getByTestId(buttonTestId) + for (let attempt = 0; attempt < 3; attempt++) { + await button.evaluate((element) => { + element.scrollIntoView({ block: 'center', inline: 'nearest' }) + }) + + await page.waitForTimeout(100) + const isVisible = await button.evaluate((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > 0 && rect.top < window.innerHeight + }) + + if (isVisible) return + } + + await expect(button).toBeInViewport() +} + +async function expectCssProperty( + page: Page, + testId: string, + property: string, + value: string, +) { + await expect + .poll(() => + page.getByTestId(testId).evaluate((element, propertyName) => { + return getComputedStyle(element).getPropertyValue(propertyName) + }, property), + ) + .toBe(value) +} + +function htmlContainsText(html: string, text: string) { + const pattern = text.split(' ').join('(?:\\s|)+') + expect(html).toMatch(new RegExp(pattern)) +} + +function getModulePreloadHrefs(html: string) { + return Array.from(html.matchAll(/]*>/g), (match) => match[0]) + .filter((tag) => /\brel="modulepreload"/.test(tag)) + .map((tag) => tag.match(/\bhref="([^"]+)"/)?.[1]) + .filter((href): href is string => !!href) +} + +async function modulePreloadContentsContain( + request: APIRequestContext, + hrefs: Array, + marker: string, +) { + for (const href of hrefs) { + const response = await request.get(href) + if (!response.ok()) continue + + const text = await response.text() + if (text.includes(marker)) return true + } + + return false +} + +async function resourceContentsContain( + page: Page, + request: APIRequestContext, + marker: string, + filter: (url: string) => boolean, +) { + const resourceUrls = await page.evaluate(() => + performance.getEntriesByType('resource').map((entry) => entry.name), + ) + + return modulePreloadContentsContain( + request, + resourceUrls.filter(filter), + marker, + ) +} + +async function documentModulePreloadHrefs(page: Page) { + return page.evaluate(() => + Array.from( + document.querySelectorAll('link[rel~="modulepreload"]'), + (link) => link.href, + ), + ) +} + +function isHydrateBoundaryResource(url: string) { + return ( + url.includes('/assets/components-') || url.includes('/static/js/async/') + ) +} + +function isClientJavaScriptResource(url: string) { + return ( + url.includes('/assets/') || + url.includes('/static/js/') || + url.includes('/static/js/async/') + ) +} + +async function expectClientRouterReady(page: Page) { + await expect + .poll(() => + page.evaluate(() => + Boolean( + ( + globalThis as typeof globalThis & { + __TSR_ROUTER__?: unknown + } + ).__TSR_ROUTER__, + ), + ), + ) + .toBe(true) +} + +test.describe('component-level Hydrate runtime strategies', () => { + test('renders SSR HTML and hydrates each runtime when appropriately', async ({ + page, + request, + }) => { + await page.goto('/components') + + await expect(page.getByTestId('component-heading')).toHaveText( + 'Component Deferred Hydration', + ) + + await clickAndExpectCount( + page, + 'component-load-button', + 'component-load-count', + '1', + ) + await clickAndExpectCount( + page, + 'component-idle-button', + 'component-idle-count', + '1', + ) + await expect( + resourceContentsContain(page, request, 'component-visible', (url) => + isHydrateBoundaryResource(url), + ), + ).resolves.toBe(false) + await expectRouteToStayUnhydrated(page, 'component-visible-button') + await scrollToBoundary(page, 'component-visible-button') + await clickAndExpectCount( + page, + 'component-visible-button', + 'component-visible-count', + '1', + ) + await expect + .poll(() => + resourceContentsContain(page, request, 'component-visible', (url) => + isHydrateBoundaryResource(url), + ), + ) + .toBe(true) + await clickAndExpectCount( + page, + 'component-media-button', + 'component-media-count', + '1', + ) + await hoverIntentAndExpectCount( + page, + 'component-interaction-button', + 'component-interaction-count', + '1', + ) + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '0', + ) + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await page.getByTestId('component-custom-single-button').hover() + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await page.getByTestId('component-custom-single-button').click() + await expectRouteToStayUnhydrated(page, 'component-custom-single-button') + await dispatchHydrationIntent( + page, + 'component-custom-single-button', + 'dblclick', + ) + await expect( + page.getByTestId('component-custom-single-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '1', + ) + await clickAndExpectCount( + page, + 'component-custom-single-button', + 'component-custom-single-count', + '1', + ) + await expect(page.getByTestId('component-on-hydrated-count')).toHaveText( + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-custom-multi-button') + await dispatchHydrationIntent( + page, + 'component-custom-multi-button', + 'contextmenu', + ) + await clickAndExpectCount( + page, + 'component-custom-multi-button', + 'component-custom-multi-count', + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-condition-button') + await page.getByTestId('component-enable-condition').click() + await clickAndExpectCount( + page, + 'component-condition-button', + 'component-condition-count', + '1', + ) + await scrollToBoundary(page, 'component-click-replay-button') + await expectRouteToStayUnhydrated(page, 'component-click-replay-button') + await trackBoundaryStability(page, 'component-click-replay-button') + await page.getByTestId('component-click-replay-button').click() + await expect( + page.getByTestId('component-click-replay-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expectBoundaryStable(page) + await expect(page.getByTestId('component-click-replay-count')).toHaveText( + '1', + ) + await expectRouteToStayUnhydrated(page, 'component-prefetch-button') + await expect( + resourceContentsContain(page, request, 'component-prefetch', (url) => + isHydrateBoundaryResource(url), + ), + ).resolves.toBe(false) + await page.mouse.move(0, 0) + await page.getByTestId('component-prefetch-button').hover() + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await expect + .poll(() => + resourceContentsContain(page, request, 'component-prefetch', (url) => + isHydrateBoundaryResource(url), + ), + ) + .toBe(true) + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'false', + ) + await page.getByTestId('component-prefetch-button').click() + await expect(page.getByTestId('component-prefetch-button')).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await expect(page.getByTestId('component-prefetch-count')).toHaveText('1') + await hoverIntentAndExpectCount( + page, + 'component-nested-child-button', + 'component-nested-child-count', + '1', + ) + + await page.getByTestId('component-never-button').click() + await expect(page.getByTestId('component-never-count')).toHaveText('0') + }) + + test('replays click after another interaction boundary hydrates first', async ({ + page, + }) => { + await page.goto('/components') + await expectClientRouterReady(page) + + await scrollToBoundary(page, 'component-custom-multi-button') + await expectRouteToStayUnhydrated(page, 'component-custom-multi-button') + await page.getByTestId('component-custom-multi-button').click({ + button: 'right', + }) + await expect( + page.getByTestId('component-custom-multi-button'), + ).toHaveAttribute('data-hydrated', 'true') + + await scrollToBoundary(page, 'component-click-replay-button') + await expectRouteToStayUnhydrated(page, 'component-click-replay-button') + await trackBoundaryStability(page, 'component-click-replay-button') + await page.getByTestId('component-click-replay-button').click() + await expect( + page.getByTestId('component-click-replay-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expectBoundaryStable(page) + await expect(page.getByTestId('component-click-replay-count')).toHaveText( + '1', + ) + }) + + test('keeps bottom scroll stable when a condition boundary hydrates', async ({ + page, + }) => { + await page.goto('/components') + await expectClientRouterReady(page) + await expectRouteToStayUnhydrated(page, 'component-condition-button') + + await page.evaluate(() => { + window.scrollTo(0, document.documentElement.scrollHeight) + }) + await page.waitForTimeout(100) + await expect( + page.getByTestId('component-enable-condition'), + ).toBeInViewport() + + await trackBoundaryStability(page, 'component-condition-button') + await page.getByTestId('component-enable-condition').click() + await expect( + page.getByTestId('component-condition-button'), + ).toHaveAttribute('data-hydrated', 'true') + await expectBoundaryStable(page) + await clickAndExpectCount( + page, + 'component-condition-button', + 'component-condition-count', + '1', + ) + }) + + test('shows fallback during a client-only mount while the child suspends', async ({ + page, + }) => { + await page.goto('/components') + await expect(page.getByTestId('component-load-button')).toHaveAttribute( + 'data-hydrated', + 'true', + ) + await page.getByTestId('component-show-client-fallback').click() + + await expect(page.getByTestId('component-client-fallback')).toHaveText( + 'client fallback', + ) + await expect(page.getByTestId('component-fallback-child')).toHaveText( + 'fallback child', + ) + await expect(page.getByTestId('component-client-fallback')).toHaveCount(0) + }) +}) + +test.describe('Hydrate CSS delivery', () => { + test('ships CSS for deferred, never, shared, and nested boundaries without JavaScript', async ({ + browser, + request, + }) => { + const response = await request.get('/css') + const html = await response.text() + + htmlContainsText(html, 'CSS Deferred Hydration') + htmlContainsText(html, 'Outer CSS') + htmlContainsText(html, 'Deferred CSS') + htmlContainsText(html, 'Never CSS') + htmlContainsText(html, 'Nested CSS') + + const context = await browser.newContext({ javaScriptEnabled: false }) + const page = await context.newPage() + + try { + await page.goto('/css') + + await expect(page.getByTestId('css-heading')).toHaveText( + 'CSS Deferred Hydration', + ) + await expect(page.getByTestId('css-deferred')).toHaveText('Deferred CSS') + await expect(page.getByTestId('css-never')).toHaveText('Never CSS') + await expect(page.getByTestId('css-nested')).toHaveText('Nested CSS') + + await expectCssProperty(page, 'css-outer', 'color', 'rgb(12, 34, 56)') + await expectCssProperty( + page, + 'css-deferred', + 'background-color', + 'rgb(23, 45, 67)', + ) + await expectCssProperty(page, 'css-never', 'color', 'rgb(45, 67, 89)') + await expectCssProperty( + page, + 'css-shared-outer', + 'border-top-color', + 'rgb(98, 76, 54)', + ) + await expectCssProperty( + page, + 'css-deferred', + 'border-top-color', + 'rgb(98, 76, 54)', + ) + await expectCssProperty( + page, + 'css-nested', + 'border-left-color', + 'rgb(67, 89, 123)', + ) + await expectCssProperty(page, 'css-nested', 'border-left-width', '5px') + } finally { + await context.close() + } + }) + + test('renders deferred content and omits never content after client-side navigation', async ({ + page, + }) => { + await page.goto('/') + await expectClientRouterReady(page) + await page.getByRole('link', { name: 'CSS', exact: true }).click() + await expect(page).toHaveURL(/\/css$/) + + await expect(page.getByTestId('css-heading')).toHaveText( + 'CSS Deferred Hydration', + ) + await expect(page.getByTestId('css-deferred')).toHaveText('Deferred CSS') + await expect(page.getByTestId('css-never')).toHaveCount(0) + await expect(page.getByTestId('css-nested')).toHaveCount(0) + + await expectCssProperty( + page, + 'css-deferred', + 'background-color', + 'rgb(23, 45, 67)', + ) + }) +}) + +test.describe('imported Hydrate boundaries', () => { + test('does not emit filtered shared Hydrate child JS on the initial document', async ({ + request, + }) => { + const response = await request.get('/imported') + const html = await response.text() + + htmlContainsText(html, 'Imported Hydrate') + htmlContainsText(html, 'Imported Hydrate Child') + + await expect( + modulePreloadContentsContain( + request, + getModulePreloadHrefs(html), + 'imported-hydrate-child', + ), + ).resolves.toBe(false) + }) + + test('does not preload Hydrate child chunks before client navigation', async ({ + page, + request, + }) => { + await page.goto('/') + await expect(page.getByTestId('home-heading')).toHaveText( + 'Deferred Hydration', + ) + await expectClientRouterReady(page) + + const link = page.getByRole('link', { name: 'imported Hydrate' }) + await page.mouse.move(0, 0) + await link.hover() + await link.focus() + + await expect( + modulePreloadContentsContain( + request, + await documentModulePreloadHrefs(page), + 'imported-hydrate-child', + ), + ).resolves.toBe(false) + await expect( + resourceContentsContain(page, request, 'imported-hydrate-child', (url) => + isClientJavaScriptResource(url), + ), + ).resolves.toBe(false) + + await page.getByRole('link', { name: 'imported Hydrate' }).click() + await expect(page).toHaveURL(/\/imported$/) + await expect(page.getByTestId('imported-hydrate-fallback')).toHaveCount(0) + await expect(page.getByTestId('imported-hydrate-child')).toContainText( + 'Imported Hydrate Child', + ) + await page.getByTestId('imported-hydrate-child').click() + await expect(page.getByTestId('imported-hydrate-count')).toHaveText('1') + }) +}) diff --git a/e2e/solid-start/deferred-hydration/tsconfig.json b/e2e/solid-start/deferred-hydration/tsconfig.json new file mode 100644 index 00000000000..76cf3401fa4 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2024", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true, + "types": ["vite/client"] + } +} diff --git a/e2e/solid-start/deferred-hydration/vite.config.ts b/e2e/solid-start/deferred-hydration/vite.config.ts new file mode 100644 index 00000000000..70f388638a8 --- /dev/null +++ b/e2e/solid-start/deferred-hydration/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import viteSolid from 'vite-plugin-solid' + +const outDir = process.env.E2E_DIST_DIR ?? 'dist-vite-ssr' + +export default defineConfig({ + resolve: { tsconfigPaths: true }, + build: { + outDir, + }, + server: { port: 3000 }, + plugins: [tanstackStart(), viteSolid({ ssr: true })], +}) diff --git a/examples/react/start-large/src/routeTree.gen.ts b/examples/react/start-large/src/routeTree.gen.ts index 03cc2b9080b..aae96a65f82 100644 --- a/examples/react/start-large/src/routeTree.gen.ts +++ b/examples/react/start-large/src/routeTree.gen.ts @@ -17,6 +17,408 @@ import { Route as ParamsRouteRouteImport } from './routes/params/route' import { Route as IndexRouteImport } from './routes/index' import { Route as SearchSearchPlaceholderRouteImport } from './routes/search/searchPlaceholder' import { Route as ParamsParamsPlaceholderRouteImport } from './routes/params/$paramsPlaceholder' +import { Route as genRelative99RouteImport } from './routes/(gen)/relative99' +import { Route as genRelative98RouteImport } from './routes/(gen)/relative98' +import { Route as genRelative97RouteImport } from './routes/(gen)/relative97' +import { Route as genRelative96RouteImport } from './routes/(gen)/relative96' +import { Route as genRelative95RouteImport } from './routes/(gen)/relative95' +import { Route as genRelative94RouteImport } from './routes/(gen)/relative94' +import { Route as genRelative93RouteImport } from './routes/(gen)/relative93' +import { Route as genRelative92RouteImport } from './routes/(gen)/relative92' +import { Route as genRelative91RouteImport } from './routes/(gen)/relative91' +import { Route as genRelative90RouteImport } from './routes/(gen)/relative90' +import { Route as genRelative9RouteImport } from './routes/(gen)/relative9' +import { Route as genRelative89RouteImport } from './routes/(gen)/relative89' +import { Route as genRelative88RouteImport } from './routes/(gen)/relative88' +import { Route as genRelative87RouteImport } from './routes/(gen)/relative87' +import { Route as genRelative86RouteImport } from './routes/(gen)/relative86' +import { Route as genRelative85RouteImport } from './routes/(gen)/relative85' +import { Route as genRelative84RouteImport } from './routes/(gen)/relative84' +import { Route as genRelative83RouteImport } from './routes/(gen)/relative83' +import { Route as genRelative82RouteImport } from './routes/(gen)/relative82' +import { Route as genRelative81RouteImport } from './routes/(gen)/relative81' +import { Route as genRelative80RouteImport } from './routes/(gen)/relative80' +import { Route as genRelative8RouteImport } from './routes/(gen)/relative8' +import { Route as genRelative79RouteImport } from './routes/(gen)/relative79' +import { Route as genRelative78RouteImport } from './routes/(gen)/relative78' +import { Route as genRelative77RouteImport } from './routes/(gen)/relative77' +import { Route as genRelative76RouteImport } from './routes/(gen)/relative76' +import { Route as genRelative75RouteImport } from './routes/(gen)/relative75' +import { Route as genRelative74RouteImport } from './routes/(gen)/relative74' +import { Route as genRelative73RouteImport } from './routes/(gen)/relative73' +import { Route as genRelative72RouteImport } from './routes/(gen)/relative72' +import { Route as genRelative71RouteImport } from './routes/(gen)/relative71' +import { Route as genRelative70RouteImport } from './routes/(gen)/relative70' +import { Route as genRelative7RouteImport } from './routes/(gen)/relative7' +import { Route as genRelative69RouteImport } from './routes/(gen)/relative69' +import { Route as genRelative68RouteImport } from './routes/(gen)/relative68' +import { Route as genRelative67RouteImport } from './routes/(gen)/relative67' +import { Route as genRelative66RouteImport } from './routes/(gen)/relative66' +import { Route as genRelative65RouteImport } from './routes/(gen)/relative65' +import { Route as genRelative64RouteImport } from './routes/(gen)/relative64' +import { Route as genRelative63RouteImport } from './routes/(gen)/relative63' +import { Route as genRelative62RouteImport } from './routes/(gen)/relative62' +import { Route as genRelative61RouteImport } from './routes/(gen)/relative61' +import { Route as genRelative60RouteImport } from './routes/(gen)/relative60' +import { Route as genRelative6RouteImport } from './routes/(gen)/relative6' +import { Route as genRelative59RouteImport } from './routes/(gen)/relative59' +import { Route as genRelative58RouteImport } from './routes/(gen)/relative58' +import { Route as genRelative57RouteImport } from './routes/(gen)/relative57' +import { Route as genRelative56RouteImport } from './routes/(gen)/relative56' +import { Route as genRelative55RouteImport } from './routes/(gen)/relative55' +import { Route as genRelative54RouteImport } from './routes/(gen)/relative54' +import { Route as genRelative53RouteImport } from './routes/(gen)/relative53' +import { Route as genRelative52RouteImport } from './routes/(gen)/relative52' +import { Route as genRelative51RouteImport } from './routes/(gen)/relative51' +import { Route as genRelative50RouteImport } from './routes/(gen)/relative50' +import { Route as genRelative5RouteImport } from './routes/(gen)/relative5' +import { Route as genRelative49RouteImport } from './routes/(gen)/relative49' +import { Route as genRelative48RouteImport } from './routes/(gen)/relative48' +import { Route as genRelative47RouteImport } from './routes/(gen)/relative47' +import { Route as genRelative46RouteImport } from './routes/(gen)/relative46' +import { Route as genRelative45RouteImport } from './routes/(gen)/relative45' +import { Route as genRelative44RouteImport } from './routes/(gen)/relative44' +import { Route as genRelative43RouteImport } from './routes/(gen)/relative43' +import { Route as genRelative42RouteImport } from './routes/(gen)/relative42' +import { Route as genRelative41RouteImport } from './routes/(gen)/relative41' +import { Route as genRelative40RouteImport } from './routes/(gen)/relative40' +import { Route as genRelative4RouteImport } from './routes/(gen)/relative4' +import { Route as genRelative39RouteImport } from './routes/(gen)/relative39' +import { Route as genRelative38RouteImport } from './routes/(gen)/relative38' +import { Route as genRelative37RouteImport } from './routes/(gen)/relative37' +import { Route as genRelative36RouteImport } from './routes/(gen)/relative36' +import { Route as genRelative35RouteImport } from './routes/(gen)/relative35' +import { Route as genRelative34RouteImport } from './routes/(gen)/relative34' +import { Route as genRelative33RouteImport } from './routes/(gen)/relative33' +import { Route as genRelative32RouteImport } from './routes/(gen)/relative32' +import { Route as genRelative31RouteImport } from './routes/(gen)/relative31' +import { Route as genRelative30RouteImport } from './routes/(gen)/relative30' +import { Route as genRelative3RouteImport } from './routes/(gen)/relative3' +import { Route as genRelative29RouteImport } from './routes/(gen)/relative29' +import { Route as genRelative28RouteImport } from './routes/(gen)/relative28' +import { Route as genRelative27RouteImport } from './routes/(gen)/relative27' +import { Route as genRelative26RouteImport } from './routes/(gen)/relative26' +import { Route as genRelative25RouteImport } from './routes/(gen)/relative25' +import { Route as genRelative24RouteImport } from './routes/(gen)/relative24' +import { Route as genRelative23RouteImport } from './routes/(gen)/relative23' +import { Route as genRelative22RouteImport } from './routes/(gen)/relative22' +import { Route as genRelative21RouteImport } from './routes/(gen)/relative21' +import { Route as genRelative20RouteImport } from './routes/(gen)/relative20' +import { Route as genRelative2RouteImport } from './routes/(gen)/relative2' +import { Route as genRelative19RouteImport } from './routes/(gen)/relative19' +import { Route as genRelative18RouteImport } from './routes/(gen)/relative18' +import { Route as genRelative17RouteImport } from './routes/(gen)/relative17' +import { Route as genRelative16RouteImport } from './routes/(gen)/relative16' +import { Route as genRelative15RouteImport } from './routes/(gen)/relative15' +import { Route as genRelative14RouteImport } from './routes/(gen)/relative14' +import { Route as genRelative13RouteImport } from './routes/(gen)/relative13' +import { Route as genRelative12RouteImport } from './routes/(gen)/relative12' +import { Route as genRelative11RouteImport } from './routes/(gen)/relative11' +import { Route as genRelative10RouteImport } from './routes/(gen)/relative10' +import { Route as genRelative1RouteImport } from './routes/(gen)/relative1' +import { Route as genRelative0RouteImport } from './routes/(gen)/relative0' +import { Route as genAbsolute99RouteImport } from './routes/(gen)/absolute99' +import { Route as genAbsolute98RouteImport } from './routes/(gen)/absolute98' +import { Route as genAbsolute97RouteImport } from './routes/(gen)/absolute97' +import { Route as genAbsolute96RouteImport } from './routes/(gen)/absolute96' +import { Route as genAbsolute95RouteImport } from './routes/(gen)/absolute95' +import { Route as genAbsolute94RouteImport } from './routes/(gen)/absolute94' +import { Route as genAbsolute93RouteImport } from './routes/(gen)/absolute93' +import { Route as genAbsolute92RouteImport } from './routes/(gen)/absolute92' +import { Route as genAbsolute91RouteImport } from './routes/(gen)/absolute91' +import { Route as genAbsolute90RouteImport } from './routes/(gen)/absolute90' +import { Route as genAbsolute9RouteImport } from './routes/(gen)/absolute9' +import { Route as genAbsolute89RouteImport } from './routes/(gen)/absolute89' +import { Route as genAbsolute88RouteImport } from './routes/(gen)/absolute88' +import { Route as genAbsolute87RouteImport } from './routes/(gen)/absolute87' +import { Route as genAbsolute86RouteImport } from './routes/(gen)/absolute86' +import { Route as genAbsolute85RouteImport } from './routes/(gen)/absolute85' +import { Route as genAbsolute84RouteImport } from './routes/(gen)/absolute84' +import { Route as genAbsolute83RouteImport } from './routes/(gen)/absolute83' +import { Route as genAbsolute82RouteImport } from './routes/(gen)/absolute82' +import { Route as genAbsolute81RouteImport } from './routes/(gen)/absolute81' +import { Route as genAbsolute80RouteImport } from './routes/(gen)/absolute80' +import { Route as genAbsolute8RouteImport } from './routes/(gen)/absolute8' +import { Route as genAbsolute79RouteImport } from './routes/(gen)/absolute79' +import { Route as genAbsolute78RouteImport } from './routes/(gen)/absolute78' +import { Route as genAbsolute77RouteImport } from './routes/(gen)/absolute77' +import { Route as genAbsolute76RouteImport } from './routes/(gen)/absolute76' +import { Route as genAbsolute75RouteImport } from './routes/(gen)/absolute75' +import { Route as genAbsolute74RouteImport } from './routes/(gen)/absolute74' +import { Route as genAbsolute73RouteImport } from './routes/(gen)/absolute73' +import { Route as genAbsolute72RouteImport } from './routes/(gen)/absolute72' +import { Route as genAbsolute71RouteImport } from './routes/(gen)/absolute71' +import { Route as genAbsolute70RouteImport } from './routes/(gen)/absolute70' +import { Route as genAbsolute7RouteImport } from './routes/(gen)/absolute7' +import { Route as genAbsolute69RouteImport } from './routes/(gen)/absolute69' +import { Route as genAbsolute68RouteImport } from './routes/(gen)/absolute68' +import { Route as genAbsolute67RouteImport } from './routes/(gen)/absolute67' +import { Route as genAbsolute66RouteImport } from './routes/(gen)/absolute66' +import { Route as genAbsolute65RouteImport } from './routes/(gen)/absolute65' +import { Route as genAbsolute64RouteImport } from './routes/(gen)/absolute64' +import { Route as genAbsolute63RouteImport } from './routes/(gen)/absolute63' +import { Route as genAbsolute62RouteImport } from './routes/(gen)/absolute62' +import { Route as genAbsolute61RouteImport } from './routes/(gen)/absolute61' +import { Route as genAbsolute60RouteImport } from './routes/(gen)/absolute60' +import { Route as genAbsolute6RouteImport } from './routes/(gen)/absolute6' +import { Route as genAbsolute59RouteImport } from './routes/(gen)/absolute59' +import { Route as genAbsolute58RouteImport } from './routes/(gen)/absolute58' +import { Route as genAbsolute57RouteImport } from './routes/(gen)/absolute57' +import { Route as genAbsolute56RouteImport } from './routes/(gen)/absolute56' +import { Route as genAbsolute55RouteImport } from './routes/(gen)/absolute55' +import { Route as genAbsolute54RouteImport } from './routes/(gen)/absolute54' +import { Route as genAbsolute53RouteImport } from './routes/(gen)/absolute53' +import { Route as genAbsolute52RouteImport } from './routes/(gen)/absolute52' +import { Route as genAbsolute51RouteImport } from './routes/(gen)/absolute51' +import { Route as genAbsolute50RouteImport } from './routes/(gen)/absolute50' +import { Route as genAbsolute5RouteImport } from './routes/(gen)/absolute5' +import { Route as genAbsolute49RouteImport } from './routes/(gen)/absolute49' +import { Route as genAbsolute48RouteImport } from './routes/(gen)/absolute48' +import { Route as genAbsolute47RouteImport } from './routes/(gen)/absolute47' +import { Route as genAbsolute46RouteImport } from './routes/(gen)/absolute46' +import { Route as genAbsolute45RouteImport } from './routes/(gen)/absolute45' +import { Route as genAbsolute44RouteImport } from './routes/(gen)/absolute44' +import { Route as genAbsolute43RouteImport } from './routes/(gen)/absolute43' +import { Route as genAbsolute42RouteImport } from './routes/(gen)/absolute42' +import { Route as genAbsolute41RouteImport } from './routes/(gen)/absolute41' +import { Route as genAbsolute40RouteImport } from './routes/(gen)/absolute40' +import { Route as genAbsolute4RouteImport } from './routes/(gen)/absolute4' +import { Route as genAbsolute39RouteImport } from './routes/(gen)/absolute39' +import { Route as genAbsolute38RouteImport } from './routes/(gen)/absolute38' +import { Route as genAbsolute37RouteImport } from './routes/(gen)/absolute37' +import { Route as genAbsolute36RouteImport } from './routes/(gen)/absolute36' +import { Route as genAbsolute35RouteImport } from './routes/(gen)/absolute35' +import { Route as genAbsolute34RouteImport } from './routes/(gen)/absolute34' +import { Route as genAbsolute33RouteImport } from './routes/(gen)/absolute33' +import { Route as genAbsolute32RouteImport } from './routes/(gen)/absolute32' +import { Route as genAbsolute31RouteImport } from './routes/(gen)/absolute31' +import { Route as genAbsolute30RouteImport } from './routes/(gen)/absolute30' +import { Route as genAbsolute3RouteImport } from './routes/(gen)/absolute3' +import { Route as genAbsolute29RouteImport } from './routes/(gen)/absolute29' +import { Route as genAbsolute28RouteImport } from './routes/(gen)/absolute28' +import { Route as genAbsolute27RouteImport } from './routes/(gen)/absolute27' +import { Route as genAbsolute26RouteImport } from './routes/(gen)/absolute26' +import { Route as genAbsolute25RouteImport } from './routes/(gen)/absolute25' +import { Route as genAbsolute24RouteImport } from './routes/(gen)/absolute24' +import { Route as genAbsolute23RouteImport } from './routes/(gen)/absolute23' +import { Route as genAbsolute22RouteImport } from './routes/(gen)/absolute22' +import { Route as genAbsolute21RouteImport } from './routes/(gen)/absolute21' +import { Route as genAbsolute20RouteImport } from './routes/(gen)/absolute20' +import { Route as genAbsolute2RouteImport } from './routes/(gen)/absolute2' +import { Route as genAbsolute19RouteImport } from './routes/(gen)/absolute19' +import { Route as genAbsolute18RouteImport } from './routes/(gen)/absolute18' +import { Route as genAbsolute17RouteImport } from './routes/(gen)/absolute17' +import { Route as genAbsolute16RouteImport } from './routes/(gen)/absolute16' +import { Route as genAbsolute15RouteImport } from './routes/(gen)/absolute15' +import { Route as genAbsolute14RouteImport } from './routes/(gen)/absolute14' +import { Route as genAbsolute13RouteImport } from './routes/(gen)/absolute13' +import { Route as genAbsolute12RouteImport } from './routes/(gen)/absolute12' +import { Route as genAbsolute11RouteImport } from './routes/(gen)/absolute11' +import { Route as genAbsolute10RouteImport } from './routes/(gen)/absolute10' +import { Route as genAbsolute1RouteImport } from './routes/(gen)/absolute1' +import { Route as genAbsolute0RouteImport } from './routes/(gen)/absolute0' +import { Route as genSearchRouteRouteImport } from './routes/(gen)/search/route' +import { Route as genParamsRouteRouteImport } from './routes/(gen)/params/route' +import { Route as genSearchSearch99RouteImport } from './routes/(gen)/search/search99' +import { Route as genSearchSearch98RouteImport } from './routes/(gen)/search/search98' +import { Route as genSearchSearch97RouteImport } from './routes/(gen)/search/search97' +import { Route as genSearchSearch96RouteImport } from './routes/(gen)/search/search96' +import { Route as genSearchSearch95RouteImport } from './routes/(gen)/search/search95' +import { Route as genSearchSearch94RouteImport } from './routes/(gen)/search/search94' +import { Route as genSearchSearch93RouteImport } from './routes/(gen)/search/search93' +import { Route as genSearchSearch92RouteImport } from './routes/(gen)/search/search92' +import { Route as genSearchSearch91RouteImport } from './routes/(gen)/search/search91' +import { Route as genSearchSearch90RouteImport } from './routes/(gen)/search/search90' +import { Route as genSearchSearch9RouteImport } from './routes/(gen)/search/search9' +import { Route as genSearchSearch89RouteImport } from './routes/(gen)/search/search89' +import { Route as genSearchSearch88RouteImport } from './routes/(gen)/search/search88' +import { Route as genSearchSearch87RouteImport } from './routes/(gen)/search/search87' +import { Route as genSearchSearch86RouteImport } from './routes/(gen)/search/search86' +import { Route as genSearchSearch85RouteImport } from './routes/(gen)/search/search85' +import { Route as genSearchSearch84RouteImport } from './routes/(gen)/search/search84' +import { Route as genSearchSearch83RouteImport } from './routes/(gen)/search/search83' +import { Route as genSearchSearch82RouteImport } from './routes/(gen)/search/search82' +import { Route as genSearchSearch81RouteImport } from './routes/(gen)/search/search81' +import { Route as genSearchSearch80RouteImport } from './routes/(gen)/search/search80' +import { Route as genSearchSearch8RouteImport } from './routes/(gen)/search/search8' +import { Route as genSearchSearch79RouteImport } from './routes/(gen)/search/search79' +import { Route as genSearchSearch78RouteImport } from './routes/(gen)/search/search78' +import { Route as genSearchSearch77RouteImport } from './routes/(gen)/search/search77' +import { Route as genSearchSearch76RouteImport } from './routes/(gen)/search/search76' +import { Route as genSearchSearch75RouteImport } from './routes/(gen)/search/search75' +import { Route as genSearchSearch74RouteImport } from './routes/(gen)/search/search74' +import { Route as genSearchSearch73RouteImport } from './routes/(gen)/search/search73' +import { Route as genSearchSearch72RouteImport } from './routes/(gen)/search/search72' +import { Route as genSearchSearch71RouteImport } from './routes/(gen)/search/search71' +import { Route as genSearchSearch70RouteImport } from './routes/(gen)/search/search70' +import { Route as genSearchSearch7RouteImport } from './routes/(gen)/search/search7' +import { Route as genSearchSearch69RouteImport } from './routes/(gen)/search/search69' +import { Route as genSearchSearch68RouteImport } from './routes/(gen)/search/search68' +import { Route as genSearchSearch67RouteImport } from './routes/(gen)/search/search67' +import { Route as genSearchSearch66RouteImport } from './routes/(gen)/search/search66' +import { Route as genSearchSearch65RouteImport } from './routes/(gen)/search/search65' +import { Route as genSearchSearch64RouteImport } from './routes/(gen)/search/search64' +import { Route as genSearchSearch63RouteImport } from './routes/(gen)/search/search63' +import { Route as genSearchSearch62RouteImport } from './routes/(gen)/search/search62' +import { Route as genSearchSearch61RouteImport } from './routes/(gen)/search/search61' +import { Route as genSearchSearch60RouteImport } from './routes/(gen)/search/search60' +import { Route as genSearchSearch6RouteImport } from './routes/(gen)/search/search6' +import { Route as genSearchSearch59RouteImport } from './routes/(gen)/search/search59' +import { Route as genSearchSearch58RouteImport } from './routes/(gen)/search/search58' +import { Route as genSearchSearch57RouteImport } from './routes/(gen)/search/search57' +import { Route as genSearchSearch56RouteImport } from './routes/(gen)/search/search56' +import { Route as genSearchSearch55RouteImport } from './routes/(gen)/search/search55' +import { Route as genSearchSearch54RouteImport } from './routes/(gen)/search/search54' +import { Route as genSearchSearch53RouteImport } from './routes/(gen)/search/search53' +import { Route as genSearchSearch52RouteImport } from './routes/(gen)/search/search52' +import { Route as genSearchSearch51RouteImport } from './routes/(gen)/search/search51' +import { Route as genSearchSearch50RouteImport } from './routes/(gen)/search/search50' +import { Route as genSearchSearch5RouteImport } from './routes/(gen)/search/search5' +import { Route as genSearchSearch49RouteImport } from './routes/(gen)/search/search49' +import { Route as genSearchSearch48RouteImport } from './routes/(gen)/search/search48' +import { Route as genSearchSearch47RouteImport } from './routes/(gen)/search/search47' +import { Route as genSearchSearch46RouteImport } from './routes/(gen)/search/search46' +import { Route as genSearchSearch45RouteImport } from './routes/(gen)/search/search45' +import { Route as genSearchSearch44RouteImport } from './routes/(gen)/search/search44' +import { Route as genSearchSearch43RouteImport } from './routes/(gen)/search/search43' +import { Route as genSearchSearch42RouteImport } from './routes/(gen)/search/search42' +import { Route as genSearchSearch41RouteImport } from './routes/(gen)/search/search41' +import { Route as genSearchSearch40RouteImport } from './routes/(gen)/search/search40' +import { Route as genSearchSearch4RouteImport } from './routes/(gen)/search/search4' +import { Route as genSearchSearch39RouteImport } from './routes/(gen)/search/search39' +import { Route as genSearchSearch38RouteImport } from './routes/(gen)/search/search38' +import { Route as genSearchSearch37RouteImport } from './routes/(gen)/search/search37' +import { Route as genSearchSearch36RouteImport } from './routes/(gen)/search/search36' +import { Route as genSearchSearch35RouteImport } from './routes/(gen)/search/search35' +import { Route as genSearchSearch34RouteImport } from './routes/(gen)/search/search34' +import { Route as genSearchSearch33RouteImport } from './routes/(gen)/search/search33' +import { Route as genSearchSearch32RouteImport } from './routes/(gen)/search/search32' +import { Route as genSearchSearch31RouteImport } from './routes/(gen)/search/search31' +import { Route as genSearchSearch30RouteImport } from './routes/(gen)/search/search30' +import { Route as genSearchSearch3RouteImport } from './routes/(gen)/search/search3' +import { Route as genSearchSearch29RouteImport } from './routes/(gen)/search/search29' +import { Route as genSearchSearch28RouteImport } from './routes/(gen)/search/search28' +import { Route as genSearchSearch27RouteImport } from './routes/(gen)/search/search27' +import { Route as genSearchSearch26RouteImport } from './routes/(gen)/search/search26' +import { Route as genSearchSearch25RouteImport } from './routes/(gen)/search/search25' +import { Route as genSearchSearch24RouteImport } from './routes/(gen)/search/search24' +import { Route as genSearchSearch23RouteImport } from './routes/(gen)/search/search23' +import { Route as genSearchSearch22RouteImport } from './routes/(gen)/search/search22' +import { Route as genSearchSearch21RouteImport } from './routes/(gen)/search/search21' +import { Route as genSearchSearch20RouteImport } from './routes/(gen)/search/search20' +import { Route as genSearchSearch2RouteImport } from './routes/(gen)/search/search2' +import { Route as genSearchSearch19RouteImport } from './routes/(gen)/search/search19' +import { Route as genSearchSearch18RouteImport } from './routes/(gen)/search/search18' +import { Route as genSearchSearch17RouteImport } from './routes/(gen)/search/search17' +import { Route as genSearchSearch16RouteImport } from './routes/(gen)/search/search16' +import { Route as genSearchSearch15RouteImport } from './routes/(gen)/search/search15' +import { Route as genSearchSearch14RouteImport } from './routes/(gen)/search/search14' +import { Route as genSearchSearch13RouteImport } from './routes/(gen)/search/search13' +import { Route as genSearchSearch12RouteImport } from './routes/(gen)/search/search12' +import { Route as genSearchSearch11RouteImport } from './routes/(gen)/search/search11' +import { Route as genSearchSearch10RouteImport } from './routes/(gen)/search/search10' +import { Route as genSearchSearch1RouteImport } from './routes/(gen)/search/search1' +import { Route as genSearchSearch0RouteImport } from './routes/(gen)/search/search0' +import { Route as genParamsParam99RouteImport } from './routes/(gen)/params/$param99' +import { Route as genParamsParam98RouteImport } from './routes/(gen)/params/$param98' +import { Route as genParamsParam97RouteImport } from './routes/(gen)/params/$param97' +import { Route as genParamsParam96RouteImport } from './routes/(gen)/params/$param96' +import { Route as genParamsParam95RouteImport } from './routes/(gen)/params/$param95' +import { Route as genParamsParam94RouteImport } from './routes/(gen)/params/$param94' +import { Route as genParamsParam93RouteImport } from './routes/(gen)/params/$param93' +import { Route as genParamsParam92RouteImport } from './routes/(gen)/params/$param92' +import { Route as genParamsParam91RouteImport } from './routes/(gen)/params/$param91' +import { Route as genParamsParam90RouteImport } from './routes/(gen)/params/$param90' +import { Route as genParamsParam9RouteImport } from './routes/(gen)/params/$param9' +import { Route as genParamsParam89RouteImport } from './routes/(gen)/params/$param89' +import { Route as genParamsParam88RouteImport } from './routes/(gen)/params/$param88' +import { Route as genParamsParam87RouteImport } from './routes/(gen)/params/$param87' +import { Route as genParamsParam86RouteImport } from './routes/(gen)/params/$param86' +import { Route as genParamsParam85RouteImport } from './routes/(gen)/params/$param85' +import { Route as genParamsParam84RouteImport } from './routes/(gen)/params/$param84' +import { Route as genParamsParam83RouteImport } from './routes/(gen)/params/$param83' +import { Route as genParamsParam82RouteImport } from './routes/(gen)/params/$param82' +import { Route as genParamsParam81RouteImport } from './routes/(gen)/params/$param81' +import { Route as genParamsParam80RouteImport } from './routes/(gen)/params/$param80' +import { Route as genParamsParam8RouteImport } from './routes/(gen)/params/$param8' +import { Route as genParamsParam79RouteImport } from './routes/(gen)/params/$param79' +import { Route as genParamsParam78RouteImport } from './routes/(gen)/params/$param78' +import { Route as genParamsParam77RouteImport } from './routes/(gen)/params/$param77' +import { Route as genParamsParam76RouteImport } from './routes/(gen)/params/$param76' +import { Route as genParamsParam75RouteImport } from './routes/(gen)/params/$param75' +import { Route as genParamsParam74RouteImport } from './routes/(gen)/params/$param74' +import { Route as genParamsParam73RouteImport } from './routes/(gen)/params/$param73' +import { Route as genParamsParam72RouteImport } from './routes/(gen)/params/$param72' +import { Route as genParamsParam71RouteImport } from './routes/(gen)/params/$param71' +import { Route as genParamsParam70RouteImport } from './routes/(gen)/params/$param70' +import { Route as genParamsParam7RouteImport } from './routes/(gen)/params/$param7' +import { Route as genParamsParam69RouteImport } from './routes/(gen)/params/$param69' +import { Route as genParamsParam68RouteImport } from './routes/(gen)/params/$param68' +import { Route as genParamsParam67RouteImport } from './routes/(gen)/params/$param67' +import { Route as genParamsParam66RouteImport } from './routes/(gen)/params/$param66' +import { Route as genParamsParam65RouteImport } from './routes/(gen)/params/$param65' +import { Route as genParamsParam64RouteImport } from './routes/(gen)/params/$param64' +import { Route as genParamsParam63RouteImport } from './routes/(gen)/params/$param63' +import { Route as genParamsParam62RouteImport } from './routes/(gen)/params/$param62' +import { Route as genParamsParam61RouteImport } from './routes/(gen)/params/$param61' +import { Route as genParamsParam60RouteImport } from './routes/(gen)/params/$param60' +import { Route as genParamsParam6RouteImport } from './routes/(gen)/params/$param6' +import { Route as genParamsParam59RouteImport } from './routes/(gen)/params/$param59' +import { Route as genParamsParam58RouteImport } from './routes/(gen)/params/$param58' +import { Route as genParamsParam57RouteImport } from './routes/(gen)/params/$param57' +import { Route as genParamsParam56RouteImport } from './routes/(gen)/params/$param56' +import { Route as genParamsParam55RouteImport } from './routes/(gen)/params/$param55' +import { Route as genParamsParam54RouteImport } from './routes/(gen)/params/$param54' +import { Route as genParamsParam53RouteImport } from './routes/(gen)/params/$param53' +import { Route as genParamsParam52RouteImport } from './routes/(gen)/params/$param52' +import { Route as genParamsParam51RouteImport } from './routes/(gen)/params/$param51' +import { Route as genParamsParam50RouteImport } from './routes/(gen)/params/$param50' +import { Route as genParamsParam5RouteImport } from './routes/(gen)/params/$param5' +import { Route as genParamsParam49RouteImport } from './routes/(gen)/params/$param49' +import { Route as genParamsParam48RouteImport } from './routes/(gen)/params/$param48' +import { Route as genParamsParam47RouteImport } from './routes/(gen)/params/$param47' +import { Route as genParamsParam46RouteImport } from './routes/(gen)/params/$param46' +import { Route as genParamsParam45RouteImport } from './routes/(gen)/params/$param45' +import { Route as genParamsParam44RouteImport } from './routes/(gen)/params/$param44' +import { Route as genParamsParam43RouteImport } from './routes/(gen)/params/$param43' +import { Route as genParamsParam42RouteImport } from './routes/(gen)/params/$param42' +import { Route as genParamsParam41RouteImport } from './routes/(gen)/params/$param41' +import { Route as genParamsParam40RouteImport } from './routes/(gen)/params/$param40' +import { Route as genParamsParam4RouteImport } from './routes/(gen)/params/$param4' +import { Route as genParamsParam39RouteImport } from './routes/(gen)/params/$param39' +import { Route as genParamsParam38RouteImport } from './routes/(gen)/params/$param38' +import { Route as genParamsParam37RouteImport } from './routes/(gen)/params/$param37' +import { Route as genParamsParam36RouteImport } from './routes/(gen)/params/$param36' +import { Route as genParamsParam35RouteImport } from './routes/(gen)/params/$param35' +import { Route as genParamsParam34RouteImport } from './routes/(gen)/params/$param34' +import { Route as genParamsParam33RouteImport } from './routes/(gen)/params/$param33' +import { Route as genParamsParam32RouteImport } from './routes/(gen)/params/$param32' +import { Route as genParamsParam31RouteImport } from './routes/(gen)/params/$param31' +import { Route as genParamsParam30RouteImport } from './routes/(gen)/params/$param30' +import { Route as genParamsParam3RouteImport } from './routes/(gen)/params/$param3' +import { Route as genParamsParam29RouteImport } from './routes/(gen)/params/$param29' +import { Route as genParamsParam28RouteImport } from './routes/(gen)/params/$param28' +import { Route as genParamsParam27RouteImport } from './routes/(gen)/params/$param27' +import { Route as genParamsParam26RouteImport } from './routes/(gen)/params/$param26' +import { Route as genParamsParam25RouteImport } from './routes/(gen)/params/$param25' +import { Route as genParamsParam24RouteImport } from './routes/(gen)/params/$param24' +import { Route as genParamsParam23RouteImport } from './routes/(gen)/params/$param23' +import { Route as genParamsParam22RouteImport } from './routes/(gen)/params/$param22' +import { Route as genParamsParam21RouteImport } from './routes/(gen)/params/$param21' +import { Route as genParamsParam20RouteImport } from './routes/(gen)/params/$param20' +import { Route as genParamsParam2RouteImport } from './routes/(gen)/params/$param2' +import { Route as genParamsParam19RouteImport } from './routes/(gen)/params/$param19' +import { Route as genParamsParam18RouteImport } from './routes/(gen)/params/$param18' +import { Route as genParamsParam17RouteImport } from './routes/(gen)/params/$param17' +import { Route as genParamsParam16RouteImport } from './routes/(gen)/params/$param16' +import { Route as genParamsParam15RouteImport } from './routes/(gen)/params/$param15' +import { Route as genParamsParam14RouteImport } from './routes/(gen)/params/$param14' +import { Route as genParamsParam13RouteImport } from './routes/(gen)/params/$param13' +import { Route as genParamsParam12RouteImport } from './routes/(gen)/params/$param12' +import { Route as genParamsParam11RouteImport } from './routes/(gen)/params/$param11' +import { Route as genParamsParam10RouteImport } from './routes/(gen)/params/$param10' +import { Route as genParamsParam1RouteImport } from './routes/(gen)/params/$param1' +import { Route as genParamsParam0RouteImport } from './routes/(gen)/params/$param0' const RelativeRoute = RelativeRouteImport.update({ id: '/relative', @@ -58,26 +460,2836 @@ const ParamsParamsPlaceholderRoute = ParamsParamsPlaceholderRouteImport.update({ path: '/$paramsPlaceholder', getParentRoute: () => ParamsRouteRoute, } as any) +const genRelative99Route = genRelative99RouteImport.update({ + id: '/(gen)/relative99', + path: '/relative99', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative98Route = genRelative98RouteImport.update({ + id: '/(gen)/relative98', + path: '/relative98', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative97Route = genRelative97RouteImport.update({ + id: '/(gen)/relative97', + path: '/relative97', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative96Route = genRelative96RouteImport.update({ + id: '/(gen)/relative96', + path: '/relative96', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative95Route = genRelative95RouteImport.update({ + id: '/(gen)/relative95', + path: '/relative95', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative94Route = genRelative94RouteImport.update({ + id: '/(gen)/relative94', + path: '/relative94', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative93Route = genRelative93RouteImport.update({ + id: '/(gen)/relative93', + path: '/relative93', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative92Route = genRelative92RouteImport.update({ + id: '/(gen)/relative92', + path: '/relative92', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative91Route = genRelative91RouteImport.update({ + id: '/(gen)/relative91', + path: '/relative91', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative90Route = genRelative90RouteImport.update({ + id: '/(gen)/relative90', + path: '/relative90', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative9Route = genRelative9RouteImport.update({ + id: '/(gen)/relative9', + path: '/relative9', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative89Route = genRelative89RouteImport.update({ + id: '/(gen)/relative89', + path: '/relative89', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative88Route = genRelative88RouteImport.update({ + id: '/(gen)/relative88', + path: '/relative88', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative87Route = genRelative87RouteImport.update({ + id: '/(gen)/relative87', + path: '/relative87', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative86Route = genRelative86RouteImport.update({ + id: '/(gen)/relative86', + path: '/relative86', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative85Route = genRelative85RouteImport.update({ + id: '/(gen)/relative85', + path: '/relative85', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative84Route = genRelative84RouteImport.update({ + id: '/(gen)/relative84', + path: '/relative84', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative83Route = genRelative83RouteImport.update({ + id: '/(gen)/relative83', + path: '/relative83', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative82Route = genRelative82RouteImport.update({ + id: '/(gen)/relative82', + path: '/relative82', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative81Route = genRelative81RouteImport.update({ + id: '/(gen)/relative81', + path: '/relative81', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative80Route = genRelative80RouteImport.update({ + id: '/(gen)/relative80', + path: '/relative80', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative8Route = genRelative8RouteImport.update({ + id: '/(gen)/relative8', + path: '/relative8', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative79Route = genRelative79RouteImport.update({ + id: '/(gen)/relative79', + path: '/relative79', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative78Route = genRelative78RouteImport.update({ + id: '/(gen)/relative78', + path: '/relative78', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative77Route = genRelative77RouteImport.update({ + id: '/(gen)/relative77', + path: '/relative77', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative76Route = genRelative76RouteImport.update({ + id: '/(gen)/relative76', + path: '/relative76', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative75Route = genRelative75RouteImport.update({ + id: '/(gen)/relative75', + path: '/relative75', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative74Route = genRelative74RouteImport.update({ + id: '/(gen)/relative74', + path: '/relative74', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative73Route = genRelative73RouteImport.update({ + id: '/(gen)/relative73', + path: '/relative73', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative72Route = genRelative72RouteImport.update({ + id: '/(gen)/relative72', + path: '/relative72', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative71Route = genRelative71RouteImport.update({ + id: '/(gen)/relative71', + path: '/relative71', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative70Route = genRelative70RouteImport.update({ + id: '/(gen)/relative70', + path: '/relative70', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative7Route = genRelative7RouteImport.update({ + id: '/(gen)/relative7', + path: '/relative7', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative69Route = genRelative69RouteImport.update({ + id: '/(gen)/relative69', + path: '/relative69', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative68Route = genRelative68RouteImport.update({ + id: '/(gen)/relative68', + path: '/relative68', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative67Route = genRelative67RouteImport.update({ + id: '/(gen)/relative67', + path: '/relative67', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative66Route = genRelative66RouteImport.update({ + id: '/(gen)/relative66', + path: '/relative66', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative65Route = genRelative65RouteImport.update({ + id: '/(gen)/relative65', + path: '/relative65', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative64Route = genRelative64RouteImport.update({ + id: '/(gen)/relative64', + path: '/relative64', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative63Route = genRelative63RouteImport.update({ + id: '/(gen)/relative63', + path: '/relative63', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative62Route = genRelative62RouteImport.update({ + id: '/(gen)/relative62', + path: '/relative62', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative61Route = genRelative61RouteImport.update({ + id: '/(gen)/relative61', + path: '/relative61', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative60Route = genRelative60RouteImport.update({ + id: '/(gen)/relative60', + path: '/relative60', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative6Route = genRelative6RouteImport.update({ + id: '/(gen)/relative6', + path: '/relative6', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative59Route = genRelative59RouteImport.update({ + id: '/(gen)/relative59', + path: '/relative59', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative58Route = genRelative58RouteImport.update({ + id: '/(gen)/relative58', + path: '/relative58', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative57Route = genRelative57RouteImport.update({ + id: '/(gen)/relative57', + path: '/relative57', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative56Route = genRelative56RouteImport.update({ + id: '/(gen)/relative56', + path: '/relative56', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative55Route = genRelative55RouteImport.update({ + id: '/(gen)/relative55', + path: '/relative55', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative54Route = genRelative54RouteImport.update({ + id: '/(gen)/relative54', + path: '/relative54', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative53Route = genRelative53RouteImport.update({ + id: '/(gen)/relative53', + path: '/relative53', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative52Route = genRelative52RouteImport.update({ + id: '/(gen)/relative52', + path: '/relative52', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative51Route = genRelative51RouteImport.update({ + id: '/(gen)/relative51', + path: '/relative51', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative50Route = genRelative50RouteImport.update({ + id: '/(gen)/relative50', + path: '/relative50', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative5Route = genRelative5RouteImport.update({ + id: '/(gen)/relative5', + path: '/relative5', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative49Route = genRelative49RouteImport.update({ + id: '/(gen)/relative49', + path: '/relative49', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative48Route = genRelative48RouteImport.update({ + id: '/(gen)/relative48', + path: '/relative48', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative47Route = genRelative47RouteImport.update({ + id: '/(gen)/relative47', + path: '/relative47', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative46Route = genRelative46RouteImport.update({ + id: '/(gen)/relative46', + path: '/relative46', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative45Route = genRelative45RouteImport.update({ + id: '/(gen)/relative45', + path: '/relative45', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative44Route = genRelative44RouteImport.update({ + id: '/(gen)/relative44', + path: '/relative44', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative43Route = genRelative43RouteImport.update({ + id: '/(gen)/relative43', + path: '/relative43', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative42Route = genRelative42RouteImport.update({ + id: '/(gen)/relative42', + path: '/relative42', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative41Route = genRelative41RouteImport.update({ + id: '/(gen)/relative41', + path: '/relative41', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative40Route = genRelative40RouteImport.update({ + id: '/(gen)/relative40', + path: '/relative40', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative4Route = genRelative4RouteImport.update({ + id: '/(gen)/relative4', + path: '/relative4', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative39Route = genRelative39RouteImport.update({ + id: '/(gen)/relative39', + path: '/relative39', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative38Route = genRelative38RouteImport.update({ + id: '/(gen)/relative38', + path: '/relative38', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative37Route = genRelative37RouteImport.update({ + id: '/(gen)/relative37', + path: '/relative37', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative36Route = genRelative36RouteImport.update({ + id: '/(gen)/relative36', + path: '/relative36', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative35Route = genRelative35RouteImport.update({ + id: '/(gen)/relative35', + path: '/relative35', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative34Route = genRelative34RouteImport.update({ + id: '/(gen)/relative34', + path: '/relative34', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative33Route = genRelative33RouteImport.update({ + id: '/(gen)/relative33', + path: '/relative33', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative32Route = genRelative32RouteImport.update({ + id: '/(gen)/relative32', + path: '/relative32', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative31Route = genRelative31RouteImport.update({ + id: '/(gen)/relative31', + path: '/relative31', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative30Route = genRelative30RouteImport.update({ + id: '/(gen)/relative30', + path: '/relative30', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative3Route = genRelative3RouteImport.update({ + id: '/(gen)/relative3', + path: '/relative3', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative29Route = genRelative29RouteImport.update({ + id: '/(gen)/relative29', + path: '/relative29', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative28Route = genRelative28RouteImport.update({ + id: '/(gen)/relative28', + path: '/relative28', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative27Route = genRelative27RouteImport.update({ + id: '/(gen)/relative27', + path: '/relative27', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative26Route = genRelative26RouteImport.update({ + id: '/(gen)/relative26', + path: '/relative26', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative25Route = genRelative25RouteImport.update({ + id: '/(gen)/relative25', + path: '/relative25', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative24Route = genRelative24RouteImport.update({ + id: '/(gen)/relative24', + path: '/relative24', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative23Route = genRelative23RouteImport.update({ + id: '/(gen)/relative23', + path: '/relative23', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative22Route = genRelative22RouteImport.update({ + id: '/(gen)/relative22', + path: '/relative22', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative21Route = genRelative21RouteImport.update({ + id: '/(gen)/relative21', + path: '/relative21', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative20Route = genRelative20RouteImport.update({ + id: '/(gen)/relative20', + path: '/relative20', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative2Route = genRelative2RouteImport.update({ + id: '/(gen)/relative2', + path: '/relative2', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative19Route = genRelative19RouteImport.update({ + id: '/(gen)/relative19', + path: '/relative19', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative18Route = genRelative18RouteImport.update({ + id: '/(gen)/relative18', + path: '/relative18', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative17Route = genRelative17RouteImport.update({ + id: '/(gen)/relative17', + path: '/relative17', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative16Route = genRelative16RouteImport.update({ + id: '/(gen)/relative16', + path: '/relative16', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative15Route = genRelative15RouteImport.update({ + id: '/(gen)/relative15', + path: '/relative15', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative14Route = genRelative14RouteImport.update({ + id: '/(gen)/relative14', + path: '/relative14', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative13Route = genRelative13RouteImport.update({ + id: '/(gen)/relative13', + path: '/relative13', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative12Route = genRelative12RouteImport.update({ + id: '/(gen)/relative12', + path: '/relative12', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative11Route = genRelative11RouteImport.update({ + id: '/(gen)/relative11', + path: '/relative11', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative10Route = genRelative10RouteImport.update({ + id: '/(gen)/relative10', + path: '/relative10', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative1Route = genRelative1RouteImport.update({ + id: '/(gen)/relative1', + path: '/relative1', + getParentRoute: () => rootRouteImport, +} as any) +const genRelative0Route = genRelative0RouteImport.update({ + id: '/(gen)/relative0', + path: '/relative0', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute99Route = genAbsolute99RouteImport.update({ + id: '/(gen)/absolute99', + path: '/absolute99', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute98Route = genAbsolute98RouteImport.update({ + id: '/(gen)/absolute98', + path: '/absolute98', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute97Route = genAbsolute97RouteImport.update({ + id: '/(gen)/absolute97', + path: '/absolute97', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute96Route = genAbsolute96RouteImport.update({ + id: '/(gen)/absolute96', + path: '/absolute96', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute95Route = genAbsolute95RouteImport.update({ + id: '/(gen)/absolute95', + path: '/absolute95', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute94Route = genAbsolute94RouteImport.update({ + id: '/(gen)/absolute94', + path: '/absolute94', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute93Route = genAbsolute93RouteImport.update({ + id: '/(gen)/absolute93', + path: '/absolute93', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute92Route = genAbsolute92RouteImport.update({ + id: '/(gen)/absolute92', + path: '/absolute92', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute91Route = genAbsolute91RouteImport.update({ + id: '/(gen)/absolute91', + path: '/absolute91', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute90Route = genAbsolute90RouteImport.update({ + id: '/(gen)/absolute90', + path: '/absolute90', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute9Route = genAbsolute9RouteImport.update({ + id: '/(gen)/absolute9', + path: '/absolute9', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute89Route = genAbsolute89RouteImport.update({ + id: '/(gen)/absolute89', + path: '/absolute89', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute88Route = genAbsolute88RouteImport.update({ + id: '/(gen)/absolute88', + path: '/absolute88', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute87Route = genAbsolute87RouteImport.update({ + id: '/(gen)/absolute87', + path: '/absolute87', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute86Route = genAbsolute86RouteImport.update({ + id: '/(gen)/absolute86', + path: '/absolute86', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute85Route = genAbsolute85RouteImport.update({ + id: '/(gen)/absolute85', + path: '/absolute85', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute84Route = genAbsolute84RouteImport.update({ + id: '/(gen)/absolute84', + path: '/absolute84', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute83Route = genAbsolute83RouteImport.update({ + id: '/(gen)/absolute83', + path: '/absolute83', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute82Route = genAbsolute82RouteImport.update({ + id: '/(gen)/absolute82', + path: '/absolute82', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute81Route = genAbsolute81RouteImport.update({ + id: '/(gen)/absolute81', + path: '/absolute81', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute80Route = genAbsolute80RouteImport.update({ + id: '/(gen)/absolute80', + path: '/absolute80', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute8Route = genAbsolute8RouteImport.update({ + id: '/(gen)/absolute8', + path: '/absolute8', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute79Route = genAbsolute79RouteImport.update({ + id: '/(gen)/absolute79', + path: '/absolute79', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute78Route = genAbsolute78RouteImport.update({ + id: '/(gen)/absolute78', + path: '/absolute78', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute77Route = genAbsolute77RouteImport.update({ + id: '/(gen)/absolute77', + path: '/absolute77', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute76Route = genAbsolute76RouteImport.update({ + id: '/(gen)/absolute76', + path: '/absolute76', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute75Route = genAbsolute75RouteImport.update({ + id: '/(gen)/absolute75', + path: '/absolute75', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute74Route = genAbsolute74RouteImport.update({ + id: '/(gen)/absolute74', + path: '/absolute74', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute73Route = genAbsolute73RouteImport.update({ + id: '/(gen)/absolute73', + path: '/absolute73', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute72Route = genAbsolute72RouteImport.update({ + id: '/(gen)/absolute72', + path: '/absolute72', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute71Route = genAbsolute71RouteImport.update({ + id: '/(gen)/absolute71', + path: '/absolute71', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute70Route = genAbsolute70RouteImport.update({ + id: '/(gen)/absolute70', + path: '/absolute70', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute7Route = genAbsolute7RouteImport.update({ + id: '/(gen)/absolute7', + path: '/absolute7', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute69Route = genAbsolute69RouteImport.update({ + id: '/(gen)/absolute69', + path: '/absolute69', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute68Route = genAbsolute68RouteImport.update({ + id: '/(gen)/absolute68', + path: '/absolute68', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute67Route = genAbsolute67RouteImport.update({ + id: '/(gen)/absolute67', + path: '/absolute67', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute66Route = genAbsolute66RouteImport.update({ + id: '/(gen)/absolute66', + path: '/absolute66', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute65Route = genAbsolute65RouteImport.update({ + id: '/(gen)/absolute65', + path: '/absolute65', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute64Route = genAbsolute64RouteImport.update({ + id: '/(gen)/absolute64', + path: '/absolute64', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute63Route = genAbsolute63RouteImport.update({ + id: '/(gen)/absolute63', + path: '/absolute63', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute62Route = genAbsolute62RouteImport.update({ + id: '/(gen)/absolute62', + path: '/absolute62', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute61Route = genAbsolute61RouteImport.update({ + id: '/(gen)/absolute61', + path: '/absolute61', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute60Route = genAbsolute60RouteImport.update({ + id: '/(gen)/absolute60', + path: '/absolute60', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute6Route = genAbsolute6RouteImport.update({ + id: '/(gen)/absolute6', + path: '/absolute6', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute59Route = genAbsolute59RouteImport.update({ + id: '/(gen)/absolute59', + path: '/absolute59', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute58Route = genAbsolute58RouteImport.update({ + id: '/(gen)/absolute58', + path: '/absolute58', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute57Route = genAbsolute57RouteImport.update({ + id: '/(gen)/absolute57', + path: '/absolute57', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute56Route = genAbsolute56RouteImport.update({ + id: '/(gen)/absolute56', + path: '/absolute56', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute55Route = genAbsolute55RouteImport.update({ + id: '/(gen)/absolute55', + path: '/absolute55', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute54Route = genAbsolute54RouteImport.update({ + id: '/(gen)/absolute54', + path: '/absolute54', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute53Route = genAbsolute53RouteImport.update({ + id: '/(gen)/absolute53', + path: '/absolute53', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute52Route = genAbsolute52RouteImport.update({ + id: '/(gen)/absolute52', + path: '/absolute52', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute51Route = genAbsolute51RouteImport.update({ + id: '/(gen)/absolute51', + path: '/absolute51', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute50Route = genAbsolute50RouteImport.update({ + id: '/(gen)/absolute50', + path: '/absolute50', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute5Route = genAbsolute5RouteImport.update({ + id: '/(gen)/absolute5', + path: '/absolute5', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute49Route = genAbsolute49RouteImport.update({ + id: '/(gen)/absolute49', + path: '/absolute49', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute48Route = genAbsolute48RouteImport.update({ + id: '/(gen)/absolute48', + path: '/absolute48', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute47Route = genAbsolute47RouteImport.update({ + id: '/(gen)/absolute47', + path: '/absolute47', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute46Route = genAbsolute46RouteImport.update({ + id: '/(gen)/absolute46', + path: '/absolute46', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute45Route = genAbsolute45RouteImport.update({ + id: '/(gen)/absolute45', + path: '/absolute45', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute44Route = genAbsolute44RouteImport.update({ + id: '/(gen)/absolute44', + path: '/absolute44', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute43Route = genAbsolute43RouteImport.update({ + id: '/(gen)/absolute43', + path: '/absolute43', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute42Route = genAbsolute42RouteImport.update({ + id: '/(gen)/absolute42', + path: '/absolute42', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute41Route = genAbsolute41RouteImport.update({ + id: '/(gen)/absolute41', + path: '/absolute41', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute40Route = genAbsolute40RouteImport.update({ + id: '/(gen)/absolute40', + path: '/absolute40', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute4Route = genAbsolute4RouteImport.update({ + id: '/(gen)/absolute4', + path: '/absolute4', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute39Route = genAbsolute39RouteImport.update({ + id: '/(gen)/absolute39', + path: '/absolute39', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute38Route = genAbsolute38RouteImport.update({ + id: '/(gen)/absolute38', + path: '/absolute38', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute37Route = genAbsolute37RouteImport.update({ + id: '/(gen)/absolute37', + path: '/absolute37', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute36Route = genAbsolute36RouteImport.update({ + id: '/(gen)/absolute36', + path: '/absolute36', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute35Route = genAbsolute35RouteImport.update({ + id: '/(gen)/absolute35', + path: '/absolute35', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute34Route = genAbsolute34RouteImport.update({ + id: '/(gen)/absolute34', + path: '/absolute34', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute33Route = genAbsolute33RouteImport.update({ + id: '/(gen)/absolute33', + path: '/absolute33', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute32Route = genAbsolute32RouteImport.update({ + id: '/(gen)/absolute32', + path: '/absolute32', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute31Route = genAbsolute31RouteImport.update({ + id: '/(gen)/absolute31', + path: '/absolute31', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute30Route = genAbsolute30RouteImport.update({ + id: '/(gen)/absolute30', + path: '/absolute30', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute3Route = genAbsolute3RouteImport.update({ + id: '/(gen)/absolute3', + path: '/absolute3', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute29Route = genAbsolute29RouteImport.update({ + id: '/(gen)/absolute29', + path: '/absolute29', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute28Route = genAbsolute28RouteImport.update({ + id: '/(gen)/absolute28', + path: '/absolute28', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute27Route = genAbsolute27RouteImport.update({ + id: '/(gen)/absolute27', + path: '/absolute27', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute26Route = genAbsolute26RouteImport.update({ + id: '/(gen)/absolute26', + path: '/absolute26', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute25Route = genAbsolute25RouteImport.update({ + id: '/(gen)/absolute25', + path: '/absolute25', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute24Route = genAbsolute24RouteImport.update({ + id: '/(gen)/absolute24', + path: '/absolute24', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute23Route = genAbsolute23RouteImport.update({ + id: '/(gen)/absolute23', + path: '/absolute23', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute22Route = genAbsolute22RouteImport.update({ + id: '/(gen)/absolute22', + path: '/absolute22', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute21Route = genAbsolute21RouteImport.update({ + id: '/(gen)/absolute21', + path: '/absolute21', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute20Route = genAbsolute20RouteImport.update({ + id: '/(gen)/absolute20', + path: '/absolute20', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute2Route = genAbsolute2RouteImport.update({ + id: '/(gen)/absolute2', + path: '/absolute2', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute19Route = genAbsolute19RouteImport.update({ + id: '/(gen)/absolute19', + path: '/absolute19', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute18Route = genAbsolute18RouteImport.update({ + id: '/(gen)/absolute18', + path: '/absolute18', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute17Route = genAbsolute17RouteImport.update({ + id: '/(gen)/absolute17', + path: '/absolute17', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute16Route = genAbsolute16RouteImport.update({ + id: '/(gen)/absolute16', + path: '/absolute16', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute15Route = genAbsolute15RouteImport.update({ + id: '/(gen)/absolute15', + path: '/absolute15', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute14Route = genAbsolute14RouteImport.update({ + id: '/(gen)/absolute14', + path: '/absolute14', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute13Route = genAbsolute13RouteImport.update({ + id: '/(gen)/absolute13', + path: '/absolute13', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute12Route = genAbsolute12RouteImport.update({ + id: '/(gen)/absolute12', + path: '/absolute12', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute11Route = genAbsolute11RouteImport.update({ + id: '/(gen)/absolute11', + path: '/absolute11', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute10Route = genAbsolute10RouteImport.update({ + id: '/(gen)/absolute10', + path: '/absolute10', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute1Route = genAbsolute1RouteImport.update({ + id: '/(gen)/absolute1', + path: '/absolute1', + getParentRoute: () => rootRouteImport, +} as any) +const genAbsolute0Route = genAbsolute0RouteImport.update({ + id: '/(gen)/absolute0', + path: '/absolute0', + getParentRoute: () => rootRouteImport, +} as any) +const genSearchRouteRoute = genSearchRouteRouteImport.update({ + id: '/(gen)/search', + path: '/search', + getParentRoute: () => rootRouteImport, +} as any) +const genParamsRouteRoute = genParamsRouteRouteImport.update({ + id: '/(gen)/params', + path: '/params', + getParentRoute: () => rootRouteImport, +} as any) +const genSearchSearch99Route = genSearchSearch99RouteImport.update({ + id: '/search99', + path: '/search99', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch98Route = genSearchSearch98RouteImport.update({ + id: '/search98', + path: '/search98', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch97Route = genSearchSearch97RouteImport.update({ + id: '/search97', + path: '/search97', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch96Route = genSearchSearch96RouteImport.update({ + id: '/search96', + path: '/search96', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch95Route = genSearchSearch95RouteImport.update({ + id: '/search95', + path: '/search95', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch94Route = genSearchSearch94RouteImport.update({ + id: '/search94', + path: '/search94', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch93Route = genSearchSearch93RouteImport.update({ + id: '/search93', + path: '/search93', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch92Route = genSearchSearch92RouteImport.update({ + id: '/search92', + path: '/search92', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch91Route = genSearchSearch91RouteImport.update({ + id: '/search91', + path: '/search91', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch90Route = genSearchSearch90RouteImport.update({ + id: '/search90', + path: '/search90', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch9Route = genSearchSearch9RouteImport.update({ + id: '/search9', + path: '/search9', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch89Route = genSearchSearch89RouteImport.update({ + id: '/search89', + path: '/search89', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch88Route = genSearchSearch88RouteImport.update({ + id: '/search88', + path: '/search88', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch87Route = genSearchSearch87RouteImport.update({ + id: '/search87', + path: '/search87', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch86Route = genSearchSearch86RouteImport.update({ + id: '/search86', + path: '/search86', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch85Route = genSearchSearch85RouteImport.update({ + id: '/search85', + path: '/search85', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch84Route = genSearchSearch84RouteImport.update({ + id: '/search84', + path: '/search84', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch83Route = genSearchSearch83RouteImport.update({ + id: '/search83', + path: '/search83', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch82Route = genSearchSearch82RouteImport.update({ + id: '/search82', + path: '/search82', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch81Route = genSearchSearch81RouteImport.update({ + id: '/search81', + path: '/search81', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch80Route = genSearchSearch80RouteImport.update({ + id: '/search80', + path: '/search80', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch8Route = genSearchSearch8RouteImport.update({ + id: '/search8', + path: '/search8', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch79Route = genSearchSearch79RouteImport.update({ + id: '/search79', + path: '/search79', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch78Route = genSearchSearch78RouteImport.update({ + id: '/search78', + path: '/search78', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch77Route = genSearchSearch77RouteImport.update({ + id: '/search77', + path: '/search77', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch76Route = genSearchSearch76RouteImport.update({ + id: '/search76', + path: '/search76', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch75Route = genSearchSearch75RouteImport.update({ + id: '/search75', + path: '/search75', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch74Route = genSearchSearch74RouteImport.update({ + id: '/search74', + path: '/search74', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch73Route = genSearchSearch73RouteImport.update({ + id: '/search73', + path: '/search73', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch72Route = genSearchSearch72RouteImport.update({ + id: '/search72', + path: '/search72', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch71Route = genSearchSearch71RouteImport.update({ + id: '/search71', + path: '/search71', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch70Route = genSearchSearch70RouteImport.update({ + id: '/search70', + path: '/search70', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch7Route = genSearchSearch7RouteImport.update({ + id: '/search7', + path: '/search7', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch69Route = genSearchSearch69RouteImport.update({ + id: '/search69', + path: '/search69', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch68Route = genSearchSearch68RouteImport.update({ + id: '/search68', + path: '/search68', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch67Route = genSearchSearch67RouteImport.update({ + id: '/search67', + path: '/search67', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch66Route = genSearchSearch66RouteImport.update({ + id: '/search66', + path: '/search66', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch65Route = genSearchSearch65RouteImport.update({ + id: '/search65', + path: '/search65', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch64Route = genSearchSearch64RouteImport.update({ + id: '/search64', + path: '/search64', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch63Route = genSearchSearch63RouteImport.update({ + id: '/search63', + path: '/search63', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch62Route = genSearchSearch62RouteImport.update({ + id: '/search62', + path: '/search62', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch61Route = genSearchSearch61RouteImport.update({ + id: '/search61', + path: '/search61', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch60Route = genSearchSearch60RouteImport.update({ + id: '/search60', + path: '/search60', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch6Route = genSearchSearch6RouteImport.update({ + id: '/search6', + path: '/search6', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch59Route = genSearchSearch59RouteImport.update({ + id: '/search59', + path: '/search59', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch58Route = genSearchSearch58RouteImport.update({ + id: '/search58', + path: '/search58', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch57Route = genSearchSearch57RouteImport.update({ + id: '/search57', + path: '/search57', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch56Route = genSearchSearch56RouteImport.update({ + id: '/search56', + path: '/search56', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch55Route = genSearchSearch55RouteImport.update({ + id: '/search55', + path: '/search55', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch54Route = genSearchSearch54RouteImport.update({ + id: '/search54', + path: '/search54', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch53Route = genSearchSearch53RouteImport.update({ + id: '/search53', + path: '/search53', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch52Route = genSearchSearch52RouteImport.update({ + id: '/search52', + path: '/search52', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch51Route = genSearchSearch51RouteImport.update({ + id: '/search51', + path: '/search51', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch50Route = genSearchSearch50RouteImport.update({ + id: '/search50', + path: '/search50', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch5Route = genSearchSearch5RouteImport.update({ + id: '/search5', + path: '/search5', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch49Route = genSearchSearch49RouteImport.update({ + id: '/search49', + path: '/search49', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch48Route = genSearchSearch48RouteImport.update({ + id: '/search48', + path: '/search48', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch47Route = genSearchSearch47RouteImport.update({ + id: '/search47', + path: '/search47', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch46Route = genSearchSearch46RouteImport.update({ + id: '/search46', + path: '/search46', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch45Route = genSearchSearch45RouteImport.update({ + id: '/search45', + path: '/search45', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch44Route = genSearchSearch44RouteImport.update({ + id: '/search44', + path: '/search44', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch43Route = genSearchSearch43RouteImport.update({ + id: '/search43', + path: '/search43', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch42Route = genSearchSearch42RouteImport.update({ + id: '/search42', + path: '/search42', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch41Route = genSearchSearch41RouteImport.update({ + id: '/search41', + path: '/search41', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch40Route = genSearchSearch40RouteImport.update({ + id: '/search40', + path: '/search40', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch4Route = genSearchSearch4RouteImport.update({ + id: '/search4', + path: '/search4', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch39Route = genSearchSearch39RouteImport.update({ + id: '/search39', + path: '/search39', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch38Route = genSearchSearch38RouteImport.update({ + id: '/search38', + path: '/search38', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch37Route = genSearchSearch37RouteImport.update({ + id: '/search37', + path: '/search37', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch36Route = genSearchSearch36RouteImport.update({ + id: '/search36', + path: '/search36', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch35Route = genSearchSearch35RouteImport.update({ + id: '/search35', + path: '/search35', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch34Route = genSearchSearch34RouteImport.update({ + id: '/search34', + path: '/search34', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch33Route = genSearchSearch33RouteImport.update({ + id: '/search33', + path: '/search33', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch32Route = genSearchSearch32RouteImport.update({ + id: '/search32', + path: '/search32', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch31Route = genSearchSearch31RouteImport.update({ + id: '/search31', + path: '/search31', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch30Route = genSearchSearch30RouteImport.update({ + id: '/search30', + path: '/search30', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch3Route = genSearchSearch3RouteImport.update({ + id: '/search3', + path: '/search3', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch29Route = genSearchSearch29RouteImport.update({ + id: '/search29', + path: '/search29', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch28Route = genSearchSearch28RouteImport.update({ + id: '/search28', + path: '/search28', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch27Route = genSearchSearch27RouteImport.update({ + id: '/search27', + path: '/search27', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch26Route = genSearchSearch26RouteImport.update({ + id: '/search26', + path: '/search26', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch25Route = genSearchSearch25RouteImport.update({ + id: '/search25', + path: '/search25', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch24Route = genSearchSearch24RouteImport.update({ + id: '/search24', + path: '/search24', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch23Route = genSearchSearch23RouteImport.update({ + id: '/search23', + path: '/search23', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch22Route = genSearchSearch22RouteImport.update({ + id: '/search22', + path: '/search22', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch21Route = genSearchSearch21RouteImport.update({ + id: '/search21', + path: '/search21', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch20Route = genSearchSearch20RouteImport.update({ + id: '/search20', + path: '/search20', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch2Route = genSearchSearch2RouteImport.update({ + id: '/search2', + path: '/search2', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch19Route = genSearchSearch19RouteImport.update({ + id: '/search19', + path: '/search19', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch18Route = genSearchSearch18RouteImport.update({ + id: '/search18', + path: '/search18', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch17Route = genSearchSearch17RouteImport.update({ + id: '/search17', + path: '/search17', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch16Route = genSearchSearch16RouteImport.update({ + id: '/search16', + path: '/search16', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch15Route = genSearchSearch15RouteImport.update({ + id: '/search15', + path: '/search15', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch14Route = genSearchSearch14RouteImport.update({ + id: '/search14', + path: '/search14', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch13Route = genSearchSearch13RouteImport.update({ + id: '/search13', + path: '/search13', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch12Route = genSearchSearch12RouteImport.update({ + id: '/search12', + path: '/search12', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch11Route = genSearchSearch11RouteImport.update({ + id: '/search11', + path: '/search11', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch10Route = genSearchSearch10RouteImport.update({ + id: '/search10', + path: '/search10', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch1Route = genSearchSearch1RouteImport.update({ + id: '/search1', + path: '/search1', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genSearchSearch0Route = genSearchSearch0RouteImport.update({ + id: '/search0', + path: '/search0', + getParentRoute: () => genSearchRouteRoute, +} as any) +const genParamsParam99Route = genParamsParam99RouteImport.update({ + id: '/$param99', + path: '/$param99', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam98Route = genParamsParam98RouteImport.update({ + id: '/$param98', + path: '/$param98', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam97Route = genParamsParam97RouteImport.update({ + id: '/$param97', + path: '/$param97', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam96Route = genParamsParam96RouteImport.update({ + id: '/$param96', + path: '/$param96', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam95Route = genParamsParam95RouteImport.update({ + id: '/$param95', + path: '/$param95', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam94Route = genParamsParam94RouteImport.update({ + id: '/$param94', + path: '/$param94', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam93Route = genParamsParam93RouteImport.update({ + id: '/$param93', + path: '/$param93', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam92Route = genParamsParam92RouteImport.update({ + id: '/$param92', + path: '/$param92', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam91Route = genParamsParam91RouteImport.update({ + id: '/$param91', + path: '/$param91', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam90Route = genParamsParam90RouteImport.update({ + id: '/$param90', + path: '/$param90', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam9Route = genParamsParam9RouteImport.update({ + id: '/$param9', + path: '/$param9', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam89Route = genParamsParam89RouteImport.update({ + id: '/$param89', + path: '/$param89', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam88Route = genParamsParam88RouteImport.update({ + id: '/$param88', + path: '/$param88', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam87Route = genParamsParam87RouteImport.update({ + id: '/$param87', + path: '/$param87', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam86Route = genParamsParam86RouteImport.update({ + id: '/$param86', + path: '/$param86', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam85Route = genParamsParam85RouteImport.update({ + id: '/$param85', + path: '/$param85', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam84Route = genParamsParam84RouteImport.update({ + id: '/$param84', + path: '/$param84', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam83Route = genParamsParam83RouteImport.update({ + id: '/$param83', + path: '/$param83', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam82Route = genParamsParam82RouteImport.update({ + id: '/$param82', + path: '/$param82', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam81Route = genParamsParam81RouteImport.update({ + id: '/$param81', + path: '/$param81', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam80Route = genParamsParam80RouteImport.update({ + id: '/$param80', + path: '/$param80', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam8Route = genParamsParam8RouteImport.update({ + id: '/$param8', + path: '/$param8', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam79Route = genParamsParam79RouteImport.update({ + id: '/$param79', + path: '/$param79', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam78Route = genParamsParam78RouteImport.update({ + id: '/$param78', + path: '/$param78', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam77Route = genParamsParam77RouteImport.update({ + id: '/$param77', + path: '/$param77', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam76Route = genParamsParam76RouteImport.update({ + id: '/$param76', + path: '/$param76', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam75Route = genParamsParam75RouteImport.update({ + id: '/$param75', + path: '/$param75', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam74Route = genParamsParam74RouteImport.update({ + id: '/$param74', + path: '/$param74', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam73Route = genParamsParam73RouteImport.update({ + id: '/$param73', + path: '/$param73', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam72Route = genParamsParam72RouteImport.update({ + id: '/$param72', + path: '/$param72', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam71Route = genParamsParam71RouteImport.update({ + id: '/$param71', + path: '/$param71', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam70Route = genParamsParam70RouteImport.update({ + id: '/$param70', + path: '/$param70', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam7Route = genParamsParam7RouteImport.update({ + id: '/$param7', + path: '/$param7', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam69Route = genParamsParam69RouteImport.update({ + id: '/$param69', + path: '/$param69', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam68Route = genParamsParam68RouteImport.update({ + id: '/$param68', + path: '/$param68', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam67Route = genParamsParam67RouteImport.update({ + id: '/$param67', + path: '/$param67', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam66Route = genParamsParam66RouteImport.update({ + id: '/$param66', + path: '/$param66', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam65Route = genParamsParam65RouteImport.update({ + id: '/$param65', + path: '/$param65', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam64Route = genParamsParam64RouteImport.update({ + id: '/$param64', + path: '/$param64', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam63Route = genParamsParam63RouteImport.update({ + id: '/$param63', + path: '/$param63', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam62Route = genParamsParam62RouteImport.update({ + id: '/$param62', + path: '/$param62', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam61Route = genParamsParam61RouteImport.update({ + id: '/$param61', + path: '/$param61', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam60Route = genParamsParam60RouteImport.update({ + id: '/$param60', + path: '/$param60', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam6Route = genParamsParam6RouteImport.update({ + id: '/$param6', + path: '/$param6', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam59Route = genParamsParam59RouteImport.update({ + id: '/$param59', + path: '/$param59', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam58Route = genParamsParam58RouteImport.update({ + id: '/$param58', + path: '/$param58', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam57Route = genParamsParam57RouteImport.update({ + id: '/$param57', + path: '/$param57', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam56Route = genParamsParam56RouteImport.update({ + id: '/$param56', + path: '/$param56', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam55Route = genParamsParam55RouteImport.update({ + id: '/$param55', + path: '/$param55', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam54Route = genParamsParam54RouteImport.update({ + id: '/$param54', + path: '/$param54', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam53Route = genParamsParam53RouteImport.update({ + id: '/$param53', + path: '/$param53', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam52Route = genParamsParam52RouteImport.update({ + id: '/$param52', + path: '/$param52', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam51Route = genParamsParam51RouteImport.update({ + id: '/$param51', + path: '/$param51', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam50Route = genParamsParam50RouteImport.update({ + id: '/$param50', + path: '/$param50', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam5Route = genParamsParam5RouteImport.update({ + id: '/$param5', + path: '/$param5', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam49Route = genParamsParam49RouteImport.update({ + id: '/$param49', + path: '/$param49', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam48Route = genParamsParam48RouteImport.update({ + id: '/$param48', + path: '/$param48', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam47Route = genParamsParam47RouteImport.update({ + id: '/$param47', + path: '/$param47', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam46Route = genParamsParam46RouteImport.update({ + id: '/$param46', + path: '/$param46', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam45Route = genParamsParam45RouteImport.update({ + id: '/$param45', + path: '/$param45', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam44Route = genParamsParam44RouteImport.update({ + id: '/$param44', + path: '/$param44', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam43Route = genParamsParam43RouteImport.update({ + id: '/$param43', + path: '/$param43', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam42Route = genParamsParam42RouteImport.update({ + id: '/$param42', + path: '/$param42', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam41Route = genParamsParam41RouteImport.update({ + id: '/$param41', + path: '/$param41', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam40Route = genParamsParam40RouteImport.update({ + id: '/$param40', + path: '/$param40', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam4Route = genParamsParam4RouteImport.update({ + id: '/$param4', + path: '/$param4', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam39Route = genParamsParam39RouteImport.update({ + id: '/$param39', + path: '/$param39', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam38Route = genParamsParam38RouteImport.update({ + id: '/$param38', + path: '/$param38', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam37Route = genParamsParam37RouteImport.update({ + id: '/$param37', + path: '/$param37', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam36Route = genParamsParam36RouteImport.update({ + id: '/$param36', + path: '/$param36', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam35Route = genParamsParam35RouteImport.update({ + id: '/$param35', + path: '/$param35', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam34Route = genParamsParam34RouteImport.update({ + id: '/$param34', + path: '/$param34', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam33Route = genParamsParam33RouteImport.update({ + id: '/$param33', + path: '/$param33', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam32Route = genParamsParam32RouteImport.update({ + id: '/$param32', + path: '/$param32', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam31Route = genParamsParam31RouteImport.update({ + id: '/$param31', + path: '/$param31', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam30Route = genParamsParam30RouteImport.update({ + id: '/$param30', + path: '/$param30', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam3Route = genParamsParam3RouteImport.update({ + id: '/$param3', + path: '/$param3', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam29Route = genParamsParam29RouteImport.update({ + id: '/$param29', + path: '/$param29', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam28Route = genParamsParam28RouteImport.update({ + id: '/$param28', + path: '/$param28', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam27Route = genParamsParam27RouteImport.update({ + id: '/$param27', + path: '/$param27', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam26Route = genParamsParam26RouteImport.update({ + id: '/$param26', + path: '/$param26', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam25Route = genParamsParam25RouteImport.update({ + id: '/$param25', + path: '/$param25', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam24Route = genParamsParam24RouteImport.update({ + id: '/$param24', + path: '/$param24', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam23Route = genParamsParam23RouteImport.update({ + id: '/$param23', + path: '/$param23', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam22Route = genParamsParam22RouteImport.update({ + id: '/$param22', + path: '/$param22', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam21Route = genParamsParam21RouteImport.update({ + id: '/$param21', + path: '/$param21', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam20Route = genParamsParam20RouteImport.update({ + id: '/$param20', + path: '/$param20', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam2Route = genParamsParam2RouteImport.update({ + id: '/$param2', + path: '/$param2', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam19Route = genParamsParam19RouteImport.update({ + id: '/$param19', + path: '/$param19', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam18Route = genParamsParam18RouteImport.update({ + id: '/$param18', + path: '/$param18', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam17Route = genParamsParam17RouteImport.update({ + id: '/$param17', + path: '/$param17', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam16Route = genParamsParam16RouteImport.update({ + id: '/$param16', + path: '/$param16', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam15Route = genParamsParam15RouteImport.update({ + id: '/$param15', + path: '/$param15', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam14Route = genParamsParam14RouteImport.update({ + id: '/$param14', + path: '/$param14', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam13Route = genParamsParam13RouteImport.update({ + id: '/$param13', + path: '/$param13', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam12Route = genParamsParam12RouteImport.update({ + id: '/$param12', + path: '/$param12', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam11Route = genParamsParam11RouteImport.update({ + id: '/$param11', + path: '/$param11', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam10Route = genParamsParam10RouteImport.update({ + id: '/$param10', + path: '/$param10', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam1Route = genParamsParam1RouteImport.update({ + id: '/$param1', + path: '/$param1', + getParentRoute: () => genParamsRouteRoute, +} as any) +const genParamsParam0Route = genParamsParam0RouteImport.update({ + id: '/$param0', + path: '/$param0', + getParentRoute: () => genParamsRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/params': typeof ParamsRouteRouteWithChildren - '/search': typeof SearchRouteRouteWithChildren + '/params': typeof genParamsRouteRouteWithChildren + '/search': typeof genSearchRouteRouteWithChildren '/absolute': typeof AbsoluteRoute '/linkProps': typeof LinkPropsRoute '/relative': typeof RelativeRoute + '/absolute0': typeof genAbsolute0Route + '/absolute1': typeof genAbsolute1Route + '/absolute10': typeof genAbsolute10Route + '/absolute11': typeof genAbsolute11Route + '/absolute12': typeof genAbsolute12Route + '/absolute13': typeof genAbsolute13Route + '/absolute14': typeof genAbsolute14Route + '/absolute15': typeof genAbsolute15Route + '/absolute16': typeof genAbsolute16Route + '/absolute17': typeof genAbsolute17Route + '/absolute18': typeof genAbsolute18Route + '/absolute19': typeof genAbsolute19Route + '/absolute2': typeof genAbsolute2Route + '/absolute20': typeof genAbsolute20Route + '/absolute21': typeof genAbsolute21Route + '/absolute22': typeof genAbsolute22Route + '/absolute23': typeof genAbsolute23Route + '/absolute24': typeof genAbsolute24Route + '/absolute25': typeof genAbsolute25Route + '/absolute26': typeof genAbsolute26Route + '/absolute27': typeof genAbsolute27Route + '/absolute28': typeof genAbsolute28Route + '/absolute29': typeof genAbsolute29Route + '/absolute3': typeof genAbsolute3Route + '/absolute30': typeof genAbsolute30Route + '/absolute31': typeof genAbsolute31Route + '/absolute32': typeof genAbsolute32Route + '/absolute33': typeof genAbsolute33Route + '/absolute34': typeof genAbsolute34Route + '/absolute35': typeof genAbsolute35Route + '/absolute36': typeof genAbsolute36Route + '/absolute37': typeof genAbsolute37Route + '/absolute38': typeof genAbsolute38Route + '/absolute39': typeof genAbsolute39Route + '/absolute4': typeof genAbsolute4Route + '/absolute40': typeof genAbsolute40Route + '/absolute41': typeof genAbsolute41Route + '/absolute42': typeof genAbsolute42Route + '/absolute43': typeof genAbsolute43Route + '/absolute44': typeof genAbsolute44Route + '/absolute45': typeof genAbsolute45Route + '/absolute46': typeof genAbsolute46Route + '/absolute47': typeof genAbsolute47Route + '/absolute48': typeof genAbsolute48Route + '/absolute49': typeof genAbsolute49Route + '/absolute5': typeof genAbsolute5Route + '/absolute50': typeof genAbsolute50Route + '/absolute51': typeof genAbsolute51Route + '/absolute52': typeof genAbsolute52Route + '/absolute53': typeof genAbsolute53Route + '/absolute54': typeof genAbsolute54Route + '/absolute55': typeof genAbsolute55Route + '/absolute56': typeof genAbsolute56Route + '/absolute57': typeof genAbsolute57Route + '/absolute58': typeof genAbsolute58Route + '/absolute59': typeof genAbsolute59Route + '/absolute6': typeof genAbsolute6Route + '/absolute60': typeof genAbsolute60Route + '/absolute61': typeof genAbsolute61Route + '/absolute62': typeof genAbsolute62Route + '/absolute63': typeof genAbsolute63Route + '/absolute64': typeof genAbsolute64Route + '/absolute65': typeof genAbsolute65Route + '/absolute66': typeof genAbsolute66Route + '/absolute67': typeof genAbsolute67Route + '/absolute68': typeof genAbsolute68Route + '/absolute69': typeof genAbsolute69Route + '/absolute7': typeof genAbsolute7Route + '/absolute70': typeof genAbsolute70Route + '/absolute71': typeof genAbsolute71Route + '/absolute72': typeof genAbsolute72Route + '/absolute73': typeof genAbsolute73Route + '/absolute74': typeof genAbsolute74Route + '/absolute75': typeof genAbsolute75Route + '/absolute76': typeof genAbsolute76Route + '/absolute77': typeof genAbsolute77Route + '/absolute78': typeof genAbsolute78Route + '/absolute79': typeof genAbsolute79Route + '/absolute8': typeof genAbsolute8Route + '/absolute80': typeof genAbsolute80Route + '/absolute81': typeof genAbsolute81Route + '/absolute82': typeof genAbsolute82Route + '/absolute83': typeof genAbsolute83Route + '/absolute84': typeof genAbsolute84Route + '/absolute85': typeof genAbsolute85Route + '/absolute86': typeof genAbsolute86Route + '/absolute87': typeof genAbsolute87Route + '/absolute88': typeof genAbsolute88Route + '/absolute89': typeof genAbsolute89Route + '/absolute9': typeof genAbsolute9Route + '/absolute90': typeof genAbsolute90Route + '/absolute91': typeof genAbsolute91Route + '/absolute92': typeof genAbsolute92Route + '/absolute93': typeof genAbsolute93Route + '/absolute94': typeof genAbsolute94Route + '/absolute95': typeof genAbsolute95Route + '/absolute96': typeof genAbsolute96Route + '/absolute97': typeof genAbsolute97Route + '/absolute98': typeof genAbsolute98Route + '/absolute99': typeof genAbsolute99Route + '/relative0': typeof genRelative0Route + '/relative1': typeof genRelative1Route + '/relative10': typeof genRelative10Route + '/relative11': typeof genRelative11Route + '/relative12': typeof genRelative12Route + '/relative13': typeof genRelative13Route + '/relative14': typeof genRelative14Route + '/relative15': typeof genRelative15Route + '/relative16': typeof genRelative16Route + '/relative17': typeof genRelative17Route + '/relative18': typeof genRelative18Route + '/relative19': typeof genRelative19Route + '/relative2': typeof genRelative2Route + '/relative20': typeof genRelative20Route + '/relative21': typeof genRelative21Route + '/relative22': typeof genRelative22Route + '/relative23': typeof genRelative23Route + '/relative24': typeof genRelative24Route + '/relative25': typeof genRelative25Route + '/relative26': typeof genRelative26Route + '/relative27': typeof genRelative27Route + '/relative28': typeof genRelative28Route + '/relative29': typeof genRelative29Route + '/relative3': typeof genRelative3Route + '/relative30': typeof genRelative30Route + '/relative31': typeof genRelative31Route + '/relative32': typeof genRelative32Route + '/relative33': typeof genRelative33Route + '/relative34': typeof genRelative34Route + '/relative35': typeof genRelative35Route + '/relative36': typeof genRelative36Route + '/relative37': typeof genRelative37Route + '/relative38': typeof genRelative38Route + '/relative39': typeof genRelative39Route + '/relative4': typeof genRelative4Route + '/relative40': typeof genRelative40Route + '/relative41': typeof genRelative41Route + '/relative42': typeof genRelative42Route + '/relative43': typeof genRelative43Route + '/relative44': typeof genRelative44Route + '/relative45': typeof genRelative45Route + '/relative46': typeof genRelative46Route + '/relative47': typeof genRelative47Route + '/relative48': typeof genRelative48Route + '/relative49': typeof genRelative49Route + '/relative5': typeof genRelative5Route + '/relative50': typeof genRelative50Route + '/relative51': typeof genRelative51Route + '/relative52': typeof genRelative52Route + '/relative53': typeof genRelative53Route + '/relative54': typeof genRelative54Route + '/relative55': typeof genRelative55Route + '/relative56': typeof genRelative56Route + '/relative57': typeof genRelative57Route + '/relative58': typeof genRelative58Route + '/relative59': typeof genRelative59Route + '/relative6': typeof genRelative6Route + '/relative60': typeof genRelative60Route + '/relative61': typeof genRelative61Route + '/relative62': typeof genRelative62Route + '/relative63': typeof genRelative63Route + '/relative64': typeof genRelative64Route + '/relative65': typeof genRelative65Route + '/relative66': typeof genRelative66Route + '/relative67': typeof genRelative67Route + '/relative68': typeof genRelative68Route + '/relative69': typeof genRelative69Route + '/relative7': typeof genRelative7Route + '/relative70': typeof genRelative70Route + '/relative71': typeof genRelative71Route + '/relative72': typeof genRelative72Route + '/relative73': typeof genRelative73Route + '/relative74': typeof genRelative74Route + '/relative75': typeof genRelative75Route + '/relative76': typeof genRelative76Route + '/relative77': typeof genRelative77Route + '/relative78': typeof genRelative78Route + '/relative79': typeof genRelative79Route + '/relative8': typeof genRelative8Route + '/relative80': typeof genRelative80Route + '/relative81': typeof genRelative81Route + '/relative82': typeof genRelative82Route + '/relative83': typeof genRelative83Route + '/relative84': typeof genRelative84Route + '/relative85': typeof genRelative85Route + '/relative86': typeof genRelative86Route + '/relative87': typeof genRelative87Route + '/relative88': typeof genRelative88Route + '/relative89': typeof genRelative89Route + '/relative9': typeof genRelative9Route + '/relative90': typeof genRelative90Route + '/relative91': typeof genRelative91Route + '/relative92': typeof genRelative92Route + '/relative93': typeof genRelative93Route + '/relative94': typeof genRelative94Route + '/relative95': typeof genRelative95Route + '/relative96': typeof genRelative96Route + '/relative97': typeof genRelative97Route + '/relative98': typeof genRelative98Route + '/relative99': typeof genRelative99Route '/params/$paramsPlaceholder': typeof ParamsParamsPlaceholderRoute '/search/searchPlaceholder': typeof SearchSearchPlaceholderRoute + '/params/$param0': typeof genParamsParam0Route + '/params/$param1': typeof genParamsParam1Route + '/params/$param10': typeof genParamsParam10Route + '/params/$param11': typeof genParamsParam11Route + '/params/$param12': typeof genParamsParam12Route + '/params/$param13': typeof genParamsParam13Route + '/params/$param14': typeof genParamsParam14Route + '/params/$param15': typeof genParamsParam15Route + '/params/$param16': typeof genParamsParam16Route + '/params/$param17': typeof genParamsParam17Route + '/params/$param18': typeof genParamsParam18Route + '/params/$param19': typeof genParamsParam19Route + '/params/$param2': typeof genParamsParam2Route + '/params/$param20': typeof genParamsParam20Route + '/params/$param21': typeof genParamsParam21Route + '/params/$param22': typeof genParamsParam22Route + '/params/$param23': typeof genParamsParam23Route + '/params/$param24': typeof genParamsParam24Route + '/params/$param25': typeof genParamsParam25Route + '/params/$param26': typeof genParamsParam26Route + '/params/$param27': typeof genParamsParam27Route + '/params/$param28': typeof genParamsParam28Route + '/params/$param29': typeof genParamsParam29Route + '/params/$param3': typeof genParamsParam3Route + '/params/$param30': typeof genParamsParam30Route + '/params/$param31': typeof genParamsParam31Route + '/params/$param32': typeof genParamsParam32Route + '/params/$param33': typeof genParamsParam33Route + '/params/$param34': typeof genParamsParam34Route + '/params/$param35': typeof genParamsParam35Route + '/params/$param36': typeof genParamsParam36Route + '/params/$param37': typeof genParamsParam37Route + '/params/$param38': typeof genParamsParam38Route + '/params/$param39': typeof genParamsParam39Route + '/params/$param4': typeof genParamsParam4Route + '/params/$param40': typeof genParamsParam40Route + '/params/$param41': typeof genParamsParam41Route + '/params/$param42': typeof genParamsParam42Route + '/params/$param43': typeof genParamsParam43Route + '/params/$param44': typeof genParamsParam44Route + '/params/$param45': typeof genParamsParam45Route + '/params/$param46': typeof genParamsParam46Route + '/params/$param47': typeof genParamsParam47Route + '/params/$param48': typeof genParamsParam48Route + '/params/$param49': typeof genParamsParam49Route + '/params/$param5': typeof genParamsParam5Route + '/params/$param50': typeof genParamsParam50Route + '/params/$param51': typeof genParamsParam51Route + '/params/$param52': typeof genParamsParam52Route + '/params/$param53': typeof genParamsParam53Route + '/params/$param54': typeof genParamsParam54Route + '/params/$param55': typeof genParamsParam55Route + '/params/$param56': typeof genParamsParam56Route + '/params/$param57': typeof genParamsParam57Route + '/params/$param58': typeof genParamsParam58Route + '/params/$param59': typeof genParamsParam59Route + '/params/$param6': typeof genParamsParam6Route + '/params/$param60': typeof genParamsParam60Route + '/params/$param61': typeof genParamsParam61Route + '/params/$param62': typeof genParamsParam62Route + '/params/$param63': typeof genParamsParam63Route + '/params/$param64': typeof genParamsParam64Route + '/params/$param65': typeof genParamsParam65Route + '/params/$param66': typeof genParamsParam66Route + '/params/$param67': typeof genParamsParam67Route + '/params/$param68': typeof genParamsParam68Route + '/params/$param69': typeof genParamsParam69Route + '/params/$param7': typeof genParamsParam7Route + '/params/$param70': typeof genParamsParam70Route + '/params/$param71': typeof genParamsParam71Route + '/params/$param72': typeof genParamsParam72Route + '/params/$param73': typeof genParamsParam73Route + '/params/$param74': typeof genParamsParam74Route + '/params/$param75': typeof genParamsParam75Route + '/params/$param76': typeof genParamsParam76Route + '/params/$param77': typeof genParamsParam77Route + '/params/$param78': typeof genParamsParam78Route + '/params/$param79': typeof genParamsParam79Route + '/params/$param8': typeof genParamsParam8Route + '/params/$param80': typeof genParamsParam80Route + '/params/$param81': typeof genParamsParam81Route + '/params/$param82': typeof genParamsParam82Route + '/params/$param83': typeof genParamsParam83Route + '/params/$param84': typeof genParamsParam84Route + '/params/$param85': typeof genParamsParam85Route + '/params/$param86': typeof genParamsParam86Route + '/params/$param87': typeof genParamsParam87Route + '/params/$param88': typeof genParamsParam88Route + '/params/$param89': typeof genParamsParam89Route + '/params/$param9': typeof genParamsParam9Route + '/params/$param90': typeof genParamsParam90Route + '/params/$param91': typeof genParamsParam91Route + '/params/$param92': typeof genParamsParam92Route + '/params/$param93': typeof genParamsParam93Route + '/params/$param94': typeof genParamsParam94Route + '/params/$param95': typeof genParamsParam95Route + '/params/$param96': typeof genParamsParam96Route + '/params/$param97': typeof genParamsParam97Route + '/params/$param98': typeof genParamsParam98Route + '/params/$param99': typeof genParamsParam99Route + '/search/search0': typeof genSearchSearch0Route + '/search/search1': typeof genSearchSearch1Route + '/search/search10': typeof genSearchSearch10Route + '/search/search11': typeof genSearchSearch11Route + '/search/search12': typeof genSearchSearch12Route + '/search/search13': typeof genSearchSearch13Route + '/search/search14': typeof genSearchSearch14Route + '/search/search15': typeof genSearchSearch15Route + '/search/search16': typeof genSearchSearch16Route + '/search/search17': typeof genSearchSearch17Route + '/search/search18': typeof genSearchSearch18Route + '/search/search19': typeof genSearchSearch19Route + '/search/search2': typeof genSearchSearch2Route + '/search/search20': typeof genSearchSearch20Route + '/search/search21': typeof genSearchSearch21Route + '/search/search22': typeof genSearchSearch22Route + '/search/search23': typeof genSearchSearch23Route + '/search/search24': typeof genSearchSearch24Route + '/search/search25': typeof genSearchSearch25Route + '/search/search26': typeof genSearchSearch26Route + '/search/search27': typeof genSearchSearch27Route + '/search/search28': typeof genSearchSearch28Route + '/search/search29': typeof genSearchSearch29Route + '/search/search3': typeof genSearchSearch3Route + '/search/search30': typeof genSearchSearch30Route + '/search/search31': typeof genSearchSearch31Route + '/search/search32': typeof genSearchSearch32Route + '/search/search33': typeof genSearchSearch33Route + '/search/search34': typeof genSearchSearch34Route + '/search/search35': typeof genSearchSearch35Route + '/search/search36': typeof genSearchSearch36Route + '/search/search37': typeof genSearchSearch37Route + '/search/search38': typeof genSearchSearch38Route + '/search/search39': typeof genSearchSearch39Route + '/search/search4': typeof genSearchSearch4Route + '/search/search40': typeof genSearchSearch40Route + '/search/search41': typeof genSearchSearch41Route + '/search/search42': typeof genSearchSearch42Route + '/search/search43': typeof genSearchSearch43Route + '/search/search44': typeof genSearchSearch44Route + '/search/search45': typeof genSearchSearch45Route + '/search/search46': typeof genSearchSearch46Route + '/search/search47': typeof genSearchSearch47Route + '/search/search48': typeof genSearchSearch48Route + '/search/search49': typeof genSearchSearch49Route + '/search/search5': typeof genSearchSearch5Route + '/search/search50': typeof genSearchSearch50Route + '/search/search51': typeof genSearchSearch51Route + '/search/search52': typeof genSearchSearch52Route + '/search/search53': typeof genSearchSearch53Route + '/search/search54': typeof genSearchSearch54Route + '/search/search55': typeof genSearchSearch55Route + '/search/search56': typeof genSearchSearch56Route + '/search/search57': typeof genSearchSearch57Route + '/search/search58': typeof genSearchSearch58Route + '/search/search59': typeof genSearchSearch59Route + '/search/search6': typeof genSearchSearch6Route + '/search/search60': typeof genSearchSearch60Route + '/search/search61': typeof genSearchSearch61Route + '/search/search62': typeof genSearchSearch62Route + '/search/search63': typeof genSearchSearch63Route + '/search/search64': typeof genSearchSearch64Route + '/search/search65': typeof genSearchSearch65Route + '/search/search66': typeof genSearchSearch66Route + '/search/search67': typeof genSearchSearch67Route + '/search/search68': typeof genSearchSearch68Route + '/search/search69': typeof genSearchSearch69Route + '/search/search7': typeof genSearchSearch7Route + '/search/search70': typeof genSearchSearch70Route + '/search/search71': typeof genSearchSearch71Route + '/search/search72': typeof genSearchSearch72Route + '/search/search73': typeof genSearchSearch73Route + '/search/search74': typeof genSearchSearch74Route + '/search/search75': typeof genSearchSearch75Route + '/search/search76': typeof genSearchSearch76Route + '/search/search77': typeof genSearchSearch77Route + '/search/search78': typeof genSearchSearch78Route + '/search/search79': typeof genSearchSearch79Route + '/search/search8': typeof genSearchSearch8Route + '/search/search80': typeof genSearchSearch80Route + '/search/search81': typeof genSearchSearch81Route + '/search/search82': typeof genSearchSearch82Route + '/search/search83': typeof genSearchSearch83Route + '/search/search84': typeof genSearchSearch84Route + '/search/search85': typeof genSearchSearch85Route + '/search/search86': typeof genSearchSearch86Route + '/search/search87': typeof genSearchSearch87Route + '/search/search88': typeof genSearchSearch88Route + '/search/search89': typeof genSearchSearch89Route + '/search/search9': typeof genSearchSearch9Route + '/search/search90': typeof genSearchSearch90Route + '/search/search91': typeof genSearchSearch91Route + '/search/search92': typeof genSearchSearch92Route + '/search/search93': typeof genSearchSearch93Route + '/search/search94': typeof genSearchSearch94Route + '/search/search95': typeof genSearchSearch95Route + '/search/search96': typeof genSearchSearch96Route + '/search/search97': typeof genSearchSearch97Route + '/search/search98': typeof genSearchSearch98Route + '/search/search99': typeof genSearchSearch99Route } export interface FileRoutesByTo { '/': typeof IndexRoute - '/params': typeof ParamsRouteRouteWithChildren - '/search': typeof SearchRouteRouteWithChildren + '/params': typeof genParamsRouteRouteWithChildren + '/search': typeof genSearchRouteRouteWithChildren '/absolute': typeof AbsoluteRoute '/linkProps': typeof LinkPropsRoute '/relative': typeof RelativeRoute + '/absolute0': typeof genAbsolute0Route + '/absolute1': typeof genAbsolute1Route + '/absolute10': typeof genAbsolute10Route + '/absolute11': typeof genAbsolute11Route + '/absolute12': typeof genAbsolute12Route + '/absolute13': typeof genAbsolute13Route + '/absolute14': typeof genAbsolute14Route + '/absolute15': typeof genAbsolute15Route + '/absolute16': typeof genAbsolute16Route + '/absolute17': typeof genAbsolute17Route + '/absolute18': typeof genAbsolute18Route + '/absolute19': typeof genAbsolute19Route + '/absolute2': typeof genAbsolute2Route + '/absolute20': typeof genAbsolute20Route + '/absolute21': typeof genAbsolute21Route + '/absolute22': typeof genAbsolute22Route + '/absolute23': typeof genAbsolute23Route + '/absolute24': typeof genAbsolute24Route + '/absolute25': typeof genAbsolute25Route + '/absolute26': typeof genAbsolute26Route + '/absolute27': typeof genAbsolute27Route + '/absolute28': typeof genAbsolute28Route + '/absolute29': typeof genAbsolute29Route + '/absolute3': typeof genAbsolute3Route + '/absolute30': typeof genAbsolute30Route + '/absolute31': typeof genAbsolute31Route + '/absolute32': typeof genAbsolute32Route + '/absolute33': typeof genAbsolute33Route + '/absolute34': typeof genAbsolute34Route + '/absolute35': typeof genAbsolute35Route + '/absolute36': typeof genAbsolute36Route + '/absolute37': typeof genAbsolute37Route + '/absolute38': typeof genAbsolute38Route + '/absolute39': typeof genAbsolute39Route + '/absolute4': typeof genAbsolute4Route + '/absolute40': typeof genAbsolute40Route + '/absolute41': typeof genAbsolute41Route + '/absolute42': typeof genAbsolute42Route + '/absolute43': typeof genAbsolute43Route + '/absolute44': typeof genAbsolute44Route + '/absolute45': typeof genAbsolute45Route + '/absolute46': typeof genAbsolute46Route + '/absolute47': typeof genAbsolute47Route + '/absolute48': typeof genAbsolute48Route + '/absolute49': typeof genAbsolute49Route + '/absolute5': typeof genAbsolute5Route + '/absolute50': typeof genAbsolute50Route + '/absolute51': typeof genAbsolute51Route + '/absolute52': typeof genAbsolute52Route + '/absolute53': typeof genAbsolute53Route + '/absolute54': typeof genAbsolute54Route + '/absolute55': typeof genAbsolute55Route + '/absolute56': typeof genAbsolute56Route + '/absolute57': typeof genAbsolute57Route + '/absolute58': typeof genAbsolute58Route + '/absolute59': typeof genAbsolute59Route + '/absolute6': typeof genAbsolute6Route + '/absolute60': typeof genAbsolute60Route + '/absolute61': typeof genAbsolute61Route + '/absolute62': typeof genAbsolute62Route + '/absolute63': typeof genAbsolute63Route + '/absolute64': typeof genAbsolute64Route + '/absolute65': typeof genAbsolute65Route + '/absolute66': typeof genAbsolute66Route + '/absolute67': typeof genAbsolute67Route + '/absolute68': typeof genAbsolute68Route + '/absolute69': typeof genAbsolute69Route + '/absolute7': typeof genAbsolute7Route + '/absolute70': typeof genAbsolute70Route + '/absolute71': typeof genAbsolute71Route + '/absolute72': typeof genAbsolute72Route + '/absolute73': typeof genAbsolute73Route + '/absolute74': typeof genAbsolute74Route + '/absolute75': typeof genAbsolute75Route + '/absolute76': typeof genAbsolute76Route + '/absolute77': typeof genAbsolute77Route + '/absolute78': typeof genAbsolute78Route + '/absolute79': typeof genAbsolute79Route + '/absolute8': typeof genAbsolute8Route + '/absolute80': typeof genAbsolute80Route + '/absolute81': typeof genAbsolute81Route + '/absolute82': typeof genAbsolute82Route + '/absolute83': typeof genAbsolute83Route + '/absolute84': typeof genAbsolute84Route + '/absolute85': typeof genAbsolute85Route + '/absolute86': typeof genAbsolute86Route + '/absolute87': typeof genAbsolute87Route + '/absolute88': typeof genAbsolute88Route + '/absolute89': typeof genAbsolute89Route + '/absolute9': typeof genAbsolute9Route + '/absolute90': typeof genAbsolute90Route + '/absolute91': typeof genAbsolute91Route + '/absolute92': typeof genAbsolute92Route + '/absolute93': typeof genAbsolute93Route + '/absolute94': typeof genAbsolute94Route + '/absolute95': typeof genAbsolute95Route + '/absolute96': typeof genAbsolute96Route + '/absolute97': typeof genAbsolute97Route + '/absolute98': typeof genAbsolute98Route + '/absolute99': typeof genAbsolute99Route + '/relative0': typeof genRelative0Route + '/relative1': typeof genRelative1Route + '/relative10': typeof genRelative10Route + '/relative11': typeof genRelative11Route + '/relative12': typeof genRelative12Route + '/relative13': typeof genRelative13Route + '/relative14': typeof genRelative14Route + '/relative15': typeof genRelative15Route + '/relative16': typeof genRelative16Route + '/relative17': typeof genRelative17Route + '/relative18': typeof genRelative18Route + '/relative19': typeof genRelative19Route + '/relative2': typeof genRelative2Route + '/relative20': typeof genRelative20Route + '/relative21': typeof genRelative21Route + '/relative22': typeof genRelative22Route + '/relative23': typeof genRelative23Route + '/relative24': typeof genRelative24Route + '/relative25': typeof genRelative25Route + '/relative26': typeof genRelative26Route + '/relative27': typeof genRelative27Route + '/relative28': typeof genRelative28Route + '/relative29': typeof genRelative29Route + '/relative3': typeof genRelative3Route + '/relative30': typeof genRelative30Route + '/relative31': typeof genRelative31Route + '/relative32': typeof genRelative32Route + '/relative33': typeof genRelative33Route + '/relative34': typeof genRelative34Route + '/relative35': typeof genRelative35Route + '/relative36': typeof genRelative36Route + '/relative37': typeof genRelative37Route + '/relative38': typeof genRelative38Route + '/relative39': typeof genRelative39Route + '/relative4': typeof genRelative4Route + '/relative40': typeof genRelative40Route + '/relative41': typeof genRelative41Route + '/relative42': typeof genRelative42Route + '/relative43': typeof genRelative43Route + '/relative44': typeof genRelative44Route + '/relative45': typeof genRelative45Route + '/relative46': typeof genRelative46Route + '/relative47': typeof genRelative47Route + '/relative48': typeof genRelative48Route + '/relative49': typeof genRelative49Route + '/relative5': typeof genRelative5Route + '/relative50': typeof genRelative50Route + '/relative51': typeof genRelative51Route + '/relative52': typeof genRelative52Route + '/relative53': typeof genRelative53Route + '/relative54': typeof genRelative54Route + '/relative55': typeof genRelative55Route + '/relative56': typeof genRelative56Route + '/relative57': typeof genRelative57Route + '/relative58': typeof genRelative58Route + '/relative59': typeof genRelative59Route + '/relative6': typeof genRelative6Route + '/relative60': typeof genRelative60Route + '/relative61': typeof genRelative61Route + '/relative62': typeof genRelative62Route + '/relative63': typeof genRelative63Route + '/relative64': typeof genRelative64Route + '/relative65': typeof genRelative65Route + '/relative66': typeof genRelative66Route + '/relative67': typeof genRelative67Route + '/relative68': typeof genRelative68Route + '/relative69': typeof genRelative69Route + '/relative7': typeof genRelative7Route + '/relative70': typeof genRelative70Route + '/relative71': typeof genRelative71Route + '/relative72': typeof genRelative72Route + '/relative73': typeof genRelative73Route + '/relative74': typeof genRelative74Route + '/relative75': typeof genRelative75Route + '/relative76': typeof genRelative76Route + '/relative77': typeof genRelative77Route + '/relative78': typeof genRelative78Route + '/relative79': typeof genRelative79Route + '/relative8': typeof genRelative8Route + '/relative80': typeof genRelative80Route + '/relative81': typeof genRelative81Route + '/relative82': typeof genRelative82Route + '/relative83': typeof genRelative83Route + '/relative84': typeof genRelative84Route + '/relative85': typeof genRelative85Route + '/relative86': typeof genRelative86Route + '/relative87': typeof genRelative87Route + '/relative88': typeof genRelative88Route + '/relative89': typeof genRelative89Route + '/relative9': typeof genRelative9Route + '/relative90': typeof genRelative90Route + '/relative91': typeof genRelative91Route + '/relative92': typeof genRelative92Route + '/relative93': typeof genRelative93Route + '/relative94': typeof genRelative94Route + '/relative95': typeof genRelative95Route + '/relative96': typeof genRelative96Route + '/relative97': typeof genRelative97Route + '/relative98': typeof genRelative98Route + '/relative99': typeof genRelative99Route '/params/$paramsPlaceholder': typeof ParamsParamsPlaceholderRoute '/search/searchPlaceholder': typeof SearchSearchPlaceholderRoute + '/params/$param0': typeof genParamsParam0Route + '/params/$param1': typeof genParamsParam1Route + '/params/$param10': typeof genParamsParam10Route + '/params/$param11': typeof genParamsParam11Route + '/params/$param12': typeof genParamsParam12Route + '/params/$param13': typeof genParamsParam13Route + '/params/$param14': typeof genParamsParam14Route + '/params/$param15': typeof genParamsParam15Route + '/params/$param16': typeof genParamsParam16Route + '/params/$param17': typeof genParamsParam17Route + '/params/$param18': typeof genParamsParam18Route + '/params/$param19': typeof genParamsParam19Route + '/params/$param2': typeof genParamsParam2Route + '/params/$param20': typeof genParamsParam20Route + '/params/$param21': typeof genParamsParam21Route + '/params/$param22': typeof genParamsParam22Route + '/params/$param23': typeof genParamsParam23Route + '/params/$param24': typeof genParamsParam24Route + '/params/$param25': typeof genParamsParam25Route + '/params/$param26': typeof genParamsParam26Route + '/params/$param27': typeof genParamsParam27Route + '/params/$param28': typeof genParamsParam28Route + '/params/$param29': typeof genParamsParam29Route + '/params/$param3': typeof genParamsParam3Route + '/params/$param30': typeof genParamsParam30Route + '/params/$param31': typeof genParamsParam31Route + '/params/$param32': typeof genParamsParam32Route + '/params/$param33': typeof genParamsParam33Route + '/params/$param34': typeof genParamsParam34Route + '/params/$param35': typeof genParamsParam35Route + '/params/$param36': typeof genParamsParam36Route + '/params/$param37': typeof genParamsParam37Route + '/params/$param38': typeof genParamsParam38Route + '/params/$param39': typeof genParamsParam39Route + '/params/$param4': typeof genParamsParam4Route + '/params/$param40': typeof genParamsParam40Route + '/params/$param41': typeof genParamsParam41Route + '/params/$param42': typeof genParamsParam42Route + '/params/$param43': typeof genParamsParam43Route + '/params/$param44': typeof genParamsParam44Route + '/params/$param45': typeof genParamsParam45Route + '/params/$param46': typeof genParamsParam46Route + '/params/$param47': typeof genParamsParam47Route + '/params/$param48': typeof genParamsParam48Route + '/params/$param49': typeof genParamsParam49Route + '/params/$param5': typeof genParamsParam5Route + '/params/$param50': typeof genParamsParam50Route + '/params/$param51': typeof genParamsParam51Route + '/params/$param52': typeof genParamsParam52Route + '/params/$param53': typeof genParamsParam53Route + '/params/$param54': typeof genParamsParam54Route + '/params/$param55': typeof genParamsParam55Route + '/params/$param56': typeof genParamsParam56Route + '/params/$param57': typeof genParamsParam57Route + '/params/$param58': typeof genParamsParam58Route + '/params/$param59': typeof genParamsParam59Route + '/params/$param6': typeof genParamsParam6Route + '/params/$param60': typeof genParamsParam60Route + '/params/$param61': typeof genParamsParam61Route + '/params/$param62': typeof genParamsParam62Route + '/params/$param63': typeof genParamsParam63Route + '/params/$param64': typeof genParamsParam64Route + '/params/$param65': typeof genParamsParam65Route + '/params/$param66': typeof genParamsParam66Route + '/params/$param67': typeof genParamsParam67Route + '/params/$param68': typeof genParamsParam68Route + '/params/$param69': typeof genParamsParam69Route + '/params/$param7': typeof genParamsParam7Route + '/params/$param70': typeof genParamsParam70Route + '/params/$param71': typeof genParamsParam71Route + '/params/$param72': typeof genParamsParam72Route + '/params/$param73': typeof genParamsParam73Route + '/params/$param74': typeof genParamsParam74Route + '/params/$param75': typeof genParamsParam75Route + '/params/$param76': typeof genParamsParam76Route + '/params/$param77': typeof genParamsParam77Route + '/params/$param78': typeof genParamsParam78Route + '/params/$param79': typeof genParamsParam79Route + '/params/$param8': typeof genParamsParam8Route + '/params/$param80': typeof genParamsParam80Route + '/params/$param81': typeof genParamsParam81Route + '/params/$param82': typeof genParamsParam82Route + '/params/$param83': typeof genParamsParam83Route + '/params/$param84': typeof genParamsParam84Route + '/params/$param85': typeof genParamsParam85Route + '/params/$param86': typeof genParamsParam86Route + '/params/$param87': typeof genParamsParam87Route + '/params/$param88': typeof genParamsParam88Route + '/params/$param89': typeof genParamsParam89Route + '/params/$param9': typeof genParamsParam9Route + '/params/$param90': typeof genParamsParam90Route + '/params/$param91': typeof genParamsParam91Route + '/params/$param92': typeof genParamsParam92Route + '/params/$param93': typeof genParamsParam93Route + '/params/$param94': typeof genParamsParam94Route + '/params/$param95': typeof genParamsParam95Route + '/params/$param96': typeof genParamsParam96Route + '/params/$param97': typeof genParamsParam97Route + '/params/$param98': typeof genParamsParam98Route + '/params/$param99': typeof genParamsParam99Route + '/search/search0': typeof genSearchSearch0Route + '/search/search1': typeof genSearchSearch1Route + '/search/search10': typeof genSearchSearch10Route + '/search/search11': typeof genSearchSearch11Route + '/search/search12': typeof genSearchSearch12Route + '/search/search13': typeof genSearchSearch13Route + '/search/search14': typeof genSearchSearch14Route + '/search/search15': typeof genSearchSearch15Route + '/search/search16': typeof genSearchSearch16Route + '/search/search17': typeof genSearchSearch17Route + '/search/search18': typeof genSearchSearch18Route + '/search/search19': typeof genSearchSearch19Route + '/search/search2': typeof genSearchSearch2Route + '/search/search20': typeof genSearchSearch20Route + '/search/search21': typeof genSearchSearch21Route + '/search/search22': typeof genSearchSearch22Route + '/search/search23': typeof genSearchSearch23Route + '/search/search24': typeof genSearchSearch24Route + '/search/search25': typeof genSearchSearch25Route + '/search/search26': typeof genSearchSearch26Route + '/search/search27': typeof genSearchSearch27Route + '/search/search28': typeof genSearchSearch28Route + '/search/search29': typeof genSearchSearch29Route + '/search/search3': typeof genSearchSearch3Route + '/search/search30': typeof genSearchSearch30Route + '/search/search31': typeof genSearchSearch31Route + '/search/search32': typeof genSearchSearch32Route + '/search/search33': typeof genSearchSearch33Route + '/search/search34': typeof genSearchSearch34Route + '/search/search35': typeof genSearchSearch35Route + '/search/search36': typeof genSearchSearch36Route + '/search/search37': typeof genSearchSearch37Route + '/search/search38': typeof genSearchSearch38Route + '/search/search39': typeof genSearchSearch39Route + '/search/search4': typeof genSearchSearch4Route + '/search/search40': typeof genSearchSearch40Route + '/search/search41': typeof genSearchSearch41Route + '/search/search42': typeof genSearchSearch42Route + '/search/search43': typeof genSearchSearch43Route + '/search/search44': typeof genSearchSearch44Route + '/search/search45': typeof genSearchSearch45Route + '/search/search46': typeof genSearchSearch46Route + '/search/search47': typeof genSearchSearch47Route + '/search/search48': typeof genSearchSearch48Route + '/search/search49': typeof genSearchSearch49Route + '/search/search5': typeof genSearchSearch5Route + '/search/search50': typeof genSearchSearch50Route + '/search/search51': typeof genSearchSearch51Route + '/search/search52': typeof genSearchSearch52Route + '/search/search53': typeof genSearchSearch53Route + '/search/search54': typeof genSearchSearch54Route + '/search/search55': typeof genSearchSearch55Route + '/search/search56': typeof genSearchSearch56Route + '/search/search57': typeof genSearchSearch57Route + '/search/search58': typeof genSearchSearch58Route + '/search/search59': typeof genSearchSearch59Route + '/search/search6': typeof genSearchSearch6Route + '/search/search60': typeof genSearchSearch60Route + '/search/search61': typeof genSearchSearch61Route + '/search/search62': typeof genSearchSearch62Route + '/search/search63': typeof genSearchSearch63Route + '/search/search64': typeof genSearchSearch64Route + '/search/search65': typeof genSearchSearch65Route + '/search/search66': typeof genSearchSearch66Route + '/search/search67': typeof genSearchSearch67Route + '/search/search68': typeof genSearchSearch68Route + '/search/search69': typeof genSearchSearch69Route + '/search/search7': typeof genSearchSearch7Route + '/search/search70': typeof genSearchSearch70Route + '/search/search71': typeof genSearchSearch71Route + '/search/search72': typeof genSearchSearch72Route + '/search/search73': typeof genSearchSearch73Route + '/search/search74': typeof genSearchSearch74Route + '/search/search75': typeof genSearchSearch75Route + '/search/search76': typeof genSearchSearch76Route + '/search/search77': typeof genSearchSearch77Route + '/search/search78': typeof genSearchSearch78Route + '/search/search79': typeof genSearchSearch79Route + '/search/search8': typeof genSearchSearch8Route + '/search/search80': typeof genSearchSearch80Route + '/search/search81': typeof genSearchSearch81Route + '/search/search82': typeof genSearchSearch82Route + '/search/search83': typeof genSearchSearch83Route + '/search/search84': typeof genSearchSearch84Route + '/search/search85': typeof genSearchSearch85Route + '/search/search86': typeof genSearchSearch86Route + '/search/search87': typeof genSearchSearch87Route + '/search/search88': typeof genSearchSearch88Route + '/search/search89': typeof genSearchSearch89Route + '/search/search9': typeof genSearchSearch9Route + '/search/search90': typeof genSearchSearch90Route + '/search/search91': typeof genSearchSearch91Route + '/search/search92': typeof genSearchSearch92Route + '/search/search93': typeof genSearchSearch93Route + '/search/search94': typeof genSearchSearch94Route + '/search/search95': typeof genSearchSearch95Route + '/search/search96': typeof genSearchSearch96Route + '/search/search97': typeof genSearchSearch97Route + '/search/search98': typeof genSearchSearch98Route + '/search/search99': typeof genSearchSearch99Route } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -87,8 +3299,410 @@ export interface FileRoutesById { '/absolute': typeof AbsoluteRoute '/linkProps': typeof LinkPropsRoute '/relative': typeof RelativeRoute + '/(gen)/params': typeof genParamsRouteRouteWithChildren + '/(gen)/search': typeof genSearchRouteRouteWithChildren + '/(gen)/absolute0': typeof genAbsolute0Route + '/(gen)/absolute1': typeof genAbsolute1Route + '/(gen)/absolute10': typeof genAbsolute10Route + '/(gen)/absolute11': typeof genAbsolute11Route + '/(gen)/absolute12': typeof genAbsolute12Route + '/(gen)/absolute13': typeof genAbsolute13Route + '/(gen)/absolute14': typeof genAbsolute14Route + '/(gen)/absolute15': typeof genAbsolute15Route + '/(gen)/absolute16': typeof genAbsolute16Route + '/(gen)/absolute17': typeof genAbsolute17Route + '/(gen)/absolute18': typeof genAbsolute18Route + '/(gen)/absolute19': typeof genAbsolute19Route + '/(gen)/absolute2': typeof genAbsolute2Route + '/(gen)/absolute20': typeof genAbsolute20Route + '/(gen)/absolute21': typeof genAbsolute21Route + '/(gen)/absolute22': typeof genAbsolute22Route + '/(gen)/absolute23': typeof genAbsolute23Route + '/(gen)/absolute24': typeof genAbsolute24Route + '/(gen)/absolute25': typeof genAbsolute25Route + '/(gen)/absolute26': typeof genAbsolute26Route + '/(gen)/absolute27': typeof genAbsolute27Route + '/(gen)/absolute28': typeof genAbsolute28Route + '/(gen)/absolute29': typeof genAbsolute29Route + '/(gen)/absolute3': typeof genAbsolute3Route + '/(gen)/absolute30': typeof genAbsolute30Route + '/(gen)/absolute31': typeof genAbsolute31Route + '/(gen)/absolute32': typeof genAbsolute32Route + '/(gen)/absolute33': typeof genAbsolute33Route + '/(gen)/absolute34': typeof genAbsolute34Route + '/(gen)/absolute35': typeof genAbsolute35Route + '/(gen)/absolute36': typeof genAbsolute36Route + '/(gen)/absolute37': typeof genAbsolute37Route + '/(gen)/absolute38': typeof genAbsolute38Route + '/(gen)/absolute39': typeof genAbsolute39Route + '/(gen)/absolute4': typeof genAbsolute4Route + '/(gen)/absolute40': typeof genAbsolute40Route + '/(gen)/absolute41': typeof genAbsolute41Route + '/(gen)/absolute42': typeof genAbsolute42Route + '/(gen)/absolute43': typeof genAbsolute43Route + '/(gen)/absolute44': typeof genAbsolute44Route + '/(gen)/absolute45': typeof genAbsolute45Route + '/(gen)/absolute46': typeof genAbsolute46Route + '/(gen)/absolute47': typeof genAbsolute47Route + '/(gen)/absolute48': typeof genAbsolute48Route + '/(gen)/absolute49': typeof genAbsolute49Route + '/(gen)/absolute5': typeof genAbsolute5Route + '/(gen)/absolute50': typeof genAbsolute50Route + '/(gen)/absolute51': typeof genAbsolute51Route + '/(gen)/absolute52': typeof genAbsolute52Route + '/(gen)/absolute53': typeof genAbsolute53Route + '/(gen)/absolute54': typeof genAbsolute54Route + '/(gen)/absolute55': typeof genAbsolute55Route + '/(gen)/absolute56': typeof genAbsolute56Route + '/(gen)/absolute57': typeof genAbsolute57Route + '/(gen)/absolute58': typeof genAbsolute58Route + '/(gen)/absolute59': typeof genAbsolute59Route + '/(gen)/absolute6': typeof genAbsolute6Route + '/(gen)/absolute60': typeof genAbsolute60Route + '/(gen)/absolute61': typeof genAbsolute61Route + '/(gen)/absolute62': typeof genAbsolute62Route + '/(gen)/absolute63': typeof genAbsolute63Route + '/(gen)/absolute64': typeof genAbsolute64Route + '/(gen)/absolute65': typeof genAbsolute65Route + '/(gen)/absolute66': typeof genAbsolute66Route + '/(gen)/absolute67': typeof genAbsolute67Route + '/(gen)/absolute68': typeof genAbsolute68Route + '/(gen)/absolute69': typeof genAbsolute69Route + '/(gen)/absolute7': typeof genAbsolute7Route + '/(gen)/absolute70': typeof genAbsolute70Route + '/(gen)/absolute71': typeof genAbsolute71Route + '/(gen)/absolute72': typeof genAbsolute72Route + '/(gen)/absolute73': typeof genAbsolute73Route + '/(gen)/absolute74': typeof genAbsolute74Route + '/(gen)/absolute75': typeof genAbsolute75Route + '/(gen)/absolute76': typeof genAbsolute76Route + '/(gen)/absolute77': typeof genAbsolute77Route + '/(gen)/absolute78': typeof genAbsolute78Route + '/(gen)/absolute79': typeof genAbsolute79Route + '/(gen)/absolute8': typeof genAbsolute8Route + '/(gen)/absolute80': typeof genAbsolute80Route + '/(gen)/absolute81': typeof genAbsolute81Route + '/(gen)/absolute82': typeof genAbsolute82Route + '/(gen)/absolute83': typeof genAbsolute83Route + '/(gen)/absolute84': typeof genAbsolute84Route + '/(gen)/absolute85': typeof genAbsolute85Route + '/(gen)/absolute86': typeof genAbsolute86Route + '/(gen)/absolute87': typeof genAbsolute87Route + '/(gen)/absolute88': typeof genAbsolute88Route + '/(gen)/absolute89': typeof genAbsolute89Route + '/(gen)/absolute9': typeof genAbsolute9Route + '/(gen)/absolute90': typeof genAbsolute90Route + '/(gen)/absolute91': typeof genAbsolute91Route + '/(gen)/absolute92': typeof genAbsolute92Route + '/(gen)/absolute93': typeof genAbsolute93Route + '/(gen)/absolute94': typeof genAbsolute94Route + '/(gen)/absolute95': typeof genAbsolute95Route + '/(gen)/absolute96': typeof genAbsolute96Route + '/(gen)/absolute97': typeof genAbsolute97Route + '/(gen)/absolute98': typeof genAbsolute98Route + '/(gen)/absolute99': typeof genAbsolute99Route + '/(gen)/relative0': typeof genRelative0Route + '/(gen)/relative1': typeof genRelative1Route + '/(gen)/relative10': typeof genRelative10Route + '/(gen)/relative11': typeof genRelative11Route + '/(gen)/relative12': typeof genRelative12Route + '/(gen)/relative13': typeof genRelative13Route + '/(gen)/relative14': typeof genRelative14Route + '/(gen)/relative15': typeof genRelative15Route + '/(gen)/relative16': typeof genRelative16Route + '/(gen)/relative17': typeof genRelative17Route + '/(gen)/relative18': typeof genRelative18Route + '/(gen)/relative19': typeof genRelative19Route + '/(gen)/relative2': typeof genRelative2Route + '/(gen)/relative20': typeof genRelative20Route + '/(gen)/relative21': typeof genRelative21Route + '/(gen)/relative22': typeof genRelative22Route + '/(gen)/relative23': typeof genRelative23Route + '/(gen)/relative24': typeof genRelative24Route + '/(gen)/relative25': typeof genRelative25Route + '/(gen)/relative26': typeof genRelative26Route + '/(gen)/relative27': typeof genRelative27Route + '/(gen)/relative28': typeof genRelative28Route + '/(gen)/relative29': typeof genRelative29Route + '/(gen)/relative3': typeof genRelative3Route + '/(gen)/relative30': typeof genRelative30Route + '/(gen)/relative31': typeof genRelative31Route + '/(gen)/relative32': typeof genRelative32Route + '/(gen)/relative33': typeof genRelative33Route + '/(gen)/relative34': typeof genRelative34Route + '/(gen)/relative35': typeof genRelative35Route + '/(gen)/relative36': typeof genRelative36Route + '/(gen)/relative37': typeof genRelative37Route + '/(gen)/relative38': typeof genRelative38Route + '/(gen)/relative39': typeof genRelative39Route + '/(gen)/relative4': typeof genRelative4Route + '/(gen)/relative40': typeof genRelative40Route + '/(gen)/relative41': typeof genRelative41Route + '/(gen)/relative42': typeof genRelative42Route + '/(gen)/relative43': typeof genRelative43Route + '/(gen)/relative44': typeof genRelative44Route + '/(gen)/relative45': typeof genRelative45Route + '/(gen)/relative46': typeof genRelative46Route + '/(gen)/relative47': typeof genRelative47Route + '/(gen)/relative48': typeof genRelative48Route + '/(gen)/relative49': typeof genRelative49Route + '/(gen)/relative5': typeof genRelative5Route + '/(gen)/relative50': typeof genRelative50Route + '/(gen)/relative51': typeof genRelative51Route + '/(gen)/relative52': typeof genRelative52Route + '/(gen)/relative53': typeof genRelative53Route + '/(gen)/relative54': typeof genRelative54Route + '/(gen)/relative55': typeof genRelative55Route + '/(gen)/relative56': typeof genRelative56Route + '/(gen)/relative57': typeof genRelative57Route + '/(gen)/relative58': typeof genRelative58Route + '/(gen)/relative59': typeof genRelative59Route + '/(gen)/relative6': typeof genRelative6Route + '/(gen)/relative60': typeof genRelative60Route + '/(gen)/relative61': typeof genRelative61Route + '/(gen)/relative62': typeof genRelative62Route + '/(gen)/relative63': typeof genRelative63Route + '/(gen)/relative64': typeof genRelative64Route + '/(gen)/relative65': typeof genRelative65Route + '/(gen)/relative66': typeof genRelative66Route + '/(gen)/relative67': typeof genRelative67Route + '/(gen)/relative68': typeof genRelative68Route + '/(gen)/relative69': typeof genRelative69Route + '/(gen)/relative7': typeof genRelative7Route + '/(gen)/relative70': typeof genRelative70Route + '/(gen)/relative71': typeof genRelative71Route + '/(gen)/relative72': typeof genRelative72Route + '/(gen)/relative73': typeof genRelative73Route + '/(gen)/relative74': typeof genRelative74Route + '/(gen)/relative75': typeof genRelative75Route + '/(gen)/relative76': typeof genRelative76Route + '/(gen)/relative77': typeof genRelative77Route + '/(gen)/relative78': typeof genRelative78Route + '/(gen)/relative79': typeof genRelative79Route + '/(gen)/relative8': typeof genRelative8Route + '/(gen)/relative80': typeof genRelative80Route + '/(gen)/relative81': typeof genRelative81Route + '/(gen)/relative82': typeof genRelative82Route + '/(gen)/relative83': typeof genRelative83Route + '/(gen)/relative84': typeof genRelative84Route + '/(gen)/relative85': typeof genRelative85Route + '/(gen)/relative86': typeof genRelative86Route + '/(gen)/relative87': typeof genRelative87Route + '/(gen)/relative88': typeof genRelative88Route + '/(gen)/relative89': typeof genRelative89Route + '/(gen)/relative9': typeof genRelative9Route + '/(gen)/relative90': typeof genRelative90Route + '/(gen)/relative91': typeof genRelative91Route + '/(gen)/relative92': typeof genRelative92Route + '/(gen)/relative93': typeof genRelative93Route + '/(gen)/relative94': typeof genRelative94Route + '/(gen)/relative95': typeof genRelative95Route + '/(gen)/relative96': typeof genRelative96Route + '/(gen)/relative97': typeof genRelative97Route + '/(gen)/relative98': typeof genRelative98Route + '/(gen)/relative99': typeof genRelative99Route '/params/$paramsPlaceholder': typeof ParamsParamsPlaceholderRoute '/search/searchPlaceholder': typeof SearchSearchPlaceholderRoute + '/(gen)/params/$param0': typeof genParamsParam0Route + '/(gen)/params/$param1': typeof genParamsParam1Route + '/(gen)/params/$param10': typeof genParamsParam10Route + '/(gen)/params/$param11': typeof genParamsParam11Route + '/(gen)/params/$param12': typeof genParamsParam12Route + '/(gen)/params/$param13': typeof genParamsParam13Route + '/(gen)/params/$param14': typeof genParamsParam14Route + '/(gen)/params/$param15': typeof genParamsParam15Route + '/(gen)/params/$param16': typeof genParamsParam16Route + '/(gen)/params/$param17': typeof genParamsParam17Route + '/(gen)/params/$param18': typeof genParamsParam18Route + '/(gen)/params/$param19': typeof genParamsParam19Route + '/(gen)/params/$param2': typeof genParamsParam2Route + '/(gen)/params/$param20': typeof genParamsParam20Route + '/(gen)/params/$param21': typeof genParamsParam21Route + '/(gen)/params/$param22': typeof genParamsParam22Route + '/(gen)/params/$param23': typeof genParamsParam23Route + '/(gen)/params/$param24': typeof genParamsParam24Route + '/(gen)/params/$param25': typeof genParamsParam25Route + '/(gen)/params/$param26': typeof genParamsParam26Route + '/(gen)/params/$param27': typeof genParamsParam27Route + '/(gen)/params/$param28': typeof genParamsParam28Route + '/(gen)/params/$param29': typeof genParamsParam29Route + '/(gen)/params/$param3': typeof genParamsParam3Route + '/(gen)/params/$param30': typeof genParamsParam30Route + '/(gen)/params/$param31': typeof genParamsParam31Route + '/(gen)/params/$param32': typeof genParamsParam32Route + '/(gen)/params/$param33': typeof genParamsParam33Route + '/(gen)/params/$param34': typeof genParamsParam34Route + '/(gen)/params/$param35': typeof genParamsParam35Route + '/(gen)/params/$param36': typeof genParamsParam36Route + '/(gen)/params/$param37': typeof genParamsParam37Route + '/(gen)/params/$param38': typeof genParamsParam38Route + '/(gen)/params/$param39': typeof genParamsParam39Route + '/(gen)/params/$param4': typeof genParamsParam4Route + '/(gen)/params/$param40': typeof genParamsParam40Route + '/(gen)/params/$param41': typeof genParamsParam41Route + '/(gen)/params/$param42': typeof genParamsParam42Route + '/(gen)/params/$param43': typeof genParamsParam43Route + '/(gen)/params/$param44': typeof genParamsParam44Route + '/(gen)/params/$param45': typeof genParamsParam45Route + '/(gen)/params/$param46': typeof genParamsParam46Route + '/(gen)/params/$param47': typeof genParamsParam47Route + '/(gen)/params/$param48': typeof genParamsParam48Route + '/(gen)/params/$param49': typeof genParamsParam49Route + '/(gen)/params/$param5': typeof genParamsParam5Route + '/(gen)/params/$param50': typeof genParamsParam50Route + '/(gen)/params/$param51': typeof genParamsParam51Route + '/(gen)/params/$param52': typeof genParamsParam52Route + '/(gen)/params/$param53': typeof genParamsParam53Route + '/(gen)/params/$param54': typeof genParamsParam54Route + '/(gen)/params/$param55': typeof genParamsParam55Route + '/(gen)/params/$param56': typeof genParamsParam56Route + '/(gen)/params/$param57': typeof genParamsParam57Route + '/(gen)/params/$param58': typeof genParamsParam58Route + '/(gen)/params/$param59': typeof genParamsParam59Route + '/(gen)/params/$param6': typeof genParamsParam6Route + '/(gen)/params/$param60': typeof genParamsParam60Route + '/(gen)/params/$param61': typeof genParamsParam61Route + '/(gen)/params/$param62': typeof genParamsParam62Route + '/(gen)/params/$param63': typeof genParamsParam63Route + '/(gen)/params/$param64': typeof genParamsParam64Route + '/(gen)/params/$param65': typeof genParamsParam65Route + '/(gen)/params/$param66': typeof genParamsParam66Route + '/(gen)/params/$param67': typeof genParamsParam67Route + '/(gen)/params/$param68': typeof genParamsParam68Route + '/(gen)/params/$param69': typeof genParamsParam69Route + '/(gen)/params/$param7': typeof genParamsParam7Route + '/(gen)/params/$param70': typeof genParamsParam70Route + '/(gen)/params/$param71': typeof genParamsParam71Route + '/(gen)/params/$param72': typeof genParamsParam72Route + '/(gen)/params/$param73': typeof genParamsParam73Route + '/(gen)/params/$param74': typeof genParamsParam74Route + '/(gen)/params/$param75': typeof genParamsParam75Route + '/(gen)/params/$param76': typeof genParamsParam76Route + '/(gen)/params/$param77': typeof genParamsParam77Route + '/(gen)/params/$param78': typeof genParamsParam78Route + '/(gen)/params/$param79': typeof genParamsParam79Route + '/(gen)/params/$param8': typeof genParamsParam8Route + '/(gen)/params/$param80': typeof genParamsParam80Route + '/(gen)/params/$param81': typeof genParamsParam81Route + '/(gen)/params/$param82': typeof genParamsParam82Route + '/(gen)/params/$param83': typeof genParamsParam83Route + '/(gen)/params/$param84': typeof genParamsParam84Route + '/(gen)/params/$param85': typeof genParamsParam85Route + '/(gen)/params/$param86': typeof genParamsParam86Route + '/(gen)/params/$param87': typeof genParamsParam87Route + '/(gen)/params/$param88': typeof genParamsParam88Route + '/(gen)/params/$param89': typeof genParamsParam89Route + '/(gen)/params/$param9': typeof genParamsParam9Route + '/(gen)/params/$param90': typeof genParamsParam90Route + '/(gen)/params/$param91': typeof genParamsParam91Route + '/(gen)/params/$param92': typeof genParamsParam92Route + '/(gen)/params/$param93': typeof genParamsParam93Route + '/(gen)/params/$param94': typeof genParamsParam94Route + '/(gen)/params/$param95': typeof genParamsParam95Route + '/(gen)/params/$param96': typeof genParamsParam96Route + '/(gen)/params/$param97': typeof genParamsParam97Route + '/(gen)/params/$param98': typeof genParamsParam98Route + '/(gen)/params/$param99': typeof genParamsParam99Route + '/(gen)/search/search0': typeof genSearchSearch0Route + '/(gen)/search/search1': typeof genSearchSearch1Route + '/(gen)/search/search10': typeof genSearchSearch10Route + '/(gen)/search/search11': typeof genSearchSearch11Route + '/(gen)/search/search12': typeof genSearchSearch12Route + '/(gen)/search/search13': typeof genSearchSearch13Route + '/(gen)/search/search14': typeof genSearchSearch14Route + '/(gen)/search/search15': typeof genSearchSearch15Route + '/(gen)/search/search16': typeof genSearchSearch16Route + '/(gen)/search/search17': typeof genSearchSearch17Route + '/(gen)/search/search18': typeof genSearchSearch18Route + '/(gen)/search/search19': typeof genSearchSearch19Route + '/(gen)/search/search2': typeof genSearchSearch2Route + '/(gen)/search/search20': typeof genSearchSearch20Route + '/(gen)/search/search21': typeof genSearchSearch21Route + '/(gen)/search/search22': typeof genSearchSearch22Route + '/(gen)/search/search23': typeof genSearchSearch23Route + '/(gen)/search/search24': typeof genSearchSearch24Route + '/(gen)/search/search25': typeof genSearchSearch25Route + '/(gen)/search/search26': typeof genSearchSearch26Route + '/(gen)/search/search27': typeof genSearchSearch27Route + '/(gen)/search/search28': typeof genSearchSearch28Route + '/(gen)/search/search29': typeof genSearchSearch29Route + '/(gen)/search/search3': typeof genSearchSearch3Route + '/(gen)/search/search30': typeof genSearchSearch30Route + '/(gen)/search/search31': typeof genSearchSearch31Route + '/(gen)/search/search32': typeof genSearchSearch32Route + '/(gen)/search/search33': typeof genSearchSearch33Route + '/(gen)/search/search34': typeof genSearchSearch34Route + '/(gen)/search/search35': typeof genSearchSearch35Route + '/(gen)/search/search36': typeof genSearchSearch36Route + '/(gen)/search/search37': typeof genSearchSearch37Route + '/(gen)/search/search38': typeof genSearchSearch38Route + '/(gen)/search/search39': typeof genSearchSearch39Route + '/(gen)/search/search4': typeof genSearchSearch4Route + '/(gen)/search/search40': typeof genSearchSearch40Route + '/(gen)/search/search41': typeof genSearchSearch41Route + '/(gen)/search/search42': typeof genSearchSearch42Route + '/(gen)/search/search43': typeof genSearchSearch43Route + '/(gen)/search/search44': typeof genSearchSearch44Route + '/(gen)/search/search45': typeof genSearchSearch45Route + '/(gen)/search/search46': typeof genSearchSearch46Route + '/(gen)/search/search47': typeof genSearchSearch47Route + '/(gen)/search/search48': typeof genSearchSearch48Route + '/(gen)/search/search49': typeof genSearchSearch49Route + '/(gen)/search/search5': typeof genSearchSearch5Route + '/(gen)/search/search50': typeof genSearchSearch50Route + '/(gen)/search/search51': typeof genSearchSearch51Route + '/(gen)/search/search52': typeof genSearchSearch52Route + '/(gen)/search/search53': typeof genSearchSearch53Route + '/(gen)/search/search54': typeof genSearchSearch54Route + '/(gen)/search/search55': typeof genSearchSearch55Route + '/(gen)/search/search56': typeof genSearchSearch56Route + '/(gen)/search/search57': typeof genSearchSearch57Route + '/(gen)/search/search58': typeof genSearchSearch58Route + '/(gen)/search/search59': typeof genSearchSearch59Route + '/(gen)/search/search6': typeof genSearchSearch6Route + '/(gen)/search/search60': typeof genSearchSearch60Route + '/(gen)/search/search61': typeof genSearchSearch61Route + '/(gen)/search/search62': typeof genSearchSearch62Route + '/(gen)/search/search63': typeof genSearchSearch63Route + '/(gen)/search/search64': typeof genSearchSearch64Route + '/(gen)/search/search65': typeof genSearchSearch65Route + '/(gen)/search/search66': typeof genSearchSearch66Route + '/(gen)/search/search67': typeof genSearchSearch67Route + '/(gen)/search/search68': typeof genSearchSearch68Route + '/(gen)/search/search69': typeof genSearchSearch69Route + '/(gen)/search/search7': typeof genSearchSearch7Route + '/(gen)/search/search70': typeof genSearchSearch70Route + '/(gen)/search/search71': typeof genSearchSearch71Route + '/(gen)/search/search72': typeof genSearchSearch72Route + '/(gen)/search/search73': typeof genSearchSearch73Route + '/(gen)/search/search74': typeof genSearchSearch74Route + '/(gen)/search/search75': typeof genSearchSearch75Route + '/(gen)/search/search76': typeof genSearchSearch76Route + '/(gen)/search/search77': typeof genSearchSearch77Route + '/(gen)/search/search78': typeof genSearchSearch78Route + '/(gen)/search/search79': typeof genSearchSearch79Route + '/(gen)/search/search8': typeof genSearchSearch8Route + '/(gen)/search/search80': typeof genSearchSearch80Route + '/(gen)/search/search81': typeof genSearchSearch81Route + '/(gen)/search/search82': typeof genSearchSearch82Route + '/(gen)/search/search83': typeof genSearchSearch83Route + '/(gen)/search/search84': typeof genSearchSearch84Route + '/(gen)/search/search85': typeof genSearchSearch85Route + '/(gen)/search/search86': typeof genSearchSearch86Route + '/(gen)/search/search87': typeof genSearchSearch87Route + '/(gen)/search/search88': typeof genSearchSearch88Route + '/(gen)/search/search89': typeof genSearchSearch89Route + '/(gen)/search/search9': typeof genSearchSearch9Route + '/(gen)/search/search90': typeof genSearchSearch90Route + '/(gen)/search/search91': typeof genSearchSearch91Route + '/(gen)/search/search92': typeof genSearchSearch92Route + '/(gen)/search/search93': typeof genSearchSearch93Route + '/(gen)/search/search94': typeof genSearchSearch94Route + '/(gen)/search/search95': typeof genSearchSearch95Route + '/(gen)/search/search96': typeof genSearchSearch96Route + '/(gen)/search/search97': typeof genSearchSearch97Route + '/(gen)/search/search98': typeof genSearchSearch98Route + '/(gen)/search/search99': typeof genSearchSearch99Route } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -99,8 +3713,408 @@ export interface FileRouteTypes { | '/absolute' | '/linkProps' | '/relative' + | '/absolute0' + | '/absolute1' + | '/absolute10' + | '/absolute11' + | '/absolute12' + | '/absolute13' + | '/absolute14' + | '/absolute15' + | '/absolute16' + | '/absolute17' + | '/absolute18' + | '/absolute19' + | '/absolute2' + | '/absolute20' + | '/absolute21' + | '/absolute22' + | '/absolute23' + | '/absolute24' + | '/absolute25' + | '/absolute26' + | '/absolute27' + | '/absolute28' + | '/absolute29' + | '/absolute3' + | '/absolute30' + | '/absolute31' + | '/absolute32' + | '/absolute33' + | '/absolute34' + | '/absolute35' + | '/absolute36' + | '/absolute37' + | '/absolute38' + | '/absolute39' + | '/absolute4' + | '/absolute40' + | '/absolute41' + | '/absolute42' + | '/absolute43' + | '/absolute44' + | '/absolute45' + | '/absolute46' + | '/absolute47' + | '/absolute48' + | '/absolute49' + | '/absolute5' + | '/absolute50' + | '/absolute51' + | '/absolute52' + | '/absolute53' + | '/absolute54' + | '/absolute55' + | '/absolute56' + | '/absolute57' + | '/absolute58' + | '/absolute59' + | '/absolute6' + | '/absolute60' + | '/absolute61' + | '/absolute62' + | '/absolute63' + | '/absolute64' + | '/absolute65' + | '/absolute66' + | '/absolute67' + | '/absolute68' + | '/absolute69' + | '/absolute7' + | '/absolute70' + | '/absolute71' + | '/absolute72' + | '/absolute73' + | '/absolute74' + | '/absolute75' + | '/absolute76' + | '/absolute77' + | '/absolute78' + | '/absolute79' + | '/absolute8' + | '/absolute80' + | '/absolute81' + | '/absolute82' + | '/absolute83' + | '/absolute84' + | '/absolute85' + | '/absolute86' + | '/absolute87' + | '/absolute88' + | '/absolute89' + | '/absolute9' + | '/absolute90' + | '/absolute91' + | '/absolute92' + | '/absolute93' + | '/absolute94' + | '/absolute95' + | '/absolute96' + | '/absolute97' + | '/absolute98' + | '/absolute99' + | '/relative0' + | '/relative1' + | '/relative10' + | '/relative11' + | '/relative12' + | '/relative13' + | '/relative14' + | '/relative15' + | '/relative16' + | '/relative17' + | '/relative18' + | '/relative19' + | '/relative2' + | '/relative20' + | '/relative21' + | '/relative22' + | '/relative23' + | '/relative24' + | '/relative25' + | '/relative26' + | '/relative27' + | '/relative28' + | '/relative29' + | '/relative3' + | '/relative30' + | '/relative31' + | '/relative32' + | '/relative33' + | '/relative34' + | '/relative35' + | '/relative36' + | '/relative37' + | '/relative38' + | '/relative39' + | '/relative4' + | '/relative40' + | '/relative41' + | '/relative42' + | '/relative43' + | '/relative44' + | '/relative45' + | '/relative46' + | '/relative47' + | '/relative48' + | '/relative49' + | '/relative5' + | '/relative50' + | '/relative51' + | '/relative52' + | '/relative53' + | '/relative54' + | '/relative55' + | '/relative56' + | '/relative57' + | '/relative58' + | '/relative59' + | '/relative6' + | '/relative60' + | '/relative61' + | '/relative62' + | '/relative63' + | '/relative64' + | '/relative65' + | '/relative66' + | '/relative67' + | '/relative68' + | '/relative69' + | '/relative7' + | '/relative70' + | '/relative71' + | '/relative72' + | '/relative73' + | '/relative74' + | '/relative75' + | '/relative76' + | '/relative77' + | '/relative78' + | '/relative79' + | '/relative8' + | '/relative80' + | '/relative81' + | '/relative82' + | '/relative83' + | '/relative84' + | '/relative85' + | '/relative86' + | '/relative87' + | '/relative88' + | '/relative89' + | '/relative9' + | '/relative90' + | '/relative91' + | '/relative92' + | '/relative93' + | '/relative94' + | '/relative95' + | '/relative96' + | '/relative97' + | '/relative98' + | '/relative99' | '/params/$paramsPlaceholder' | '/search/searchPlaceholder' + | '/params/$param0' + | '/params/$param1' + | '/params/$param10' + | '/params/$param11' + | '/params/$param12' + | '/params/$param13' + | '/params/$param14' + | '/params/$param15' + | '/params/$param16' + | '/params/$param17' + | '/params/$param18' + | '/params/$param19' + | '/params/$param2' + | '/params/$param20' + | '/params/$param21' + | '/params/$param22' + | '/params/$param23' + | '/params/$param24' + | '/params/$param25' + | '/params/$param26' + | '/params/$param27' + | '/params/$param28' + | '/params/$param29' + | '/params/$param3' + | '/params/$param30' + | '/params/$param31' + | '/params/$param32' + | '/params/$param33' + | '/params/$param34' + | '/params/$param35' + | '/params/$param36' + | '/params/$param37' + | '/params/$param38' + | '/params/$param39' + | '/params/$param4' + | '/params/$param40' + | '/params/$param41' + | '/params/$param42' + | '/params/$param43' + | '/params/$param44' + | '/params/$param45' + | '/params/$param46' + | '/params/$param47' + | '/params/$param48' + | '/params/$param49' + | '/params/$param5' + | '/params/$param50' + | '/params/$param51' + | '/params/$param52' + | '/params/$param53' + | '/params/$param54' + | '/params/$param55' + | '/params/$param56' + | '/params/$param57' + | '/params/$param58' + | '/params/$param59' + | '/params/$param6' + | '/params/$param60' + | '/params/$param61' + | '/params/$param62' + | '/params/$param63' + | '/params/$param64' + | '/params/$param65' + | '/params/$param66' + | '/params/$param67' + | '/params/$param68' + | '/params/$param69' + | '/params/$param7' + | '/params/$param70' + | '/params/$param71' + | '/params/$param72' + | '/params/$param73' + | '/params/$param74' + | '/params/$param75' + | '/params/$param76' + | '/params/$param77' + | '/params/$param78' + | '/params/$param79' + | '/params/$param8' + | '/params/$param80' + | '/params/$param81' + | '/params/$param82' + | '/params/$param83' + | '/params/$param84' + | '/params/$param85' + | '/params/$param86' + | '/params/$param87' + | '/params/$param88' + | '/params/$param89' + | '/params/$param9' + | '/params/$param90' + | '/params/$param91' + | '/params/$param92' + | '/params/$param93' + | '/params/$param94' + | '/params/$param95' + | '/params/$param96' + | '/params/$param97' + | '/params/$param98' + | '/params/$param99' + | '/search/search0' + | '/search/search1' + | '/search/search10' + | '/search/search11' + | '/search/search12' + | '/search/search13' + | '/search/search14' + | '/search/search15' + | '/search/search16' + | '/search/search17' + | '/search/search18' + | '/search/search19' + | '/search/search2' + | '/search/search20' + | '/search/search21' + | '/search/search22' + | '/search/search23' + | '/search/search24' + | '/search/search25' + | '/search/search26' + | '/search/search27' + | '/search/search28' + | '/search/search29' + | '/search/search3' + | '/search/search30' + | '/search/search31' + | '/search/search32' + | '/search/search33' + | '/search/search34' + | '/search/search35' + | '/search/search36' + | '/search/search37' + | '/search/search38' + | '/search/search39' + | '/search/search4' + | '/search/search40' + | '/search/search41' + | '/search/search42' + | '/search/search43' + | '/search/search44' + | '/search/search45' + | '/search/search46' + | '/search/search47' + | '/search/search48' + | '/search/search49' + | '/search/search5' + | '/search/search50' + | '/search/search51' + | '/search/search52' + | '/search/search53' + | '/search/search54' + | '/search/search55' + | '/search/search56' + | '/search/search57' + | '/search/search58' + | '/search/search59' + | '/search/search6' + | '/search/search60' + | '/search/search61' + | '/search/search62' + | '/search/search63' + | '/search/search64' + | '/search/search65' + | '/search/search66' + | '/search/search67' + | '/search/search68' + | '/search/search69' + | '/search/search7' + | '/search/search70' + | '/search/search71' + | '/search/search72' + | '/search/search73' + | '/search/search74' + | '/search/search75' + | '/search/search76' + | '/search/search77' + | '/search/search78' + | '/search/search79' + | '/search/search8' + | '/search/search80' + | '/search/search81' + | '/search/search82' + | '/search/search83' + | '/search/search84' + | '/search/search85' + | '/search/search86' + | '/search/search87' + | '/search/search88' + | '/search/search89' + | '/search/search9' + | '/search/search90' + | '/search/search91' + | '/search/search92' + | '/search/search93' + | '/search/search94' + | '/search/search95' + | '/search/search96' + | '/search/search97' + | '/search/search98' + | '/search/search99' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -109,8 +4123,408 @@ export interface FileRouteTypes { | '/absolute' | '/linkProps' | '/relative' + | '/absolute0' + | '/absolute1' + | '/absolute10' + | '/absolute11' + | '/absolute12' + | '/absolute13' + | '/absolute14' + | '/absolute15' + | '/absolute16' + | '/absolute17' + | '/absolute18' + | '/absolute19' + | '/absolute2' + | '/absolute20' + | '/absolute21' + | '/absolute22' + | '/absolute23' + | '/absolute24' + | '/absolute25' + | '/absolute26' + | '/absolute27' + | '/absolute28' + | '/absolute29' + | '/absolute3' + | '/absolute30' + | '/absolute31' + | '/absolute32' + | '/absolute33' + | '/absolute34' + | '/absolute35' + | '/absolute36' + | '/absolute37' + | '/absolute38' + | '/absolute39' + | '/absolute4' + | '/absolute40' + | '/absolute41' + | '/absolute42' + | '/absolute43' + | '/absolute44' + | '/absolute45' + | '/absolute46' + | '/absolute47' + | '/absolute48' + | '/absolute49' + | '/absolute5' + | '/absolute50' + | '/absolute51' + | '/absolute52' + | '/absolute53' + | '/absolute54' + | '/absolute55' + | '/absolute56' + | '/absolute57' + | '/absolute58' + | '/absolute59' + | '/absolute6' + | '/absolute60' + | '/absolute61' + | '/absolute62' + | '/absolute63' + | '/absolute64' + | '/absolute65' + | '/absolute66' + | '/absolute67' + | '/absolute68' + | '/absolute69' + | '/absolute7' + | '/absolute70' + | '/absolute71' + | '/absolute72' + | '/absolute73' + | '/absolute74' + | '/absolute75' + | '/absolute76' + | '/absolute77' + | '/absolute78' + | '/absolute79' + | '/absolute8' + | '/absolute80' + | '/absolute81' + | '/absolute82' + | '/absolute83' + | '/absolute84' + | '/absolute85' + | '/absolute86' + | '/absolute87' + | '/absolute88' + | '/absolute89' + | '/absolute9' + | '/absolute90' + | '/absolute91' + | '/absolute92' + | '/absolute93' + | '/absolute94' + | '/absolute95' + | '/absolute96' + | '/absolute97' + | '/absolute98' + | '/absolute99' + | '/relative0' + | '/relative1' + | '/relative10' + | '/relative11' + | '/relative12' + | '/relative13' + | '/relative14' + | '/relative15' + | '/relative16' + | '/relative17' + | '/relative18' + | '/relative19' + | '/relative2' + | '/relative20' + | '/relative21' + | '/relative22' + | '/relative23' + | '/relative24' + | '/relative25' + | '/relative26' + | '/relative27' + | '/relative28' + | '/relative29' + | '/relative3' + | '/relative30' + | '/relative31' + | '/relative32' + | '/relative33' + | '/relative34' + | '/relative35' + | '/relative36' + | '/relative37' + | '/relative38' + | '/relative39' + | '/relative4' + | '/relative40' + | '/relative41' + | '/relative42' + | '/relative43' + | '/relative44' + | '/relative45' + | '/relative46' + | '/relative47' + | '/relative48' + | '/relative49' + | '/relative5' + | '/relative50' + | '/relative51' + | '/relative52' + | '/relative53' + | '/relative54' + | '/relative55' + | '/relative56' + | '/relative57' + | '/relative58' + | '/relative59' + | '/relative6' + | '/relative60' + | '/relative61' + | '/relative62' + | '/relative63' + | '/relative64' + | '/relative65' + | '/relative66' + | '/relative67' + | '/relative68' + | '/relative69' + | '/relative7' + | '/relative70' + | '/relative71' + | '/relative72' + | '/relative73' + | '/relative74' + | '/relative75' + | '/relative76' + | '/relative77' + | '/relative78' + | '/relative79' + | '/relative8' + | '/relative80' + | '/relative81' + | '/relative82' + | '/relative83' + | '/relative84' + | '/relative85' + | '/relative86' + | '/relative87' + | '/relative88' + | '/relative89' + | '/relative9' + | '/relative90' + | '/relative91' + | '/relative92' + | '/relative93' + | '/relative94' + | '/relative95' + | '/relative96' + | '/relative97' + | '/relative98' + | '/relative99' | '/params/$paramsPlaceholder' | '/search/searchPlaceholder' + | '/params/$param0' + | '/params/$param1' + | '/params/$param10' + | '/params/$param11' + | '/params/$param12' + | '/params/$param13' + | '/params/$param14' + | '/params/$param15' + | '/params/$param16' + | '/params/$param17' + | '/params/$param18' + | '/params/$param19' + | '/params/$param2' + | '/params/$param20' + | '/params/$param21' + | '/params/$param22' + | '/params/$param23' + | '/params/$param24' + | '/params/$param25' + | '/params/$param26' + | '/params/$param27' + | '/params/$param28' + | '/params/$param29' + | '/params/$param3' + | '/params/$param30' + | '/params/$param31' + | '/params/$param32' + | '/params/$param33' + | '/params/$param34' + | '/params/$param35' + | '/params/$param36' + | '/params/$param37' + | '/params/$param38' + | '/params/$param39' + | '/params/$param4' + | '/params/$param40' + | '/params/$param41' + | '/params/$param42' + | '/params/$param43' + | '/params/$param44' + | '/params/$param45' + | '/params/$param46' + | '/params/$param47' + | '/params/$param48' + | '/params/$param49' + | '/params/$param5' + | '/params/$param50' + | '/params/$param51' + | '/params/$param52' + | '/params/$param53' + | '/params/$param54' + | '/params/$param55' + | '/params/$param56' + | '/params/$param57' + | '/params/$param58' + | '/params/$param59' + | '/params/$param6' + | '/params/$param60' + | '/params/$param61' + | '/params/$param62' + | '/params/$param63' + | '/params/$param64' + | '/params/$param65' + | '/params/$param66' + | '/params/$param67' + | '/params/$param68' + | '/params/$param69' + | '/params/$param7' + | '/params/$param70' + | '/params/$param71' + | '/params/$param72' + | '/params/$param73' + | '/params/$param74' + | '/params/$param75' + | '/params/$param76' + | '/params/$param77' + | '/params/$param78' + | '/params/$param79' + | '/params/$param8' + | '/params/$param80' + | '/params/$param81' + | '/params/$param82' + | '/params/$param83' + | '/params/$param84' + | '/params/$param85' + | '/params/$param86' + | '/params/$param87' + | '/params/$param88' + | '/params/$param89' + | '/params/$param9' + | '/params/$param90' + | '/params/$param91' + | '/params/$param92' + | '/params/$param93' + | '/params/$param94' + | '/params/$param95' + | '/params/$param96' + | '/params/$param97' + | '/params/$param98' + | '/params/$param99' + | '/search/search0' + | '/search/search1' + | '/search/search10' + | '/search/search11' + | '/search/search12' + | '/search/search13' + | '/search/search14' + | '/search/search15' + | '/search/search16' + | '/search/search17' + | '/search/search18' + | '/search/search19' + | '/search/search2' + | '/search/search20' + | '/search/search21' + | '/search/search22' + | '/search/search23' + | '/search/search24' + | '/search/search25' + | '/search/search26' + | '/search/search27' + | '/search/search28' + | '/search/search29' + | '/search/search3' + | '/search/search30' + | '/search/search31' + | '/search/search32' + | '/search/search33' + | '/search/search34' + | '/search/search35' + | '/search/search36' + | '/search/search37' + | '/search/search38' + | '/search/search39' + | '/search/search4' + | '/search/search40' + | '/search/search41' + | '/search/search42' + | '/search/search43' + | '/search/search44' + | '/search/search45' + | '/search/search46' + | '/search/search47' + | '/search/search48' + | '/search/search49' + | '/search/search5' + | '/search/search50' + | '/search/search51' + | '/search/search52' + | '/search/search53' + | '/search/search54' + | '/search/search55' + | '/search/search56' + | '/search/search57' + | '/search/search58' + | '/search/search59' + | '/search/search6' + | '/search/search60' + | '/search/search61' + | '/search/search62' + | '/search/search63' + | '/search/search64' + | '/search/search65' + | '/search/search66' + | '/search/search67' + | '/search/search68' + | '/search/search69' + | '/search/search7' + | '/search/search70' + | '/search/search71' + | '/search/search72' + | '/search/search73' + | '/search/search74' + | '/search/search75' + | '/search/search76' + | '/search/search77' + | '/search/search78' + | '/search/search79' + | '/search/search8' + | '/search/search80' + | '/search/search81' + | '/search/search82' + | '/search/search83' + | '/search/search84' + | '/search/search85' + | '/search/search86' + | '/search/search87' + | '/search/search88' + | '/search/search89' + | '/search/search9' + | '/search/search90' + | '/search/search91' + | '/search/search92' + | '/search/search93' + | '/search/search94' + | '/search/search95' + | '/search/search96' + | '/search/search97' + | '/search/search98' + | '/search/search99' id: | '__root__' | '/' @@ -119,8 +4533,410 @@ export interface FileRouteTypes { | '/absolute' | '/linkProps' | '/relative' + | '/(gen)/params' + | '/(gen)/search' + | '/(gen)/absolute0' + | '/(gen)/absolute1' + | '/(gen)/absolute10' + | '/(gen)/absolute11' + | '/(gen)/absolute12' + | '/(gen)/absolute13' + | '/(gen)/absolute14' + | '/(gen)/absolute15' + | '/(gen)/absolute16' + | '/(gen)/absolute17' + | '/(gen)/absolute18' + | '/(gen)/absolute19' + | '/(gen)/absolute2' + | '/(gen)/absolute20' + | '/(gen)/absolute21' + | '/(gen)/absolute22' + | '/(gen)/absolute23' + | '/(gen)/absolute24' + | '/(gen)/absolute25' + | '/(gen)/absolute26' + | '/(gen)/absolute27' + | '/(gen)/absolute28' + | '/(gen)/absolute29' + | '/(gen)/absolute3' + | '/(gen)/absolute30' + | '/(gen)/absolute31' + | '/(gen)/absolute32' + | '/(gen)/absolute33' + | '/(gen)/absolute34' + | '/(gen)/absolute35' + | '/(gen)/absolute36' + | '/(gen)/absolute37' + | '/(gen)/absolute38' + | '/(gen)/absolute39' + | '/(gen)/absolute4' + | '/(gen)/absolute40' + | '/(gen)/absolute41' + | '/(gen)/absolute42' + | '/(gen)/absolute43' + | '/(gen)/absolute44' + | '/(gen)/absolute45' + | '/(gen)/absolute46' + | '/(gen)/absolute47' + | '/(gen)/absolute48' + | '/(gen)/absolute49' + | '/(gen)/absolute5' + | '/(gen)/absolute50' + | '/(gen)/absolute51' + | '/(gen)/absolute52' + | '/(gen)/absolute53' + | '/(gen)/absolute54' + | '/(gen)/absolute55' + | '/(gen)/absolute56' + | '/(gen)/absolute57' + | '/(gen)/absolute58' + | '/(gen)/absolute59' + | '/(gen)/absolute6' + | '/(gen)/absolute60' + | '/(gen)/absolute61' + | '/(gen)/absolute62' + | '/(gen)/absolute63' + | '/(gen)/absolute64' + | '/(gen)/absolute65' + | '/(gen)/absolute66' + | '/(gen)/absolute67' + | '/(gen)/absolute68' + | '/(gen)/absolute69' + | '/(gen)/absolute7' + | '/(gen)/absolute70' + | '/(gen)/absolute71' + | '/(gen)/absolute72' + | '/(gen)/absolute73' + | '/(gen)/absolute74' + | '/(gen)/absolute75' + | '/(gen)/absolute76' + | '/(gen)/absolute77' + | '/(gen)/absolute78' + | '/(gen)/absolute79' + | '/(gen)/absolute8' + | '/(gen)/absolute80' + | '/(gen)/absolute81' + | '/(gen)/absolute82' + | '/(gen)/absolute83' + | '/(gen)/absolute84' + | '/(gen)/absolute85' + | '/(gen)/absolute86' + | '/(gen)/absolute87' + | '/(gen)/absolute88' + | '/(gen)/absolute89' + | '/(gen)/absolute9' + | '/(gen)/absolute90' + | '/(gen)/absolute91' + | '/(gen)/absolute92' + | '/(gen)/absolute93' + | '/(gen)/absolute94' + | '/(gen)/absolute95' + | '/(gen)/absolute96' + | '/(gen)/absolute97' + | '/(gen)/absolute98' + | '/(gen)/absolute99' + | '/(gen)/relative0' + | '/(gen)/relative1' + | '/(gen)/relative10' + | '/(gen)/relative11' + | '/(gen)/relative12' + | '/(gen)/relative13' + | '/(gen)/relative14' + | '/(gen)/relative15' + | '/(gen)/relative16' + | '/(gen)/relative17' + | '/(gen)/relative18' + | '/(gen)/relative19' + | '/(gen)/relative2' + | '/(gen)/relative20' + | '/(gen)/relative21' + | '/(gen)/relative22' + | '/(gen)/relative23' + | '/(gen)/relative24' + | '/(gen)/relative25' + | '/(gen)/relative26' + | '/(gen)/relative27' + | '/(gen)/relative28' + | '/(gen)/relative29' + | '/(gen)/relative3' + | '/(gen)/relative30' + | '/(gen)/relative31' + | '/(gen)/relative32' + | '/(gen)/relative33' + | '/(gen)/relative34' + | '/(gen)/relative35' + | '/(gen)/relative36' + | '/(gen)/relative37' + | '/(gen)/relative38' + | '/(gen)/relative39' + | '/(gen)/relative4' + | '/(gen)/relative40' + | '/(gen)/relative41' + | '/(gen)/relative42' + | '/(gen)/relative43' + | '/(gen)/relative44' + | '/(gen)/relative45' + | '/(gen)/relative46' + | '/(gen)/relative47' + | '/(gen)/relative48' + | '/(gen)/relative49' + | '/(gen)/relative5' + | '/(gen)/relative50' + | '/(gen)/relative51' + | '/(gen)/relative52' + | '/(gen)/relative53' + | '/(gen)/relative54' + | '/(gen)/relative55' + | '/(gen)/relative56' + | '/(gen)/relative57' + | '/(gen)/relative58' + | '/(gen)/relative59' + | '/(gen)/relative6' + | '/(gen)/relative60' + | '/(gen)/relative61' + | '/(gen)/relative62' + | '/(gen)/relative63' + | '/(gen)/relative64' + | '/(gen)/relative65' + | '/(gen)/relative66' + | '/(gen)/relative67' + | '/(gen)/relative68' + | '/(gen)/relative69' + | '/(gen)/relative7' + | '/(gen)/relative70' + | '/(gen)/relative71' + | '/(gen)/relative72' + | '/(gen)/relative73' + | '/(gen)/relative74' + | '/(gen)/relative75' + | '/(gen)/relative76' + | '/(gen)/relative77' + | '/(gen)/relative78' + | '/(gen)/relative79' + | '/(gen)/relative8' + | '/(gen)/relative80' + | '/(gen)/relative81' + | '/(gen)/relative82' + | '/(gen)/relative83' + | '/(gen)/relative84' + | '/(gen)/relative85' + | '/(gen)/relative86' + | '/(gen)/relative87' + | '/(gen)/relative88' + | '/(gen)/relative89' + | '/(gen)/relative9' + | '/(gen)/relative90' + | '/(gen)/relative91' + | '/(gen)/relative92' + | '/(gen)/relative93' + | '/(gen)/relative94' + | '/(gen)/relative95' + | '/(gen)/relative96' + | '/(gen)/relative97' + | '/(gen)/relative98' + | '/(gen)/relative99' | '/params/$paramsPlaceholder' | '/search/searchPlaceholder' + | '/(gen)/params/$param0' + | '/(gen)/params/$param1' + | '/(gen)/params/$param10' + | '/(gen)/params/$param11' + | '/(gen)/params/$param12' + | '/(gen)/params/$param13' + | '/(gen)/params/$param14' + | '/(gen)/params/$param15' + | '/(gen)/params/$param16' + | '/(gen)/params/$param17' + | '/(gen)/params/$param18' + | '/(gen)/params/$param19' + | '/(gen)/params/$param2' + | '/(gen)/params/$param20' + | '/(gen)/params/$param21' + | '/(gen)/params/$param22' + | '/(gen)/params/$param23' + | '/(gen)/params/$param24' + | '/(gen)/params/$param25' + | '/(gen)/params/$param26' + | '/(gen)/params/$param27' + | '/(gen)/params/$param28' + | '/(gen)/params/$param29' + | '/(gen)/params/$param3' + | '/(gen)/params/$param30' + | '/(gen)/params/$param31' + | '/(gen)/params/$param32' + | '/(gen)/params/$param33' + | '/(gen)/params/$param34' + | '/(gen)/params/$param35' + | '/(gen)/params/$param36' + | '/(gen)/params/$param37' + | '/(gen)/params/$param38' + | '/(gen)/params/$param39' + | '/(gen)/params/$param4' + | '/(gen)/params/$param40' + | '/(gen)/params/$param41' + | '/(gen)/params/$param42' + | '/(gen)/params/$param43' + | '/(gen)/params/$param44' + | '/(gen)/params/$param45' + | '/(gen)/params/$param46' + | '/(gen)/params/$param47' + | '/(gen)/params/$param48' + | '/(gen)/params/$param49' + | '/(gen)/params/$param5' + | '/(gen)/params/$param50' + | '/(gen)/params/$param51' + | '/(gen)/params/$param52' + | '/(gen)/params/$param53' + | '/(gen)/params/$param54' + | '/(gen)/params/$param55' + | '/(gen)/params/$param56' + | '/(gen)/params/$param57' + | '/(gen)/params/$param58' + | '/(gen)/params/$param59' + | '/(gen)/params/$param6' + | '/(gen)/params/$param60' + | '/(gen)/params/$param61' + | '/(gen)/params/$param62' + | '/(gen)/params/$param63' + | '/(gen)/params/$param64' + | '/(gen)/params/$param65' + | '/(gen)/params/$param66' + | '/(gen)/params/$param67' + | '/(gen)/params/$param68' + | '/(gen)/params/$param69' + | '/(gen)/params/$param7' + | '/(gen)/params/$param70' + | '/(gen)/params/$param71' + | '/(gen)/params/$param72' + | '/(gen)/params/$param73' + | '/(gen)/params/$param74' + | '/(gen)/params/$param75' + | '/(gen)/params/$param76' + | '/(gen)/params/$param77' + | '/(gen)/params/$param78' + | '/(gen)/params/$param79' + | '/(gen)/params/$param8' + | '/(gen)/params/$param80' + | '/(gen)/params/$param81' + | '/(gen)/params/$param82' + | '/(gen)/params/$param83' + | '/(gen)/params/$param84' + | '/(gen)/params/$param85' + | '/(gen)/params/$param86' + | '/(gen)/params/$param87' + | '/(gen)/params/$param88' + | '/(gen)/params/$param89' + | '/(gen)/params/$param9' + | '/(gen)/params/$param90' + | '/(gen)/params/$param91' + | '/(gen)/params/$param92' + | '/(gen)/params/$param93' + | '/(gen)/params/$param94' + | '/(gen)/params/$param95' + | '/(gen)/params/$param96' + | '/(gen)/params/$param97' + | '/(gen)/params/$param98' + | '/(gen)/params/$param99' + | '/(gen)/search/search0' + | '/(gen)/search/search1' + | '/(gen)/search/search10' + | '/(gen)/search/search11' + | '/(gen)/search/search12' + | '/(gen)/search/search13' + | '/(gen)/search/search14' + | '/(gen)/search/search15' + | '/(gen)/search/search16' + | '/(gen)/search/search17' + | '/(gen)/search/search18' + | '/(gen)/search/search19' + | '/(gen)/search/search2' + | '/(gen)/search/search20' + | '/(gen)/search/search21' + | '/(gen)/search/search22' + | '/(gen)/search/search23' + | '/(gen)/search/search24' + | '/(gen)/search/search25' + | '/(gen)/search/search26' + | '/(gen)/search/search27' + | '/(gen)/search/search28' + | '/(gen)/search/search29' + | '/(gen)/search/search3' + | '/(gen)/search/search30' + | '/(gen)/search/search31' + | '/(gen)/search/search32' + | '/(gen)/search/search33' + | '/(gen)/search/search34' + | '/(gen)/search/search35' + | '/(gen)/search/search36' + | '/(gen)/search/search37' + | '/(gen)/search/search38' + | '/(gen)/search/search39' + | '/(gen)/search/search4' + | '/(gen)/search/search40' + | '/(gen)/search/search41' + | '/(gen)/search/search42' + | '/(gen)/search/search43' + | '/(gen)/search/search44' + | '/(gen)/search/search45' + | '/(gen)/search/search46' + | '/(gen)/search/search47' + | '/(gen)/search/search48' + | '/(gen)/search/search49' + | '/(gen)/search/search5' + | '/(gen)/search/search50' + | '/(gen)/search/search51' + | '/(gen)/search/search52' + | '/(gen)/search/search53' + | '/(gen)/search/search54' + | '/(gen)/search/search55' + | '/(gen)/search/search56' + | '/(gen)/search/search57' + | '/(gen)/search/search58' + | '/(gen)/search/search59' + | '/(gen)/search/search6' + | '/(gen)/search/search60' + | '/(gen)/search/search61' + | '/(gen)/search/search62' + | '/(gen)/search/search63' + | '/(gen)/search/search64' + | '/(gen)/search/search65' + | '/(gen)/search/search66' + | '/(gen)/search/search67' + | '/(gen)/search/search68' + | '/(gen)/search/search69' + | '/(gen)/search/search7' + | '/(gen)/search/search70' + | '/(gen)/search/search71' + | '/(gen)/search/search72' + | '/(gen)/search/search73' + | '/(gen)/search/search74' + | '/(gen)/search/search75' + | '/(gen)/search/search76' + | '/(gen)/search/search77' + | '/(gen)/search/search78' + | '/(gen)/search/search79' + | '/(gen)/search/search8' + | '/(gen)/search/search80' + | '/(gen)/search/search81' + | '/(gen)/search/search82' + | '/(gen)/search/search83' + | '/(gen)/search/search84' + | '/(gen)/search/search85' + | '/(gen)/search/search86' + | '/(gen)/search/search87' + | '/(gen)/search/search88' + | '/(gen)/search/search89' + | '/(gen)/search/search9' + | '/(gen)/search/search90' + | '/(gen)/search/search91' + | '/(gen)/search/search92' + | '/(gen)/search/search93' + | '/(gen)/search/search94' + | '/(gen)/search/search95' + | '/(gen)/search/search96' + | '/(gen)/search/search97' + | '/(gen)/search/search98' + | '/(gen)/search/search99' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -130,6 +4946,208 @@ export interface RootRouteChildren { AbsoluteRoute: typeof AbsoluteRoute LinkPropsRoute: typeof LinkPropsRoute RelativeRoute: typeof RelativeRoute + genParamsRouteRoute: typeof genParamsRouteRouteWithChildren + genSearchRouteRoute: typeof genSearchRouteRouteWithChildren + genAbsolute0Route: typeof genAbsolute0Route + genAbsolute1Route: typeof genAbsolute1Route + genAbsolute10Route: typeof genAbsolute10Route + genAbsolute11Route: typeof genAbsolute11Route + genAbsolute12Route: typeof genAbsolute12Route + genAbsolute13Route: typeof genAbsolute13Route + genAbsolute14Route: typeof genAbsolute14Route + genAbsolute15Route: typeof genAbsolute15Route + genAbsolute16Route: typeof genAbsolute16Route + genAbsolute17Route: typeof genAbsolute17Route + genAbsolute18Route: typeof genAbsolute18Route + genAbsolute19Route: typeof genAbsolute19Route + genAbsolute2Route: typeof genAbsolute2Route + genAbsolute20Route: typeof genAbsolute20Route + genAbsolute21Route: typeof genAbsolute21Route + genAbsolute22Route: typeof genAbsolute22Route + genAbsolute23Route: typeof genAbsolute23Route + genAbsolute24Route: typeof genAbsolute24Route + genAbsolute25Route: typeof genAbsolute25Route + genAbsolute26Route: typeof genAbsolute26Route + genAbsolute27Route: typeof genAbsolute27Route + genAbsolute28Route: typeof genAbsolute28Route + genAbsolute29Route: typeof genAbsolute29Route + genAbsolute3Route: typeof genAbsolute3Route + genAbsolute30Route: typeof genAbsolute30Route + genAbsolute31Route: typeof genAbsolute31Route + genAbsolute32Route: typeof genAbsolute32Route + genAbsolute33Route: typeof genAbsolute33Route + genAbsolute34Route: typeof genAbsolute34Route + genAbsolute35Route: typeof genAbsolute35Route + genAbsolute36Route: typeof genAbsolute36Route + genAbsolute37Route: typeof genAbsolute37Route + genAbsolute38Route: typeof genAbsolute38Route + genAbsolute39Route: typeof genAbsolute39Route + genAbsolute4Route: typeof genAbsolute4Route + genAbsolute40Route: typeof genAbsolute40Route + genAbsolute41Route: typeof genAbsolute41Route + genAbsolute42Route: typeof genAbsolute42Route + genAbsolute43Route: typeof genAbsolute43Route + genAbsolute44Route: typeof genAbsolute44Route + genAbsolute45Route: typeof genAbsolute45Route + genAbsolute46Route: typeof genAbsolute46Route + genAbsolute47Route: typeof genAbsolute47Route + genAbsolute48Route: typeof genAbsolute48Route + genAbsolute49Route: typeof genAbsolute49Route + genAbsolute5Route: typeof genAbsolute5Route + genAbsolute50Route: typeof genAbsolute50Route + genAbsolute51Route: typeof genAbsolute51Route + genAbsolute52Route: typeof genAbsolute52Route + genAbsolute53Route: typeof genAbsolute53Route + genAbsolute54Route: typeof genAbsolute54Route + genAbsolute55Route: typeof genAbsolute55Route + genAbsolute56Route: typeof genAbsolute56Route + genAbsolute57Route: typeof genAbsolute57Route + genAbsolute58Route: typeof genAbsolute58Route + genAbsolute59Route: typeof genAbsolute59Route + genAbsolute6Route: typeof genAbsolute6Route + genAbsolute60Route: typeof genAbsolute60Route + genAbsolute61Route: typeof genAbsolute61Route + genAbsolute62Route: typeof genAbsolute62Route + genAbsolute63Route: typeof genAbsolute63Route + genAbsolute64Route: typeof genAbsolute64Route + genAbsolute65Route: typeof genAbsolute65Route + genAbsolute66Route: typeof genAbsolute66Route + genAbsolute67Route: typeof genAbsolute67Route + genAbsolute68Route: typeof genAbsolute68Route + genAbsolute69Route: typeof genAbsolute69Route + genAbsolute7Route: typeof genAbsolute7Route + genAbsolute70Route: typeof genAbsolute70Route + genAbsolute71Route: typeof genAbsolute71Route + genAbsolute72Route: typeof genAbsolute72Route + genAbsolute73Route: typeof genAbsolute73Route + genAbsolute74Route: typeof genAbsolute74Route + genAbsolute75Route: typeof genAbsolute75Route + genAbsolute76Route: typeof genAbsolute76Route + genAbsolute77Route: typeof genAbsolute77Route + genAbsolute78Route: typeof genAbsolute78Route + genAbsolute79Route: typeof genAbsolute79Route + genAbsolute8Route: typeof genAbsolute8Route + genAbsolute80Route: typeof genAbsolute80Route + genAbsolute81Route: typeof genAbsolute81Route + genAbsolute82Route: typeof genAbsolute82Route + genAbsolute83Route: typeof genAbsolute83Route + genAbsolute84Route: typeof genAbsolute84Route + genAbsolute85Route: typeof genAbsolute85Route + genAbsolute86Route: typeof genAbsolute86Route + genAbsolute87Route: typeof genAbsolute87Route + genAbsolute88Route: typeof genAbsolute88Route + genAbsolute89Route: typeof genAbsolute89Route + genAbsolute9Route: typeof genAbsolute9Route + genAbsolute90Route: typeof genAbsolute90Route + genAbsolute91Route: typeof genAbsolute91Route + genAbsolute92Route: typeof genAbsolute92Route + genAbsolute93Route: typeof genAbsolute93Route + genAbsolute94Route: typeof genAbsolute94Route + genAbsolute95Route: typeof genAbsolute95Route + genAbsolute96Route: typeof genAbsolute96Route + genAbsolute97Route: typeof genAbsolute97Route + genAbsolute98Route: typeof genAbsolute98Route + genAbsolute99Route: typeof genAbsolute99Route + genRelative0Route: typeof genRelative0Route + genRelative1Route: typeof genRelative1Route + genRelative10Route: typeof genRelative10Route + genRelative11Route: typeof genRelative11Route + genRelative12Route: typeof genRelative12Route + genRelative13Route: typeof genRelative13Route + genRelative14Route: typeof genRelative14Route + genRelative15Route: typeof genRelative15Route + genRelative16Route: typeof genRelative16Route + genRelative17Route: typeof genRelative17Route + genRelative18Route: typeof genRelative18Route + genRelative19Route: typeof genRelative19Route + genRelative2Route: typeof genRelative2Route + genRelative20Route: typeof genRelative20Route + genRelative21Route: typeof genRelative21Route + genRelative22Route: typeof genRelative22Route + genRelative23Route: typeof genRelative23Route + genRelative24Route: typeof genRelative24Route + genRelative25Route: typeof genRelative25Route + genRelative26Route: typeof genRelative26Route + genRelative27Route: typeof genRelative27Route + genRelative28Route: typeof genRelative28Route + genRelative29Route: typeof genRelative29Route + genRelative3Route: typeof genRelative3Route + genRelative30Route: typeof genRelative30Route + genRelative31Route: typeof genRelative31Route + genRelative32Route: typeof genRelative32Route + genRelative33Route: typeof genRelative33Route + genRelative34Route: typeof genRelative34Route + genRelative35Route: typeof genRelative35Route + genRelative36Route: typeof genRelative36Route + genRelative37Route: typeof genRelative37Route + genRelative38Route: typeof genRelative38Route + genRelative39Route: typeof genRelative39Route + genRelative4Route: typeof genRelative4Route + genRelative40Route: typeof genRelative40Route + genRelative41Route: typeof genRelative41Route + genRelative42Route: typeof genRelative42Route + genRelative43Route: typeof genRelative43Route + genRelative44Route: typeof genRelative44Route + genRelative45Route: typeof genRelative45Route + genRelative46Route: typeof genRelative46Route + genRelative47Route: typeof genRelative47Route + genRelative48Route: typeof genRelative48Route + genRelative49Route: typeof genRelative49Route + genRelative5Route: typeof genRelative5Route + genRelative50Route: typeof genRelative50Route + genRelative51Route: typeof genRelative51Route + genRelative52Route: typeof genRelative52Route + genRelative53Route: typeof genRelative53Route + genRelative54Route: typeof genRelative54Route + genRelative55Route: typeof genRelative55Route + genRelative56Route: typeof genRelative56Route + genRelative57Route: typeof genRelative57Route + genRelative58Route: typeof genRelative58Route + genRelative59Route: typeof genRelative59Route + genRelative6Route: typeof genRelative6Route + genRelative60Route: typeof genRelative60Route + genRelative61Route: typeof genRelative61Route + genRelative62Route: typeof genRelative62Route + genRelative63Route: typeof genRelative63Route + genRelative64Route: typeof genRelative64Route + genRelative65Route: typeof genRelative65Route + genRelative66Route: typeof genRelative66Route + genRelative67Route: typeof genRelative67Route + genRelative68Route: typeof genRelative68Route + genRelative69Route: typeof genRelative69Route + genRelative7Route: typeof genRelative7Route + genRelative70Route: typeof genRelative70Route + genRelative71Route: typeof genRelative71Route + genRelative72Route: typeof genRelative72Route + genRelative73Route: typeof genRelative73Route + genRelative74Route: typeof genRelative74Route + genRelative75Route: typeof genRelative75Route + genRelative76Route: typeof genRelative76Route + genRelative77Route: typeof genRelative77Route + genRelative78Route: typeof genRelative78Route + genRelative79Route: typeof genRelative79Route + genRelative8Route: typeof genRelative8Route + genRelative80Route: typeof genRelative80Route + genRelative81Route: typeof genRelative81Route + genRelative82Route: typeof genRelative82Route + genRelative83Route: typeof genRelative83Route + genRelative84Route: typeof genRelative84Route + genRelative85Route: typeof genRelative85Route + genRelative86Route: typeof genRelative86Route + genRelative87Route: typeof genRelative87Route + genRelative88Route: typeof genRelative88Route + genRelative89Route: typeof genRelative89Route + genRelative9Route: typeof genRelative9Route + genRelative90Route: typeof genRelative90Route + genRelative91Route: typeof genRelative91Route + genRelative92Route: typeof genRelative92Route + genRelative93Route: typeof genRelative93Route + genRelative94Route: typeof genRelative94Route + genRelative95Route: typeof genRelative95Route + genRelative96Route: typeof genRelative96Route + genRelative97Route: typeof genRelative97Route + genRelative98Route: typeof genRelative98Route + genRelative99Route: typeof genRelative99Route } declare module '@tanstack/react-router' { @@ -141,54 +5159,2868 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RelativeRouteImport parentRoute: typeof rootRouteImport } - '/linkProps': { - id: '/linkProps' - path: '/linkProps' - fullPath: '/linkProps' - preLoaderRoute: typeof LinkPropsRouteImport + '/linkProps': { + id: '/linkProps' + path: '/linkProps' + fullPath: '/linkProps' + preLoaderRoute: typeof LinkPropsRouteImport + parentRoute: typeof rootRouteImport + } + '/absolute': { + id: '/absolute' + path: '/absolute' + fullPath: '/absolute' + preLoaderRoute: typeof AbsoluteRouteImport + parentRoute: typeof rootRouteImport + } + '/search': { + id: '/search' + path: '/search' + fullPath: '/search' + preLoaderRoute: typeof SearchRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/params': { + id: '/params' + path: '/params' + fullPath: '/params' + preLoaderRoute: typeof ParamsRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/search/searchPlaceholder': { + id: '/search/searchPlaceholder' + path: '/searchPlaceholder' + fullPath: '/search/searchPlaceholder' + preLoaderRoute: typeof SearchSearchPlaceholderRouteImport + parentRoute: typeof SearchRouteRoute + } + '/params/$paramsPlaceholder': { + id: '/params/$paramsPlaceholder' + path: '/$paramsPlaceholder' + fullPath: '/params/$paramsPlaceholder' + preLoaderRoute: typeof ParamsParamsPlaceholderRouteImport + parentRoute: typeof ParamsRouteRoute + } + '/(gen)/relative99': { + id: '/(gen)/relative99' + path: '/relative99' + fullPath: '/relative99' + preLoaderRoute: typeof genRelative99RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative98': { + id: '/(gen)/relative98' + path: '/relative98' + fullPath: '/relative98' + preLoaderRoute: typeof genRelative98RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative97': { + id: '/(gen)/relative97' + path: '/relative97' + fullPath: '/relative97' + preLoaderRoute: typeof genRelative97RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative96': { + id: '/(gen)/relative96' + path: '/relative96' + fullPath: '/relative96' + preLoaderRoute: typeof genRelative96RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative95': { + id: '/(gen)/relative95' + path: '/relative95' + fullPath: '/relative95' + preLoaderRoute: typeof genRelative95RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative94': { + id: '/(gen)/relative94' + path: '/relative94' + fullPath: '/relative94' + preLoaderRoute: typeof genRelative94RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative93': { + id: '/(gen)/relative93' + path: '/relative93' + fullPath: '/relative93' + preLoaderRoute: typeof genRelative93RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative92': { + id: '/(gen)/relative92' + path: '/relative92' + fullPath: '/relative92' + preLoaderRoute: typeof genRelative92RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative91': { + id: '/(gen)/relative91' + path: '/relative91' + fullPath: '/relative91' + preLoaderRoute: typeof genRelative91RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative90': { + id: '/(gen)/relative90' + path: '/relative90' + fullPath: '/relative90' + preLoaderRoute: typeof genRelative90RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative9': { + id: '/(gen)/relative9' + path: '/relative9' + fullPath: '/relative9' + preLoaderRoute: typeof genRelative9RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative89': { + id: '/(gen)/relative89' + path: '/relative89' + fullPath: '/relative89' + preLoaderRoute: typeof genRelative89RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative88': { + id: '/(gen)/relative88' + path: '/relative88' + fullPath: '/relative88' + preLoaderRoute: typeof genRelative88RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative87': { + id: '/(gen)/relative87' + path: '/relative87' + fullPath: '/relative87' + preLoaderRoute: typeof genRelative87RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative86': { + id: '/(gen)/relative86' + path: '/relative86' + fullPath: '/relative86' + preLoaderRoute: typeof genRelative86RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative85': { + id: '/(gen)/relative85' + path: '/relative85' + fullPath: '/relative85' + preLoaderRoute: typeof genRelative85RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative84': { + id: '/(gen)/relative84' + path: '/relative84' + fullPath: '/relative84' + preLoaderRoute: typeof genRelative84RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative83': { + id: '/(gen)/relative83' + path: '/relative83' + fullPath: '/relative83' + preLoaderRoute: typeof genRelative83RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative82': { + id: '/(gen)/relative82' + path: '/relative82' + fullPath: '/relative82' + preLoaderRoute: typeof genRelative82RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative81': { + id: '/(gen)/relative81' + path: '/relative81' + fullPath: '/relative81' + preLoaderRoute: typeof genRelative81RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative80': { + id: '/(gen)/relative80' + path: '/relative80' + fullPath: '/relative80' + preLoaderRoute: typeof genRelative80RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative8': { + id: '/(gen)/relative8' + path: '/relative8' + fullPath: '/relative8' + preLoaderRoute: typeof genRelative8RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative79': { + id: '/(gen)/relative79' + path: '/relative79' + fullPath: '/relative79' + preLoaderRoute: typeof genRelative79RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative78': { + id: '/(gen)/relative78' + path: '/relative78' + fullPath: '/relative78' + preLoaderRoute: typeof genRelative78RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative77': { + id: '/(gen)/relative77' + path: '/relative77' + fullPath: '/relative77' + preLoaderRoute: typeof genRelative77RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative76': { + id: '/(gen)/relative76' + path: '/relative76' + fullPath: '/relative76' + preLoaderRoute: typeof genRelative76RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative75': { + id: '/(gen)/relative75' + path: '/relative75' + fullPath: '/relative75' + preLoaderRoute: typeof genRelative75RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative74': { + id: '/(gen)/relative74' + path: '/relative74' + fullPath: '/relative74' + preLoaderRoute: typeof genRelative74RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative73': { + id: '/(gen)/relative73' + path: '/relative73' + fullPath: '/relative73' + preLoaderRoute: typeof genRelative73RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative72': { + id: '/(gen)/relative72' + path: '/relative72' + fullPath: '/relative72' + preLoaderRoute: typeof genRelative72RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative71': { + id: '/(gen)/relative71' + path: '/relative71' + fullPath: '/relative71' + preLoaderRoute: typeof genRelative71RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative70': { + id: '/(gen)/relative70' + path: '/relative70' + fullPath: '/relative70' + preLoaderRoute: typeof genRelative70RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative7': { + id: '/(gen)/relative7' + path: '/relative7' + fullPath: '/relative7' + preLoaderRoute: typeof genRelative7RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative69': { + id: '/(gen)/relative69' + path: '/relative69' + fullPath: '/relative69' + preLoaderRoute: typeof genRelative69RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative68': { + id: '/(gen)/relative68' + path: '/relative68' + fullPath: '/relative68' + preLoaderRoute: typeof genRelative68RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative67': { + id: '/(gen)/relative67' + path: '/relative67' + fullPath: '/relative67' + preLoaderRoute: typeof genRelative67RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative66': { + id: '/(gen)/relative66' + path: '/relative66' + fullPath: '/relative66' + preLoaderRoute: typeof genRelative66RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative65': { + id: '/(gen)/relative65' + path: '/relative65' + fullPath: '/relative65' + preLoaderRoute: typeof genRelative65RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative64': { + id: '/(gen)/relative64' + path: '/relative64' + fullPath: '/relative64' + preLoaderRoute: typeof genRelative64RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative63': { + id: '/(gen)/relative63' + path: '/relative63' + fullPath: '/relative63' + preLoaderRoute: typeof genRelative63RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative62': { + id: '/(gen)/relative62' + path: '/relative62' + fullPath: '/relative62' + preLoaderRoute: typeof genRelative62RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative61': { + id: '/(gen)/relative61' + path: '/relative61' + fullPath: '/relative61' + preLoaderRoute: typeof genRelative61RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative60': { + id: '/(gen)/relative60' + path: '/relative60' + fullPath: '/relative60' + preLoaderRoute: typeof genRelative60RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative6': { + id: '/(gen)/relative6' + path: '/relative6' + fullPath: '/relative6' + preLoaderRoute: typeof genRelative6RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative59': { + id: '/(gen)/relative59' + path: '/relative59' + fullPath: '/relative59' + preLoaderRoute: typeof genRelative59RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative58': { + id: '/(gen)/relative58' + path: '/relative58' + fullPath: '/relative58' + preLoaderRoute: typeof genRelative58RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative57': { + id: '/(gen)/relative57' + path: '/relative57' + fullPath: '/relative57' + preLoaderRoute: typeof genRelative57RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative56': { + id: '/(gen)/relative56' + path: '/relative56' + fullPath: '/relative56' + preLoaderRoute: typeof genRelative56RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative55': { + id: '/(gen)/relative55' + path: '/relative55' + fullPath: '/relative55' + preLoaderRoute: typeof genRelative55RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative54': { + id: '/(gen)/relative54' + path: '/relative54' + fullPath: '/relative54' + preLoaderRoute: typeof genRelative54RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative53': { + id: '/(gen)/relative53' + path: '/relative53' + fullPath: '/relative53' + preLoaderRoute: typeof genRelative53RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative52': { + id: '/(gen)/relative52' + path: '/relative52' + fullPath: '/relative52' + preLoaderRoute: typeof genRelative52RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative51': { + id: '/(gen)/relative51' + path: '/relative51' + fullPath: '/relative51' + preLoaderRoute: typeof genRelative51RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative50': { + id: '/(gen)/relative50' + path: '/relative50' + fullPath: '/relative50' + preLoaderRoute: typeof genRelative50RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative5': { + id: '/(gen)/relative5' + path: '/relative5' + fullPath: '/relative5' + preLoaderRoute: typeof genRelative5RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative49': { + id: '/(gen)/relative49' + path: '/relative49' + fullPath: '/relative49' + preLoaderRoute: typeof genRelative49RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative48': { + id: '/(gen)/relative48' + path: '/relative48' + fullPath: '/relative48' + preLoaderRoute: typeof genRelative48RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative47': { + id: '/(gen)/relative47' + path: '/relative47' + fullPath: '/relative47' + preLoaderRoute: typeof genRelative47RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative46': { + id: '/(gen)/relative46' + path: '/relative46' + fullPath: '/relative46' + preLoaderRoute: typeof genRelative46RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative45': { + id: '/(gen)/relative45' + path: '/relative45' + fullPath: '/relative45' + preLoaderRoute: typeof genRelative45RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative44': { + id: '/(gen)/relative44' + path: '/relative44' + fullPath: '/relative44' + preLoaderRoute: typeof genRelative44RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative43': { + id: '/(gen)/relative43' + path: '/relative43' + fullPath: '/relative43' + preLoaderRoute: typeof genRelative43RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative42': { + id: '/(gen)/relative42' + path: '/relative42' + fullPath: '/relative42' + preLoaderRoute: typeof genRelative42RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative41': { + id: '/(gen)/relative41' + path: '/relative41' + fullPath: '/relative41' + preLoaderRoute: typeof genRelative41RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative40': { + id: '/(gen)/relative40' + path: '/relative40' + fullPath: '/relative40' + preLoaderRoute: typeof genRelative40RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative4': { + id: '/(gen)/relative4' + path: '/relative4' + fullPath: '/relative4' + preLoaderRoute: typeof genRelative4RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative39': { + id: '/(gen)/relative39' + path: '/relative39' + fullPath: '/relative39' + preLoaderRoute: typeof genRelative39RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative38': { + id: '/(gen)/relative38' + path: '/relative38' + fullPath: '/relative38' + preLoaderRoute: typeof genRelative38RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative37': { + id: '/(gen)/relative37' + path: '/relative37' + fullPath: '/relative37' + preLoaderRoute: typeof genRelative37RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative36': { + id: '/(gen)/relative36' + path: '/relative36' + fullPath: '/relative36' + preLoaderRoute: typeof genRelative36RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative35': { + id: '/(gen)/relative35' + path: '/relative35' + fullPath: '/relative35' + preLoaderRoute: typeof genRelative35RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative34': { + id: '/(gen)/relative34' + path: '/relative34' + fullPath: '/relative34' + preLoaderRoute: typeof genRelative34RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative33': { + id: '/(gen)/relative33' + path: '/relative33' + fullPath: '/relative33' + preLoaderRoute: typeof genRelative33RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative32': { + id: '/(gen)/relative32' + path: '/relative32' + fullPath: '/relative32' + preLoaderRoute: typeof genRelative32RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative31': { + id: '/(gen)/relative31' + path: '/relative31' + fullPath: '/relative31' + preLoaderRoute: typeof genRelative31RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative30': { + id: '/(gen)/relative30' + path: '/relative30' + fullPath: '/relative30' + preLoaderRoute: typeof genRelative30RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative3': { + id: '/(gen)/relative3' + path: '/relative3' + fullPath: '/relative3' + preLoaderRoute: typeof genRelative3RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative29': { + id: '/(gen)/relative29' + path: '/relative29' + fullPath: '/relative29' + preLoaderRoute: typeof genRelative29RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative28': { + id: '/(gen)/relative28' + path: '/relative28' + fullPath: '/relative28' + preLoaderRoute: typeof genRelative28RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative27': { + id: '/(gen)/relative27' + path: '/relative27' + fullPath: '/relative27' + preLoaderRoute: typeof genRelative27RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative26': { + id: '/(gen)/relative26' + path: '/relative26' + fullPath: '/relative26' + preLoaderRoute: typeof genRelative26RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative25': { + id: '/(gen)/relative25' + path: '/relative25' + fullPath: '/relative25' + preLoaderRoute: typeof genRelative25RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative24': { + id: '/(gen)/relative24' + path: '/relative24' + fullPath: '/relative24' + preLoaderRoute: typeof genRelative24RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative23': { + id: '/(gen)/relative23' + path: '/relative23' + fullPath: '/relative23' + preLoaderRoute: typeof genRelative23RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative22': { + id: '/(gen)/relative22' + path: '/relative22' + fullPath: '/relative22' + preLoaderRoute: typeof genRelative22RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative21': { + id: '/(gen)/relative21' + path: '/relative21' + fullPath: '/relative21' + preLoaderRoute: typeof genRelative21RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative20': { + id: '/(gen)/relative20' + path: '/relative20' + fullPath: '/relative20' + preLoaderRoute: typeof genRelative20RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative2': { + id: '/(gen)/relative2' + path: '/relative2' + fullPath: '/relative2' + preLoaderRoute: typeof genRelative2RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative19': { + id: '/(gen)/relative19' + path: '/relative19' + fullPath: '/relative19' + preLoaderRoute: typeof genRelative19RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative18': { + id: '/(gen)/relative18' + path: '/relative18' + fullPath: '/relative18' + preLoaderRoute: typeof genRelative18RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative17': { + id: '/(gen)/relative17' + path: '/relative17' + fullPath: '/relative17' + preLoaderRoute: typeof genRelative17RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative16': { + id: '/(gen)/relative16' + path: '/relative16' + fullPath: '/relative16' + preLoaderRoute: typeof genRelative16RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative15': { + id: '/(gen)/relative15' + path: '/relative15' + fullPath: '/relative15' + preLoaderRoute: typeof genRelative15RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative14': { + id: '/(gen)/relative14' + path: '/relative14' + fullPath: '/relative14' + preLoaderRoute: typeof genRelative14RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative13': { + id: '/(gen)/relative13' + path: '/relative13' + fullPath: '/relative13' + preLoaderRoute: typeof genRelative13RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative12': { + id: '/(gen)/relative12' + path: '/relative12' + fullPath: '/relative12' + preLoaderRoute: typeof genRelative12RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative11': { + id: '/(gen)/relative11' + path: '/relative11' + fullPath: '/relative11' + preLoaderRoute: typeof genRelative11RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative10': { + id: '/(gen)/relative10' + path: '/relative10' + fullPath: '/relative10' + preLoaderRoute: typeof genRelative10RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative1': { + id: '/(gen)/relative1' + path: '/relative1' + fullPath: '/relative1' + preLoaderRoute: typeof genRelative1RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/relative0': { + id: '/(gen)/relative0' + path: '/relative0' + fullPath: '/relative0' + preLoaderRoute: typeof genRelative0RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute99': { + id: '/(gen)/absolute99' + path: '/absolute99' + fullPath: '/absolute99' + preLoaderRoute: typeof genAbsolute99RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute98': { + id: '/(gen)/absolute98' + path: '/absolute98' + fullPath: '/absolute98' + preLoaderRoute: typeof genAbsolute98RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute97': { + id: '/(gen)/absolute97' + path: '/absolute97' + fullPath: '/absolute97' + preLoaderRoute: typeof genAbsolute97RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute96': { + id: '/(gen)/absolute96' + path: '/absolute96' + fullPath: '/absolute96' + preLoaderRoute: typeof genAbsolute96RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute95': { + id: '/(gen)/absolute95' + path: '/absolute95' + fullPath: '/absolute95' + preLoaderRoute: typeof genAbsolute95RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute94': { + id: '/(gen)/absolute94' + path: '/absolute94' + fullPath: '/absolute94' + preLoaderRoute: typeof genAbsolute94RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute93': { + id: '/(gen)/absolute93' + path: '/absolute93' + fullPath: '/absolute93' + preLoaderRoute: typeof genAbsolute93RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute92': { + id: '/(gen)/absolute92' + path: '/absolute92' + fullPath: '/absolute92' + preLoaderRoute: typeof genAbsolute92RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute91': { + id: '/(gen)/absolute91' + path: '/absolute91' + fullPath: '/absolute91' + preLoaderRoute: typeof genAbsolute91RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute90': { + id: '/(gen)/absolute90' + path: '/absolute90' + fullPath: '/absolute90' + preLoaderRoute: typeof genAbsolute90RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute9': { + id: '/(gen)/absolute9' + path: '/absolute9' + fullPath: '/absolute9' + preLoaderRoute: typeof genAbsolute9RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute89': { + id: '/(gen)/absolute89' + path: '/absolute89' + fullPath: '/absolute89' + preLoaderRoute: typeof genAbsolute89RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute88': { + id: '/(gen)/absolute88' + path: '/absolute88' + fullPath: '/absolute88' + preLoaderRoute: typeof genAbsolute88RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute87': { + id: '/(gen)/absolute87' + path: '/absolute87' + fullPath: '/absolute87' + preLoaderRoute: typeof genAbsolute87RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute86': { + id: '/(gen)/absolute86' + path: '/absolute86' + fullPath: '/absolute86' + preLoaderRoute: typeof genAbsolute86RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute85': { + id: '/(gen)/absolute85' + path: '/absolute85' + fullPath: '/absolute85' + preLoaderRoute: typeof genAbsolute85RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute84': { + id: '/(gen)/absolute84' + path: '/absolute84' + fullPath: '/absolute84' + preLoaderRoute: typeof genAbsolute84RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute83': { + id: '/(gen)/absolute83' + path: '/absolute83' + fullPath: '/absolute83' + preLoaderRoute: typeof genAbsolute83RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute82': { + id: '/(gen)/absolute82' + path: '/absolute82' + fullPath: '/absolute82' + preLoaderRoute: typeof genAbsolute82RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute81': { + id: '/(gen)/absolute81' + path: '/absolute81' + fullPath: '/absolute81' + preLoaderRoute: typeof genAbsolute81RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute80': { + id: '/(gen)/absolute80' + path: '/absolute80' + fullPath: '/absolute80' + preLoaderRoute: typeof genAbsolute80RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute8': { + id: '/(gen)/absolute8' + path: '/absolute8' + fullPath: '/absolute8' + preLoaderRoute: typeof genAbsolute8RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute79': { + id: '/(gen)/absolute79' + path: '/absolute79' + fullPath: '/absolute79' + preLoaderRoute: typeof genAbsolute79RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute78': { + id: '/(gen)/absolute78' + path: '/absolute78' + fullPath: '/absolute78' + preLoaderRoute: typeof genAbsolute78RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute77': { + id: '/(gen)/absolute77' + path: '/absolute77' + fullPath: '/absolute77' + preLoaderRoute: typeof genAbsolute77RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute76': { + id: '/(gen)/absolute76' + path: '/absolute76' + fullPath: '/absolute76' + preLoaderRoute: typeof genAbsolute76RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute75': { + id: '/(gen)/absolute75' + path: '/absolute75' + fullPath: '/absolute75' + preLoaderRoute: typeof genAbsolute75RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute74': { + id: '/(gen)/absolute74' + path: '/absolute74' + fullPath: '/absolute74' + preLoaderRoute: typeof genAbsolute74RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute73': { + id: '/(gen)/absolute73' + path: '/absolute73' + fullPath: '/absolute73' + preLoaderRoute: typeof genAbsolute73RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute72': { + id: '/(gen)/absolute72' + path: '/absolute72' + fullPath: '/absolute72' + preLoaderRoute: typeof genAbsolute72RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute71': { + id: '/(gen)/absolute71' + path: '/absolute71' + fullPath: '/absolute71' + preLoaderRoute: typeof genAbsolute71RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute70': { + id: '/(gen)/absolute70' + path: '/absolute70' + fullPath: '/absolute70' + preLoaderRoute: typeof genAbsolute70RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute7': { + id: '/(gen)/absolute7' + path: '/absolute7' + fullPath: '/absolute7' + preLoaderRoute: typeof genAbsolute7RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute69': { + id: '/(gen)/absolute69' + path: '/absolute69' + fullPath: '/absolute69' + preLoaderRoute: typeof genAbsolute69RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute68': { + id: '/(gen)/absolute68' + path: '/absolute68' + fullPath: '/absolute68' + preLoaderRoute: typeof genAbsolute68RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute67': { + id: '/(gen)/absolute67' + path: '/absolute67' + fullPath: '/absolute67' + preLoaderRoute: typeof genAbsolute67RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute66': { + id: '/(gen)/absolute66' + path: '/absolute66' + fullPath: '/absolute66' + preLoaderRoute: typeof genAbsolute66RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute65': { + id: '/(gen)/absolute65' + path: '/absolute65' + fullPath: '/absolute65' + preLoaderRoute: typeof genAbsolute65RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute64': { + id: '/(gen)/absolute64' + path: '/absolute64' + fullPath: '/absolute64' + preLoaderRoute: typeof genAbsolute64RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute63': { + id: '/(gen)/absolute63' + path: '/absolute63' + fullPath: '/absolute63' + preLoaderRoute: typeof genAbsolute63RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute62': { + id: '/(gen)/absolute62' + path: '/absolute62' + fullPath: '/absolute62' + preLoaderRoute: typeof genAbsolute62RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute61': { + id: '/(gen)/absolute61' + path: '/absolute61' + fullPath: '/absolute61' + preLoaderRoute: typeof genAbsolute61RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute60': { + id: '/(gen)/absolute60' + path: '/absolute60' + fullPath: '/absolute60' + preLoaderRoute: typeof genAbsolute60RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute6': { + id: '/(gen)/absolute6' + path: '/absolute6' + fullPath: '/absolute6' + preLoaderRoute: typeof genAbsolute6RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute59': { + id: '/(gen)/absolute59' + path: '/absolute59' + fullPath: '/absolute59' + preLoaderRoute: typeof genAbsolute59RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute58': { + id: '/(gen)/absolute58' + path: '/absolute58' + fullPath: '/absolute58' + preLoaderRoute: typeof genAbsolute58RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute57': { + id: '/(gen)/absolute57' + path: '/absolute57' + fullPath: '/absolute57' + preLoaderRoute: typeof genAbsolute57RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute56': { + id: '/(gen)/absolute56' + path: '/absolute56' + fullPath: '/absolute56' + preLoaderRoute: typeof genAbsolute56RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute55': { + id: '/(gen)/absolute55' + path: '/absolute55' + fullPath: '/absolute55' + preLoaderRoute: typeof genAbsolute55RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute54': { + id: '/(gen)/absolute54' + path: '/absolute54' + fullPath: '/absolute54' + preLoaderRoute: typeof genAbsolute54RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute53': { + id: '/(gen)/absolute53' + path: '/absolute53' + fullPath: '/absolute53' + preLoaderRoute: typeof genAbsolute53RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute52': { + id: '/(gen)/absolute52' + path: '/absolute52' + fullPath: '/absolute52' + preLoaderRoute: typeof genAbsolute52RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute51': { + id: '/(gen)/absolute51' + path: '/absolute51' + fullPath: '/absolute51' + preLoaderRoute: typeof genAbsolute51RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute50': { + id: '/(gen)/absolute50' + path: '/absolute50' + fullPath: '/absolute50' + preLoaderRoute: typeof genAbsolute50RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute5': { + id: '/(gen)/absolute5' + path: '/absolute5' + fullPath: '/absolute5' + preLoaderRoute: typeof genAbsolute5RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute49': { + id: '/(gen)/absolute49' + path: '/absolute49' + fullPath: '/absolute49' + preLoaderRoute: typeof genAbsolute49RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute48': { + id: '/(gen)/absolute48' + path: '/absolute48' + fullPath: '/absolute48' + preLoaderRoute: typeof genAbsolute48RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute47': { + id: '/(gen)/absolute47' + path: '/absolute47' + fullPath: '/absolute47' + preLoaderRoute: typeof genAbsolute47RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute46': { + id: '/(gen)/absolute46' + path: '/absolute46' + fullPath: '/absolute46' + preLoaderRoute: typeof genAbsolute46RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute45': { + id: '/(gen)/absolute45' + path: '/absolute45' + fullPath: '/absolute45' + preLoaderRoute: typeof genAbsolute45RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute44': { + id: '/(gen)/absolute44' + path: '/absolute44' + fullPath: '/absolute44' + preLoaderRoute: typeof genAbsolute44RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute43': { + id: '/(gen)/absolute43' + path: '/absolute43' + fullPath: '/absolute43' + preLoaderRoute: typeof genAbsolute43RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute42': { + id: '/(gen)/absolute42' + path: '/absolute42' + fullPath: '/absolute42' + preLoaderRoute: typeof genAbsolute42RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute41': { + id: '/(gen)/absolute41' + path: '/absolute41' + fullPath: '/absolute41' + preLoaderRoute: typeof genAbsolute41RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute40': { + id: '/(gen)/absolute40' + path: '/absolute40' + fullPath: '/absolute40' + preLoaderRoute: typeof genAbsolute40RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute4': { + id: '/(gen)/absolute4' + path: '/absolute4' + fullPath: '/absolute4' + preLoaderRoute: typeof genAbsolute4RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute39': { + id: '/(gen)/absolute39' + path: '/absolute39' + fullPath: '/absolute39' + preLoaderRoute: typeof genAbsolute39RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute38': { + id: '/(gen)/absolute38' + path: '/absolute38' + fullPath: '/absolute38' + preLoaderRoute: typeof genAbsolute38RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute37': { + id: '/(gen)/absolute37' + path: '/absolute37' + fullPath: '/absolute37' + preLoaderRoute: typeof genAbsolute37RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute36': { + id: '/(gen)/absolute36' + path: '/absolute36' + fullPath: '/absolute36' + preLoaderRoute: typeof genAbsolute36RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute35': { + id: '/(gen)/absolute35' + path: '/absolute35' + fullPath: '/absolute35' + preLoaderRoute: typeof genAbsolute35RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute34': { + id: '/(gen)/absolute34' + path: '/absolute34' + fullPath: '/absolute34' + preLoaderRoute: typeof genAbsolute34RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute33': { + id: '/(gen)/absolute33' + path: '/absolute33' + fullPath: '/absolute33' + preLoaderRoute: typeof genAbsolute33RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute32': { + id: '/(gen)/absolute32' + path: '/absolute32' + fullPath: '/absolute32' + preLoaderRoute: typeof genAbsolute32RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute31': { + id: '/(gen)/absolute31' + path: '/absolute31' + fullPath: '/absolute31' + preLoaderRoute: typeof genAbsolute31RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute30': { + id: '/(gen)/absolute30' + path: '/absolute30' + fullPath: '/absolute30' + preLoaderRoute: typeof genAbsolute30RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute3': { + id: '/(gen)/absolute3' + path: '/absolute3' + fullPath: '/absolute3' + preLoaderRoute: typeof genAbsolute3RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute29': { + id: '/(gen)/absolute29' + path: '/absolute29' + fullPath: '/absolute29' + preLoaderRoute: typeof genAbsolute29RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute28': { + id: '/(gen)/absolute28' + path: '/absolute28' + fullPath: '/absolute28' + preLoaderRoute: typeof genAbsolute28RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute27': { + id: '/(gen)/absolute27' + path: '/absolute27' + fullPath: '/absolute27' + preLoaderRoute: typeof genAbsolute27RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute26': { + id: '/(gen)/absolute26' + path: '/absolute26' + fullPath: '/absolute26' + preLoaderRoute: typeof genAbsolute26RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute25': { + id: '/(gen)/absolute25' + path: '/absolute25' + fullPath: '/absolute25' + preLoaderRoute: typeof genAbsolute25RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute24': { + id: '/(gen)/absolute24' + path: '/absolute24' + fullPath: '/absolute24' + preLoaderRoute: typeof genAbsolute24RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute23': { + id: '/(gen)/absolute23' + path: '/absolute23' + fullPath: '/absolute23' + preLoaderRoute: typeof genAbsolute23RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute22': { + id: '/(gen)/absolute22' + path: '/absolute22' + fullPath: '/absolute22' + preLoaderRoute: typeof genAbsolute22RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute21': { + id: '/(gen)/absolute21' + path: '/absolute21' + fullPath: '/absolute21' + preLoaderRoute: typeof genAbsolute21RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute20': { + id: '/(gen)/absolute20' + path: '/absolute20' + fullPath: '/absolute20' + preLoaderRoute: typeof genAbsolute20RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute2': { + id: '/(gen)/absolute2' + path: '/absolute2' + fullPath: '/absolute2' + preLoaderRoute: typeof genAbsolute2RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute19': { + id: '/(gen)/absolute19' + path: '/absolute19' + fullPath: '/absolute19' + preLoaderRoute: typeof genAbsolute19RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute18': { + id: '/(gen)/absolute18' + path: '/absolute18' + fullPath: '/absolute18' + preLoaderRoute: typeof genAbsolute18RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute17': { + id: '/(gen)/absolute17' + path: '/absolute17' + fullPath: '/absolute17' + preLoaderRoute: typeof genAbsolute17RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute16': { + id: '/(gen)/absolute16' + path: '/absolute16' + fullPath: '/absolute16' + preLoaderRoute: typeof genAbsolute16RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute15': { + id: '/(gen)/absolute15' + path: '/absolute15' + fullPath: '/absolute15' + preLoaderRoute: typeof genAbsolute15RouteImport parentRoute: typeof rootRouteImport } - '/absolute': { - id: '/absolute' - path: '/absolute' - fullPath: '/absolute' - preLoaderRoute: typeof AbsoluteRouteImport + '/(gen)/absolute14': { + id: '/(gen)/absolute14' + path: '/absolute14' + fullPath: '/absolute14' + preLoaderRoute: typeof genAbsolute14RouteImport parentRoute: typeof rootRouteImport } - '/search': { - id: '/search' + '/(gen)/absolute13': { + id: '/(gen)/absolute13' + path: '/absolute13' + fullPath: '/absolute13' + preLoaderRoute: typeof genAbsolute13RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute12': { + id: '/(gen)/absolute12' + path: '/absolute12' + fullPath: '/absolute12' + preLoaderRoute: typeof genAbsolute12RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute11': { + id: '/(gen)/absolute11' + path: '/absolute11' + fullPath: '/absolute11' + preLoaderRoute: typeof genAbsolute11RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute10': { + id: '/(gen)/absolute10' + path: '/absolute10' + fullPath: '/absolute10' + preLoaderRoute: typeof genAbsolute10RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute1': { + id: '/(gen)/absolute1' + path: '/absolute1' + fullPath: '/absolute1' + preLoaderRoute: typeof genAbsolute1RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/absolute0': { + id: '/(gen)/absolute0' + path: '/absolute0' + fullPath: '/absolute0' + preLoaderRoute: typeof genAbsolute0RouteImport + parentRoute: typeof rootRouteImport + } + '/(gen)/search': { + id: '/(gen)/search' path: '/search' fullPath: '/search' - preLoaderRoute: typeof SearchRouteRouteImport + preLoaderRoute: typeof genSearchRouteRouteImport parentRoute: typeof rootRouteImport } - '/params': { - id: '/params' + '/(gen)/params': { + id: '/(gen)/params' path: '/params' fullPath: '/params' - preLoaderRoute: typeof ParamsRouteRouteImport + preLoaderRoute: typeof genParamsRouteRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport - parentRoute: typeof rootRouteImport + '/(gen)/search/search99': { + id: '/(gen)/search/search99' + path: '/search99' + fullPath: '/search/search99' + preLoaderRoute: typeof genSearchSearch99RouteImport + parentRoute: typeof genSearchRouteRoute } - '/search/searchPlaceholder': { - id: '/search/searchPlaceholder' - path: '/searchPlaceholder' - fullPath: '/search/searchPlaceholder' - preLoaderRoute: typeof SearchSearchPlaceholderRouteImport - parentRoute: typeof SearchRouteRoute + '/(gen)/search/search98': { + id: '/(gen)/search/search98' + path: '/search98' + fullPath: '/search/search98' + preLoaderRoute: typeof genSearchSearch98RouteImport + parentRoute: typeof genSearchRouteRoute } - '/params/$paramsPlaceholder': { - id: '/params/$paramsPlaceholder' - path: '/$paramsPlaceholder' - fullPath: '/params/$paramsPlaceholder' - preLoaderRoute: typeof ParamsParamsPlaceholderRouteImport - parentRoute: typeof ParamsRouteRoute + '/(gen)/search/search97': { + id: '/(gen)/search/search97' + path: '/search97' + fullPath: '/search/search97' + preLoaderRoute: typeof genSearchSearch97RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search96': { + id: '/(gen)/search/search96' + path: '/search96' + fullPath: '/search/search96' + preLoaderRoute: typeof genSearchSearch96RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search95': { + id: '/(gen)/search/search95' + path: '/search95' + fullPath: '/search/search95' + preLoaderRoute: typeof genSearchSearch95RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search94': { + id: '/(gen)/search/search94' + path: '/search94' + fullPath: '/search/search94' + preLoaderRoute: typeof genSearchSearch94RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search93': { + id: '/(gen)/search/search93' + path: '/search93' + fullPath: '/search/search93' + preLoaderRoute: typeof genSearchSearch93RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search92': { + id: '/(gen)/search/search92' + path: '/search92' + fullPath: '/search/search92' + preLoaderRoute: typeof genSearchSearch92RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search91': { + id: '/(gen)/search/search91' + path: '/search91' + fullPath: '/search/search91' + preLoaderRoute: typeof genSearchSearch91RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search90': { + id: '/(gen)/search/search90' + path: '/search90' + fullPath: '/search/search90' + preLoaderRoute: typeof genSearchSearch90RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search9': { + id: '/(gen)/search/search9' + path: '/search9' + fullPath: '/search/search9' + preLoaderRoute: typeof genSearchSearch9RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search89': { + id: '/(gen)/search/search89' + path: '/search89' + fullPath: '/search/search89' + preLoaderRoute: typeof genSearchSearch89RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search88': { + id: '/(gen)/search/search88' + path: '/search88' + fullPath: '/search/search88' + preLoaderRoute: typeof genSearchSearch88RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search87': { + id: '/(gen)/search/search87' + path: '/search87' + fullPath: '/search/search87' + preLoaderRoute: typeof genSearchSearch87RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search86': { + id: '/(gen)/search/search86' + path: '/search86' + fullPath: '/search/search86' + preLoaderRoute: typeof genSearchSearch86RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search85': { + id: '/(gen)/search/search85' + path: '/search85' + fullPath: '/search/search85' + preLoaderRoute: typeof genSearchSearch85RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search84': { + id: '/(gen)/search/search84' + path: '/search84' + fullPath: '/search/search84' + preLoaderRoute: typeof genSearchSearch84RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search83': { + id: '/(gen)/search/search83' + path: '/search83' + fullPath: '/search/search83' + preLoaderRoute: typeof genSearchSearch83RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search82': { + id: '/(gen)/search/search82' + path: '/search82' + fullPath: '/search/search82' + preLoaderRoute: typeof genSearchSearch82RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search81': { + id: '/(gen)/search/search81' + path: '/search81' + fullPath: '/search/search81' + preLoaderRoute: typeof genSearchSearch81RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search80': { + id: '/(gen)/search/search80' + path: '/search80' + fullPath: '/search/search80' + preLoaderRoute: typeof genSearchSearch80RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search8': { + id: '/(gen)/search/search8' + path: '/search8' + fullPath: '/search/search8' + preLoaderRoute: typeof genSearchSearch8RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search79': { + id: '/(gen)/search/search79' + path: '/search79' + fullPath: '/search/search79' + preLoaderRoute: typeof genSearchSearch79RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search78': { + id: '/(gen)/search/search78' + path: '/search78' + fullPath: '/search/search78' + preLoaderRoute: typeof genSearchSearch78RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search77': { + id: '/(gen)/search/search77' + path: '/search77' + fullPath: '/search/search77' + preLoaderRoute: typeof genSearchSearch77RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search76': { + id: '/(gen)/search/search76' + path: '/search76' + fullPath: '/search/search76' + preLoaderRoute: typeof genSearchSearch76RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search75': { + id: '/(gen)/search/search75' + path: '/search75' + fullPath: '/search/search75' + preLoaderRoute: typeof genSearchSearch75RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search74': { + id: '/(gen)/search/search74' + path: '/search74' + fullPath: '/search/search74' + preLoaderRoute: typeof genSearchSearch74RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search73': { + id: '/(gen)/search/search73' + path: '/search73' + fullPath: '/search/search73' + preLoaderRoute: typeof genSearchSearch73RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search72': { + id: '/(gen)/search/search72' + path: '/search72' + fullPath: '/search/search72' + preLoaderRoute: typeof genSearchSearch72RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search71': { + id: '/(gen)/search/search71' + path: '/search71' + fullPath: '/search/search71' + preLoaderRoute: typeof genSearchSearch71RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search70': { + id: '/(gen)/search/search70' + path: '/search70' + fullPath: '/search/search70' + preLoaderRoute: typeof genSearchSearch70RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search7': { + id: '/(gen)/search/search7' + path: '/search7' + fullPath: '/search/search7' + preLoaderRoute: typeof genSearchSearch7RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search69': { + id: '/(gen)/search/search69' + path: '/search69' + fullPath: '/search/search69' + preLoaderRoute: typeof genSearchSearch69RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search68': { + id: '/(gen)/search/search68' + path: '/search68' + fullPath: '/search/search68' + preLoaderRoute: typeof genSearchSearch68RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search67': { + id: '/(gen)/search/search67' + path: '/search67' + fullPath: '/search/search67' + preLoaderRoute: typeof genSearchSearch67RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search66': { + id: '/(gen)/search/search66' + path: '/search66' + fullPath: '/search/search66' + preLoaderRoute: typeof genSearchSearch66RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search65': { + id: '/(gen)/search/search65' + path: '/search65' + fullPath: '/search/search65' + preLoaderRoute: typeof genSearchSearch65RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search64': { + id: '/(gen)/search/search64' + path: '/search64' + fullPath: '/search/search64' + preLoaderRoute: typeof genSearchSearch64RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search63': { + id: '/(gen)/search/search63' + path: '/search63' + fullPath: '/search/search63' + preLoaderRoute: typeof genSearchSearch63RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search62': { + id: '/(gen)/search/search62' + path: '/search62' + fullPath: '/search/search62' + preLoaderRoute: typeof genSearchSearch62RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search61': { + id: '/(gen)/search/search61' + path: '/search61' + fullPath: '/search/search61' + preLoaderRoute: typeof genSearchSearch61RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search60': { + id: '/(gen)/search/search60' + path: '/search60' + fullPath: '/search/search60' + preLoaderRoute: typeof genSearchSearch60RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search6': { + id: '/(gen)/search/search6' + path: '/search6' + fullPath: '/search/search6' + preLoaderRoute: typeof genSearchSearch6RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search59': { + id: '/(gen)/search/search59' + path: '/search59' + fullPath: '/search/search59' + preLoaderRoute: typeof genSearchSearch59RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search58': { + id: '/(gen)/search/search58' + path: '/search58' + fullPath: '/search/search58' + preLoaderRoute: typeof genSearchSearch58RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search57': { + id: '/(gen)/search/search57' + path: '/search57' + fullPath: '/search/search57' + preLoaderRoute: typeof genSearchSearch57RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search56': { + id: '/(gen)/search/search56' + path: '/search56' + fullPath: '/search/search56' + preLoaderRoute: typeof genSearchSearch56RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search55': { + id: '/(gen)/search/search55' + path: '/search55' + fullPath: '/search/search55' + preLoaderRoute: typeof genSearchSearch55RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search54': { + id: '/(gen)/search/search54' + path: '/search54' + fullPath: '/search/search54' + preLoaderRoute: typeof genSearchSearch54RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search53': { + id: '/(gen)/search/search53' + path: '/search53' + fullPath: '/search/search53' + preLoaderRoute: typeof genSearchSearch53RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search52': { + id: '/(gen)/search/search52' + path: '/search52' + fullPath: '/search/search52' + preLoaderRoute: typeof genSearchSearch52RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search51': { + id: '/(gen)/search/search51' + path: '/search51' + fullPath: '/search/search51' + preLoaderRoute: typeof genSearchSearch51RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search50': { + id: '/(gen)/search/search50' + path: '/search50' + fullPath: '/search/search50' + preLoaderRoute: typeof genSearchSearch50RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search5': { + id: '/(gen)/search/search5' + path: '/search5' + fullPath: '/search/search5' + preLoaderRoute: typeof genSearchSearch5RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search49': { + id: '/(gen)/search/search49' + path: '/search49' + fullPath: '/search/search49' + preLoaderRoute: typeof genSearchSearch49RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search48': { + id: '/(gen)/search/search48' + path: '/search48' + fullPath: '/search/search48' + preLoaderRoute: typeof genSearchSearch48RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search47': { + id: '/(gen)/search/search47' + path: '/search47' + fullPath: '/search/search47' + preLoaderRoute: typeof genSearchSearch47RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search46': { + id: '/(gen)/search/search46' + path: '/search46' + fullPath: '/search/search46' + preLoaderRoute: typeof genSearchSearch46RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search45': { + id: '/(gen)/search/search45' + path: '/search45' + fullPath: '/search/search45' + preLoaderRoute: typeof genSearchSearch45RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search44': { + id: '/(gen)/search/search44' + path: '/search44' + fullPath: '/search/search44' + preLoaderRoute: typeof genSearchSearch44RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search43': { + id: '/(gen)/search/search43' + path: '/search43' + fullPath: '/search/search43' + preLoaderRoute: typeof genSearchSearch43RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search42': { + id: '/(gen)/search/search42' + path: '/search42' + fullPath: '/search/search42' + preLoaderRoute: typeof genSearchSearch42RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search41': { + id: '/(gen)/search/search41' + path: '/search41' + fullPath: '/search/search41' + preLoaderRoute: typeof genSearchSearch41RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search40': { + id: '/(gen)/search/search40' + path: '/search40' + fullPath: '/search/search40' + preLoaderRoute: typeof genSearchSearch40RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search4': { + id: '/(gen)/search/search4' + path: '/search4' + fullPath: '/search/search4' + preLoaderRoute: typeof genSearchSearch4RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search39': { + id: '/(gen)/search/search39' + path: '/search39' + fullPath: '/search/search39' + preLoaderRoute: typeof genSearchSearch39RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search38': { + id: '/(gen)/search/search38' + path: '/search38' + fullPath: '/search/search38' + preLoaderRoute: typeof genSearchSearch38RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search37': { + id: '/(gen)/search/search37' + path: '/search37' + fullPath: '/search/search37' + preLoaderRoute: typeof genSearchSearch37RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search36': { + id: '/(gen)/search/search36' + path: '/search36' + fullPath: '/search/search36' + preLoaderRoute: typeof genSearchSearch36RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search35': { + id: '/(gen)/search/search35' + path: '/search35' + fullPath: '/search/search35' + preLoaderRoute: typeof genSearchSearch35RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search34': { + id: '/(gen)/search/search34' + path: '/search34' + fullPath: '/search/search34' + preLoaderRoute: typeof genSearchSearch34RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search33': { + id: '/(gen)/search/search33' + path: '/search33' + fullPath: '/search/search33' + preLoaderRoute: typeof genSearchSearch33RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search32': { + id: '/(gen)/search/search32' + path: '/search32' + fullPath: '/search/search32' + preLoaderRoute: typeof genSearchSearch32RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search31': { + id: '/(gen)/search/search31' + path: '/search31' + fullPath: '/search/search31' + preLoaderRoute: typeof genSearchSearch31RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search30': { + id: '/(gen)/search/search30' + path: '/search30' + fullPath: '/search/search30' + preLoaderRoute: typeof genSearchSearch30RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search3': { + id: '/(gen)/search/search3' + path: '/search3' + fullPath: '/search/search3' + preLoaderRoute: typeof genSearchSearch3RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search29': { + id: '/(gen)/search/search29' + path: '/search29' + fullPath: '/search/search29' + preLoaderRoute: typeof genSearchSearch29RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search28': { + id: '/(gen)/search/search28' + path: '/search28' + fullPath: '/search/search28' + preLoaderRoute: typeof genSearchSearch28RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search27': { + id: '/(gen)/search/search27' + path: '/search27' + fullPath: '/search/search27' + preLoaderRoute: typeof genSearchSearch27RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search26': { + id: '/(gen)/search/search26' + path: '/search26' + fullPath: '/search/search26' + preLoaderRoute: typeof genSearchSearch26RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search25': { + id: '/(gen)/search/search25' + path: '/search25' + fullPath: '/search/search25' + preLoaderRoute: typeof genSearchSearch25RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search24': { + id: '/(gen)/search/search24' + path: '/search24' + fullPath: '/search/search24' + preLoaderRoute: typeof genSearchSearch24RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search23': { + id: '/(gen)/search/search23' + path: '/search23' + fullPath: '/search/search23' + preLoaderRoute: typeof genSearchSearch23RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search22': { + id: '/(gen)/search/search22' + path: '/search22' + fullPath: '/search/search22' + preLoaderRoute: typeof genSearchSearch22RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search21': { + id: '/(gen)/search/search21' + path: '/search21' + fullPath: '/search/search21' + preLoaderRoute: typeof genSearchSearch21RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search20': { + id: '/(gen)/search/search20' + path: '/search20' + fullPath: '/search/search20' + preLoaderRoute: typeof genSearchSearch20RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search2': { + id: '/(gen)/search/search2' + path: '/search2' + fullPath: '/search/search2' + preLoaderRoute: typeof genSearchSearch2RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search19': { + id: '/(gen)/search/search19' + path: '/search19' + fullPath: '/search/search19' + preLoaderRoute: typeof genSearchSearch19RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search18': { + id: '/(gen)/search/search18' + path: '/search18' + fullPath: '/search/search18' + preLoaderRoute: typeof genSearchSearch18RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search17': { + id: '/(gen)/search/search17' + path: '/search17' + fullPath: '/search/search17' + preLoaderRoute: typeof genSearchSearch17RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search16': { + id: '/(gen)/search/search16' + path: '/search16' + fullPath: '/search/search16' + preLoaderRoute: typeof genSearchSearch16RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search15': { + id: '/(gen)/search/search15' + path: '/search15' + fullPath: '/search/search15' + preLoaderRoute: typeof genSearchSearch15RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search14': { + id: '/(gen)/search/search14' + path: '/search14' + fullPath: '/search/search14' + preLoaderRoute: typeof genSearchSearch14RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search13': { + id: '/(gen)/search/search13' + path: '/search13' + fullPath: '/search/search13' + preLoaderRoute: typeof genSearchSearch13RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search12': { + id: '/(gen)/search/search12' + path: '/search12' + fullPath: '/search/search12' + preLoaderRoute: typeof genSearchSearch12RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search11': { + id: '/(gen)/search/search11' + path: '/search11' + fullPath: '/search/search11' + preLoaderRoute: typeof genSearchSearch11RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search10': { + id: '/(gen)/search/search10' + path: '/search10' + fullPath: '/search/search10' + preLoaderRoute: typeof genSearchSearch10RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search1': { + id: '/(gen)/search/search1' + path: '/search1' + fullPath: '/search/search1' + preLoaderRoute: typeof genSearchSearch1RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/search/search0': { + id: '/(gen)/search/search0' + path: '/search0' + fullPath: '/search/search0' + preLoaderRoute: typeof genSearchSearch0RouteImport + parentRoute: typeof genSearchRouteRoute + } + '/(gen)/params/$param99': { + id: '/(gen)/params/$param99' + path: '/$param99' + fullPath: '/params/$param99' + preLoaderRoute: typeof genParamsParam99RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param98': { + id: '/(gen)/params/$param98' + path: '/$param98' + fullPath: '/params/$param98' + preLoaderRoute: typeof genParamsParam98RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param97': { + id: '/(gen)/params/$param97' + path: '/$param97' + fullPath: '/params/$param97' + preLoaderRoute: typeof genParamsParam97RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param96': { + id: '/(gen)/params/$param96' + path: '/$param96' + fullPath: '/params/$param96' + preLoaderRoute: typeof genParamsParam96RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param95': { + id: '/(gen)/params/$param95' + path: '/$param95' + fullPath: '/params/$param95' + preLoaderRoute: typeof genParamsParam95RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param94': { + id: '/(gen)/params/$param94' + path: '/$param94' + fullPath: '/params/$param94' + preLoaderRoute: typeof genParamsParam94RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param93': { + id: '/(gen)/params/$param93' + path: '/$param93' + fullPath: '/params/$param93' + preLoaderRoute: typeof genParamsParam93RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param92': { + id: '/(gen)/params/$param92' + path: '/$param92' + fullPath: '/params/$param92' + preLoaderRoute: typeof genParamsParam92RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param91': { + id: '/(gen)/params/$param91' + path: '/$param91' + fullPath: '/params/$param91' + preLoaderRoute: typeof genParamsParam91RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param90': { + id: '/(gen)/params/$param90' + path: '/$param90' + fullPath: '/params/$param90' + preLoaderRoute: typeof genParamsParam90RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param9': { + id: '/(gen)/params/$param9' + path: '/$param9' + fullPath: '/params/$param9' + preLoaderRoute: typeof genParamsParam9RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param89': { + id: '/(gen)/params/$param89' + path: '/$param89' + fullPath: '/params/$param89' + preLoaderRoute: typeof genParamsParam89RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param88': { + id: '/(gen)/params/$param88' + path: '/$param88' + fullPath: '/params/$param88' + preLoaderRoute: typeof genParamsParam88RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param87': { + id: '/(gen)/params/$param87' + path: '/$param87' + fullPath: '/params/$param87' + preLoaderRoute: typeof genParamsParam87RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param86': { + id: '/(gen)/params/$param86' + path: '/$param86' + fullPath: '/params/$param86' + preLoaderRoute: typeof genParamsParam86RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param85': { + id: '/(gen)/params/$param85' + path: '/$param85' + fullPath: '/params/$param85' + preLoaderRoute: typeof genParamsParam85RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param84': { + id: '/(gen)/params/$param84' + path: '/$param84' + fullPath: '/params/$param84' + preLoaderRoute: typeof genParamsParam84RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param83': { + id: '/(gen)/params/$param83' + path: '/$param83' + fullPath: '/params/$param83' + preLoaderRoute: typeof genParamsParam83RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param82': { + id: '/(gen)/params/$param82' + path: '/$param82' + fullPath: '/params/$param82' + preLoaderRoute: typeof genParamsParam82RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param81': { + id: '/(gen)/params/$param81' + path: '/$param81' + fullPath: '/params/$param81' + preLoaderRoute: typeof genParamsParam81RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param80': { + id: '/(gen)/params/$param80' + path: '/$param80' + fullPath: '/params/$param80' + preLoaderRoute: typeof genParamsParam80RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param8': { + id: '/(gen)/params/$param8' + path: '/$param8' + fullPath: '/params/$param8' + preLoaderRoute: typeof genParamsParam8RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param79': { + id: '/(gen)/params/$param79' + path: '/$param79' + fullPath: '/params/$param79' + preLoaderRoute: typeof genParamsParam79RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param78': { + id: '/(gen)/params/$param78' + path: '/$param78' + fullPath: '/params/$param78' + preLoaderRoute: typeof genParamsParam78RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param77': { + id: '/(gen)/params/$param77' + path: '/$param77' + fullPath: '/params/$param77' + preLoaderRoute: typeof genParamsParam77RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param76': { + id: '/(gen)/params/$param76' + path: '/$param76' + fullPath: '/params/$param76' + preLoaderRoute: typeof genParamsParam76RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param75': { + id: '/(gen)/params/$param75' + path: '/$param75' + fullPath: '/params/$param75' + preLoaderRoute: typeof genParamsParam75RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param74': { + id: '/(gen)/params/$param74' + path: '/$param74' + fullPath: '/params/$param74' + preLoaderRoute: typeof genParamsParam74RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param73': { + id: '/(gen)/params/$param73' + path: '/$param73' + fullPath: '/params/$param73' + preLoaderRoute: typeof genParamsParam73RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param72': { + id: '/(gen)/params/$param72' + path: '/$param72' + fullPath: '/params/$param72' + preLoaderRoute: typeof genParamsParam72RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param71': { + id: '/(gen)/params/$param71' + path: '/$param71' + fullPath: '/params/$param71' + preLoaderRoute: typeof genParamsParam71RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param70': { + id: '/(gen)/params/$param70' + path: '/$param70' + fullPath: '/params/$param70' + preLoaderRoute: typeof genParamsParam70RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param7': { + id: '/(gen)/params/$param7' + path: '/$param7' + fullPath: '/params/$param7' + preLoaderRoute: typeof genParamsParam7RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param69': { + id: '/(gen)/params/$param69' + path: '/$param69' + fullPath: '/params/$param69' + preLoaderRoute: typeof genParamsParam69RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param68': { + id: '/(gen)/params/$param68' + path: '/$param68' + fullPath: '/params/$param68' + preLoaderRoute: typeof genParamsParam68RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param67': { + id: '/(gen)/params/$param67' + path: '/$param67' + fullPath: '/params/$param67' + preLoaderRoute: typeof genParamsParam67RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param66': { + id: '/(gen)/params/$param66' + path: '/$param66' + fullPath: '/params/$param66' + preLoaderRoute: typeof genParamsParam66RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param65': { + id: '/(gen)/params/$param65' + path: '/$param65' + fullPath: '/params/$param65' + preLoaderRoute: typeof genParamsParam65RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param64': { + id: '/(gen)/params/$param64' + path: '/$param64' + fullPath: '/params/$param64' + preLoaderRoute: typeof genParamsParam64RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param63': { + id: '/(gen)/params/$param63' + path: '/$param63' + fullPath: '/params/$param63' + preLoaderRoute: typeof genParamsParam63RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param62': { + id: '/(gen)/params/$param62' + path: '/$param62' + fullPath: '/params/$param62' + preLoaderRoute: typeof genParamsParam62RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param61': { + id: '/(gen)/params/$param61' + path: '/$param61' + fullPath: '/params/$param61' + preLoaderRoute: typeof genParamsParam61RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param60': { + id: '/(gen)/params/$param60' + path: '/$param60' + fullPath: '/params/$param60' + preLoaderRoute: typeof genParamsParam60RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param6': { + id: '/(gen)/params/$param6' + path: '/$param6' + fullPath: '/params/$param6' + preLoaderRoute: typeof genParamsParam6RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param59': { + id: '/(gen)/params/$param59' + path: '/$param59' + fullPath: '/params/$param59' + preLoaderRoute: typeof genParamsParam59RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param58': { + id: '/(gen)/params/$param58' + path: '/$param58' + fullPath: '/params/$param58' + preLoaderRoute: typeof genParamsParam58RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param57': { + id: '/(gen)/params/$param57' + path: '/$param57' + fullPath: '/params/$param57' + preLoaderRoute: typeof genParamsParam57RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param56': { + id: '/(gen)/params/$param56' + path: '/$param56' + fullPath: '/params/$param56' + preLoaderRoute: typeof genParamsParam56RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param55': { + id: '/(gen)/params/$param55' + path: '/$param55' + fullPath: '/params/$param55' + preLoaderRoute: typeof genParamsParam55RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param54': { + id: '/(gen)/params/$param54' + path: '/$param54' + fullPath: '/params/$param54' + preLoaderRoute: typeof genParamsParam54RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param53': { + id: '/(gen)/params/$param53' + path: '/$param53' + fullPath: '/params/$param53' + preLoaderRoute: typeof genParamsParam53RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param52': { + id: '/(gen)/params/$param52' + path: '/$param52' + fullPath: '/params/$param52' + preLoaderRoute: typeof genParamsParam52RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param51': { + id: '/(gen)/params/$param51' + path: '/$param51' + fullPath: '/params/$param51' + preLoaderRoute: typeof genParamsParam51RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param50': { + id: '/(gen)/params/$param50' + path: '/$param50' + fullPath: '/params/$param50' + preLoaderRoute: typeof genParamsParam50RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param5': { + id: '/(gen)/params/$param5' + path: '/$param5' + fullPath: '/params/$param5' + preLoaderRoute: typeof genParamsParam5RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param49': { + id: '/(gen)/params/$param49' + path: '/$param49' + fullPath: '/params/$param49' + preLoaderRoute: typeof genParamsParam49RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param48': { + id: '/(gen)/params/$param48' + path: '/$param48' + fullPath: '/params/$param48' + preLoaderRoute: typeof genParamsParam48RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param47': { + id: '/(gen)/params/$param47' + path: '/$param47' + fullPath: '/params/$param47' + preLoaderRoute: typeof genParamsParam47RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param46': { + id: '/(gen)/params/$param46' + path: '/$param46' + fullPath: '/params/$param46' + preLoaderRoute: typeof genParamsParam46RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param45': { + id: '/(gen)/params/$param45' + path: '/$param45' + fullPath: '/params/$param45' + preLoaderRoute: typeof genParamsParam45RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param44': { + id: '/(gen)/params/$param44' + path: '/$param44' + fullPath: '/params/$param44' + preLoaderRoute: typeof genParamsParam44RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param43': { + id: '/(gen)/params/$param43' + path: '/$param43' + fullPath: '/params/$param43' + preLoaderRoute: typeof genParamsParam43RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param42': { + id: '/(gen)/params/$param42' + path: '/$param42' + fullPath: '/params/$param42' + preLoaderRoute: typeof genParamsParam42RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param41': { + id: '/(gen)/params/$param41' + path: '/$param41' + fullPath: '/params/$param41' + preLoaderRoute: typeof genParamsParam41RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param40': { + id: '/(gen)/params/$param40' + path: '/$param40' + fullPath: '/params/$param40' + preLoaderRoute: typeof genParamsParam40RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param4': { + id: '/(gen)/params/$param4' + path: '/$param4' + fullPath: '/params/$param4' + preLoaderRoute: typeof genParamsParam4RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param39': { + id: '/(gen)/params/$param39' + path: '/$param39' + fullPath: '/params/$param39' + preLoaderRoute: typeof genParamsParam39RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param38': { + id: '/(gen)/params/$param38' + path: '/$param38' + fullPath: '/params/$param38' + preLoaderRoute: typeof genParamsParam38RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param37': { + id: '/(gen)/params/$param37' + path: '/$param37' + fullPath: '/params/$param37' + preLoaderRoute: typeof genParamsParam37RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param36': { + id: '/(gen)/params/$param36' + path: '/$param36' + fullPath: '/params/$param36' + preLoaderRoute: typeof genParamsParam36RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param35': { + id: '/(gen)/params/$param35' + path: '/$param35' + fullPath: '/params/$param35' + preLoaderRoute: typeof genParamsParam35RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param34': { + id: '/(gen)/params/$param34' + path: '/$param34' + fullPath: '/params/$param34' + preLoaderRoute: typeof genParamsParam34RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param33': { + id: '/(gen)/params/$param33' + path: '/$param33' + fullPath: '/params/$param33' + preLoaderRoute: typeof genParamsParam33RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param32': { + id: '/(gen)/params/$param32' + path: '/$param32' + fullPath: '/params/$param32' + preLoaderRoute: typeof genParamsParam32RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param31': { + id: '/(gen)/params/$param31' + path: '/$param31' + fullPath: '/params/$param31' + preLoaderRoute: typeof genParamsParam31RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param30': { + id: '/(gen)/params/$param30' + path: '/$param30' + fullPath: '/params/$param30' + preLoaderRoute: typeof genParamsParam30RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param3': { + id: '/(gen)/params/$param3' + path: '/$param3' + fullPath: '/params/$param3' + preLoaderRoute: typeof genParamsParam3RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param29': { + id: '/(gen)/params/$param29' + path: '/$param29' + fullPath: '/params/$param29' + preLoaderRoute: typeof genParamsParam29RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param28': { + id: '/(gen)/params/$param28' + path: '/$param28' + fullPath: '/params/$param28' + preLoaderRoute: typeof genParamsParam28RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param27': { + id: '/(gen)/params/$param27' + path: '/$param27' + fullPath: '/params/$param27' + preLoaderRoute: typeof genParamsParam27RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param26': { + id: '/(gen)/params/$param26' + path: '/$param26' + fullPath: '/params/$param26' + preLoaderRoute: typeof genParamsParam26RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param25': { + id: '/(gen)/params/$param25' + path: '/$param25' + fullPath: '/params/$param25' + preLoaderRoute: typeof genParamsParam25RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param24': { + id: '/(gen)/params/$param24' + path: '/$param24' + fullPath: '/params/$param24' + preLoaderRoute: typeof genParamsParam24RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param23': { + id: '/(gen)/params/$param23' + path: '/$param23' + fullPath: '/params/$param23' + preLoaderRoute: typeof genParamsParam23RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param22': { + id: '/(gen)/params/$param22' + path: '/$param22' + fullPath: '/params/$param22' + preLoaderRoute: typeof genParamsParam22RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param21': { + id: '/(gen)/params/$param21' + path: '/$param21' + fullPath: '/params/$param21' + preLoaderRoute: typeof genParamsParam21RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param20': { + id: '/(gen)/params/$param20' + path: '/$param20' + fullPath: '/params/$param20' + preLoaderRoute: typeof genParamsParam20RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param2': { + id: '/(gen)/params/$param2' + path: '/$param2' + fullPath: '/params/$param2' + preLoaderRoute: typeof genParamsParam2RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param19': { + id: '/(gen)/params/$param19' + path: '/$param19' + fullPath: '/params/$param19' + preLoaderRoute: typeof genParamsParam19RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param18': { + id: '/(gen)/params/$param18' + path: '/$param18' + fullPath: '/params/$param18' + preLoaderRoute: typeof genParamsParam18RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param17': { + id: '/(gen)/params/$param17' + path: '/$param17' + fullPath: '/params/$param17' + preLoaderRoute: typeof genParamsParam17RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param16': { + id: '/(gen)/params/$param16' + path: '/$param16' + fullPath: '/params/$param16' + preLoaderRoute: typeof genParamsParam16RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param15': { + id: '/(gen)/params/$param15' + path: '/$param15' + fullPath: '/params/$param15' + preLoaderRoute: typeof genParamsParam15RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param14': { + id: '/(gen)/params/$param14' + path: '/$param14' + fullPath: '/params/$param14' + preLoaderRoute: typeof genParamsParam14RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param13': { + id: '/(gen)/params/$param13' + path: '/$param13' + fullPath: '/params/$param13' + preLoaderRoute: typeof genParamsParam13RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param12': { + id: '/(gen)/params/$param12' + path: '/$param12' + fullPath: '/params/$param12' + preLoaderRoute: typeof genParamsParam12RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param11': { + id: '/(gen)/params/$param11' + path: '/$param11' + fullPath: '/params/$param11' + preLoaderRoute: typeof genParamsParam11RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param10': { + id: '/(gen)/params/$param10' + path: '/$param10' + fullPath: '/params/$param10' + preLoaderRoute: typeof genParamsParam10RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param1': { + id: '/(gen)/params/$param1' + path: '/$param1' + fullPath: '/params/$param1' + preLoaderRoute: typeof genParamsParam1RouteImport + parentRoute: typeof genParamsRouteRoute + } + '/(gen)/params/$param0': { + id: '/(gen)/params/$param0' + path: '/$param0' + fullPath: '/params/$param0' + preLoaderRoute: typeof genParamsParam0RouteImport + parentRoute: typeof genParamsRouteRoute } } } @@ -217,6 +8049,426 @@ const SearchRouteRouteWithChildren = SearchRouteRoute._addFileChildren( SearchRouteRouteChildren, ) +interface genParamsRouteRouteChildren { + genParamsParam0Route: typeof genParamsParam0Route + genParamsParam1Route: typeof genParamsParam1Route + genParamsParam10Route: typeof genParamsParam10Route + genParamsParam11Route: typeof genParamsParam11Route + genParamsParam12Route: typeof genParamsParam12Route + genParamsParam13Route: typeof genParamsParam13Route + genParamsParam14Route: typeof genParamsParam14Route + genParamsParam15Route: typeof genParamsParam15Route + genParamsParam16Route: typeof genParamsParam16Route + genParamsParam17Route: typeof genParamsParam17Route + genParamsParam18Route: typeof genParamsParam18Route + genParamsParam19Route: typeof genParamsParam19Route + genParamsParam2Route: typeof genParamsParam2Route + genParamsParam20Route: typeof genParamsParam20Route + genParamsParam21Route: typeof genParamsParam21Route + genParamsParam22Route: typeof genParamsParam22Route + genParamsParam23Route: typeof genParamsParam23Route + genParamsParam24Route: typeof genParamsParam24Route + genParamsParam25Route: typeof genParamsParam25Route + genParamsParam26Route: typeof genParamsParam26Route + genParamsParam27Route: typeof genParamsParam27Route + genParamsParam28Route: typeof genParamsParam28Route + genParamsParam29Route: typeof genParamsParam29Route + genParamsParam3Route: typeof genParamsParam3Route + genParamsParam30Route: typeof genParamsParam30Route + genParamsParam31Route: typeof genParamsParam31Route + genParamsParam32Route: typeof genParamsParam32Route + genParamsParam33Route: typeof genParamsParam33Route + genParamsParam34Route: typeof genParamsParam34Route + genParamsParam35Route: typeof genParamsParam35Route + genParamsParam36Route: typeof genParamsParam36Route + genParamsParam37Route: typeof genParamsParam37Route + genParamsParam38Route: typeof genParamsParam38Route + genParamsParam39Route: typeof genParamsParam39Route + genParamsParam4Route: typeof genParamsParam4Route + genParamsParam40Route: typeof genParamsParam40Route + genParamsParam41Route: typeof genParamsParam41Route + genParamsParam42Route: typeof genParamsParam42Route + genParamsParam43Route: typeof genParamsParam43Route + genParamsParam44Route: typeof genParamsParam44Route + genParamsParam45Route: typeof genParamsParam45Route + genParamsParam46Route: typeof genParamsParam46Route + genParamsParam47Route: typeof genParamsParam47Route + genParamsParam48Route: typeof genParamsParam48Route + genParamsParam49Route: typeof genParamsParam49Route + genParamsParam5Route: typeof genParamsParam5Route + genParamsParam50Route: typeof genParamsParam50Route + genParamsParam51Route: typeof genParamsParam51Route + genParamsParam52Route: typeof genParamsParam52Route + genParamsParam53Route: typeof genParamsParam53Route + genParamsParam54Route: typeof genParamsParam54Route + genParamsParam55Route: typeof genParamsParam55Route + genParamsParam56Route: typeof genParamsParam56Route + genParamsParam57Route: typeof genParamsParam57Route + genParamsParam58Route: typeof genParamsParam58Route + genParamsParam59Route: typeof genParamsParam59Route + genParamsParam6Route: typeof genParamsParam6Route + genParamsParam60Route: typeof genParamsParam60Route + genParamsParam61Route: typeof genParamsParam61Route + genParamsParam62Route: typeof genParamsParam62Route + genParamsParam63Route: typeof genParamsParam63Route + genParamsParam64Route: typeof genParamsParam64Route + genParamsParam65Route: typeof genParamsParam65Route + genParamsParam66Route: typeof genParamsParam66Route + genParamsParam67Route: typeof genParamsParam67Route + genParamsParam68Route: typeof genParamsParam68Route + genParamsParam69Route: typeof genParamsParam69Route + genParamsParam7Route: typeof genParamsParam7Route + genParamsParam70Route: typeof genParamsParam70Route + genParamsParam71Route: typeof genParamsParam71Route + genParamsParam72Route: typeof genParamsParam72Route + genParamsParam73Route: typeof genParamsParam73Route + genParamsParam74Route: typeof genParamsParam74Route + genParamsParam75Route: typeof genParamsParam75Route + genParamsParam76Route: typeof genParamsParam76Route + genParamsParam77Route: typeof genParamsParam77Route + genParamsParam78Route: typeof genParamsParam78Route + genParamsParam79Route: typeof genParamsParam79Route + genParamsParam8Route: typeof genParamsParam8Route + genParamsParam80Route: typeof genParamsParam80Route + genParamsParam81Route: typeof genParamsParam81Route + genParamsParam82Route: typeof genParamsParam82Route + genParamsParam83Route: typeof genParamsParam83Route + genParamsParam84Route: typeof genParamsParam84Route + genParamsParam85Route: typeof genParamsParam85Route + genParamsParam86Route: typeof genParamsParam86Route + genParamsParam87Route: typeof genParamsParam87Route + genParamsParam88Route: typeof genParamsParam88Route + genParamsParam89Route: typeof genParamsParam89Route + genParamsParam9Route: typeof genParamsParam9Route + genParamsParam90Route: typeof genParamsParam90Route + genParamsParam91Route: typeof genParamsParam91Route + genParamsParam92Route: typeof genParamsParam92Route + genParamsParam93Route: typeof genParamsParam93Route + genParamsParam94Route: typeof genParamsParam94Route + genParamsParam95Route: typeof genParamsParam95Route + genParamsParam96Route: typeof genParamsParam96Route + genParamsParam97Route: typeof genParamsParam97Route + genParamsParam98Route: typeof genParamsParam98Route + genParamsParam99Route: typeof genParamsParam99Route +} + +const genParamsRouteRouteChildren: genParamsRouteRouteChildren = { + genParamsParam0Route: genParamsParam0Route, + genParamsParam1Route: genParamsParam1Route, + genParamsParam10Route: genParamsParam10Route, + genParamsParam11Route: genParamsParam11Route, + genParamsParam12Route: genParamsParam12Route, + genParamsParam13Route: genParamsParam13Route, + genParamsParam14Route: genParamsParam14Route, + genParamsParam15Route: genParamsParam15Route, + genParamsParam16Route: genParamsParam16Route, + genParamsParam17Route: genParamsParam17Route, + genParamsParam18Route: genParamsParam18Route, + genParamsParam19Route: genParamsParam19Route, + genParamsParam2Route: genParamsParam2Route, + genParamsParam20Route: genParamsParam20Route, + genParamsParam21Route: genParamsParam21Route, + genParamsParam22Route: genParamsParam22Route, + genParamsParam23Route: genParamsParam23Route, + genParamsParam24Route: genParamsParam24Route, + genParamsParam25Route: genParamsParam25Route, + genParamsParam26Route: genParamsParam26Route, + genParamsParam27Route: genParamsParam27Route, + genParamsParam28Route: genParamsParam28Route, + genParamsParam29Route: genParamsParam29Route, + genParamsParam3Route: genParamsParam3Route, + genParamsParam30Route: genParamsParam30Route, + genParamsParam31Route: genParamsParam31Route, + genParamsParam32Route: genParamsParam32Route, + genParamsParam33Route: genParamsParam33Route, + genParamsParam34Route: genParamsParam34Route, + genParamsParam35Route: genParamsParam35Route, + genParamsParam36Route: genParamsParam36Route, + genParamsParam37Route: genParamsParam37Route, + genParamsParam38Route: genParamsParam38Route, + genParamsParam39Route: genParamsParam39Route, + genParamsParam4Route: genParamsParam4Route, + genParamsParam40Route: genParamsParam40Route, + genParamsParam41Route: genParamsParam41Route, + genParamsParam42Route: genParamsParam42Route, + genParamsParam43Route: genParamsParam43Route, + genParamsParam44Route: genParamsParam44Route, + genParamsParam45Route: genParamsParam45Route, + genParamsParam46Route: genParamsParam46Route, + genParamsParam47Route: genParamsParam47Route, + genParamsParam48Route: genParamsParam48Route, + genParamsParam49Route: genParamsParam49Route, + genParamsParam5Route: genParamsParam5Route, + genParamsParam50Route: genParamsParam50Route, + genParamsParam51Route: genParamsParam51Route, + genParamsParam52Route: genParamsParam52Route, + genParamsParam53Route: genParamsParam53Route, + genParamsParam54Route: genParamsParam54Route, + genParamsParam55Route: genParamsParam55Route, + genParamsParam56Route: genParamsParam56Route, + genParamsParam57Route: genParamsParam57Route, + genParamsParam58Route: genParamsParam58Route, + genParamsParam59Route: genParamsParam59Route, + genParamsParam6Route: genParamsParam6Route, + genParamsParam60Route: genParamsParam60Route, + genParamsParam61Route: genParamsParam61Route, + genParamsParam62Route: genParamsParam62Route, + genParamsParam63Route: genParamsParam63Route, + genParamsParam64Route: genParamsParam64Route, + genParamsParam65Route: genParamsParam65Route, + genParamsParam66Route: genParamsParam66Route, + genParamsParam67Route: genParamsParam67Route, + genParamsParam68Route: genParamsParam68Route, + genParamsParam69Route: genParamsParam69Route, + genParamsParam7Route: genParamsParam7Route, + genParamsParam70Route: genParamsParam70Route, + genParamsParam71Route: genParamsParam71Route, + genParamsParam72Route: genParamsParam72Route, + genParamsParam73Route: genParamsParam73Route, + genParamsParam74Route: genParamsParam74Route, + genParamsParam75Route: genParamsParam75Route, + genParamsParam76Route: genParamsParam76Route, + genParamsParam77Route: genParamsParam77Route, + genParamsParam78Route: genParamsParam78Route, + genParamsParam79Route: genParamsParam79Route, + genParamsParam8Route: genParamsParam8Route, + genParamsParam80Route: genParamsParam80Route, + genParamsParam81Route: genParamsParam81Route, + genParamsParam82Route: genParamsParam82Route, + genParamsParam83Route: genParamsParam83Route, + genParamsParam84Route: genParamsParam84Route, + genParamsParam85Route: genParamsParam85Route, + genParamsParam86Route: genParamsParam86Route, + genParamsParam87Route: genParamsParam87Route, + genParamsParam88Route: genParamsParam88Route, + genParamsParam89Route: genParamsParam89Route, + genParamsParam9Route: genParamsParam9Route, + genParamsParam90Route: genParamsParam90Route, + genParamsParam91Route: genParamsParam91Route, + genParamsParam92Route: genParamsParam92Route, + genParamsParam93Route: genParamsParam93Route, + genParamsParam94Route: genParamsParam94Route, + genParamsParam95Route: genParamsParam95Route, + genParamsParam96Route: genParamsParam96Route, + genParamsParam97Route: genParamsParam97Route, + genParamsParam98Route: genParamsParam98Route, + genParamsParam99Route: genParamsParam99Route, +} + +const genParamsRouteRouteWithChildren = genParamsRouteRoute._addFileChildren( + genParamsRouteRouteChildren, +) + +interface genSearchRouteRouteChildren { + genSearchSearch0Route: typeof genSearchSearch0Route + genSearchSearch1Route: typeof genSearchSearch1Route + genSearchSearch10Route: typeof genSearchSearch10Route + genSearchSearch11Route: typeof genSearchSearch11Route + genSearchSearch12Route: typeof genSearchSearch12Route + genSearchSearch13Route: typeof genSearchSearch13Route + genSearchSearch14Route: typeof genSearchSearch14Route + genSearchSearch15Route: typeof genSearchSearch15Route + genSearchSearch16Route: typeof genSearchSearch16Route + genSearchSearch17Route: typeof genSearchSearch17Route + genSearchSearch18Route: typeof genSearchSearch18Route + genSearchSearch19Route: typeof genSearchSearch19Route + genSearchSearch2Route: typeof genSearchSearch2Route + genSearchSearch20Route: typeof genSearchSearch20Route + genSearchSearch21Route: typeof genSearchSearch21Route + genSearchSearch22Route: typeof genSearchSearch22Route + genSearchSearch23Route: typeof genSearchSearch23Route + genSearchSearch24Route: typeof genSearchSearch24Route + genSearchSearch25Route: typeof genSearchSearch25Route + genSearchSearch26Route: typeof genSearchSearch26Route + genSearchSearch27Route: typeof genSearchSearch27Route + genSearchSearch28Route: typeof genSearchSearch28Route + genSearchSearch29Route: typeof genSearchSearch29Route + genSearchSearch3Route: typeof genSearchSearch3Route + genSearchSearch30Route: typeof genSearchSearch30Route + genSearchSearch31Route: typeof genSearchSearch31Route + genSearchSearch32Route: typeof genSearchSearch32Route + genSearchSearch33Route: typeof genSearchSearch33Route + genSearchSearch34Route: typeof genSearchSearch34Route + genSearchSearch35Route: typeof genSearchSearch35Route + genSearchSearch36Route: typeof genSearchSearch36Route + genSearchSearch37Route: typeof genSearchSearch37Route + genSearchSearch38Route: typeof genSearchSearch38Route + genSearchSearch39Route: typeof genSearchSearch39Route + genSearchSearch4Route: typeof genSearchSearch4Route + genSearchSearch40Route: typeof genSearchSearch40Route + genSearchSearch41Route: typeof genSearchSearch41Route + genSearchSearch42Route: typeof genSearchSearch42Route + genSearchSearch43Route: typeof genSearchSearch43Route + genSearchSearch44Route: typeof genSearchSearch44Route + genSearchSearch45Route: typeof genSearchSearch45Route + genSearchSearch46Route: typeof genSearchSearch46Route + genSearchSearch47Route: typeof genSearchSearch47Route + genSearchSearch48Route: typeof genSearchSearch48Route + genSearchSearch49Route: typeof genSearchSearch49Route + genSearchSearch5Route: typeof genSearchSearch5Route + genSearchSearch50Route: typeof genSearchSearch50Route + genSearchSearch51Route: typeof genSearchSearch51Route + genSearchSearch52Route: typeof genSearchSearch52Route + genSearchSearch53Route: typeof genSearchSearch53Route + genSearchSearch54Route: typeof genSearchSearch54Route + genSearchSearch55Route: typeof genSearchSearch55Route + genSearchSearch56Route: typeof genSearchSearch56Route + genSearchSearch57Route: typeof genSearchSearch57Route + genSearchSearch58Route: typeof genSearchSearch58Route + genSearchSearch59Route: typeof genSearchSearch59Route + genSearchSearch6Route: typeof genSearchSearch6Route + genSearchSearch60Route: typeof genSearchSearch60Route + genSearchSearch61Route: typeof genSearchSearch61Route + genSearchSearch62Route: typeof genSearchSearch62Route + genSearchSearch63Route: typeof genSearchSearch63Route + genSearchSearch64Route: typeof genSearchSearch64Route + genSearchSearch65Route: typeof genSearchSearch65Route + genSearchSearch66Route: typeof genSearchSearch66Route + genSearchSearch67Route: typeof genSearchSearch67Route + genSearchSearch68Route: typeof genSearchSearch68Route + genSearchSearch69Route: typeof genSearchSearch69Route + genSearchSearch7Route: typeof genSearchSearch7Route + genSearchSearch70Route: typeof genSearchSearch70Route + genSearchSearch71Route: typeof genSearchSearch71Route + genSearchSearch72Route: typeof genSearchSearch72Route + genSearchSearch73Route: typeof genSearchSearch73Route + genSearchSearch74Route: typeof genSearchSearch74Route + genSearchSearch75Route: typeof genSearchSearch75Route + genSearchSearch76Route: typeof genSearchSearch76Route + genSearchSearch77Route: typeof genSearchSearch77Route + genSearchSearch78Route: typeof genSearchSearch78Route + genSearchSearch79Route: typeof genSearchSearch79Route + genSearchSearch8Route: typeof genSearchSearch8Route + genSearchSearch80Route: typeof genSearchSearch80Route + genSearchSearch81Route: typeof genSearchSearch81Route + genSearchSearch82Route: typeof genSearchSearch82Route + genSearchSearch83Route: typeof genSearchSearch83Route + genSearchSearch84Route: typeof genSearchSearch84Route + genSearchSearch85Route: typeof genSearchSearch85Route + genSearchSearch86Route: typeof genSearchSearch86Route + genSearchSearch87Route: typeof genSearchSearch87Route + genSearchSearch88Route: typeof genSearchSearch88Route + genSearchSearch89Route: typeof genSearchSearch89Route + genSearchSearch9Route: typeof genSearchSearch9Route + genSearchSearch90Route: typeof genSearchSearch90Route + genSearchSearch91Route: typeof genSearchSearch91Route + genSearchSearch92Route: typeof genSearchSearch92Route + genSearchSearch93Route: typeof genSearchSearch93Route + genSearchSearch94Route: typeof genSearchSearch94Route + genSearchSearch95Route: typeof genSearchSearch95Route + genSearchSearch96Route: typeof genSearchSearch96Route + genSearchSearch97Route: typeof genSearchSearch97Route + genSearchSearch98Route: typeof genSearchSearch98Route + genSearchSearch99Route: typeof genSearchSearch99Route +} + +const genSearchRouteRouteChildren: genSearchRouteRouteChildren = { + genSearchSearch0Route: genSearchSearch0Route, + genSearchSearch1Route: genSearchSearch1Route, + genSearchSearch10Route: genSearchSearch10Route, + genSearchSearch11Route: genSearchSearch11Route, + genSearchSearch12Route: genSearchSearch12Route, + genSearchSearch13Route: genSearchSearch13Route, + genSearchSearch14Route: genSearchSearch14Route, + genSearchSearch15Route: genSearchSearch15Route, + genSearchSearch16Route: genSearchSearch16Route, + genSearchSearch17Route: genSearchSearch17Route, + genSearchSearch18Route: genSearchSearch18Route, + genSearchSearch19Route: genSearchSearch19Route, + genSearchSearch2Route: genSearchSearch2Route, + genSearchSearch20Route: genSearchSearch20Route, + genSearchSearch21Route: genSearchSearch21Route, + genSearchSearch22Route: genSearchSearch22Route, + genSearchSearch23Route: genSearchSearch23Route, + genSearchSearch24Route: genSearchSearch24Route, + genSearchSearch25Route: genSearchSearch25Route, + genSearchSearch26Route: genSearchSearch26Route, + genSearchSearch27Route: genSearchSearch27Route, + genSearchSearch28Route: genSearchSearch28Route, + genSearchSearch29Route: genSearchSearch29Route, + genSearchSearch3Route: genSearchSearch3Route, + genSearchSearch30Route: genSearchSearch30Route, + genSearchSearch31Route: genSearchSearch31Route, + genSearchSearch32Route: genSearchSearch32Route, + genSearchSearch33Route: genSearchSearch33Route, + genSearchSearch34Route: genSearchSearch34Route, + genSearchSearch35Route: genSearchSearch35Route, + genSearchSearch36Route: genSearchSearch36Route, + genSearchSearch37Route: genSearchSearch37Route, + genSearchSearch38Route: genSearchSearch38Route, + genSearchSearch39Route: genSearchSearch39Route, + genSearchSearch4Route: genSearchSearch4Route, + genSearchSearch40Route: genSearchSearch40Route, + genSearchSearch41Route: genSearchSearch41Route, + genSearchSearch42Route: genSearchSearch42Route, + genSearchSearch43Route: genSearchSearch43Route, + genSearchSearch44Route: genSearchSearch44Route, + genSearchSearch45Route: genSearchSearch45Route, + genSearchSearch46Route: genSearchSearch46Route, + genSearchSearch47Route: genSearchSearch47Route, + genSearchSearch48Route: genSearchSearch48Route, + genSearchSearch49Route: genSearchSearch49Route, + genSearchSearch5Route: genSearchSearch5Route, + genSearchSearch50Route: genSearchSearch50Route, + genSearchSearch51Route: genSearchSearch51Route, + genSearchSearch52Route: genSearchSearch52Route, + genSearchSearch53Route: genSearchSearch53Route, + genSearchSearch54Route: genSearchSearch54Route, + genSearchSearch55Route: genSearchSearch55Route, + genSearchSearch56Route: genSearchSearch56Route, + genSearchSearch57Route: genSearchSearch57Route, + genSearchSearch58Route: genSearchSearch58Route, + genSearchSearch59Route: genSearchSearch59Route, + genSearchSearch6Route: genSearchSearch6Route, + genSearchSearch60Route: genSearchSearch60Route, + genSearchSearch61Route: genSearchSearch61Route, + genSearchSearch62Route: genSearchSearch62Route, + genSearchSearch63Route: genSearchSearch63Route, + genSearchSearch64Route: genSearchSearch64Route, + genSearchSearch65Route: genSearchSearch65Route, + genSearchSearch66Route: genSearchSearch66Route, + genSearchSearch67Route: genSearchSearch67Route, + genSearchSearch68Route: genSearchSearch68Route, + genSearchSearch69Route: genSearchSearch69Route, + genSearchSearch7Route: genSearchSearch7Route, + genSearchSearch70Route: genSearchSearch70Route, + genSearchSearch71Route: genSearchSearch71Route, + genSearchSearch72Route: genSearchSearch72Route, + genSearchSearch73Route: genSearchSearch73Route, + genSearchSearch74Route: genSearchSearch74Route, + genSearchSearch75Route: genSearchSearch75Route, + genSearchSearch76Route: genSearchSearch76Route, + genSearchSearch77Route: genSearchSearch77Route, + genSearchSearch78Route: genSearchSearch78Route, + genSearchSearch79Route: genSearchSearch79Route, + genSearchSearch8Route: genSearchSearch8Route, + genSearchSearch80Route: genSearchSearch80Route, + genSearchSearch81Route: genSearchSearch81Route, + genSearchSearch82Route: genSearchSearch82Route, + genSearchSearch83Route: genSearchSearch83Route, + genSearchSearch84Route: genSearchSearch84Route, + genSearchSearch85Route: genSearchSearch85Route, + genSearchSearch86Route: genSearchSearch86Route, + genSearchSearch87Route: genSearchSearch87Route, + genSearchSearch88Route: genSearchSearch88Route, + genSearchSearch89Route: genSearchSearch89Route, + genSearchSearch9Route: genSearchSearch9Route, + genSearchSearch90Route: genSearchSearch90Route, + genSearchSearch91Route: genSearchSearch91Route, + genSearchSearch92Route: genSearchSearch92Route, + genSearchSearch93Route: genSearchSearch93Route, + genSearchSearch94Route: genSearchSearch94Route, + genSearchSearch95Route: genSearchSearch95Route, + genSearchSearch96Route: genSearchSearch96Route, + genSearchSearch97Route: genSearchSearch97Route, + genSearchSearch98Route: genSearchSearch98Route, + genSearchSearch99Route: genSearchSearch99Route, +} + +const genSearchRouteRouteWithChildren = genSearchRouteRoute._addFileChildren( + genSearchRouteRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ParamsRouteRoute: ParamsRouteRouteWithChildren, @@ -224,6 +8476,208 @@ const rootRouteChildren: RootRouteChildren = { AbsoluteRoute: AbsoluteRoute, LinkPropsRoute: LinkPropsRoute, RelativeRoute: RelativeRoute, + genParamsRouteRoute: genParamsRouteRouteWithChildren, + genSearchRouteRoute: genSearchRouteRouteWithChildren, + genAbsolute0Route: genAbsolute0Route, + genAbsolute1Route: genAbsolute1Route, + genAbsolute10Route: genAbsolute10Route, + genAbsolute11Route: genAbsolute11Route, + genAbsolute12Route: genAbsolute12Route, + genAbsolute13Route: genAbsolute13Route, + genAbsolute14Route: genAbsolute14Route, + genAbsolute15Route: genAbsolute15Route, + genAbsolute16Route: genAbsolute16Route, + genAbsolute17Route: genAbsolute17Route, + genAbsolute18Route: genAbsolute18Route, + genAbsolute19Route: genAbsolute19Route, + genAbsolute2Route: genAbsolute2Route, + genAbsolute20Route: genAbsolute20Route, + genAbsolute21Route: genAbsolute21Route, + genAbsolute22Route: genAbsolute22Route, + genAbsolute23Route: genAbsolute23Route, + genAbsolute24Route: genAbsolute24Route, + genAbsolute25Route: genAbsolute25Route, + genAbsolute26Route: genAbsolute26Route, + genAbsolute27Route: genAbsolute27Route, + genAbsolute28Route: genAbsolute28Route, + genAbsolute29Route: genAbsolute29Route, + genAbsolute3Route: genAbsolute3Route, + genAbsolute30Route: genAbsolute30Route, + genAbsolute31Route: genAbsolute31Route, + genAbsolute32Route: genAbsolute32Route, + genAbsolute33Route: genAbsolute33Route, + genAbsolute34Route: genAbsolute34Route, + genAbsolute35Route: genAbsolute35Route, + genAbsolute36Route: genAbsolute36Route, + genAbsolute37Route: genAbsolute37Route, + genAbsolute38Route: genAbsolute38Route, + genAbsolute39Route: genAbsolute39Route, + genAbsolute4Route: genAbsolute4Route, + genAbsolute40Route: genAbsolute40Route, + genAbsolute41Route: genAbsolute41Route, + genAbsolute42Route: genAbsolute42Route, + genAbsolute43Route: genAbsolute43Route, + genAbsolute44Route: genAbsolute44Route, + genAbsolute45Route: genAbsolute45Route, + genAbsolute46Route: genAbsolute46Route, + genAbsolute47Route: genAbsolute47Route, + genAbsolute48Route: genAbsolute48Route, + genAbsolute49Route: genAbsolute49Route, + genAbsolute5Route: genAbsolute5Route, + genAbsolute50Route: genAbsolute50Route, + genAbsolute51Route: genAbsolute51Route, + genAbsolute52Route: genAbsolute52Route, + genAbsolute53Route: genAbsolute53Route, + genAbsolute54Route: genAbsolute54Route, + genAbsolute55Route: genAbsolute55Route, + genAbsolute56Route: genAbsolute56Route, + genAbsolute57Route: genAbsolute57Route, + genAbsolute58Route: genAbsolute58Route, + genAbsolute59Route: genAbsolute59Route, + genAbsolute6Route: genAbsolute6Route, + genAbsolute60Route: genAbsolute60Route, + genAbsolute61Route: genAbsolute61Route, + genAbsolute62Route: genAbsolute62Route, + genAbsolute63Route: genAbsolute63Route, + genAbsolute64Route: genAbsolute64Route, + genAbsolute65Route: genAbsolute65Route, + genAbsolute66Route: genAbsolute66Route, + genAbsolute67Route: genAbsolute67Route, + genAbsolute68Route: genAbsolute68Route, + genAbsolute69Route: genAbsolute69Route, + genAbsolute7Route: genAbsolute7Route, + genAbsolute70Route: genAbsolute70Route, + genAbsolute71Route: genAbsolute71Route, + genAbsolute72Route: genAbsolute72Route, + genAbsolute73Route: genAbsolute73Route, + genAbsolute74Route: genAbsolute74Route, + genAbsolute75Route: genAbsolute75Route, + genAbsolute76Route: genAbsolute76Route, + genAbsolute77Route: genAbsolute77Route, + genAbsolute78Route: genAbsolute78Route, + genAbsolute79Route: genAbsolute79Route, + genAbsolute8Route: genAbsolute8Route, + genAbsolute80Route: genAbsolute80Route, + genAbsolute81Route: genAbsolute81Route, + genAbsolute82Route: genAbsolute82Route, + genAbsolute83Route: genAbsolute83Route, + genAbsolute84Route: genAbsolute84Route, + genAbsolute85Route: genAbsolute85Route, + genAbsolute86Route: genAbsolute86Route, + genAbsolute87Route: genAbsolute87Route, + genAbsolute88Route: genAbsolute88Route, + genAbsolute89Route: genAbsolute89Route, + genAbsolute9Route: genAbsolute9Route, + genAbsolute90Route: genAbsolute90Route, + genAbsolute91Route: genAbsolute91Route, + genAbsolute92Route: genAbsolute92Route, + genAbsolute93Route: genAbsolute93Route, + genAbsolute94Route: genAbsolute94Route, + genAbsolute95Route: genAbsolute95Route, + genAbsolute96Route: genAbsolute96Route, + genAbsolute97Route: genAbsolute97Route, + genAbsolute98Route: genAbsolute98Route, + genAbsolute99Route: genAbsolute99Route, + genRelative0Route: genRelative0Route, + genRelative1Route: genRelative1Route, + genRelative10Route: genRelative10Route, + genRelative11Route: genRelative11Route, + genRelative12Route: genRelative12Route, + genRelative13Route: genRelative13Route, + genRelative14Route: genRelative14Route, + genRelative15Route: genRelative15Route, + genRelative16Route: genRelative16Route, + genRelative17Route: genRelative17Route, + genRelative18Route: genRelative18Route, + genRelative19Route: genRelative19Route, + genRelative2Route: genRelative2Route, + genRelative20Route: genRelative20Route, + genRelative21Route: genRelative21Route, + genRelative22Route: genRelative22Route, + genRelative23Route: genRelative23Route, + genRelative24Route: genRelative24Route, + genRelative25Route: genRelative25Route, + genRelative26Route: genRelative26Route, + genRelative27Route: genRelative27Route, + genRelative28Route: genRelative28Route, + genRelative29Route: genRelative29Route, + genRelative3Route: genRelative3Route, + genRelative30Route: genRelative30Route, + genRelative31Route: genRelative31Route, + genRelative32Route: genRelative32Route, + genRelative33Route: genRelative33Route, + genRelative34Route: genRelative34Route, + genRelative35Route: genRelative35Route, + genRelative36Route: genRelative36Route, + genRelative37Route: genRelative37Route, + genRelative38Route: genRelative38Route, + genRelative39Route: genRelative39Route, + genRelative4Route: genRelative4Route, + genRelative40Route: genRelative40Route, + genRelative41Route: genRelative41Route, + genRelative42Route: genRelative42Route, + genRelative43Route: genRelative43Route, + genRelative44Route: genRelative44Route, + genRelative45Route: genRelative45Route, + genRelative46Route: genRelative46Route, + genRelative47Route: genRelative47Route, + genRelative48Route: genRelative48Route, + genRelative49Route: genRelative49Route, + genRelative5Route: genRelative5Route, + genRelative50Route: genRelative50Route, + genRelative51Route: genRelative51Route, + genRelative52Route: genRelative52Route, + genRelative53Route: genRelative53Route, + genRelative54Route: genRelative54Route, + genRelative55Route: genRelative55Route, + genRelative56Route: genRelative56Route, + genRelative57Route: genRelative57Route, + genRelative58Route: genRelative58Route, + genRelative59Route: genRelative59Route, + genRelative6Route: genRelative6Route, + genRelative60Route: genRelative60Route, + genRelative61Route: genRelative61Route, + genRelative62Route: genRelative62Route, + genRelative63Route: genRelative63Route, + genRelative64Route: genRelative64Route, + genRelative65Route: genRelative65Route, + genRelative66Route: genRelative66Route, + genRelative67Route: genRelative67Route, + genRelative68Route: genRelative68Route, + genRelative69Route: genRelative69Route, + genRelative7Route: genRelative7Route, + genRelative70Route: genRelative70Route, + genRelative71Route: genRelative71Route, + genRelative72Route: genRelative72Route, + genRelative73Route: genRelative73Route, + genRelative74Route: genRelative74Route, + genRelative75Route: genRelative75Route, + genRelative76Route: genRelative76Route, + genRelative77Route: genRelative77Route, + genRelative78Route: genRelative78Route, + genRelative79Route: genRelative79Route, + genRelative8Route: genRelative8Route, + genRelative80Route: genRelative80Route, + genRelative81Route: genRelative81Route, + genRelative82Route: genRelative82Route, + genRelative83Route: genRelative83Route, + genRelative84Route: genRelative84Route, + genRelative85Route: genRelative85Route, + genRelative86Route: genRelative86Route, + genRelative87Route: genRelative87Route, + genRelative88Route: genRelative88Route, + genRelative89Route: genRelative89Route, + genRelative9Route: genRelative9Route, + genRelative90Route: genRelative90Route, + genRelative91Route: genRelative91Route, + genRelative92Route: genRelative92Route, + genRelative93Route: genRelative93Route, + genRelative94Route: genRelative94Route, + genRelative95Route: genRelative95Route, + genRelative96Route: genRelative96Route, + genRelative97Route: genRelative97Route, + genRelative98Route: genRelative98Route, + genRelative99Route: genRelative99Route, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx index 48de353b2e2..cae215bf804 100644 --- a/packages/react-router/src/index.tsx +++ b/packages/react-router/src/index.tsx @@ -129,6 +129,7 @@ export type { AwaitOptions } from './awaited' export { CatchBoundary, ErrorComponent } from './CatchBoundary' export { ClientOnly, useHydrated } from './ClientOnly' +export { reactUse } from './utils' export { FileRoute, diff --git a/packages/react-start-client/package.json b/packages/react-start-client/package.json index e4cf8de7d79..6981d7dee71 100644 --- a/packages/react-start-client/package.json +++ b/packages/react-start-client/package.json @@ -48,6 +48,12 @@ "default": "./dist/esm/index.js" } }, + "./hydration": { + "import": { + "types": "./dist/esm/hydration.d.ts", + "default": "./dist/esm/hydration.js" + } + }, "./package.json": "./package.json" }, "sideEffects": false, diff --git a/packages/react-start-client/src/GenericHydrate.tsx b/packages/react-start-client/src/GenericHydrate.tsx new file mode 100644 index 00000000000..e9b680587ed --- /dev/null +++ b/packages/react-start-client/src/GenericHydrate.tsx @@ -0,0 +1,287 @@ +'use client' + +import * as React from 'react' + +import { useHydrated } from '@tanstack/react-router' +import { isServer } from '@tanstack/router-core/isServer' +import { + hydrateIdAttribute, + hydrateWhenAttribute, +} from '@tanstack/start-client-core/hydration/constants' +import { + createResolvedGate, + getFallbackHtml, + getOrCreateGate, + onGateResolve, + releaseGate, + saveFallbackHtml, +} from '@tanstack/start-client-core/hydration/runtime' +import type { HydrationRuntimeContext } from '@tanstack/start-client-core/hydration' +import type { HydrationGateRecord } from '@tanstack/start-client-core/hydration/runtime' +import type { HydrateProps, InternalHydrateProps } from './Hydrate' + +type Gate = HydrationGateRecord & { promise: Promise } + +function shouldDeferHydration(strategy: InternalHydrateProps['when']) { + return strategy._d ? strategy._d() : strategy._t !== 'load' +} + +function useLatest(value: T) { + const ref = React.useRef(value) + ref.current = value + return ref +} + +function runStrategyCleanup(cleanup: void | (() => void)) { + if (typeof cleanup === 'function') return cleanup + return undefined +} + +function useHydrationGate(props: InternalHydrateProps) { + const hydrated = useHydrated() + const reactId = React.useId() + const id = props.h ? `${props.h}${reactId}` : reactId + const hydrateStrategy = props.when + const latestRef = useLatest({ + hydrateStrategy, + prefetch: props.prefetch, + delegated: props.g, + preload: props.p, + }) + const gateRef = React.useRef(undefined) + const markerElementRef = React.useRef(null) + const shouldPreserveServerHTMLRef = React.useRef( + undefined, + ) + const shouldDeferInitialHydrationRef = React.useRef( + undefined, + ) + const didPrefetchRef = React.useRef(false) + + shouldPreserveServerHTMLRef.current ??= + ((isServer as boolean | undefined) ?? typeof window === 'undefined') || + !hydrated + shouldDeferInitialHydrationRef.current ??= + !hydrated && shouldDeferHydration(hydrateStrategy) + + if (!gateRef.current) { + gateRef.current = + ((isServer as boolean | undefined) ?? typeof window === 'undefined') + ? createResolvedGate(id, hydrateStrategy._t!) + : getOrCreateGate(id, hydrateStrategy._t!) + } + + gateRef.current.when = hydrateStrategy._t! + + if ( + !((isServer as boolean | undefined) ?? typeof window === 'undefined') && + hydrateStrategy._t !== 'never' && + (!shouldDeferInitialHydrationRef.current || + !shouldDeferHydration(hydrateStrategy)) + ) { + gateRef.current.resolve() + } + + const markerRef = React.useCallback( + (element: HTMLDivElement | null) => { + markerElementRef.current = element + if (element) { + if ( + latestRef.current.hydrateStrategy._t === 'never' && + !shouldPreserveServerHTMLRef.current + ) { + element.replaceChildren() + } + saveFallbackHtml(id, element) + } + }, + [id, latestRef], + ) + + React.useEffect(() => { + const gate = gateRef.current! + return () => { + releaseGate(gate) + } + }, []) + + React.useEffect(() => { + if ( + ((isServer as boolean | undefined) ?? typeof window === 'undefined') || + !latestRef.current.preload || + !latestRef.current.prefetch || + didPrefetchRef.current + ) { + return + } + + const prefetch = () => { + if (didPrefetchRef.current) return + didPrefetchRef.current = true + void latestRef.current.preload?.() + } + + return runStrategyCleanup( + latestRef.current.prefetch._s?.({ + element: markerElementRef.current, + prefetch, + }), + ) + }, [latestRef, props.prefetch, props.p]) + + React.useEffect(() => { + const gate = gateRef.current! + const { hydrateStrategy, delegated: delegatedStrategy } = latestRef.current + if ( + gate.resolved || + !shouldDeferInitialHydrationRef.current || + hydrateStrategy._t === 'never' + ) { + return + } + + const cleanups: Array<() => void> = [] + let removeResolveListener = () => {} + let disposed = false + + const cleanup = () => { + if (disposed) return + disposed = true + removeResolveListener() + cleanups.forEach((fn) => fn()) + } + + const addCleanup = (fn: void | (() => void)) => { + if (!fn) return + if (disposed || gate.resolved) { + fn() + return + } + cleanups.push(fn) + } + + removeResolveListener = onGateResolve(gate, cleanup) + + const context: HydrationRuntimeContext = { + element: markerElementRef.current, + gate, + } + addCleanup(runStrategyCleanup(hydrateStrategy._s?.(context))) + + if (delegatedStrategy?._s) { + addCleanup( + runStrategyCleanup( + delegatedStrategy._s({ + ...context, + delegated: true, + }), + ), + ) + } + + return cleanup + }, [latestRef, props.g, props.when]) + + return { + gate: gateRef.current, + markerRef, + hydrateStrategy, + shouldPreserveServerHTML: shouldPreserveServerHTMLRef.current, + } +} + +function HydrationGate(props: { gate: Gate; children: React.ReactNode }) { + if ((isServer as boolean | undefined) ?? typeof window === 'undefined') { + return props.children as React.JSX.Element + } + + if (props.gate.resolved) { + return props.children as React.JSX.Element + } + + throw props.gate.promise +} + +function HydratedBoundary(props: { + id: string + onHydrated?: () => void + onStrategyHydrated?: (id: string) => void + children: React.ReactNode +}) { + const { id, onHydrated, onStrategyHydrated } = props + const didHydrateRef = React.useRef(false) + + React.useEffect(() => { + if (didHydrateRef.current) return + didHydrateRef.current = true + onHydrated?.() + onStrategyHydrated?.(id) + }, [id, onHydrated, onStrategyHydrated]) + + return props.children as React.JSX.Element +} + +function HydrationFallback(props: { id: string }) { + const html = getFallbackHtml(props.id) + + if (!html) return null + + return ( +
+ ) +} + +export function GenericHydrate(props: HydrateProps): React.JSX.Element { + const internalProps = props as InternalHydrateProps + const { gate, hydrateStrategy, markerRef, shouldPreserveServerHTML } = + useHydrationGate(internalProps) + const fallback = shouldPreserveServerHTML ? ( + + ) : ( + (props.fallback ?? null) + ) + const markerAttributes = hydrateStrategy._a?.() + + const hydrateType = hydrateStrategy._t! + + if (hydrateType === 'never' && !shouldPreserveServerHTML) { + return ( +
+ {props.fallback ?? null} +
+ ) + } + + return ( +
+ + + + {props.children} + + + +
+ ) +} diff --git a/packages/react-start-client/src/Hydrate.tsx b/packages/react-start-client/src/Hydrate.tsx new file mode 100644 index 00000000000..ef914ddca39 --- /dev/null +++ b/packages/react-start-client/src/Hydrate.tsx @@ -0,0 +1,60 @@ +'use client' + +import type * as React from 'react' + +import type { + HydrationStrategy as CoreHydrationStrategy, + HydrationPrefetchStrategy, + HydrationWhen, +} from '@tanstack/start-client-core/hydration' + +export type { + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationWhen, +} from '@tanstack/start-client-core/hydration' + +export type ReactHydrationStrategy< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = CoreHydrationStrategy & { + _h: (props: HydrateProps) => React.JSX.Element +} + +export type HydrationStrategy< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = ReactHydrationStrategy + +export type HydrateOptions = { + when: ReactHydrationStrategy +} + +type HydrateCommonProps = { + fallback?: React.ReactNode + onHydrated?: () => void + children: React.ReactNode +} + +export type HydrateProps = + | (HydrateCommonProps & + HydrateOptions & { + prefetch?: never + split?: boolean + }) + | (HydrateCommonProps & + HydrateOptions & { + prefetch: HydrationPrefetchStrategy + split?: true + }) + +export type InternalHydrateProps = HydrateProps & { + g?: CoreHydrationStrategy + h?: string + p?: () => Promise +} + +export function Hydrate(props: HydrateProps): React.JSX.Element { + return props.when._h(props) +} diff --git a/packages/react-start-client/src/hydration.ts b/packages/react-start-client/src/hydration.ts new file mode 100644 index 00000000000..b8a0dc04b33 --- /dev/null +++ b/packages/react-start-client/src/hydration.ts @@ -0,0 +1,19 @@ +'use client' + +export { condition, interaction, media } from './hydration/generic' +export { idle } from './hydration/idle' +export { load } from './hydration/load' +export { never } from './hydration/never' +export { visible } from './hydration/visible' +export type { + HydrationCondition, + HydrationInteractionEvent, + HydrationInteractionEvents, + IdleHydrationOptions, + HydrationPrefetchWhen, + HydrationPrefetchStrategy, + HydrationStrategyTypes, + HydrationWhen, + VisibleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +export type { HydrationStrategy, ReactHydrationStrategy } from './Hydrate' diff --git a/packages/react-start-client/src/hydration/generic.ts b/packages/react-start-client/src/hydration/generic.ts new file mode 100644 index 00000000000..4023e6b8891 --- /dev/null +++ b/packages/react-start-client/src/hydration/generic.ts @@ -0,0 +1,46 @@ +'use client' + +import { + condition as coreCondition, + interaction as coreInteraction, + media as coreMedia, +} from '@tanstack/start-client-core/hydration' +import { GenericHydrate } from '../GenericHydrate' +import type { + HydrationCondition, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, +} from '@tanstack/start-client-core/hydration' +import type { ReactHydrationStrategy } from '../Hydrate' + +/* @__NO_SIDE_EFFECTS__ */ +function withGenericRenderer( + strategy: T, +): T & ReactHydrationStrategy { + return /* @__PURE__ */ Object.assign(strategy, { + _h: GenericHydrate, + }) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function media( + query: string, +): ReactHydrationStrategy<'media', true> & HydrationPrefetchStrategy<'media'> { + return /* @__PURE__ */ withGenericRenderer(coreMedia(query)) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function condition( + condition: HydrationCondition, +): ReactHydrationStrategy<'condition', false> { + return /* @__PURE__ */ withGenericRenderer(coreCondition(condition)) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function interaction(options?: { + events?: HydrationInteractionEvents +}): ReactHydrationStrategy<'interaction', true> & + HydrationPrefetchStrategy<'interaction'> { + return /* @__PURE__ */ withGenericRenderer(coreInteraction(options)) +} diff --git a/packages/react-start-client/src/hydration/idle.ts b/packages/react-start-client/src/hydration/idle.ts new file mode 100644 index 00000000000..3860c3b17c7 --- /dev/null +++ b/packages/react-start-client/src/hydration/idle.ts @@ -0,0 +1,23 @@ +'use client' + +import { StrategyHydrate } from './visible' +import { + scheduleIdle, + type HydrationPrefetchStrategy, + type IdleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +import type { ReactHydrationStrategy } from '../Hydrate' + +/* @__NO_SIDE_EFFECTS__ */ +export function idle( + options: IdleHydrationOptions = {}, +): ReactHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'> { + const timeout = options.timeout ?? 2000 + const setup = (context: any) => + scheduleIdle(context.prefetch ?? context.gate.resolve, timeout) + + return { + _s: setup, + _h: StrategyHydrate, + } as ReactHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'> +} diff --git a/packages/react-start-client/src/hydration/load.tsx b/packages/react-start-client/src/hydration/load.tsx new file mode 100644 index 00000000000..41d3b70d835 --- /dev/null +++ b/packages/react-start-client/src/hydration/load.tsx @@ -0,0 +1,69 @@ +'use client' + +import * as React from 'react' + +import { + hydrateIdAttribute, + hydrateWhenAttribute, +} from '@tanstack/start-client-core/hydration/constants' +import type { + HydrationPrefetchStrategy, + HydrationRuntimeContext, +} from '@tanstack/start-client-core/hydration' +import type { + HydrateProps, + InternalHydrateProps, + ReactHydrationStrategy, +} from '../Hydrate' + +const loadType = 'load' + +function HydratedBoundary(props: { + onHydrated?: () => void + children: React.ReactNode +}) { + const { onHydrated, children } = props + const didHydrateRef = React.useRef(false) + + React.useEffect(() => { + if (didHydrateRef.current) return + didHydrateRef.current = true + onHydrated?.() + }, [onHydrated]) + + return children as React.JSX.Element +} + +export function LoadHydrate(props: HydrateProps): React.JSX.Element { + const internalProps = props as InternalHydrateProps + const reactId = React.useId() + const id = internalProps.h ? `${internalProps.h}${reactId}` : reactId + + return ( +
+ + + {props.children} + + +
+ ) +} + +const loadStrategy = { + _s: ({ gate, prefetch }: HydrationRuntimeContext) => { + ;(prefetch ?? gate!.resolve)() + }, + _h: LoadHydrate, +} as ReactHydrationStrategy<'load', true> & HydrationPrefetchStrategy<'load'> + +/* @__NO_SIDE_EFFECTS__ */ +export function load(): ReactHydrationStrategy<'load', true> & + HydrationPrefetchStrategy<'load'> { + return loadStrategy +} diff --git a/packages/react-start-client/src/hydration/never.tsx b/packages/react-start-client/src/hydration/never.tsx new file mode 100644 index 00000000000..43bc3caf052 --- /dev/null +++ b/packages/react-start-client/src/hydration/never.tsx @@ -0,0 +1,92 @@ +'use client' + +import * as React from 'react' + +import { useHydrated } from '@tanstack/react-router' +import { isServer } from '@tanstack/router-core/isServer' +import { never as coreNever } from '@tanstack/start-client-core/hydration' +import { + hydrateIdAttribute, + hydrateWhenAttribute, +} from '@tanstack/start-client-core/hydration/constants' +import { + getFallbackHtml, + saveFallbackHtml, +} from '@tanstack/start-client-core/hydration/runtime' +import type { + HydrateProps, + InternalHydrateProps, + ReactHydrationStrategy, +} from '../Hydrate' + +const neverType = 'never' +const neverPromise = new Promise(() => {}) + +function NeverGate(props: { children: React.ReactNode }) { + if ((isServer as boolean | undefined) ?? typeof window === 'undefined') { + return props.children as React.JSX.Element + } + + throw neverPromise +} + +function HydrationFallback(props: { id: string; fallback: React.ReactNode }) { + const html = getFallbackHtml(props.id) + + if (!html) return props.fallback as React.JSX.Element | null + + return ( +
+ ) +} + +export function NeverHydrate(props: HydrateProps): React.JSX.Element { + const internalProps = props as InternalHydrateProps + const hydrated = useHydrated() + const reactId = React.useId() + const id = internalProps.h ? `${internalProps.h}${reactId}` : reactId + const shouldPreserveServerHTMLRef = React.useRef( + undefined, + ) + shouldPreserveServerHTMLRef.current ??= + ((isServer as boolean | undefined) ?? typeof window === 'undefined') || + !hydrated + const markerRef = React.useCallback( + (element: HTMLDivElement | null) => { + if (!element) return + if (!shouldPreserveServerHTMLRef.current) { + element.replaceChildren() + } else { + saveFallbackHtml(id, element) + } + }, + [id], + ) + const markerProps = { + ref: markerRef, + [hydrateIdAttribute]: id, + [hydrateWhenAttribute]: neverType, + } + + return ( +
+ + } + > + {props.children} + +
+ ) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function never(): ReactHydrationStrategy<'never', false> { + return /* @__PURE__ */ Object.assign(coreNever(), { + _h: NeverHydrate, + }) +} diff --git a/packages/react-start-client/src/hydration/visible.tsx b/packages/react-start-client/src/hydration/visible.tsx new file mode 100644 index 00000000000..a9cfb190c7a --- /dev/null +++ b/packages/react-start-client/src/hydration/visible.tsx @@ -0,0 +1,134 @@ +'use client' + +import * as React from 'react' + +import { reactUse } from '@tanstack/react-router' +import { isServer } from '@tanstack/router-core/isServer' +import type { + HydrationPrefetchStrategy, + VisibleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +import type { + HydrateProps, + InternalHydrateProps, + ReactHydrationStrategy, +} from '../Hydrate' + +type VisibleGate = { + p: Promise + r: boolean + resolve: () => void +} + +function HydrationBoundary(props: { + g: VisibleGate + o?: () => void + children?: React.ReactNode +}) { + const { g, o } = props + + if (!g.r) { + if (reactUse) { + reactUse(g.p) + } else { + throw g.p + } + } + + React.useEffect(() => { + o?.() + }, [o]) + + return props.children as React.JSX.Element +} + +export function StrategyHydrate(props: HydrateProps): React.JSX.Element { + const strategy = props.when + const prefetchStrategy = props.prefetch + const preload = (props as InternalHydrateProps).p + const markerRef = React.useRef(null) + const [gate] = React.useState(() => { + let resolvePromise!: () => void + const nextGate: VisibleGate = { + p: new Promise((resolve) => { + resolvePromise = resolve + }), + r: false, + resolve: () => { + nextGate.r = true + resolvePromise() + }, + } + if ((isServer as boolean | undefined) ?? typeof window === 'undefined') { + nextGate.resolve() + } + + return nextGate + }) + + React.useEffect(() => { + if (!preload || !prefetchStrategy) return + + const cleanup = prefetchStrategy._s!({ + element: markerRef.current, + prefetch: preload, + }) + if (typeof cleanup === 'function') return cleanup + return undefined + }, [prefetchStrategy, preload]) + + React.useEffect(() => { + if (gate.r) return + + const cleanup = strategy._s!({ + element: markerRef.current, + gate: gate as never, + }) + if (typeof cleanup === 'function') return cleanup + return undefined + }, [gate, strategy]) + + return React.createElement( + 'div', + { ref: markerRef }, + React.createElement( + React.Suspense, + { fallback: props.fallback }, + React.createElement( + HydrationBoundary, + { + g: gate, + o: props.onHydrated, + }, + props.children, + ), + ), + ) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function visible( + options?: VisibleHydrationOptions, +): ReactHydrationStrategy<'visible', true> & + HydrationPrefetchStrategy<'visible'> { + const observerOptions = { + rootMargin: options?.rootMargin || '600px', + threshold: options?.threshold || 0, + } + const setup = (context: any) => { + const callback = context.prefetch ?? context.gate.resolve + const observer = new IntersectionObserver((entries) => { + if (!entries[0]!.isIntersecting) return + observer.disconnect() + callback() + }, observerOptions) + observer.observe(context.element) + return () => observer.disconnect() + } + + return { + _s: setup, + _h: StrategyHydrate, + } as ReactHydrationStrategy<'visible', true> & + HydrationPrefetchStrategy<'visible'> +} diff --git a/packages/react-start-client/src/index.tsx b/packages/react-start-client/src/index.tsx index aa73990a576..3e1039a9d9d 100644 --- a/packages/react-start-client/src/index.tsx +++ b/packages/react-start-client/src/index.tsx @@ -1,2 +1,14 @@ +'use client' + export { StartClient } from './StartClient' export { hydrateStart } from './hydrateStart' +export { Hydrate } from './Hydrate' +export type { + HydrateOptions, + HydrateProps, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, +} from './Hydrate' diff --git a/packages/react-start-client/src/tests/Hydrate.test-d.tsx b/packages/react-start-client/src/tests/Hydrate.test-d.tsx new file mode 100644 index 00000000000..8476cc1e8b8 --- /dev/null +++ b/packages/react-start-client/src/tests/Hydrate.test-d.tsx @@ -0,0 +1,85 @@ +import { expectTypeOf, test } from 'vitest' +import { Hydrate } from '../Hydrate' +import type { + HydrateProps, + HydrationPrefetchStrategy, + HydrationStrategy, +} from '../Hydrate' +import type { HydrationStrategy as CoreHydrationStrategy } from '@tanstack/start-client-core/hydration' +import type { visible } from '../hydration' +import type { ReactNode } from 'react' + +type CommonHydrateProps = { + fallback?: ReactNode + onHydrated?: () => void + children: ReactNode +} + +type SplitHydrateProps = CommonHydrateProps & { + when: HydrationStrategy + prefetch?: never + split?: boolean +} + +type PrefetchHydrateProps = CommonHydrateProps & { + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split?: true +} + +test('Hydrate component accepts the public HydrateProps type', () => { + expectTypeOf(Hydrate).toBeFunction() + expectTypeOf(Hydrate).parameter(0).branded.toEqualTypeOf() +}) + +test('Hydrate props are exact for strategy and prefetch forms', () => { + expectTypeOf< + Extract + >().branded.toEqualTypeOf() + expectTypeOf< + Extract + >().branded.toEqualTypeOf() +}) + +test('Hydrate requires a strategy', () => { + expectTypeOf<{ + when: HydrationStrategy + children: ReactNode + }>().toMatchTypeOf() + + expectTypeOf<{ + children: ReactNode + }>().not.toMatchTypeOf() +}) + +test('Hydrate requires a framework-renderable strategy', () => { + expectTypeOf().not.toMatchTypeOf() + expectTypeOf>().toMatchTypeOf() + + expectTypeOf<{ + when: CoreHydrationStrategy + children: ReactNode + }>().not.toMatchTypeOf() +}) + +test('Hydrate enforces prefetch only with split boundaries', () => { + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + children: ReactNode + }>().toMatchTypeOf() + + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split: true + children: ReactNode + }>().toMatchTypeOf() + + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split: false + children: ReactNode + }>().not.toMatchTypeOf() +}) diff --git a/packages/react-start-client/src/tests/Hydrate.test.tsx b/packages/react-start-client/src/tests/Hydrate.test.tsx new file mode 100644 index 00000000000..3ae8e8fb430 --- /dev/null +++ b/packages/react-start-client/src/tests/Hydrate.test.tsx @@ -0,0 +1,396 @@ +import * as React from 'react' +import { renderToString } from 'react-dom/server' +import { hydrateRoot } from 'react-dom/client' +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { hydrateIdAttribute } from '@tanstack/start-client-core/hydration/constants' +import { Hydrate } from '../Hydrate' +import { idle, interaction, load, never } from '../hydration' +import type { HydrateProps } from '../Hydrate' + +const InternalHydrate = Hydrate as React.ComponentType< + HydrateProps & { p?: () => Promise; h?: string } +> + +const hydrateIdSelector = `[${hydrateIdAttribute}]` + +function getMarker() { + const marker = document.querySelector(hydrateIdSelector) + + if (!marker) { + throw new Error('Expected Hydrate marker to exist') + } + + return marker +} + +function InteractiveChild() { + const [count, setCount] = React.useState(0) + const [hydrated, setHydrated] = React.useState(false) + + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( + + ) +} + +function NamedInteractiveChild(props: { id: string }) { + const [hydrated, setHydrated] = React.useState(false) + + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( + + ) +} + +function createSuspendingChild() { + let resolve!: () => void + let resolved = false + const promise = new Promise((resolvePromise) => { + resolve = () => { + resolved = true + resolvePromise() + } + }) + + function SuspendingChild() { + if (!resolved) { + throw promise + } + + return
child
+ } + + return { resolve, SuspendingChild } +} + +async function expectNoHydrationAfterDefaultIntentEvents() { + const marker = getMarker() + + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'false', + ) + + await act(async () => { + fireEvent.pointerEnter(marker) + fireEvent.focusIn(marker) + fireEvent.pointerDown(marker) + fireEvent.click(marker) + await new Promise((resolve) => setTimeout(resolve, 20)) + }) + + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'false', + ) +} + +async function fireIntent(event: () => void) { + await act(async () => { + event() + await Promise.resolve() + }) +} + +async function renderAsync(ui: React.ReactElement) { + await act(async () => { + render(ui) + await Promise.resolve() + }) +} + +async function hydrateFromServer(ui: React.ReactElement) { + vi.stubGlobal('window', undefined) + const html = renderToString(ui) + vi.unstubAllGlobals() + + const container = document.createElement('div') + document.body.append(container) + container.innerHTML = html + + let root!: ReturnType + await act(async () => { + root = hydrateRoot(container, ui) + await Promise.resolve() + }) + + return { container, html, root } +} + +async function unmountHydratedRoot( + root: ReturnType, + container: Element, +) { + await act(async () => { + root.unmount() + }) + container.remove() +} + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() +}) + +describe('Hydrate', () => { + it('uses a single custom interaction event instead of the default intent events', async () => { + const { container, html, root } = await hydrateFromServer( + fallback
} + > + + , + ) + + try { + expect(html).toContain('data-testid="child"') + expect(html).not.toContain('data-testid="fallback"') + expect(screen.queryByTestId('fallback')).toBeNull() + await expectNoHydrationAfterDefaultIntentEvents() + + await fireIntent(() => + getMarker().dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true }), + ), + ) + + await waitFor(() => + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'true', + ), + ) + } finally { + await unmountHydratedRoot(root, container) + } + }) + + it('uses every event in a custom interaction event list', async () => { + const { container, root } = await hydrateFromServer( + fallback
} + > + + , + ) + + try { + expect(screen.queryByTestId('fallback')).toBeNull() + await expectNoHydrationAfterDefaultIntentEvents() + + await fireIntent(() => + getMarker().dispatchEvent( + new MouseEvent('contextmenu', { bubbles: true, cancelable: true }), + ), + ) + + await waitFor(() => + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'true', + ), + ) + } finally { + await unmountHydratedRoot(root, container) + } + }) + + it('omits never content when mounted after the app is already hydrated', async () => { + await renderAsync( + + + , + ) + + expect(screen.queryByTestId('child')).toBeNull() + }) + + it('shows fallback for a client-only mount while children suspend', async () => { + const { resolve, SuspendingChild } = createSuspendingChild() + + await renderAsync( + fallback} + > + + , + ) + + expect(screen.getByTestId('fallback').textContent).toBe('fallback') + expect(screen.queryByTestId('child')).toBeNull() + + await act(async () => { + resolve() + await Promise.resolve() + }) + + await screen.findByTestId('child') + expect(screen.queryByTestId('fallback')).toBeNull() + }) + + it('does not use fallback for an initial never boundary', async () => { + const { container, html, root } = await hydrateFromServer( + fallback} + > + + , + ) + + try { + expect(html).toContain('data-testid="child"') + expect(html).not.toContain('data-testid="fallback"') + expect(screen.queryByTestId('fallback')).toBeNull() + + fireEvent.click(screen.getByTestId('child')) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'false', + ) + expect(screen.getByTestId('child').textContent).toBe('0') + } finally { + await unmountHydratedRoot(root, container) + } + }) + + it('keeps repeated split boundaries independently gated', async () => { + const { container, root } = await hydrateFromServer( + <> + + + + + + + , + ) + + try { + const markers = container.querySelectorAll(hydrateIdSelector) + + expect(markers).toHaveLength(2) + expect(markers[0]!.getAttribute(hydrateIdAttribute)).not.toBe( + markers[1]!.getAttribute(hydrateIdAttribute), + ) + expect( + screen.getByTestId('child-one').getAttribute('data-hydrated'), + ).toBe('false') + expect( + screen.getByTestId('child-two').getAttribute('data-hydrated'), + ).toBe('false') + + await fireIntent(() => + markers[0]!.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true }), + ), + ) + + await waitFor(() => + expect( + screen.getByTestId('child-one').getAttribute('data-hydrated'), + ).toBe('true'), + ) + expect( + screen.getByTestId('child-two').getAttribute('data-hydrated'), + ).toBe('false') + } finally { + await unmountHydratedRoot(root, container) + } + }) + + it('fires onHydrated once after the client hydration commit', async () => { + const onHydrated = vi.fn() + const app = ( + +
child
+
+ ) + + vi.stubGlobal('window', undefined) + const html = renderToString(app) + expect(html).toContain('child') + expect(onHydrated).not.toHaveBeenCalled() + vi.unstubAllGlobals() + + const container = document.createElement('div') + document.body.append(container) + container.innerHTML = html + + let root!: ReturnType + await act(async () => { + root = hydrateRoot(container, app) + }) + + await waitFor(() => expect(onHydrated).toHaveBeenCalledTimes(1)) + + fireEvent.click(screen.getByTestId('child')) + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(onHydrated).toHaveBeenCalledTimes(1) + + await act(async () => { + root.unmount() + }) + container.remove() + }) + + it('prefetches split children without hydrating the boundary', async () => { + const preload = vi.fn(() => Promise.resolve()) + + const { container, root } = await hydrateFromServer( + + + , + ) + + try { + await waitFor(() => expect(preload).toHaveBeenCalledTimes(1)) + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'false', + ) + + await fireIntent(() => + getMarker().dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true }), + ), + ) + + await waitFor(() => + expect(screen.getByTestId('child').getAttribute('data-hydrated')).toBe( + 'true', + ), + ) + expect(preload).toHaveBeenCalledTimes(1) + } finally { + await unmountHydratedRoot(root, container) + } + }) +}) diff --git a/packages/react-start-client/vite.config.ts b/packages/react-start-client/vite.config.ts index 32119eee3af..40e07174ac0 100644 --- a/packages/react-start-client/vite.config.ts +++ b/packages/react-start-client/vite.config.ts @@ -20,7 +20,7 @@ export default mergeConfig( tanstackViteConfig({ tsconfigPath: './tsconfig.build.json', srcDir: './src', - entry: './src/index.tsx', + entry: ['./src/index.tsx', './src/hydration.ts'], cjs: false, }), ) diff --git a/packages/react-start/package.json b/packages/react-start/package.json index fd7055236c7..2aef9a6fdb2 100644 --- a/packages/react-start/package.json +++ b/packages/react-start/package.json @@ -48,6 +48,12 @@ "default": "./dist/esm/client.js" } }, + "./hydration": { + "import": { + "types": "./dist/esm/hydration.d.ts", + "default": "./dist/esm/hydration.js" + } + }, "./client-rpc": { "import": { "types": "./dist/esm/client-rpc.d.ts", diff --git a/packages/react-start/src/hydration.ts b/packages/react-start/src/hydration.ts new file mode 100644 index 00000000000..e86169e71dc --- /dev/null +++ b/packages/react-start/src/hydration.ts @@ -0,0 +1,18 @@ +export { + condition, + idle, + interaction, + load, + media, + never, + visible, +} from '@tanstack/react-start-client/hydration' +export type { + HydrationCondition, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, + VisibleHydrationOptions, +} from '@tanstack/react-start-client/hydration' diff --git a/packages/react-start/src/index.ts b/packages/react-start/src/index.ts index 8b51b6c7832..65b116ec2e6 100644 --- a/packages/react-start/src/index.ts +++ b/packages/react-start/src/index.ts @@ -1,2 +1,13 @@ export { useServerFn } from './useServerFn' export * from '@tanstack/start-client-core' +export { createServerFn } from '@tanstack/start-client-core' +export { Hydrate } from '@tanstack/react-start-client' +export type { + HydrateOptions, + HydrateProps, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, +} from '@tanstack/react-start-client' diff --git a/packages/react-start/vite.config.ts b/packages/react-start/vite.config.ts index 0995b1f9b46..652e4ef8425 100644 --- a/packages/react-start/vite.config.ts +++ b/packages/react-start/vite.config.ts @@ -27,6 +27,7 @@ export default mergeConfig( entry: [ './src/index.ts', './src/client.tsx', + './src/hydration.ts', './src/client-rpc.ts', './src/server.tsx', './src/server.rsc.ts', diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts index ded15fff6df..e737fa50f8a 100644 --- a/packages/router-core/src/index.ts +++ b/packages/router-core/src/index.ts @@ -185,6 +185,7 @@ export type { RouteContextFn, ContextOptions, RouteContextOptions, + SsrContextOptions, BeforeLoadContextOptions, RootRouteOptions, RootRouteOptionsExtensions, diff --git a/packages/router-core/src/ssr/ssr-server.ts b/packages/router-core/src/ssr/ssr-server.ts index 2bca7009d9e..c1e467f2fbf 100644 --- a/packages/router-core/src/ssr/ssr-server.ts +++ b/packages/router-core/src/ssr/ssr-server.ts @@ -292,23 +292,27 @@ export function attachRouterServerSsrUtils({ }) { router.ssr = { get manifest() { + if (!manifest) return manifest + const requestAssets = getRequestAssets?.() - const inlineCssAsset = getInlineCssAssetForMatches( - manifest, - router.stores.matches.get(), - ) - if (!requestAssets?.length && !inlineCssAsset) return manifest + const matches = router.stores.matches.get() + const inlineCssAsset = getInlineCssAssetForMatches(manifest, matches) + + if (!requestAssets?.length && !inlineCssAsset) { + return manifest + } + // Merge request-scoped assets into root route without mutating cached manifest return { ...manifest, routes: { - ...manifest?.routes, + ...manifest.routes, [rootRouteId]: { - ...manifest?.routes?.[rootRouteId], + ...manifest.routes[rootRouteId], assets: [ ...(requestAssets ?? []), ...(inlineCssAsset ? [inlineCssAsset] : []), - ...(manifest?.routes?.[rootRouteId]?.assets ?? []), + ...(manifest.routes[rootRouteId]?.assets ?? []), ], }, }, diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index d59b4fa9afb..32eade99af1 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -2,15 +2,27 @@ import * as t from '@babel/types' import * as babel from '@babel/core' import * as template from '@babel/template' import { + buildDeclarationMap, + buildDependencyGraph, + collectIdentifiersFromPattern, + collectLocalBindingsFromStatement, + collectModuleLevelRefsFromNode, + createIdentifier, deadCodeElimination, + expandDestructuredDeclarations, + expandSharedDestructuredDeclarators, + expandTransitively, findReferencedIdentifiers, generateFromAst, parseAst, + removeBindingsTransitivelyDependingOn, + retainModuleLevelDeclarations, + stripUnreferencedTopLevelExpressionStatements, + unwrapExportedDeclarations, } from '@tanstack/router-utils' import { tsrShared, tsrSplit } from '../constants' import { createRouteHmrStatement } from '../hmr' import { getObjectPropertyKeyName } from '../utils' -import { createIdentifier } from './path-ids' import { getFrameworkOptions } from './framework-options' import type { CompileCodeSplitReferenceRouteOptions, @@ -20,6 +32,25 @@ import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' import type { CodeSplitGroupings, SplitRouteIdentNodes } from '../constants' import type { SplitNodeMeta } from './types' +export { + buildDeclarationMap, + buildDependencyGraph, + collectIdentifiersFromNode, + collectLocalBindingsFromStatement, + collectModuleLevelRefsFromNode, + expandDestructuredDeclarations, + expandSharedDestructuredDeclarators, + expandTransitively, + removeBindingsTransitivelyDependingOn, +} from '@tanstack/router-utils' + +export function removeBindingsDependingOnRoute( + bindings: Set, + dependencyGraph: Map>, +) { + removeBindingsTransitivelyDependingOn(bindings, dependencyGraph, ['Route']) +} + const SPLIT_NODES_CONFIG = new Map([ [ 'loader', @@ -108,124 +139,6 @@ const allCreateRouteFns = [ ...unsplittableCreateRouteFns, ] -/** - * Recursively walk an AST node and collect referenced identifier-like names. - * Much cheaper than babel.traverse — no path/scope overhead. - * - * Notes: - * - Uses @babel/types `isReferenced` to avoid collecting non-references like - * object keys, member expression properties, or binding identifiers. - * - Also handles JSX identifiers for component references. - */ -export function collectIdentifiersFromNode(node: t.Node): Set { - const ids = new Set() - - ;(function walk( - n: t.Node | null | undefined, - parent?: t.Node, - grandparent?: t.Node, - parentKey?: string, - ) { - if (!n) return - - if (t.isIdentifier(n)) { - // When we don't have parent info (node passed in isolation), treat as referenced. - if (!parent || t.isReferenced(n, parent, grandparent)) { - ids.add(n.name) - } - return - } - - if (t.isJSXIdentifier(n)) { - // Skip attribute names:
- if (parent && t.isJSXAttribute(parent) && parentKey === 'name') { - return - } - - // Skip member properties: should count Foo, not Bar - if ( - parent && - t.isJSXMemberExpression(parent) && - parentKey === 'property' - ) { - return - } - - // Intrinsic elements (lowercase) are not identifiers - const first = n.name[0] - if (first && first === first.toLowerCase()) { - return - } - - ids.add(n.name) - return - } - - for (const key of t.VISITOR_KEYS[n.type] || []) { - const child = (n as any)[key] - if (Array.isArray(child)) { - for (const c of child) { - if (c && typeof c.type === 'string') { - walk(c, n, parent, key) - } - } - } else if (child && typeof child.type === 'string') { - walk(child, n, parent, key) - } - } - })(node) - - return ids -} - -/** - * Build a map from binding name → declaration AST node for all - * locally-declared module-level bindings. Built once, O(1) lookup. - */ -export function buildDeclarationMap(ast: t.File): Map { - const map = new Map() - for (const stmt of ast.program.body) { - const decl = - t.isExportNamedDeclaration(stmt) && stmt.declaration - ? stmt.declaration - : stmt - - if (t.isVariableDeclaration(decl)) { - for (const declarator of decl.declarations) { - for (const name of collectIdentifiersFromPattern(declarator.id)) { - map.set(name, declarator) - } - } - } else if (t.isFunctionDeclaration(decl) && decl.id) { - map.set(decl.id.name, decl) - } else if (t.isClassDeclaration(decl) && decl.id) { - map.set(decl.id.name, decl) - } - } - return map -} - -/** - * Build a dependency graph: for each local binding, the set of other local - * bindings its declaration references. Built once via simple node walking. - */ -export function buildDependencyGraph( - declMap: Map, - localBindings: Set, -): Map> { - const graph = new Map>() - for (const [name, declNode] of declMap) { - if (!localBindings.has(name)) continue - const allIds = collectIdentifiersFromNode(declNode) - const deps = new Set() - for (const id of allIds) { - if (id !== name && localBindings.has(id)) deps.add(id) - } - graph.set(name, deps) - } - return graph -} - /** * Computes module-level bindings that are shared between split and non-split * route properties. These bindings need to be extracted into a shared virtual @@ -381,199 +294,11 @@ export function computeSharedBindings(opts: { // Remove shared bindings that transitively depend on `Route`. // The Route singleton must stay in the reference file; extracting a // binding that references it would duplicate Route in the shared module. - removeBindingsDependingOnRoute(shared, fullDepGraph) + removeBindingsTransitivelyDependingOn(shared, fullDepGraph, ['Route']) return shared } -/** - * If bindings from the same destructured declarator are referenced by - * different groups, mark all bindings from that declarator as shared. - */ -export function expandSharedDestructuredDeclarators( - ast: t.File, - refsByGroup: Map>, - shared: Set, -) { - for (const stmt of ast.program.body) { - const decl = - t.isExportNamedDeclaration(stmt) && stmt.declaration - ? stmt.declaration - : stmt - - if (!t.isVariableDeclaration(decl)) continue - - for (const declarator of decl.declarations) { - if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id)) - continue - - const names = collectIdentifiersFromPattern(declarator.id) - - const usedGroups = new Set() - for (const name of names) { - const groups = refsByGroup.get(name) - if (!groups) continue - for (const g of groups) usedGroups.add(g) - } - - if (usedGroups.size >= 2) { - for (const name of names) { - shared.add(name) - } - } - } - } -} - -/** - * Collect locally-declared module-level binding names from a statement. - * Pure node inspection, no traversal. - */ -export function collectLocalBindingsFromStatement( - node: t.Statement | t.ModuleDeclaration, - bindings: Set, -) { - const decl = - t.isExportNamedDeclaration(node) && node.declaration - ? node.declaration - : node - - if (t.isVariableDeclaration(decl)) { - for (const declarator of decl.declarations) { - for (const name of collectIdentifiersFromPattern(declarator.id)) { - bindings.add(name) - } - } - } else if (t.isFunctionDeclaration(decl) && decl.id) { - bindings.add(decl.id.name) - } else if (t.isClassDeclaration(decl) && decl.id) { - bindings.add(decl.id.name) - } -} - -/** - * Collect direct module-level binding names referenced from a given AST node. - * Uses a simple recursive walk instead of babel.traverse. - */ -export function collectModuleLevelRefsFromNode( - node: t.Node, - localModuleLevelBindings: Set, -): Set { - const allIds = collectIdentifiersFromNode(node) - const refs = new Set() - for (const name of allIds) { - if (localModuleLevelBindings.has(name)) refs.add(name) - } - return refs -} - -/** - * Expand the shared set transitively using a prebuilt dependency graph. - * No AST traversals — pure graph BFS. - */ -export function expandTransitively( - shared: Set, - depGraph: Map>, -) { - const queue = [...shared] - const visited = new Set() - - while (queue.length > 0) { - const name = queue.pop()! - if (visited.has(name)) continue - visited.add(name) - - const deps = depGraph.get(name) - if (!deps) continue - - for (const dep of deps) { - if (!shared.has(dep)) { - shared.add(dep) - queue.push(dep) - } - } - } -} - -/** - * Remove any bindings from `shared` that transitively depend on `Route`. - * The Route singleton must remain in the reference file; if a shared binding - * references it (directly or transitively), extracting that binding would - * duplicate Route in the shared module. - * - * Uses `depGraph` which must include `Route` as a node so the dependency - * chain is visible. - */ -export function removeBindingsDependingOnRoute( - shared: Set, - depGraph: Map>, -) { - const reverseGraph = new Map>() - for (const [name, deps] of depGraph) { - for (const dep of deps) { - let parents = reverseGraph.get(dep) - if (!parents) { - parents = new Set() - reverseGraph.set(dep, parents) - } - parents.add(name) - } - } - - // Walk backwards from Route to find all bindings that can reach it. - const visited = new Set() - const queue = ['Route'] - while (queue.length > 0) { - const cur = queue.pop()! - if (visited.has(cur)) continue - visited.add(cur) - - const parents = reverseGraph.get(cur) - if (!parents) continue - for (const parent of parents) { - if (!visited.has(parent)) queue.push(parent) - } - } - - for (const name of [...shared]) { - if (visited.has(name)) { - shared.delete(name) - } - } -} - -/** - * If any binding from a destructured declaration is shared, - * ensure all bindings from that same declaration are also shared. - * Pure node inspection of program.body, no traversal. - */ -export function expandDestructuredDeclarations( - ast: t.File, - shared: Set, -) { - for (const stmt of ast.program.body) { - const decl = - t.isExportNamedDeclaration(stmt) && stmt.declaration - ? stmt.declaration - : stmt - - if (!t.isVariableDeclaration(decl)) continue - - for (const declarator of decl.declarations) { - if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id)) - continue - - const names = collectIdentifiersFromPattern(declarator.id) - const hasShared = names.some((n) => shared.has(n)) - if (hasShared) { - for (const n of names) { - shared.add(n) - } - } - } - } -} - /** * Find which shared bindings are user-exported in the original source. * These need to be re-exported from the shared module. @@ -740,6 +465,21 @@ export function compileCodeSplitReferenceRoute( if (t.isObjectExpression(routeOptions)) { const insertionPath = path.getStatementParent() ?? path + opts.compilerPlugins?.forEach((plugin) => { + const pluginResult = plugin.onRouteOptions?.({ + programPath, + callExpressionPath: path, + insertionPath, + routeOptions, + createRouteFn, + opts: opts as CompileCodeSplitReferenceRouteOptions, + }) + + if (pluginResult?.modified) { + modified = true + } + }) + if (opts.deleteNodes && opts.deleteNodes.size > 0) { routeOptions.properties = routeOptions.properties.filter( (prop) => { @@ -1525,22 +1265,7 @@ export function compileCodeSplitVirtualRoute( }) deadCodeElimination(ast, refIdents) - - // Strip top-level expression statements that reference no locally-bound names. - // DCE only removes unused declarations; bare side-effect statements like - // `console.log(...)` survive even when the virtual file has no exports. - { - const locallyBound = new Set() - for (const stmt of ast.program.body) { - collectLocalBindingsFromStatement(stmt, locallyBound) - } - ast.program.body = ast.program.body.filter((stmt) => { - if (!t.isExpressionStatement(stmt)) return true - const refs = collectIdentifiersFromNode(stmt) - // Keep if it references at least one locally-bound identifier - return [...refs].some((name) => locallyBound.has(name)) - }) - } + stripUnreferencedTopLevelExpressionStatements(ast) // If the body is empty after DCE, strip directive prologues too. // A file containing only `'use client'` with no real code is useless. @@ -1595,49 +1320,8 @@ export function compileCodeSplitSharedRoute( keepBindings.delete('Route') expandTransitively(keepBindings, depGraph) - // Remove all statements except: - // - Import declarations (needed for deps; DCE will clean unused ones) - // - Declarations of bindings in keepBindings - ast.program.body = ast.program.body.filter((stmt) => { - // Always keep imports — DCE will remove unused ones - if (t.isImportDeclaration(stmt)) return true - - const decl = - t.isExportNamedDeclaration(stmt) && stmt.declaration - ? stmt.declaration - : stmt - - if (t.isVariableDeclaration(decl)) { - // Keep declarators where at least one binding is in keepBindings - decl.declarations = decl.declarations.filter((declarator) => { - const names = collectIdentifiersFromPattern(declarator.id) - return names.some((n) => keepBindings.has(n)) - }) - if (decl.declarations.length === 0) return false - - // Strip the `export` wrapper — shared module controls its own exports - if (t.isExportNamedDeclaration(stmt) && stmt.declaration) { - return true // keep for now, we'll convert below - } - return true - } else if (t.isFunctionDeclaration(decl) && decl.id) { - return keepBindings.has(decl.id.name) - } else if (t.isClassDeclaration(decl) && decl.id) { - return keepBindings.has(decl.id.name) - } - - // Remove everything else (expression statements, other exports, etc.) - return false - }) - - // Convert `export const/function/class` to plain declarations - // (we'll add our own export statement at the end) - ast.program.body = ast.program.body.map((stmt) => { - if (t.isExportNamedDeclaration(stmt) && stmt.declaration) { - return stmt.declaration - } - return stmt - }) + retainModuleLevelDeclarations(ast, keepBindings) + unwrapExportedDeclarations(ast) // Export all shared bindings (sorted for deterministic output) const exportNames = [...opts.sharedBindings].sort((a, b) => @@ -1837,50 +1521,6 @@ function getImportSpecifierAndPathFromLocalName( return { specifier, path } } -/** - * Recursively collects all identifier names from a destructuring pattern - * (ObjectPattern, ArrayPattern, AssignmentPattern, RestElement). - */ -function collectIdentifiersFromPattern( - node: t.LVal | t.Node | null | undefined, -): Array { - if (!node) { - return [] - } - - if (t.isIdentifier(node)) { - return [node.name] - } - - if (t.isAssignmentPattern(node)) { - return collectIdentifiersFromPattern(node.left) - } - - if (t.isRestElement(node)) { - return collectIdentifiersFromPattern(node.argument) - } - - if (t.isObjectPattern(node)) { - return node.properties.flatMap((prop) => { - if (t.isObjectProperty(prop)) { - return collectIdentifiersFromPattern(prop.value as t.LVal) - } - if (t.isRestElement(prop)) { - return collectIdentifiersFromPattern(prop.argument) - } - return [] - }) - } - - if (t.isArrayPattern(node)) { - return node.elements.flatMap((element) => - collectIdentifiersFromPattern(element), - ) - } - - return [] -} - // Reusable function to get literal value or resolve variable to literal function resolveIdentifier(path: any, node: any): t.Node | undefined { if (t.isIdentifier(node)) { diff --git a/packages/router-plugin/src/core/code-splitter/plugins.ts b/packages/router-plugin/src/core/code-splitter/plugins.ts index 4b2363e3615..ebf1f4aa305 100644 --- a/packages/router-plugin/src/core/code-splitter/plugins.ts +++ b/packages/router-plugin/src/core/code-splitter/plugins.ts @@ -43,6 +43,9 @@ export type ReferenceRouteCompilerPluginResult = { export type ReferenceRouteCompilerPlugin = { name: string getStableRouteOptionKeys?: () => Array + onRouteOptions?: ( + ctx: ReferenceRouteCompilerPluginContext, + ) => void | ReferenceRouteCompilerPluginResult onAddHmr?: ( ctx: ReferenceRouteCompilerPluginContext, ) => void | ReferenceRouteCompilerPluginResult diff --git a/packages/router-plugin/src/core/config.ts b/packages/router-plugin/src/core/config.ts index f764bfe8bb8..d1d36bbe370 100644 --- a/packages/router-plugin/src/core/config.ts +++ b/packages/router-plugin/src/core/config.ts @@ -9,6 +9,7 @@ import type { RouteIds, } from '@tanstack/router-core' import type { CodeSplitGroupings } from './constants' +import type { ReferenceRouteCompilerPlugin } from './code-splitter/plugins' export const splitGroupingsSchema = z .array( @@ -70,6 +71,12 @@ export type CodeSplittingOptions = { * @default true */ addHmr?: boolean + + /** + * Internal compiler plugins used by framework integrations. + * @internal + */ + compilerPlugins?: Array } export type HmrStyle = 'vite' | 'webpack' diff --git a/packages/router-plugin/src/core/router-code-splitter-plugin.ts b/packages/router-plugin/src/core/router-code-splitter-plugin.ts index e291b05fe9c..f197f4890af 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -4,7 +4,7 @@ */ import { fileURLToPath, pathToFileURL } from 'node:url' -import { logDiff } from '@tanstack/router-utils' +import { decodeIdentifier, logDiff } from '@tanstack/router-utils' import { getConfig, splitGroupingsSchema } from './config' import { compileCodeSplitReferenceRoute, @@ -20,7 +20,6 @@ import { tsrShared, tsrSplit, } from './constants' -import { decodeIdentifier } from './code-splitter/path-ids' import { debug, normalizePath } from './utils' import { createRouterPluginContext } from './router-plugin-context' import type { CodeSplitGroupings, SplitRouteIdentNodes } from './constants' @@ -177,11 +176,14 @@ export function createRouterCodeSplitterPlugin( hmrStyle, hmrRouteId: generatorNodeInfo.routeId, sharedBindings: sharedBindings.size > 0 ? sharedBindings : undefined, - compilerPlugins: getReferenceRouteCompilerPlugins({ - targetFramework: userConfig.target, - addHmr, - hmrStyle, - }), + compilerPlugins: [ + ...(getReferenceRouteCompilerPlugins({ + targetFramework: userConfig.target, + addHmr, + hmrStyle, + }) ?? []), + ...(userConfig.codeSplittingOptions?.compilerPlugins ?? []), + ], }) if (compiledReferenceRoute === null) { diff --git a/packages/router-plugin/src/index.ts b/packages/router-plugin/src/index.ts index a56e097d3a8..e390c046a22 100644 --- a/packages/router-plugin/src/index.ts +++ b/packages/router-plugin/src/index.ts @@ -11,6 +11,11 @@ export type { HmrOptions, } from './core/config' export type { RouterPluginContext } from './core/router-plugin-context' +export { getObjectPropertyKeyName } from './core/utils' +export type { + ReferenceRouteCompilerPlugin, + ReferenceRouteCompilerPluginContext, +} from './core/code-splitter/plugins' export { tsrSplit, splitRouteIdentNodes, diff --git a/packages/router-plugin/tests/code-splitter.test.ts b/packages/router-plugin/tests/code-splitter.test.ts index 5a68f9453fe..ae64e2778b3 100644 --- a/packages/router-plugin/tests/code-splitter.test.ts +++ b/packages/router-plugin/tests/code-splitter.test.ts @@ -18,7 +18,7 @@ import { expandTransitively, removeBindingsDependingOnRoute, } from '../src/core/code-splitter/compilers' -import { createIdentifier } from '../src/core/code-splitter/path-ids' +import { createIdentifier } from '@tanstack/router-utils' import { defaultCodeSplitGroupings } from '../src/core/constants' import { frameworks } from './constants' import type { CodeSplitGroupings } from '../src/core/constants' diff --git a/packages/router-utils/src/compiler-helpers.ts b/packages/router-utils/src/compiler-helpers.ts new file mode 100644 index 00000000000..8f4a07b5e3c --- /dev/null +++ b/packages/router-utils/src/compiler-helpers.ts @@ -0,0 +1,843 @@ +import * as t from '@babel/types' + +type IdentifierScopeFrame = { + kind: 'program' | 'function' | 'block' + bindings: Set +} +type IdentifierScopeStack = Array + +export type ModuleInfoBinding = + | { + type: 'import' + source: string + importedName: string + } + | { + type: 'var' + init: t.Expression | null + } + +export interface ExtractedModuleInfo { + bindings: Map + exports: Map + reExportAllSources: Array +} + +function getModuleExportName(node: t.Identifier | t.StringLiteral) { + return t.isIdentifier(node) ? node.name : node.value +} + +function addVariableDeclarationModuleInfo( + declaration: t.VariableDeclaration, + bindings: Map, + exports?: Map, +) { + for (const declarator of declaration.declarations) { + for (const name of collectIdentifiersFromPattern(declarator.id)) { + bindings.set(name, { + type: 'var', + init: declarator.init ?? null, + }) + exports?.set(name, name) + } + } +} + +function addDeclarationModuleInfo( + declaration: t.Declaration, + bindings: Map, + exports?: Map, +) { + if (t.isVariableDeclaration(declaration)) { + addVariableDeclarationModuleInfo(declaration, bindings, exports) + return + } + + if ( + (t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration)) && + declaration.id + ) { + bindings.set(declaration.id.name, { + type: 'var', + init: null, + }) + exports?.set(declaration.id.name, declaration.id.name) + } +} + +function hasIdentifierBinding(scopes: IdentifierScopeStack, name: string) { + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i]!.bindings.has(name)) { + return true + } + } + return false +} + +function currentIdentifierScope(scopes: IdentifierScopeStack) { + return scopes[scopes.length - 1]! +} + +function nearestFunctionIdentifierScope(scopes: IdentifierScopeStack) { + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]! + if (scope.kind === 'function' || scope.kind === 'program') { + return scope + } + } + return currentIdentifierScope(scopes) +} + +function addIdentifierPatternBindings( + pattern: t.LVal | t.Node | null | undefined, + scope: IdentifierScopeFrame, +) { + for (const name of collectIdentifiersFromPattern(pattern)) { + scope.bindings.add(name) + } +} + +function addIdentifierDeclarationBindings( + declaration: t.Node, + scopes: IdentifierScopeStack, +) { + if (t.isVariableDeclaration(declaration)) { + const scope = + declaration.kind === 'var' + ? nearestFunctionIdentifierScope(scopes) + : currentIdentifierScope(scopes) + for (const declarator of declaration.declarations) { + addIdentifierPatternBindings(declarator.id, scope) + } + return + } + + if ( + (t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration) || + t.isTSTypeAliasDeclaration(declaration) || + t.isTSInterfaceDeclaration(declaration) || + t.isTSEnumDeclaration(declaration)) && + declaration.id + ) { + currentIdentifierScope(scopes).bindings.add(declaration.id.name) + } +} + +function addIdentifierImportBindings( + node: t.ImportDeclaration, + scope: IdentifierScopeFrame, +) { + for (const specifier of node.specifiers) { + scope.bindings.add(specifier.local.name) + } +} + +function createNestedIdentifierScope( + kind: IdentifierScopeFrame['kind'], + scopes: IdentifierScopeStack, +): IdentifierScopeStack { + return [...scopes, { kind, bindings: new Set() }] +} + +function addIdentifierBlockBindings( + body: Array, + scopes: IdentifierScopeStack, +) { + for (const statement of body) { + if (t.isImportDeclaration(statement)) { + addIdentifierImportBindings(statement, currentIdentifierScope(scopes)) + } else if (t.isExportNamedDeclaration(statement) && statement.declaration) { + addIdentifierDeclarationBindings(statement.declaration, scopes) + } else { + addIdentifierDeclarationBindings(statement, scopes) + } + } +} + +function walkIdentifierChildren( + current: t.Node, + parent: t.Node | undefined, + scopes: IdentifierScopeStack, + ids: Set, +) { + for (const key of t.VISITOR_KEYS[current.type] ?? []) { + const child = (current as any)[key] + if (Array.isArray(child)) { + for (const item of child) { + if (item && typeof item.type === 'string') { + walkIdentifierNode(item, current, parent, key, scopes, ids) + } + } + } else if (child && typeof child.type === 'string') { + walkIdentifierNode(child, current, parent, key, scopes, ids) + } + } +} + +function walkIdentifierNode( + current: t.Node | null | undefined, + parent: t.Node | undefined, + grandparent: t.Node | undefined, + parentKey: string | undefined, + scopes: IdentifierScopeStack, + ids: Set, +) { + if (!current) return + + if (t.isIdentifier(current)) { + if ( + (!parent || t.isReferenced(current, parent, grandparent)) && + !hasIdentifierBinding(scopes, current.name) + ) { + ids.add(current.name) + } + return + } + + if (t.isJSXIdentifier(current)) { + if (parent && t.isJSXAttribute(parent) && parentKey === 'name') { + return + } + + if (parent && t.isJSXMemberExpression(parent) && parentKey === 'property') { + return + } + + const first = current.name[0] + if (first && first === first.toLowerCase()) { + return + } + + if (!hasIdentifierBinding(scopes, current.name)) { + ids.add(current.name) + } + return + } + + if (t.isProgram(current)) { + const nestedScopes = createNestedIdentifierScope('program', scopes) + addIdentifierBlockBindings(current.body, nestedScopes) + for (const child of current.body) { + walkIdentifierNode(child, current, parent, 'body', nestedScopes, ids) + } + return + } + + if (t.isBlockStatement(current)) { + const nestedScopes = createNestedIdentifierScope('block', scopes) + addIdentifierBlockBindings(current.body, nestedScopes) + for (const child of current.body) { + walkIdentifierNode(child, current, parent, 'body', nestedScopes, ids) + } + return + } + + if ( + t.isFunctionDeclaration(current) || + t.isFunctionExpression(current) || + t.isArrowFunctionExpression(current) || + t.isObjectMethod(current) || + t.isClassMethod(current) || + t.isClassPrivateMethod(current) + ) { + if (t.isFunctionDeclaration(current) && current.id) { + currentIdentifierScope(scopes).bindings.add(current.id.name) + } + + const nestedScopes = createNestedIdentifierScope('function', scopes) + if ( + (t.isFunctionDeclaration(current) || t.isFunctionExpression(current)) && + current.id + ) { + currentIdentifierScope(nestedScopes).bindings.add(current.id.name) + } + for (const param of current.params) { + addIdentifierPatternBindings(param, currentIdentifierScope(nestedScopes)) + } + + walkIdentifierChildren(current, parent, nestedScopes, ids) + return + } + + if (t.isCatchClause(current)) { + const nestedScopes = createNestedIdentifierScope('block', scopes) + addIdentifierPatternBindings( + current.param, + currentIdentifierScope(nestedScopes), + ) + walkIdentifierNode( + current.param, + current, + parent, + 'param', + nestedScopes, + ids, + ) + walkIdentifierNode(current.body, current, parent, 'body', nestedScopes, ids) + return + } + + if (t.isImportDeclaration(current)) { + addIdentifierImportBindings(current, currentIdentifierScope(scopes)) + return + } + + if (t.isClassDeclaration(current) || t.isClassExpression(current)) { + if (t.isClassDeclaration(current) && current.id) { + currentIdentifierScope(scopes).bindings.add(current.id.name) + } + + const nestedScopes = current.id + ? createNestedIdentifierScope('block', scopes) + : scopes + if (current.id) { + currentIdentifierScope(nestedScopes).bindings.add(current.id.name) + } + + walkIdentifierChildren(current, parent, nestedScopes, ids) + return + } + + if (t.isVariableDeclaration(current)) { + addIdentifierDeclarationBindings(current, scopes) + } else if (t.isVariableDeclarator(current)) { + const scope = + parent && t.isVariableDeclaration(parent) && parent.kind === 'var' + ? nearestFunctionIdentifierScope(scopes) + : currentIdentifierScope(scopes) + addIdentifierPatternBindings(current.id, scope) + } else if ( + t.isTSTypeAliasDeclaration(current) || + t.isTSInterfaceDeclaration(current) || + t.isTSEnumDeclaration(current) + ) { + currentIdentifierScope(scopes).bindings.add(current.id.name) + } + + walkIdentifierChildren(current, parent, scopes, ids) +} + +/** + * Recursively walk an AST node and collect referenced identifier-like names. + * This avoids Babel path/scope allocation for module-level dependency scans. + */ +export function collectIdentifiersFromNode(node: t.Node): Set { + const ids = new Set() + walkIdentifierNode( + node, + undefined, + undefined, + undefined, + [{ kind: 'program', bindings: new Set() }], + ids, + ) + return ids +} + +export function collectIdentifiersFromPattern( + node: t.LVal | t.Node | null | undefined, +): Array { + if (!node) { + return [] + } + + if (t.isIdentifier(node)) { + return [node.name] + } + + if (t.isAssignmentPattern(node)) { + return collectIdentifiersFromPattern(node.left) + } + + if (t.isRestElement(node)) { + return collectIdentifiersFromPattern(node.argument) + } + + if (t.isObjectPattern(node)) { + return node.properties.flatMap((prop) => { + if (t.isObjectProperty(prop)) { + return collectIdentifiersFromPattern(prop.value as t.LVal) + } + if (t.isRestElement(prop)) { + return collectIdentifiersFromPattern(prop.argument) + } + return [] + }) + } + + if (t.isArrayPattern(node)) { + return node.elements.flatMap((element) => + collectIdentifiersFromPattern(element), + ) + } + + return [] +} + +export function collectLocalBindingsFromStatement( + node: t.Statement | t.ModuleDeclaration, + bindings: Set, +) { + const declaration = + t.isExportNamedDeclaration(node) && node.declaration + ? node.declaration + : t.isExportDefaultDeclaration(node) + ? node.declaration + : node + + if (t.isVariableDeclaration(declaration)) { + for (const declarator of declaration.declarations) { + for (const name of collectIdentifiersFromPattern(declarator.id)) { + bindings.add(name) + } + } + } else if (t.isFunctionDeclaration(declaration) && declaration.id) { + bindings.add(declaration.id.name) + } else if (t.isClassDeclaration(declaration) && declaration.id) { + bindings.add(declaration.id.name) + } +} + +export function extractModuleInfoFromAst(ast: t.File): ExtractedModuleInfo { + const bindings = new Map() + const exports = new Map() + const reExportAllSources: Array = [] + + for (const node of ast.program.body) { + if (t.isImportDeclaration(node)) { + const source = node.source.value + for (const specifier of node.specifiers) { + if (t.isImportSpecifier(specifier)) { + bindings.set(specifier.local.name, { + type: 'import', + source, + importedName: getModuleExportName(specifier.imported), + }) + } else if (t.isImportDefaultSpecifier(specifier)) { + bindings.set(specifier.local.name, { + type: 'import', + source, + importedName: 'default', + }) + } else if (t.isImportNamespaceSpecifier(specifier)) { + bindings.set(specifier.local.name, { + type: 'import', + source, + importedName: '*', + }) + } + } + continue + } + + if (t.isVariableDeclaration(node)) { + addVariableDeclarationModuleInfo(node, bindings) + continue + } + + if (t.isFunctionDeclaration(node) || t.isClassDeclaration(node)) { + addDeclarationModuleInfo(node, bindings) + continue + } + + if (t.isExportNamedDeclaration(node)) { + if (node.declaration) { + addDeclarationModuleInfo(node.declaration, bindings, exports) + } + + for (const specifier of node.specifiers) { + if (t.isExportNamespaceSpecifier(specifier)) { + const exported = getModuleExportName(specifier.exported) + exports.set(exported, exported) + if (node.source) { + bindings.set(exported, { + type: 'import', + source: node.source.value, + importedName: '*', + }) + } + } else if (t.isExportSpecifier(specifier)) { + const local = getModuleExportName(specifier.local) + const exported = getModuleExportName(specifier.exported) + exports.set(exported, local) + + if (node.source) { + bindings.set(local, { + type: 'import', + source: node.source.value, + importedName: local, + }) + } + } + } + continue + } + + if (t.isExportDefaultDeclaration(node)) { + const declaration = node.declaration + if (t.isIdentifier(declaration)) { + exports.set('default', declaration.name) + } else if ( + (t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration)) && + declaration.id + ) { + bindings.set(declaration.id.name, { + type: 'var', + init: null, + }) + exports.set('default', declaration.id.name) + } else { + const synth = '__default_export__' + bindings.set(synth, { + type: 'var', + init: t.isExpression(declaration) ? declaration : null, + }) + exports.set('default', synth) + } + continue + } + + if (t.isExportAllDeclaration(node)) { + reExportAllSources.push(node.source.value) + } + } + + return { + bindings, + exports, + reExportAllSources, + } +} + +export function buildDeclarationMap(ast: t.File): Map { + const map = new Map() + + for (const statement of ast.program.body) { + const declaration = + t.isExportNamedDeclaration(statement) && statement.declaration + ? statement.declaration + : t.isExportDefaultDeclaration(statement) + ? statement.declaration + : statement + + if (t.isVariableDeclaration(declaration)) { + for (const declarator of declaration.declarations) { + for (const name of collectIdentifiersFromPattern(declarator.id)) { + map.set(name, declarator) + } + } + } else if (t.isFunctionDeclaration(declaration) && declaration.id) { + map.set(declaration.id.name, declaration) + } else if (t.isClassDeclaration(declaration) && declaration.id) { + map.set(declaration.id.name, declaration) + } + } + + return map +} + +export function buildDependencyGraph( + declarationMap: Map, + localBindings: Set, +): Map> { + const graph = new Map>() + + for (const [name, declarationNode] of declarationMap) { + if (!localBindings.has(name)) continue + + const dependencies = new Set() + for (const id of collectIdentifiersFromNode(declarationNode)) { + if (id !== name && localBindings.has(id)) { + dependencies.add(id) + } + } + graph.set(name, dependencies) + } + + return graph +} + +export function collectModuleLevelRefsFromNode( + node: t.Node, + localModuleLevelBindings: Set, +): Set { + const refs = new Set() + + for (const name of collectIdentifiersFromNode(node)) { + if (localModuleLevelBindings.has(name)) { + refs.add(name) + } + } + + return refs +} + +export function expandTransitively( + bindings: Set, + dependencyGraph: Map>, +) { + const queue = [...bindings] + const visited = new Set() + + while (queue.length > 0) { + const name = queue.pop()! + if (visited.has(name)) continue + visited.add(name) + + const dependencies = dependencyGraph.get(name) + if (!dependencies) continue + + for (const dependency of dependencies) { + if (!bindings.has(dependency)) { + bindings.add(dependency) + queue.push(dependency) + } + } + } +} + +export function expandSharedDestructuredDeclarators( + ast: t.File, + refsByGroup: Map>, + sharedBindings: Set, +) { + for (const statement of ast.program.body) { + const declaration = + t.isExportNamedDeclaration(statement) && statement.declaration + ? statement.declaration + : statement + + if (!t.isVariableDeclaration(declaration)) continue + + for (const declarator of declaration.declarations) { + if ( + !t.isObjectPattern(declarator.id) && + !t.isArrayPattern(declarator.id) + ) { + continue + } + + const names = collectIdentifiersFromPattern(declarator.id) + const usedGroups = new Set() + + for (const name of names) { + const groups = refsByGroup.get(name) + if (!groups) continue + for (const group of groups) { + usedGroups.add(group) + } + } + + if (usedGroups.size >= 2) { + for (const name of names) { + sharedBindings.add(name) + } + } + } + } +} + +export function expandDestructuredDeclarations( + ast: t.File, + bindings: Set, +) { + for (const statement of ast.program.body) { + const declaration = + t.isExportNamedDeclaration(statement) && statement.declaration + ? statement.declaration + : statement + + if (!t.isVariableDeclaration(declaration)) continue + + for (const declarator of declaration.declarations) { + if ( + !t.isObjectPattern(declarator.id) && + !t.isArrayPattern(declarator.id) + ) { + continue + } + + const names = collectIdentifiersFromPattern(declarator.id) + if (names.some((name) => bindings.has(name))) { + for (const name of names) { + bindings.add(name) + } + } + } + } +} + +export function removeBindingsTransitivelyDependingOn( + bindings: Set, + dependencyGraph: Map>, + roots: Iterable, +) { + const reverseGraph = new Map>() + + for (const [name, dependencies] of dependencyGraph) { + for (const dependency of dependencies) { + let parents = reverseGraph.get(dependency) + if (!parents) { + parents = new Set() + reverseGraph.set(dependency, parents) + } + parents.add(name) + } + } + + const visited = new Set() + const queue = [...roots] + + while (queue.length > 0) { + const current = queue.pop()! + if (visited.has(current)) continue + visited.add(current) + + const parents = reverseGraph.get(current) + if (!parents) continue + + for (const parent of parents) { + if (!visited.has(parent)) { + queue.push(parent) + } + } + } + + for (const name of [...bindings]) { + if (visited.has(name)) { + bindings.delete(name) + } + } +} + +export function removeModuleLevelBindings( + ast: t.File, + namesToRemove: Set, +) { + ast.program.body = ast.program.body.filter((statement) => { + const declaration = + t.isExportNamedDeclaration(statement) && statement.declaration + ? statement.declaration + : statement + + if (t.isVariableDeclaration(declaration)) { + declaration.declarations = declaration.declarations.filter( + (declarator) => + !collectIdentifiersFromPattern(declarator.id).some((name) => + namesToRemove.has(name), + ), + ) + return declaration.declarations.length > 0 + } + + if (t.isFunctionDeclaration(declaration) && declaration.id) { + return !namesToRemove.has(declaration.id.name) + } + + if (t.isClassDeclaration(declaration) && declaration.id) { + return !namesToRemove.has(declaration.id.name) + } + + if (t.isExportDefaultDeclaration(statement)) { + const defaultDeclaration = statement.declaration + if ( + (t.isFunctionDeclaration(defaultDeclaration) || + t.isClassDeclaration(defaultDeclaration)) && + defaultDeclaration.id + ) { + return !namesToRemove.has(defaultDeclaration.id.name) + } + } + + return true + }) +} + +export function retainModuleLevelDeclarations( + ast: t.File, + bindingsToKeep: Set, +) { + ast.program.body = ast.program.body.filter((statement) => { + if (t.isImportDeclaration(statement)) return true + + const declaration = + t.isExportNamedDeclaration(statement) && statement.declaration + ? statement.declaration + : statement + + if (t.isVariableDeclaration(declaration)) { + declaration.declarations = declaration.declarations.filter((declarator) => + collectIdentifiersFromPattern(declarator.id).some((name) => + bindingsToKeep.has(name), + ), + ) + return declaration.declarations.length > 0 + } + + if (t.isFunctionDeclaration(declaration) && declaration.id) { + return bindingsToKeep.has(declaration.id.name) + } + + if (t.isClassDeclaration(declaration) && declaration.id) { + return bindingsToKeep.has(declaration.id.name) + } + + return false + }) +} + +export function unwrapExportedDeclarations(ast: t.File) { + const body: Array = [] + + for (const statement of ast.program.body) { + if (t.isExportNamedDeclaration(statement)) { + if (statement.declaration) { + body.push(statement.declaration) + } + continue + } + + if (t.isExportDefaultDeclaration(statement)) { + const declaration = statement.declaration + if ( + (t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration)) && + declaration.id + ) { + body.push(declaration) + } + continue + } + + if (t.isExportAllDeclaration(statement)) { + continue + } + + body.push(statement) + } + + ast.program.body = body +} + +export function stripUnreferencedTopLevelExpressionStatements(ast: t.File) { + const locallyBound = new Set() + + for (const statement of ast.program.body) { + collectLocalBindingsFromStatement(statement, locallyBound) + } + + ast.program.body = ast.program.body.filter((statement) => { + if (!t.isExpressionStatement(statement)) return true + + for (const name of collectIdentifiersFromNode(statement)) { + if (locallyBound.has(name)) { + return true + } + } + + return false + }) +} diff --git a/packages/router-utils/src/index.ts b/packages/router-utils/src/index.ts index 3b072ae4199..ce0de90824e 100644 --- a/packages/router-utils/src/index.ts +++ b/packages/router-utils/src/index.ts @@ -9,3 +9,24 @@ export type { ParseAstOptions, ParseAstResult, GeneratorResult } from './ast' export { logDiff } from './logger' export { copyFilesPlugin } from './copy-files-plugin' + +export { createIdentifier, decodeIdentifier } from './path-ids' + +export { + buildDeclarationMap, + buildDependencyGraph, + collectIdentifiersFromNode, + collectIdentifiersFromPattern, + collectLocalBindingsFromStatement, + collectModuleLevelRefsFromNode, + expandDestructuredDeclarations, + expandSharedDestructuredDeclarators, + expandTransitively, + extractModuleInfoFromAst, + removeBindingsTransitivelyDependingOn, + removeModuleLevelBindings, + retainModuleLevelDeclarations, + stripUnreferencedTopLevelExpressionStatements, + unwrapExportedDeclarations, +} from './compiler-helpers' +export type { ExtractedModuleInfo, ModuleInfoBinding } from './compiler-helpers' diff --git a/packages/router-plugin/src/core/code-splitter/path-ids.ts b/packages/router-utils/src/path-ids.ts similarity index 100% rename from packages/router-plugin/src/core/code-splitter/path-ids.ts rename to packages/router-utils/src/path-ids.ts diff --git a/packages/router-utils/tests/compiler-helpers.test.ts b/packages/router-utils/tests/compiler-helpers.test.ts new file mode 100644 index 00000000000..00b75d67e51 --- /dev/null +++ b/packages/router-utils/tests/compiler-helpers.test.ts @@ -0,0 +1,249 @@ +import * as t from '@babel/types' +import { describe, expect, test } from 'vitest' +import { + buildDeclarationMap, + collectIdentifiersFromNode, + collectLocalBindingsFromStatement, + extractModuleInfoFromAst, +} from '../src/compiler-helpers' +import { parseAst } from '../src/ast' + +function getVariableInit(code: string) { + const ast = parseAst({ code, filename: 'test.tsx' }) + const declaration = ast.program.body.find((node) => + t.isVariableDeclaration(node), + ) as t.VariableDeclaration + return declaration.declarations[0]!.init! +} + +function collectSortedIdentifiers(node: t.Node) { + return [...collectIdentifiersFromNode(node)].sort() +} + +function collectSortedStatementBindings(code: string) { + const ast = parseAst({ code, filename: 'test.tsx' }) + const bindings = new Set() + for (const statement of ast.program.body) { + collectLocalBindingsFromStatement(statement, bindings) + } + return [...bindings].sort() +} + +function collectSortedDeclarationMapEntries(code: string) { + const ast = parseAst({ code, filename: 'test.tsx' }) + return [...buildDeclarationMap(ast)] + .map(([name, node]): [string, string] => [name, node.type]) + .sort((left, right) => (left[0] < right[0] ? -1 : 1)) +} + +function collectModuleInfoSnapshot(code: string) { + const ast = parseAst({ code, filename: 'test.tsx' }) + const info = extractModuleInfoFromAst(ast) + return { + bindings: [...info.bindings] + .map(([name, binding]) => [ + name, + binding.type === 'import' + ? `${binding.source}:${binding.importedName}` + : (binding.init?.type ?? null), + ]) + .sort((left, right) => (left[0]! < right[0]! ? -1 : 1)), + exports: [...info.exports].sort((left, right) => + left[0] < right[0] ? -1 : 1, + ), + reExportAllSources: info.reExportAllSources, + } +} + +describe('collectIdentifiersFromNode', () => { + test('collects free identifiers without reporting nested local bindings', () => { + const init = getVariableInit(` +const value = outer + (() => { + const local = dep + + function nested(param = fallback) { + const Inner = () => + const LocalExpr = class NamedLocal { + method() { + return NamedLocal + local + param + } + } + + class LocalComponent {} + + return local + param + imported + Inner + LocalExpr + } + + return nested() +})() +`) + + expect(collectSortedIdentifiers(init)).toMatchInlineSnapshot(` + [ + "dep", + "fallback", + "imported", + "outer", + ] + `) + }) + + test('respects nested variable, parameter, catch, and import shadowing', () => { + const ast = parseAst({ + code: ` +import { external as localImport } from 'pkg' + +const value = (localParam = defaultValue) => { + const localShadow = factory(localImport) + + try { + throw thrown + } catch (thrown) { + const factory = () => localShadow + localParam + thrown + return factory() + } +} +`, + filename: 'test.ts', + }) + + expect(collectSortedIdentifiers(ast.program)).toMatchInlineSnapshot(` + [ + "defaultValue", + "factory", + "thrown", + ] + `) + }) +}) + +describe('collectLocalBindingsFromStatement', () => { + test('collects ids from named default function and class declarations', () => { + expect({ + class: collectSortedStatementBindings( + `export default class DefaultComponent {}`, + ), + function: collectSortedStatementBindings( + `export default function DefaultRoute() {}`, + ), + anonymous: collectSortedStatementBindings( + `export default function () {}`, + ), + }).toMatchInlineSnapshot(` + { + "anonymous": [], + "class": [ + "DefaultComponent", + ], + "function": [ + "DefaultRoute", + ], + } + `) + }) +}) + +describe('buildDeclarationMap', () => { + test('maps named default function and class declarations', () => { + expect({ + class: collectSortedDeclarationMapEntries( + `export default class DefaultComponent {}`, + ), + function: collectSortedDeclarationMapEntries( + `export default function DefaultRoute() {}`, + ), + anonymous: collectSortedDeclarationMapEntries(`export default class {}`), + }).toMatchInlineSnapshot(` + { + "anonymous": [], + "class": [ + [ + "DefaultComponent", + "ClassDeclaration", + ], + ], + "function": [ + [ + "DefaultRoute", + "FunctionDeclaration", + ], + ], + } + `) + }) +}) + +describe('extractModuleInfoFromAst', () => { + test('extracts imports, local exports, default exports, and re-export sources', () => { + expect( + collectModuleInfoSnapshot(` + import defaultImport, { named as localNamed } from 'pkg' + import * as ns from 'pkg-ns' + const local = localNamed + export const exported = local + export function loader() {} + export default class DefaultRoute {} + export { remote as renamed } from './remote' + export * from './all' + `), + ).toMatchInlineSnapshot(` + { + "bindings": [ + [ + "DefaultRoute", + null, + ], + [ + "defaultImport", + "pkg:default", + ], + [ + "exported", + "Identifier", + ], + [ + "loader", + null, + ], + [ + "local", + "Identifier", + ], + [ + "localNamed", + "pkg:named", + ], + [ + "ns", + "pkg-ns:*", + ], + [ + "remote", + "./remote:remote", + ], + ], + "exports": [ + [ + "default", + "DefaultRoute", + ], + [ + "exported", + "exported", + ], + [ + "loader", + "loader", + ], + [ + "renamed", + "remote", + ], + ], + "reExportAllSources": [ + "./all", + ], + } + `) + }) +}) diff --git a/packages/router-utils/tests/stripTypeExports.test.ts b/packages/router-utils/tests/stripTypeExports.test.ts index d0c6af3ad78..32d25f1cbe9 100644 --- a/packages/router-utils/tests/stripTypeExports.test.ts +++ b/packages/router-utils/tests/stripTypeExports.test.ts @@ -123,7 +123,7 @@ type TypeOnly = string; export { value, type TypeOnly };` const result = transform(code) expect(result).toContain('export { value }') - expect(result).not.toContain('TypeOnly') + expect(result).toContain('type TypeOnly = string') }) test('removes entire export if all specifiers are type-only', () => { @@ -133,8 +133,8 @@ export { type Foo, type Bar }; export const value = 1;` const result = transform(code) expect(result).not.toContain('export { type Foo') - expect(result).not.toContain('Foo') - expect(result).not.toContain('Bar') + expect(result).toContain('type Foo = string') + expect(result).toContain('type Bar = number') expect(result).toContain('export const value = 1') }) }) @@ -233,9 +233,11 @@ export { helper, type HelperType };` expect(result).not.toContain('./types') expect(result).not.toContain('HelperType') - // Should remove type declarations - expect(result).not.toContain('type LocalType') - expect(result).not.toContain('interface LocalInterface') + // Should preserve local type declarations + expect(result).toContain('type LocalType = string') + expect(result).toContain('interface LocalInterface') + + // Should remove exported type declarations expect(result).not.toContain('type ExportedType') expect(result).not.toContain('interface ExportedInterface') diff --git a/packages/solid-router/src/ClientOnly.tsx b/packages/solid-router/src/ClientOnly.tsx index 6ed10d267b3..a7c4b718197 100644 --- a/packages/solid-router/src/ClientOnly.tsx +++ b/packages/solid-router/src/ClientOnly.tsx @@ -56,10 +56,15 @@ export function ClientOnly(props: ClientOnlyProps) { * ``` * @returns True if the JS has been hydrated already, false otherwise. */ +let globalHydrated = false + export function useHydrated(): Solid.Accessor { - const [hydrated, setHydrated] = Solid.createSignal(false) + const [hydrated, setHydrated] = Solid.createSignal(globalHydrated) + Solid.onMount(() => { + globalHydrated = true setHydrated(true) }) + return hydrated } diff --git a/packages/solid-start-client/package.json b/packages/solid-start-client/package.json index 2fbe68cba4c..7c730f3f9b4 100644 --- a/packages/solid-start-client/package.json +++ b/packages/solid-start-client/package.json @@ -48,6 +48,12 @@ "default": "./dist/esm/index.js" } }, + "./hydration": { + "import": { + "types": "./dist/esm/hydration.d.ts", + "default": "./dist/esm/hydration.js" + } + }, "./package.json": "./package.json" }, "sideEffects": false, diff --git a/packages/solid-start-client/src/GenericHydrate.tsx b/packages/solid-start-client/src/GenericHydrate.tsx new file mode 100644 index 00000000000..45e1eb1fc0d --- /dev/null +++ b/packages/solid-start-client/src/GenericHydrate.tsx @@ -0,0 +1,262 @@ +import * as Solid from 'solid-js' +import { Dynamic } from 'solid-js/web' + +import { useHydrated } from '@tanstack/solid-router' +import { isServer } from '@tanstack/router-core/isServer' +import { + hydrateIdAttribute, + hydrateWhenAttribute, +} from '@tanstack/start-client-core/hydration/constants' +import { + createResolvedGate, + getFallbackHtml, + getOrCreateGate, + onGateResolve, + releaseGate, + saveFallbackHtml, +} from '@tanstack/start-client-core/hydration/runtime' +import type { HydrationRuntimeContext } from '@tanstack/start-client-core/hydration' +import type { HydrationGateRecord } from '@tanstack/start-client-core/hydration/runtime' +import type { InternalHydrateProps } from './Hydrate' +import type { DynamicProps } from 'solid-js/web' + +type HydrationFallbackDynamicProps = DynamicProps<'div'> +type HydrationMarkerDynamicProps = DynamicProps<'div'> & { + [hydrateIdAttribute]: string + [hydrateWhenAttribute]: NonNullable + [key: `data-${string}`]: string | undefined +} + +const hydrateIdSelector = `[${hydrateIdAttribute}]` + +function shouldDeferHydration(strategy: InternalHydrateProps['when']) { + return strategy._d ? strategy._d() : strategy._t !== 'load' +} + +function runStrategyCleanup(cleanup: void | (() => void)) { + if (typeof cleanup === 'function') return cleanup + return undefined +} + +function HydratedBoundary(props: { + id: string + onHydrated?: () => void + onStrategyHydrated?: (id: string) => void + children: Solid.JSX.Element +}) { + let didHydrate = false + + Solid.onMount(() => { + if (didHydrate) return + didHydrate = true + props.onHydrated?.() + props.onStrategyHydrated?.(props.id) + }) + + return props.children +} + +function HydrationFallback(props: { id: string }) { + const html = getFallbackHtml(props.id) + + if (!html) return null + + const fallbackProps: HydrationFallbackDynamicProps = { + component: 'div', + style: { display: 'contents' }, + innerHTML: html, + } + + return +} + +export function GenericHydrate(props: InternalHydrateProps) { + const hydrateStrategy = () => props.when + const prefetchStrategy = () => props.prefetch + const delegatedStrategy = () => props.g + const hydrated = useHydrated() + const uniqueId = Solid.createUniqueId() + const id = props.h ? `${props.h}${uniqueId}` : uniqueId + const initialHydrateStrategy = hydrateStrategy() + const initialHydrateType = initialHydrateStrategy._t! + const isServerEnvironment = + (isServer as boolean | undefined) ?? typeof window === 'undefined' + const shouldPreserveServerHTML = isServerEnvironment || !hydrated() + const shouldDeferInitialHydration = + !hydrated() && shouldDeferHydration(initialHydrateStrategy) + const gate: HydrationGateRecord = isServerEnvironment + ? createResolvedGate(id, initialHydrateType) + : getOrCreateGate(id, initialHydrateType) + const [ready, setReady] = Solid.createSignal( + isServerEnvironment || + (!shouldDeferInitialHydration && initialHydrateType !== 'never'), + ) + let didPrefetch = false + let markerElement: HTMLDivElement | undefined + + if ( + !isServerEnvironment && + initialHydrateType !== 'never' && + (!shouldDeferInitialHydration || + !shouldDeferHydration(initialHydrateStrategy)) + ) { + gate.resolve() + } + + Solid.onMount(() => { + const currentHydrateStrategy = hydrateStrategy() + const currentPrefetchStrategy = prefetchStrategy() + const currentDelegatedStrategy = delegatedStrategy() + const currentHydrateType = currentHydrateStrategy._t! + gate.when = currentHydrateType + for (const element of document.querySelectorAll( + hydrateIdSelector, + )) { + if (element.getAttribute(hydrateIdAttribute) === id) { + markerElement = element + saveFallbackHtml(id, element) + break + } + } + + if ( + currentHydrateType === 'never' && + !shouldPreserveServerHTML && + markerElement + ) { + markerElement.replaceChildren() + } + + if (props.p && currentPrefetchStrategy) { + const prefetch = () => { + if (didPrefetch) return + didPrefetch = true + void props.p?.() + } + const cleanupPrefetch = runStrategyCleanup( + currentPrefetchStrategy._s?.({ + element: markerElement ?? null, + prefetch, + }), + ) + if (cleanupPrefetch) Solid.onCleanup(cleanupPrefetch) + } + + if ( + currentHydrateType !== 'never' && + (!shouldDeferInitialHydration || + !shouldDeferHydration(currentHydrateStrategy)) + ) { + gate.resolve() + setReady(true) + } + + const cleanups: Array<() => void> = [] + let removeResolveListener = () => {} + let disposed = false + + const resolveBoundary = () => { + setReady(true) + } + + const cleanup = () => { + if (disposed) return + disposed = true + removeResolveListener() + cleanups.forEach((fn) => fn()) + } + + const addCleanup = (fn: void | (() => void)) => { + if (!fn) return + if (disposed || gate.resolved) { + fn() + return + } + cleanups.push(fn) + } + + Solid.onCleanup(() => { + cleanup() + releaseGate(gate) + }) + + removeResolveListener = onGateResolve(gate, () => { + cleanup() + resolveBoundary() + }) + + if ( + gate.resolved || + !shouldDeferInitialHydration || + currentHydrateType === 'never' + ) { + if (gate.resolved) resolveBoundary() + return + } + + const context: HydrationRuntimeContext = { + element: markerElement ?? null, + gate, + } + addCleanup(runStrategyCleanup(currentHydrateStrategy._s?.(context))) + + if (currentDelegatedStrategy?._s) { + addCleanup( + runStrategyCleanup( + currentDelegatedStrategy._s({ + ...context, + delegated: true, + }), + ), + ) + } + }) + + Solid.createRenderEffect(() => { + const currentHydrateStrategy = hydrateStrategy() + if ( + isServerEnvironment || + gate.resolved || + currentHydrateStrategy._t === 'never' || + shouldDeferHydration(currentHydrateStrategy) + ) { + return + } + + gate.resolve() + }) + + const markerAttributes = initialHydrateStrategy._a?.() + const markerProps: HydrationMarkerDynamicProps = { + component: 'div', + [hydrateIdAttribute]: id, + [hydrateWhenAttribute]: initialHydrateType, + ...markerAttributes, + } + const fallback = () => + shouldPreserveServerHTML ? ( + + ) : ( + (props.fallback ?? null) + ) + + return ( + + {initialHydrateType === 'never' && !shouldPreserveServerHTML ? ( + (props.fallback ?? null) + ) : ( + + + + {props.children} + + + + )} + + ) +} diff --git a/packages/solid-start-client/src/Hydrate.tsx b/packages/solid-start-client/src/Hydrate.tsx new file mode 100644 index 00000000000..9dbc105a402 --- /dev/null +++ b/packages/solid-start-client/src/Hydrate.tsx @@ -0,0 +1,58 @@ +import type * as Solid from 'solid-js' + +import type { + HydrationStrategy as CoreHydrationStrategy, + HydrationPrefetchStrategy, + HydrationWhen, +} from '@tanstack/start-client-core/hydration' + +export type { + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationWhen, +} from '@tanstack/start-client-core/hydration' + +export type SolidHydrationStrategy< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = CoreHydrationStrategy & { + _h: (props: HydrateProps) => Solid.JSX.Element +} + +export type HydrationStrategy< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = SolidHydrationStrategy + +export type HydrateOptions = { + when: SolidHydrationStrategy +} + +type HydrateCommonProps = { + fallback?: Solid.JSX.Element + onHydrated?: () => void + children: Solid.JSX.Element +} + +export type HydrateProps = + | (HydrateCommonProps & + HydrateOptions & { + prefetch?: never + split?: boolean + }) + | (HydrateCommonProps & + HydrateOptions & { + prefetch: HydrationPrefetchStrategy + split?: true + }) + +export type InternalHydrateProps = HydrateProps & { + g?: CoreHydrationStrategy + h?: string + p?: () => Promise +} + +export function Hydrate(props: HydrateProps) { + return props.when._h(props) +} diff --git a/packages/solid-start-client/src/hydration.ts b/packages/solid-start-client/src/hydration.ts new file mode 100644 index 00000000000..c5e79f1de70 --- /dev/null +++ b/packages/solid-start-client/src/hydration.ts @@ -0,0 +1,17 @@ +export { condition, interaction, media } from './hydration/generic' +export { idle } from './hydration/idle' +export { load } from './hydration/load' +export { never } from './hydration/never' +export { visible } from './hydration/visible' +export type { + HydrationCondition, + HydrationInteractionEvent, + HydrationInteractionEvents, + IdleHydrationOptions, + HydrationPrefetchWhen, + HydrationPrefetchStrategy, + HydrationStrategyTypes, + HydrationWhen, + VisibleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +export type { HydrationStrategy, SolidHydrationStrategy } from './Hydrate' diff --git a/packages/solid-start-client/src/hydration/generic.ts b/packages/solid-start-client/src/hydration/generic.ts new file mode 100644 index 00000000000..1cd0a68d37a --- /dev/null +++ b/packages/solid-start-client/src/hydration/generic.ts @@ -0,0 +1,44 @@ +import { + condition as coreCondition, + interaction as coreInteraction, + media as coreMedia, +} from '@tanstack/start-client-core/hydration' +import { GenericHydrate } from '../GenericHydrate' +import type { + HydrationCondition, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, +} from '@tanstack/start-client-core/hydration' +import type { SolidHydrationStrategy } from '../Hydrate' + +/* @__NO_SIDE_EFFECTS__ */ +function withGenericRenderer( + strategy: T, +): T & SolidHydrationStrategy { + return /* @__PURE__ */ Object.assign(strategy, { + _h: GenericHydrate, + }) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function media( + query: string, +): SolidHydrationStrategy<'media', true> & HydrationPrefetchStrategy<'media'> { + return /* @__PURE__ */ withGenericRenderer(coreMedia(query)) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function condition( + condition: HydrationCondition, +): SolidHydrationStrategy<'condition', false> { + return /* @__PURE__ */ withGenericRenderer(coreCondition(condition)) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function interaction(options?: { + events?: HydrationInteractionEvents +}): SolidHydrationStrategy<'interaction', true> & + HydrationPrefetchStrategy<'interaction'> { + return /* @__PURE__ */ withGenericRenderer(coreInteraction(options)) +} diff --git a/packages/solid-start-client/src/hydration/idle.ts b/packages/solid-start-client/src/hydration/idle.ts new file mode 100644 index 00000000000..35695830d8a --- /dev/null +++ b/packages/solid-start-client/src/hydration/idle.ts @@ -0,0 +1,21 @@ +import { StrategyHydrate } from './visible' +import { + scheduleIdle, + type HydrationPrefetchStrategy, + type IdleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +import type { SolidHydrationStrategy } from '../Hydrate' + +/* @__NO_SIDE_EFFECTS__ */ +export function idle( + options: IdleHydrationOptions = {}, +): SolidHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'> { + const timeout = options.timeout ?? 2000 + const setup = (context: any) => + scheduleIdle(context.prefetch ?? context.gate.resolve, timeout) + + return { + _s: setup, + _h: StrategyHydrate, + } as SolidHydrationStrategy<'idle', true> & HydrationPrefetchStrategy<'idle'> +} diff --git a/packages/solid-start-client/src/hydration/load.tsx b/packages/solid-start-client/src/hydration/load.tsx new file mode 100644 index 00000000000..4a4bba3dae4 --- /dev/null +++ b/packages/solid-start-client/src/hydration/load.tsx @@ -0,0 +1,44 @@ +import * as Solid from 'solid-js' + +import type { + HydrationPrefetchStrategy, + HydrationRuntimeContext, +} from '@tanstack/start-client-core/hydration' +import type { + HydrateProps, + InternalHydrateProps, + SolidHydrationStrategy, +} from '../Hydrate' + +const loadType = 'load' + +export function LoadHydrate(props: HydrateProps) { + const internalProps = props as InternalHydrateProps + const uniqueId = Solid.createUniqueId() + const id = internalProps.h ? `${internalProps.h}${uniqueId}` : uniqueId + + Solid.onMount(() => { + props.onHydrated?.() + }) + + return ( +
+ + {props.children} + +
+ ) +} + +const loadStrategy = { + _s: ({ gate, prefetch }: HydrationRuntimeContext) => { + ;(prefetch ?? gate!.resolve)() + }, + _h: LoadHydrate, +} as SolidHydrationStrategy<'load', true> & HydrationPrefetchStrategy<'load'> + +/* @__NO_SIDE_EFFECTS__ */ +export function load(): SolidHydrationStrategy<'load', true> & + HydrationPrefetchStrategy<'load'> { + return loadStrategy +} diff --git a/packages/solid-start-client/src/hydration/never.ts b/packages/solid-start-client/src/hydration/never.ts new file mode 100644 index 00000000000..ee74c141c69 --- /dev/null +++ b/packages/solid-start-client/src/hydration/never.ts @@ -0,0 +1,10 @@ +import { never as coreNever } from '@tanstack/start-client-core/hydration' +import { GenericHydrate } from '../GenericHydrate' +import type { SolidHydrationStrategy } from '../Hydrate' + +/* @__NO_SIDE_EFFECTS__ */ +export function never(): SolidHydrationStrategy<'never', false> { + return /* @__PURE__ */ Object.assign(coreNever(), { + _h: GenericHydrate, + }) +} diff --git a/packages/solid-start-client/src/hydration/visible.tsx b/packages/solid-start-client/src/hydration/visible.tsx new file mode 100644 index 00000000000..9bb90792b12 --- /dev/null +++ b/packages/solid-start-client/src/hydration/visible.tsx @@ -0,0 +1,90 @@ +import * as Solid from 'solid-js' +import { Dynamic, createComponent } from 'solid-js/web' + +import { isServer } from '@tanstack/router-core/isServer' +import type { + HydrationPrefetchStrategy, + VisibleHydrationOptions, +} from '@tanstack/start-client-core/hydration' +import type { + HydrateProps, + InternalHydrateProps, + SolidHydrationStrategy, +} from '../Hydrate' + +export function StrategyHydrate(props: HydrateProps) { + const internalProps = props as InternalHydrateProps + const strategy = internalProps.when + const prefetchStrategy = internalProps.prefetch + const preload = internalProps.p + const uniqueId = Solid.createUniqueId() + const id = internalProps.h ? `${internalProps.h}${uniqueId}` : uniqueId + const isServerEnvironment = + (isServer as boolean | undefined) ?? typeof window === 'undefined' + let resolved = isServerEnvironment + const [ready, setReady] = Solid.createSignal(resolved) + const gate = { + resolve: () => { + if (resolved) return + resolved = true + setReady(true) + props.onHydrated?.() + }, + } + let markerElement: HTMLDivElement | undefined + + Solid.onMount(() => { + if (preload && prefetchStrategy) { + const cleanupPrefetch = prefetchStrategy._s!({ + element: markerElement!, + prefetch: preload, + }) + if (typeof cleanupPrefetch === 'function') + Solid.onCleanup(cleanupPrefetch) + } + + const cleanup = strategy._s!({ + element: markerElement!, + gate: gate as never, + }) + if (typeof cleanup === 'function') Solid.onCleanup(cleanup) + }) + + return createComponent(Dynamic as any, { + component: 'div', + ref(element: HTMLDivElement) { + markerElement = element + }, + 'data-ts-hydrate-id': id, + get children() { + return ready() ? props.children : (props.fallback ?? null) + }, + }) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function visible( + options?: VisibleHydrationOptions, +): SolidHydrationStrategy<'visible', true> & + HydrationPrefetchStrategy<'visible'> { + const observerOptions = { + rootMargin: options?.rootMargin ?? '600px', + threshold: options?.threshold ?? 0, + } + const setup = (context: any) => { + const callback = context.prefetch ?? context.gate.resolve + const observer = new IntersectionObserver((entries) => { + if (!entries[0]!.isIntersecting) return + observer.disconnect() + callback() + }, observerOptions) + observer.observe(context.element) + return () => observer.disconnect() + } + + return { + _s: setup, + _h: StrategyHydrate, + } as SolidHydrationStrategy<'visible', true> & + HydrationPrefetchStrategy<'visible'> +} diff --git a/packages/solid-start-client/src/index.tsx b/packages/solid-start-client/src/index.tsx index aa73990a576..02026e2645a 100644 --- a/packages/solid-start-client/src/index.tsx +++ b/packages/solid-start-client/src/index.tsx @@ -1,2 +1,12 @@ export { StartClient } from './StartClient' export { hydrateStart } from './hydrateStart' +export { Hydrate } from './Hydrate' +export type { + HydrateOptions, + HydrateProps, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, +} from './Hydrate' diff --git a/packages/solid-start-client/src/tests/Hydrate.test-d.tsx b/packages/solid-start-client/src/tests/Hydrate.test-d.tsx new file mode 100644 index 00000000000..218f5da1735 --- /dev/null +++ b/packages/solid-start-client/src/tests/Hydrate.test-d.tsx @@ -0,0 +1,85 @@ +import { expectTypeOf, test } from 'vitest' +import { Hydrate } from '../Hydrate' +import type { + HydrateProps, + HydrationPrefetchStrategy, + HydrationStrategy, +} from '../Hydrate' +import type { HydrationStrategy as CoreHydrationStrategy } from '@tanstack/start-client-core/hydration' +import type { visible } from '../hydration' +import type { JSX } from 'solid-js' + +type CommonHydrateProps = { + fallback?: JSX.Element + onHydrated?: () => void + children: JSX.Element +} + +type SplitHydrateProps = CommonHydrateProps & { + when: HydrationStrategy + prefetch?: never + split?: boolean +} + +type PrefetchHydrateProps = CommonHydrateProps & { + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split?: true +} + +test('Hydrate component accepts the public HydrateProps type', () => { + expectTypeOf(Hydrate).toBeFunction() + expectTypeOf(Hydrate).parameter(0).branded.toEqualTypeOf() +}) + +test('Hydrate props are exact for strategy and prefetch forms', () => { + expectTypeOf< + Extract + >().branded.toEqualTypeOf() + expectTypeOf< + Extract + >().branded.toEqualTypeOf() +}) + +test('Hydrate requires a strategy', () => { + expectTypeOf<{ + when: HydrationStrategy + children: JSX.Element + }>().toMatchTypeOf() + + expectTypeOf<{ + children: JSX.Element + }>().not.toMatchTypeOf() +}) + +test('Hydrate requires a framework-renderable strategy', () => { + expectTypeOf().not.toMatchTypeOf() + expectTypeOf>().toMatchTypeOf() + + expectTypeOf<{ + when: CoreHydrationStrategy + children: JSX.Element + }>().not.toMatchTypeOf() +}) + +test('Hydrate enforces prefetch only with split boundaries', () => { + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + children: JSX.Element + }>().toMatchTypeOf() + + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split: true + children: JSX.Element + }>().toMatchTypeOf() + + expectTypeOf<{ + when: HydrationStrategy + prefetch: HydrationPrefetchStrategy + split: false + children: JSX.Element + }>().not.toMatchTypeOf() +}) diff --git a/packages/solid-start-client/vite.config.ts b/packages/solid-start-client/vite.config.ts index be71bab5183..dbfb60ea039 100644 --- a/packages/solid-start-client/vite.config.ts +++ b/packages/solid-start-client/vite.config.ts @@ -20,7 +20,7 @@ export default mergeConfig( tanstackViteConfig({ tsconfigPath: './tsconfig.build.json', srcDir: './src', - entry: './src/index.tsx', + entry: ['./src/index.tsx', './src/Hydrate.tsx', './src/hydration.ts'], cjs: false, }), ) diff --git a/packages/solid-start/package.json b/packages/solid-start/package.json index fd830062e06..9a5b4e8a79e 100644 --- a/packages/solid-start/package.json +++ b/packages/solid-start/package.json @@ -44,6 +44,12 @@ "default": "./dist/esm/client.js" } }, + "./hydration": { + "import": { + "types": "./dist/esm/hydration.d.ts", + "default": "./dist/esm/hydration.js" + } + }, "./client-rpc": { "import": { "types": "./dist/esm/client-rpc.d.ts", diff --git a/packages/solid-start/src/hydration.ts b/packages/solid-start/src/hydration.ts new file mode 100644 index 00000000000..fc660e9dc51 --- /dev/null +++ b/packages/solid-start/src/hydration.ts @@ -0,0 +1,18 @@ +export { + condition, + idle, + interaction, + load, + media, + never, + visible, +} from '@tanstack/solid-start-client/hydration' +export type { + HydrationCondition, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, + VisibleHydrationOptions, +} from '@tanstack/solid-start-client/hydration' diff --git a/packages/solid-start/src/index.ts b/packages/solid-start/src/index.ts index 8b51b6c7832..0cdac87c812 100644 --- a/packages/solid-start/src/index.ts +++ b/packages/solid-start/src/index.ts @@ -1,2 +1,12 @@ export { useServerFn } from './useServerFn' export * from '@tanstack/start-client-core' +export { Hydrate } from '@tanstack/solid-start-client' +export type { + HydrateOptions, + HydrateProps, + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationStrategy, + HydrationWhen, +} from '@tanstack/solid-start-client' diff --git a/packages/solid-start/vite.config.ts b/packages/solid-start/vite.config.ts index 262208de090..2c1a42d0e49 100644 --- a/packages/solid-start/vite.config.ts +++ b/packages/solid-start/vite.config.ts @@ -27,6 +27,7 @@ export default mergeConfig( entry: [ './src/index.ts', './src/client.tsx', + './src/hydration.ts', './src/client-rpc.ts', './src/ssr-rpc.ts', './src/server-rpc.ts', diff --git a/packages/start-client-core/package.json b/packages/start-client-core/package.json index 0d3d845dd96..1f5629f790b 100644 --- a/packages/start-client-core/package.json +++ b/packages/start-client-core/package.json @@ -60,16 +60,37 @@ "default": "./dist/esm/client-rpc/index.js" } }, + "./hydration": { + "import": { + "types": "./dist/esm/hydration.d.ts", + "default": "./dist/esm/hydration.js" + } + }, + "./hydration/constants": { + "import": { + "types": "./dist/esm/hydration/constants.d.ts", + "default": "./dist/esm/hydration/constants.js" + } + }, + "./hydration/runtime": { + "import": { + "types": "./dist/esm/hydration/runtime.d.ts", + "default": "./dist/esm/hydration/runtime.js" + } + }, "./package.json": "./package.json" }, "imports": { "#tanstack-start-entry": { + "types": "./src/start-entry.d.ts", "default": "./dist/esm/fake-entries/start.js" }, "#tanstack-router-entry": { + "types": "./src/start-entry.d.ts", "default": "./dist/esm/fake-entries/router.js" }, "#tanstack-start-plugin-adapters": { + "types": "./src/start-entry.d.ts", "default": "./dist/esm/fake-entries/plugin-adapters.js" } }, diff --git a/packages/start-client-core/src/client/hydrateStart.ts b/packages/start-client-core/src/client/hydrateStart.ts index 206b70505cc..48b58158bc2 100644 --- a/packages/start-client-core/src/client/hydrateStart.ts +++ b/packages/start-client-core/src/client/hydrateStart.ts @@ -10,7 +10,19 @@ import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializatio import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core' import type { AnyStartInstanceOptions } from '../createStart' -export async function hydrateStart(): Promise { +type HotContext = { + data?: Record + dispose?: (cb: (data: Record) => void) => void +} + +declare global { + interface ImportMeta { + hot?: HotContext + webpackHot?: HotContext + } +} + +async function hydrateStart(): Promise { const router = await getRouter() let serializationAdapters: Array @@ -47,3 +59,38 @@ export async function hydrateStart(): Promise { return router } + +function hydrateStartWithHmr(): Promise { + const hot = import.meta.hot ?? import.meta.webpackHot + + if (!hot) { + return hydrateStart() + } + + const key = 'tss-hydrate-start-promise' + const hotData = (hot.data ??= {}) + let hydrationPromise = hotData[key] as Promise | undefined + + if (!hydrationPromise) { + hydrationPromise = hydrateStart().catch((error) => { + if (hotData[key] === hydrationPromise) { + hotData[key] = undefined + } + + throw error + }) + + hotData[key] = hydrationPromise + } + + hot.dispose?.((data) => { + data[key] = hotData[key] + }) + + return hydrationPromise +} + +const exportedHydrateStart = + process.env.NODE_ENV !== 'production' ? hydrateStartWithHmr : hydrateStart + +export { exportedHydrateStart as hydrateStart } diff --git a/packages/start-client-core/src/hydration.ts b/packages/start-client-core/src/hydration.ts new file mode 100644 index 00000000000..f862d983345 --- /dev/null +++ b/packages/start-client-core/src/hydration.ts @@ -0,0 +1,42 @@ +import { hydrateIdAttribute } from './hydration/constants' + +export { condition } from './hydration/condition' +export type { HydrationCondition } from './hydration/condition' +export { + hydrateIdAttribute, + hydrateInteractionEventsAttribute, + hydrateWhenAttribute, +} from './hydration/constants' +export const hydrateIdSelector = `[${hydrateIdAttribute}]` +export { idle, scheduleIdle } from './hydration/idle' +export type { IdleHydrationOptions } from './hydration/idle' +export { interaction } from './hydration/interaction' +export { load } from './hydration/load' +export { media } from './hydration/media' +export { never } from './hydration/never' +export { + clearResolvedGateIdsInMarker, + createResolvedGate, + getFallbackHtml, + getMarkerGate, + getOrCreateGate, + onGateResolve, + releaseGate, + resolveHydrationMarker, + saveFallbackHtml, +} from './hydration/runtime' +export { visible } from './hydration/visible' +export type { VisibleHydrationOptions } from './hydration/visible' +export type { HydrationGateRecord } from './hydration/runtime' +export type { + HydrationInteractionEvent, + HydrationInteractionEvents, + HydrationMarkerAttributes, + HydrationPrefetchWhen, + HydrationPrefetchStrategy, + HydrationRuntimeContext, + HydrationRuntimeGate, + HydrationStrategy, + HydrationStrategyTypes, + HydrationWhen, +} from './hydration/types' diff --git a/packages/start-client-core/src/hydration/condition.ts b/packages/start-client-core/src/hydration/condition.ts new file mode 100644 index 00000000000..c5d8fb15d5e --- /dev/null +++ b/packages/start-client-core/src/hydration/condition.ts @@ -0,0 +1,22 @@ +import type { HydrationStrategy } from './types' + +const conditionType = 'condition' + +export type HydrationCondition = boolean | (() => boolean) + +function readCondition(condition: HydrationCondition) { + return typeof condition === 'function' ? condition() : condition +} + +/* @__NO_SIDE_EFFECTS__ */ +export function condition( + condition: HydrationCondition, +): HydrationStrategy { + return { + _t: conditionType, + _d: () => !readCondition(condition), + _s: ({ gate }) => { + if (readCondition(condition)) gate!.resolve() + }, + } +} diff --git a/packages/start-client-core/src/hydration/constants.ts b/packages/start-client-core/src/hydration/constants.ts new file mode 100644 index 00000000000..4b7a29313d6 --- /dev/null +++ b/packages/start-client-core/src/hydration/constants.ts @@ -0,0 +1,4 @@ +export const hydrateIdAttribute = 'data-ts-hydrate-id' +export const hydrateWhenAttribute = 'data-ts-hydrate-when' +export const hydrateInteractionEventsAttribute = + 'data-ts-hydrate-interaction-events' diff --git a/packages/start-client-core/src/hydration/idle.ts b/packages/start-client-core/src/hydration/idle.ts new file mode 100644 index 00000000000..40bcf6e59b1 --- /dev/null +++ b/packages/start-client-core/src/hydration/idle.ts @@ -0,0 +1,39 @@ +import type { HydrationPrefetchStrategy } from './types' + +const idleType = 'idle' + +export type IdleHydrationOptions = { + timeout?: number +} + +type IdleScheduler = { + requestIdleCallback?: ( + callback: IdleRequestCallback, + options?: IdleRequestOptions, + ) => number + cancelIdleCallback?: (handle: number) => void +} + +export function scheduleIdle(callback: () => void, timeout: number) { + const schedule = globalThis as unknown as IdleScheduler + if (schedule.requestIdleCallback) { + const handle = schedule.requestIdleCallback(callback, { timeout }) + return () => schedule.cancelIdleCallback?.(handle) + } + + const timeoutId = globalThis.setTimeout(callback, timeout) + return () => globalThis.clearTimeout(timeoutId) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function idle( + options: IdleHydrationOptions = {}, +): HydrationPrefetchStrategy { + const timeout = options.timeout ?? 2000 + + return { + _t: idleType, + _s: ({ gate, prefetch }) => + scheduleIdle(prefetch ?? gate!.resolve, timeout), + } +} diff --git a/packages/start-client-core/src/hydration/interaction.ts b/packages/start-client-core/src/hydration/interaction.ts new file mode 100644 index 00000000000..8a78ad6703e --- /dev/null +++ b/packages/start-client-core/src/hydration/interaction.ts @@ -0,0 +1,269 @@ +import { + hydrateIdAttribute, + hydrateInteractionEventsAttribute, + hydrateWhenAttribute, +} from './constants' +import { + clearResolvedGateIdsInMarker, + getMarkerGate, + resolveHydrationMarker, +} from './runtime' +import type { + HydrationInteractionEvents, + HydrationPrefetchStrategy, + HydrationRuntimeContext, +} from './types' + +export type InteractionHydrationOptions = { + events?: HydrationInteractionEvents +} + +const hydrateIdSelector = `[${hydrateIdAttribute}]` + +type PendingReplayEvent = { + marker: Element + targetPath: Array + type: string + event: Event +} + +const defaultInteractionEvents = [ + 'pointerenter', + 'focusin', + 'pointerdown', + 'click', +] as const +const interactionType = 'interaction' +const interactionHydrateSelector = `[${hydrateWhenAttribute}="${interactionType}"]` +const replayEventsByGateId = new Map>() + +function normalizeInteractionEvents( + events?: HydrationInteractionEvents, +): ReadonlyArray { + if (events === undefined) return defaultInteractionEvents + + const eventList: ReadonlyArray = + typeof events === 'string' ? [events] : events + const normalizedEvents: Array = [] + const seen = new Set() + + for (const eventName of eventList) { + if (!eventName || seen.has(eventName)) continue + seen.add(eventName) + normalizedEvents.push(eventName) + } + + return normalizedEvents +} + +function getIntentListenerEvents( + marker: Element, + events: ReadonlyArray, +) { + const listenerEvents = new Set(events) + + marker.querySelectorAll(interactionHydrateSelector).forEach((childMarker) => { + const attr = childMarker.getAttribute(hydrateInteractionEventsAttribute) + for (const eventName of attr === null + ? defaultInteractionEvents + : attr.split(/\s+/).filter(Boolean)) { + listenerEvents.add(eventName) + } + }) + + return [...listenerEvents] +} + +function getReplayTargetPath(marker: Element, target: EventTarget) { + if (!(target instanceof Node) || !marker.contains(target)) return [] + + const path: Array = [] + let node: Element | null = + target instanceof Element ? target : target.parentElement + + while (node && node !== marker) { + const parent = node.parentElement + if (!parent) return [] + path.push(Array.prototype.indexOf.call(parent.children, node)) + node = parent + } + + return path.reverse() +} + +function queueHydrationReplayEvent(marker: Element, event: Event) { + if (!event.bubbles) return + + const id = marker.getAttribute(hydrateIdAttribute) + if (!id || marker.getAttribute(hydrateWhenAttribute) === 'never') return + + const target = event.target + if (!target) return + + const gate = getMarkerGate(marker) + if (!gate || gate.resolved) return + + event.preventDefault() + event.stopPropagation() + event.stopImmediatePropagation() + + const pendingEvents = replayEventsByGateId.get(id) ?? [] + pendingEvents.push({ + marker, + targetPath: getReplayTargetPath(marker, target), + type: event.type, + event, + }) + replayEventsByGateId.set(id, pendingEvents) +} + +function createReplayEvent(event: Event) { + if (event instanceof MouseEvent) { + return new MouseEvent(event.type, event) + } + + if (event instanceof FocusEvent) { + return new FocusEvent(event.type, event) + } + + return new Event(event.type, event) +} + +function replayHydrationEvents(id: string) { + const pendingEvents = replayEventsByGateId.get(id) + if (!pendingEvents?.length) return + + replayEventsByGateId.delete(id) + + for (const pendingEvent of pendingEvents) { + let replayTarget: Element | null = pendingEvent.marker + for (const index of pendingEvent.targetPath) { + replayTarget = replayTarget.children[index] ?? null + if (!replayTarget) break + } + + replayTarget ??= pendingEvent.marker + replayTarget.dispatchEvent(createReplayEvent(pendingEvent.event)) + } +} + +function getIntentMarkers(rootMarker: Element, event: Event) { + const target = event.target + if (!(target instanceof Element)) return [rootMarker] + + const closestMarker = target.closest(hydrateIdSelector) + let marker: Element | null = + closestMarker && rootMarker.contains(closestMarker) + ? closestMarker + : rootMarker + + const markers: Array = [] + while (marker) { + if (marker.hasAttribute(hydrateIdAttribute)) { + markers.push(marker) + } + if (marker === rootMarker) break + marker = marker.parentElement + } + + if (!markers.includes(rootMarker)) { + markers.push(rootMarker) + } + + return markers.reverse() +} + +function listenForIntent( + element: Element, + events: ReadonlyArray, + context: HydrationRuntimeContext, +) { + const onIntent = (event: Event) => { + const markers = getIntentMarkers(element, event) + if ( + context.delegated && + !markers.some( + (marker) => + marker.getAttribute(hydrateWhenAttribute) === interactionType, + ) + ) { + return + } + + markers.forEach((marker) => { + queueHydrationReplayEvent(marker, event) + resolveHydrationMarker(marker) + }) + } + let disposed = false + + events.forEach((eventName) => { + element.addEventListener(eventName, onIntent, true) + }) + + return () => { + if (disposed) return + disposed = true + events.forEach((eventName) => { + element.removeEventListener(eventName, onIntent, true) + }) + } +} + +function listenForPrefetchIntent( + element: Element, + events: ReadonlyArray, + prefetch: () => void, +) { + let disposed = false + + events.forEach((eventName) => { + element.addEventListener(eventName, prefetch, true) + }) + + return () => { + if (disposed) return + disposed = true + events.forEach((eventName) => { + element.removeEventListener(eventName, prefetch, true) + }) + } +} + +/* @__NO_SIDE_EFFECTS__ */ +export function interaction( + options: InteractionHydrationOptions = {}, +): HydrationPrefetchStrategy { + const events = normalizeInteractionEvents(options.events) + const eventKey = events.join(' ') + + return { + _t: interactionType, + _s: (context) => { + const element = context.element + if (!element) return + if (context.prefetch) { + if (!events.length) return + return listenForPrefetchIntent(element, events, context.prefetch) + } + + const listenerEvents = getIntentListenerEvents(element, events) + const cleanupIntent = listenerEvents.length + ? listenForIntent(element, listenerEvents, context) + : undefined + return () => { + cleanupIntent?.() + clearResolvedGateIdsInMarker(element) + } + }, + _o: (id) => { + globalThis.requestAnimationFrame(() => replayHydrationEvents(id)) + }, + _a: () => + options.events === undefined + ? undefined + : { + [hydrateInteractionEventsAttribute]: eventKey, + }, + } +} diff --git a/packages/start-client-core/src/hydration/load.ts b/packages/start-client-core/src/hydration/load.ts new file mode 100644 index 00000000000..b78d6d6a423 --- /dev/null +++ b/packages/start-client-core/src/hydration/load.ts @@ -0,0 +1,16 @@ +import type { HydrationPrefetchStrategy } from './types' + +const loadType = 'load' + +const loadStrategy: HydrationPrefetchStrategy = { + _t: loadType, + _d: () => false, + _s: ({ gate, prefetch }) => { + ;(prefetch ?? gate!.resolve)() + }, +} + +/* @__NO_SIDE_EFFECTS__ */ +export function load(): HydrationPrefetchStrategy { + return loadStrategy +} diff --git a/packages/start-client-core/src/hydration/media.ts b/packages/start-client-core/src/hydration/media.ts new file mode 100644 index 00000000000..64d6be79ce4 --- /dev/null +++ b/packages/start-client-core/src/hydration/media.ts @@ -0,0 +1,27 @@ +import type { HydrationPrefetchStrategy } from './types' + +const mediaType = 'media' + +function listenForMedia(query: string, callback: () => void) { + if (!query) return + + const mediaQuery = window.matchMedia(query) + const onChange = () => { + if (mediaQuery.matches) callback() + } + mediaQuery.addEventListener('change', onChange) + onChange() + + return () => mediaQuery.removeEventListener('change', onChange) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function media( + query: string, +): HydrationPrefetchStrategy { + return { + _t: mediaType, + _s: ({ gate, prefetch }) => + listenForMedia(query, prefetch ?? gate!.resolve), + } +} diff --git a/packages/start-client-core/src/hydration/never.ts b/packages/start-client-core/src/hydration/never.ts new file mode 100644 index 00000000000..cf4c565a1f8 --- /dev/null +++ b/packages/start-client-core/src/hydration/never.ts @@ -0,0 +1,15 @@ +import type { HydrationStrategy } from './types' + +const neverType = 'never' + +const neverStrategy: HydrationStrategy = { + _t: neverType, + _d: () => true, +} + +/* @__NO_SIDE_EFFECTS__ */ +function neverHydrate(): HydrationStrategy { + return neverStrategy +} + +export { neverHydrate as never } diff --git a/packages/start-client-core/src/hydration/runtime.ts b/packages/start-client-core/src/hydration/runtime.ts new file mode 100644 index 00000000000..d7d23a658c4 --- /dev/null +++ b/packages/start-client-core/src/hydration/runtime.ts @@ -0,0 +1,139 @@ +import { hydrateIdAttribute, hydrateWhenAttribute } from './constants' +import type { HydrationRuntimeGate, HydrationWhen } from './types' + +const hydrateIdSelector = `[${hydrateIdAttribute}]` + +export type HydrationGateRecord = HydrationRuntimeGate & { + id: string + when: HydrationWhen + promise: Promise + consumers: number + resolveListeners: Set<() => void> +} + +const gateRegistry = new Map() +const resolvedGateIds = new Set() +const fallbackHtmlByGateId = new Map() + +export function createResolvedGate( + id: string, + when: HydrationWhen, +): HydrationGateRecord { + return { + id, + when, + promise: Promise.resolve(), + resolve: () => {}, + resolved: true, + consumers: 0, + resolveListeners: new Set<() => void>(), + } +} + +export function getOrCreateGate( + id: string, + when: HydrationWhen, +): HydrationGateRecord { + const existing = gateRegistry.get(id) + if (existing?.when === when) { + existing.consumers++ + return existing + } + + let resolvePromise!: () => void + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + + const gate: HydrationGateRecord = { + id, + promise, + resolved: false, + consumers: 1, + when, + resolveListeners: new Set(), + resolve: () => { + if (gate.resolved) return + gate.resolved = true + resolvePromise() + gate.resolveListeners.forEach((listener) => listener()) + gate.resolveListeners.clear() + }, + } + + gateRegistry.set(id, gate) + if (when !== 'never' && resolvedGateIds.has(id)) { + resolvedGateIds.delete(id) + gate.resolve() + } + return gate +} + +export function releaseGate(gate: HydrationGateRecord) { + resolvedGateIds.delete(gate.id) + gate.consumers-- + if (gate.consumers > 0) return + if (gateRegistry.get(gate.id) === gate) { + gateRegistry.delete(gate.id) + fallbackHtmlByGateId.delete(gate.id) + gate.resolveListeners.clear() + } +} + +export function onGateResolve(gate: HydrationGateRecord, listener: () => void) { + if (gate.resolved) { + listener() + return () => {} + } + + gate.resolveListeners.add(listener) + return () => { + gate.resolveListeners.delete(listener) + } +} + +export function getMarkerGate(marker: Element) { + const id = marker.getAttribute(hydrateIdAttribute) + return id ? gateRegistry.get(id) : undefined +} + +export function resolveHydrationMarker(marker: Element) { + const id = marker.getAttribute(hydrateIdAttribute) + if (!id || marker.getAttribute(hydrateWhenAttribute) === 'never') { + return + } + + const gate = gateRegistry.get(id) + if (gate) { + if (gate.when !== 'never') { + gate.resolve() + } + return + } + + resolvedGateIds.add(id) +} + +export function clearResolvedGateIdsInMarker(marker: Element) { + const ownId = marker.getAttribute(hydrateIdAttribute) + if (ownId) { + resolvedGateIds.delete(ownId) + } + + marker.querySelectorAll(hydrateIdSelector).forEach((childMarker) => { + const childId = childMarker.getAttribute(hydrateIdAttribute) + if (childId) { + resolvedGateIds.delete(childId) + } + }) +} + +export function saveFallbackHtml(id: string, element: Element) { + if (!fallbackHtmlByGateId.has(id)) { + fallbackHtmlByGateId.set(id, element.innerHTML) + } +} + +export function getFallbackHtml(id: string) { + return fallbackHtmlByGateId.get(id) +} diff --git a/packages/start-client-core/src/hydration/types.ts b/packages/start-client-core/src/hydration/types.ts new file mode 100644 index 00000000000..77ccefde337 --- /dev/null +++ b/packages/start-client-core/src/hydration/types.ts @@ -0,0 +1,74 @@ +export type HydrationWhen = + | 'load' + | 'idle' + | 'visible' + | 'media' + | 'interaction' + | 'condition' + | 'never' + +export type HydrationInteractionEvent = + | 'auxclick' + | 'click' + | 'contextmenu' + | 'dblclick' + | 'focusin' + | 'keydown' + | 'keyup' + | 'mousedown' + | 'mouseenter' + | 'mouseover' + | 'mouseup' + | 'pointerdown' + | 'pointerenter' + | 'pointerover' + | 'pointerup' + +export type HydrationInteractionEvents = + | HydrationInteractionEvent + | ReadonlyArray + +export type HydrationMarkerAttributes = Record + +export type HydrationRuntimeGate = { + id?: string + when?: HydrationWhen + resolved: boolean + resolve: () => void +} + +export type HydrationRuntimeContext = { + element: Element | null + gate?: HydrationRuntimeGate + prefetch?: () => void + delegated?: boolean +} + +export type HydrationStrategyTypes< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = { + when: TWhen + canPrefetch: TCanPrefetch +} + +export type HydrationStrategy< + TWhen extends HydrationWhen = HydrationWhen, + TCanPrefetch extends boolean = boolean, +> = { + _t?: TWhen + readonly '~types'?: HydrationStrategyTypes + _d?: () => boolean + _s?: (context: HydrationRuntimeContext) => void | (() => void) + _o?: (id: string) => void + _a?: () => HydrationMarkerAttributes | undefined +} + +export type HydrationPrefetchWhen = Exclude< + HydrationWhen, + 'condition' | 'never' +> + +export type HydrationPrefetchStrategy< + TWhen extends HydrationPrefetchWhen = HydrationPrefetchWhen, +> = HydrationStrategy diff --git a/packages/start-client-core/src/hydration/visible.ts b/packages/start-client-core/src/hydration/visible.ts new file mode 100644 index 00000000000..91d1ddefbc1 --- /dev/null +++ b/packages/start-client-core/src/hydration/visible.ts @@ -0,0 +1,115 @@ +import type { HydrationPrefetchStrategy } from './types' + +const visibleType = 'visible' + +export type VisibleHydrationOptions = { + rootMargin?: string + threshold?: number | Array +} + +type VisibleObserverEntry = { + key: string + observer: IntersectionObserver + elements: Map void>> +} + +const observerRegistry = new Map() + +function getVisibleKey(rootMargin: string, threshold: number | Array) { + return `${rootMargin}|${ + Array.isArray(threshold) ? threshold.join(',') : String(threshold) + }` +} + +function getObserver(rootMargin: string, threshold: number | Array) { + const key = getVisibleKey(rootMargin, threshold) + const existing = observerRegistry.get(key) + if (existing) return existing + + const entry: VisibleObserverEntry = { + key, + elements: new Map void>>(), + observer: new IntersectionObserver( + (entries) => { + for (const observerEntry of entries) { + if (!observerEntry.isIntersecting) continue + resolveVisibleElement(entry, observerEntry.target) + } + }, + { rootMargin, threshold }, + ), + } + + observerRegistry.set(key, entry) + return entry +} + +function cleanupVisibleObserverEntry(observerEntry: VisibleObserverEntry) { + if (observerEntry.elements.size > 0) return + observerEntry.observer.disconnect() + observerRegistry.delete(observerEntry.key) +} + +function unobserveVisibleCallback( + observerEntry: VisibleObserverEntry, + element: Element, + callback: () => void, +) { + const currentCallbacks = observerEntry.elements.get(element) + currentCallbacks?.delete(callback) + if (currentCallbacks?.size === 0) { + observerEntry.elements.delete(element) + observerEntry.observer.unobserve(element) + } + cleanupVisibleObserverEntry(observerEntry) +} + +function resolveVisibleElement( + observerEntry: VisibleObserverEntry, + element: Element, +) { + const callbacks = observerEntry.elements.get(element) + if (!callbacks) return + + callbacks.forEach((callback) => callback()) + observerEntry.elements.delete(element) + observerEntry.observer.unobserve(element) + cleanupVisibleObserverEntry(observerEntry) +} + +function observeVisible( + element: Element | null, + callback: () => void, + rootMargin: string, + threshold: number | Array, +) { + if (!element) { + callback() + return + } + + const observerEntry = getObserver(rootMargin, threshold) + let callbacks = observerEntry.elements.get(element) + if (!callbacks) { + callbacks = new Set() + observerEntry.elements.set(element, callbacks) + observerEntry.observer.observe(element) + } + callbacks.add(callback) + + return () => unobserveVisibleCallback(observerEntry, element, callback) +} + +/* @__NO_SIDE_EFFECTS__ */ +export function visible( + options: VisibleHydrationOptions = {}, +): HydrationPrefetchStrategy { + const rootMargin = options.rootMargin ?? '600px' + const threshold = options.threshold ?? 0 + + return { + _t: visibleType, + _s: ({ element, gate, prefetch }) => + observeVisible(element, prefetch ?? gate!.resolve, rootMargin, threshold), + } +} diff --git a/packages/start-client-core/vite.config.ts b/packages/start-client-core/vite.config.ts index e893ba2c8da..c17997fc31b 100644 --- a/packages/start-client-core/vite.config.ts +++ b/packages/start-client-core/vite.config.ts @@ -2,33 +2,42 @@ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import packageJson from './package.json' -const config = defineConfig({ - test: { - typecheck: { enabled: true }, - name: packageJson.name, - watch: false, - environment: 'jsdom', - }, -}) - -export default mergeConfig( - config, - tanstackViteConfig({ - tsconfigPath: './tsconfig.build.json', - srcDir: './src', - entry: [ - './src/index.tsx', - './src/client/index.ts', - './src/client-rpc/index.ts', - './src/fake-entries/start.ts', - './src/fake-entries/router.ts', - './src/fake-entries/plugin-adapters.ts', - ], - cjs: false, - externalDeps: [ - '#tanstack-start-entry', - '#tanstack-router-entry', - '#tanstack-start-plugin-adapters', - ], - }), +export default defineConfig(({ command }) => + mergeConfig( + { + define: + command === 'build' + ? { + 'import.meta.hot': 'import.meta.hot', + } + : undefined, + test: { + typecheck: { enabled: true }, + name: packageJson.name, + watch: false, + environment: 'jsdom', + }, + }, + tanstackViteConfig({ + tsconfigPath: './tsconfig.build.json', + srcDir: './src', + entry: [ + './src/index.tsx', + './src/client/index.ts', + './src/client-rpc/index.ts', + './src/hydration/constants.ts', + './src/hydration.ts', + './src/hydration/runtime.ts', + './src/fake-entries/start.ts', + './src/fake-entries/router.ts', + './src/fake-entries/plugin-adapters.ts', + ], + cjs: false, + externalDeps: [ + '#tanstack-start-entry', + '#tanstack-router-entry', + '#tanstack-start-plugin-adapters', + ], + }), + ), ) diff --git a/packages/start-plugin-core/src/hydrate-when-transform.ts b/packages/start-plugin-core/src/hydrate-when-transform.ts new file mode 100644 index 00000000000..a73a5454abc --- /dev/null +++ b/packages/start-plugin-core/src/hydrate-when-transform.ts @@ -0,0 +1,992 @@ +import { relative } from 'node:path' +import crypto from 'node:crypto' +import babel from '@babel/core' +import * as t from '@babel/types' +import { + buildDeclarationMap, + buildDependencyGraph, + collectIdentifiersFromNode, + collectLocalBindingsFromStatement, + deadCodeElimination, + expandTransitively, + findReferencedIdentifiers, + generateFromAst, + parseAst, + removeModuleLevelBindings, + retainModuleLevelDeclarations, + stripUnreferencedTopLevelExpressionStatements, + unwrapExportedDeclarations, +} from '@tanstack/router-utils' +import { tssHydrate } from './hydration-constants' +import { cleanId, codeFrameError } from './start-compiler/utils' +import type { + CompileStartFrameworkOptions, + StartCompilerPlugin, + StartCompilerTransformResult, +} from './types' + +type HydrationImport = { + hydrateLocalName: string + source: string + interactionLocalNames: Set +} + +export class MissingHydrateSourceError extends Error { + constructor(id: string) { + super( + `Missing Hydrate source for virtual module ${id}. The parent module must be transformed before its Hydrate child chunk is loaded.`, + ) + } +} + +const hydrateImportSources = new Set([ + '@tanstack/react-start', + '@tanstack/solid-start', +]) + +/** + * Detection pattern used by the transform code filter to pre-scan files for + * `` JSX before any AST parsing happens. + */ +const HYDRATE_DETECTION_PATTERN = /\bHydrate\b/ + +function createBoundaryId(root: string, sourceId: string) { + const normalized = relative(root, sourceId).replaceAll('\\', '/') + const sourceHash = crypto + .createHash('sha1') + .update(normalized) + .digest('hex') + .slice(0, 10) + + return (index: number) => { + return `${index.toString(36)}_${sourceHash}` + } +} + +function parseBoundaryIndex(splitId: string | null) { + if (!splitId) return -1 + const separatorIndex = splitId.indexOf('_') + if (separatorIndex <= 0) return -1 + const boundaryIndex = Number.parseInt(splitId.slice(0, separatorIndex), 36) + return Number.isInteger(boundaryIndex) ? boundaryIndex : -1 +} + +function getJSXElementName(node: t.JSXElement) { + const name = node.openingElement.name + return t.isJSXIdentifier(name) ? name.name : undefined +} + +function getJSXAttribute(node: t.JSXOpeningElement, name: string) { + for (const item of node.attributes) { + if (t.isJSXAttribute(item) && t.isJSXIdentifier(item.name, { name })) { + return item + } + } + + return undefined +} + +function getBooleanProp(node: t.JSXOpeningElement, name: string) { + const attr = getJSXAttribute(node, name) + if (!attr) return undefined + if (!attr.value) return true + if (t.isStringLiteral(attr.value)) return attr.value.value !== 'false' + if (t.isJSXExpressionContainer(attr.value)) { + if (t.isBooleanLiteral(attr.value.expression)) { + return attr.value.expression.value + } + } + return undefined +} + +function isInteractionCall( + node: t.Node | null | undefined, + interactionLocalNames: Set, +) { + if (!t.isCallExpression(node)) return false + return ( + t.isIdentifier(node.callee) && interactionLocalNames.has(node.callee.name) + ) +} + +function getWhenExpression(node: t.JSXOpeningElement) { + const when = getJSXAttribute(node, 'when') + if (!when?.value || !t.isJSXExpressionContainer(when.value)) return undefined + return when.value.expression +} + +function mayHavePrefetchProp(node: t.JSXOpeningElement) { + return node.attributes.some((attribute) => { + if (t.isJSXSpreadAttribute(attribute)) return true + return ( + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name, { name: 'prefetch' }) + ) + }) +} + +function parseHydrateVirtualId(id: string) { + const queryIndex = id.indexOf('?') + const sourceId = cleanId(queryIndex === -1 ? id : id.slice(0, queryIndex)) + if (queryIndex === -1) { + return { sourceId, splitId: null, boundaryIndex: -1 } + } + + const rawQuery = id.slice(queryIndex + 1) + const params = new URLSearchParams(rawQuery) + const splitId = params.get(tssHydrate) + + return { + sourceId, + splitId, + boundaryIndex: parseBoundaryIndex(splitId), + } +} + +function createHydrateImportId(sourceId: string, id: string) { + const params = new URLSearchParams() + params.set(tssHydrate, id) + return `${sourceId}?${params.toString()}` +} + +function upsertHydrateId(node: t.JSXOpeningElement, id: string) { + const existing = getJSXAttribute(node, 'h') + + if (existing) { + existing.value = t.stringLiteral(id) + return + } + + node.attributes.push( + t.jsxAttribute(t.jsxIdentifier('h'), t.stringLiteral(id)), + ) +} + +function isObjectPropertyName( + property: t.ObjectMethod | t.ObjectProperty, + name: string, +) { + if (t.isIdentifier(property.key) && !property.computed) { + return property.key.name === name + } + + return t.isStringLiteral(property.key) && property.key.value === name +} + +function isReferenceInsideNode( + referencePath: babel.NodePath, + node: t.Node, +): boolean { + if (referencePath.node === node) return true + return Boolean(referencePath.findParent((parent) => parent.node === node)) +} + +function removeBindingDeclaration( + binding: NonNullable>, +) { + if (binding.path.isVariableDeclarator()) { + const declarationPath = binding.path.parentPath + if ( + declarationPath.isVariableDeclaration() && + declarationPath.node.declarations.length === 1 + ) { + declarationPath.remove() + return + } + + binding.path.remove() + return + } + + if ( + binding.path.isImportSpecifier() || + binding.path.isImportDefaultSpecifier() || + binding.path.isImportNamespaceSpecifier() + ) { + const importPath = binding.path.parentPath + if ( + importPath.isImportDeclaration() && + importPath.node.specifiers.length === 1 + ) { + importPath.remove() + return + } + + binding.path.remove() + return + } + + binding.path.remove() +} + +function stripBindingsOnlyReferencedBy( + path: babel.NodePath, + node: t.Node, + seen = new Set(), +) { + for (const name of collectIdentifiersFromNode(node)) { + if (seen.has(name)) continue + const binding = path.scope.getBinding(name) + if (!binding?.constant) continue + if (binding.referencePaths.length === 0) continue + if ( + !binding.referencePaths.every((referencePath) => + isReferenceInsideNode(referencePath, node), + ) + ) { + continue + } + + seen.add(name) + + const bindingNode = binding.path.node + if (t.isVariableDeclarator(bindingNode) && bindingNode.init) { + stripBindingsOnlyReferencedBy(path, bindingNode.init, seen) + } else if ( + t.isFunctionDeclaration(bindingNode) || + t.isClassDeclaration(bindingNode) + ) { + stripBindingsOnlyReferencedBy(path, bindingNode, seen) + } + + removeBindingDeclaration(binding) + } +} + +function stripObjectExpressionProperty( + path: babel.NodePath, + node: t.ObjectExpression, + name: string, +) { + let modified = false + + node.properties = node.properties.filter((property) => { + if ( + (t.isObjectMethod(property) || t.isObjectProperty(property)) && + isObjectPropertyName(property, name) + ) { + stripBindingsOnlyReferencedBy( + path, + t.isObjectProperty(property) ? property.value : property.body, + ) + modified = true + return false + } + + return true + }) + + return modified +} + +function stripSingleUseObjectBindingProperty( + path: babel.NodePath, + identifier: t.Identifier, + name: string, +) { + const binding = path.scope.getBinding(identifier.name) + if (!binding?.constant) return false + if (binding.referencePaths.length !== 1) return false + if (binding.referencePaths[0]?.node !== identifier) return false + if (!binding.path.isVariableDeclarator()) return false + if (!t.isObjectExpression(binding.path.node.init)) return false + + return stripObjectExpressionProperty(path, binding.path.node.init, name) +} + +function stripJSXAttribute(path: babel.NodePath, name: string) { + const node = path.node.openingElement + let modified = false + + node.attributes = node.attributes.filter((item) => { + if (t.isJSXAttribute(item) && t.isJSXIdentifier(item.name, { name })) { + if (item.value) { + stripBindingsOnlyReferencedBy(path, item.value) + } + modified = true + return false + } + + if (t.isJSXSpreadAttribute(item) && t.isObjectExpression(item.argument)) { + if (stripObjectExpressionProperty(path, item.argument, name)) { + modified = true + } + return item.argument.properties.length > 0 + } + + if (t.isJSXSpreadAttribute(item) && t.isIdentifier(item.argument)) { + if (stripSingleUseObjectBindingProperty(path, item.argument, name)) { + modified = true + } + } + + return true + }) + + return modified +} + +function throwBoundaryError( + code: string, + path: babel.NodePath, + message: string, +): never { + if (path.node.loc) { + throw codeFrameError(code, path.node.loc, message) + } + throw new Error(message) +} + +function inspectSplitBoundary(options: { + code: string + path: babel.NodePath + validate?: boolean + collectCaptured?: boolean + nestedHydrate?: { + localName: string + interactionLocalNames: Set + } +}) { + const { path } = options + const capturedNames = options.collectCaptured ? new Set() : undefined + const nestedHydrate = options.nestedHydrate + let nestedBoundaryCount = 0 + let hasNestedInteraction = false + + if (options.validate) { + for (const child of path.node.children) { + if ( + t.isJSXExpressionContainer(child) && + (t.isFunctionExpression(child.expression) || + t.isArrowFunctionExpression(child.expression)) + ) { + throwBoundaryError( + options.code, + path, + 'Hydrate cannot code-split function-as-children. Use split={false} for this boundary.', + ) + } + } + } + + const validateVisitors = options.validate + ? { + CallExpression(callPath: babel.NodePath) { + if (!t.isIdentifier(callPath.node.callee)) return + if (!/^use[A-Z0-9]/.test(callPath.node.callee.name)) return + + throwBoundaryError( + options.code, + path, + 'Hydrate cannot code-split JSX that calls hooks during render. Move the hook call into a child component or use split={false}.', + ) + }, + ThisExpression(thisPath: babel.NodePath) { + void thisPath + throwBoundaryError( + options.code, + path, + 'Hydrate cannot code-split JSX that captures this.', + ) + }, + Super(superPath: babel.NodePath) { + void superPath + throwBoundaryError( + options.code, + path, + 'Hydrate cannot code-split JSX that captures super.', + ) + }, + } + : {} + + const nestedHydrateVisitors = nestedHydrate + ? { + JSXElement(nestedPath: babel.NodePath) { + if (getJSXElementName(nestedPath.node) !== nestedHydrate.localName) { + return + } + + const split = getBooleanProp(nestedPath.node.openingElement, 'split') + if (split === false) return + + nestedBoundaryCount++ + if ( + isInteractionCall( + getWhenExpression(nestedPath.node.openingElement), + nestedHydrate.interactionLocalNames, + ) + ) { + hasNestedInteraction = true + } + }, + } + : {} + + const captureVisitors = capturedNames + ? { + Identifier(identifierPath: babel.NodePath) { + if ( + identifierPath.findParent( + (parent) => parent.node === path.node.openingElement, + ) + ) { + return + } + + const parent = identifierPath.parent + if ( + t.isJSXOpeningElement(parent) || + t.isJSXClosingElement(parent) || + (t.isObjectProperty(parent, { key: identifierPath.node }) && + !parent.computed && + !parent.shorthand) || + (t.isMemberExpression(parent, { + property: identifierPath.node, + }) && + !parent.computed) + ) { + return + } + + const binding = identifierPath.scope.getBinding( + identifierPath.node.name, + ) + if (!binding) return + if (t.isProgram(binding.scope.block)) return + if ( + path.node === binding.scope.block || + path.isAncestor(binding.path) + ) + return + + capturedNames.add(identifierPath.node.name) + }, + JSXIdentifier(identifierPath: babel.NodePath) { + if ( + identifierPath.findParent( + (parent) => parent.node === path.node.openingElement, + ) + ) { + return + } + + if (identifierPath.parentKey !== 'name') return + const name = identifierPath.node.name + if (!/^[A-Z]/.test(name)) return + const binding = identifierPath.scope.getBinding(name) + if (!binding) return + if (t.isProgram(binding.scope.block)) return + + capturedNames.add(name) + }, + } + : {} + + path.traverse({ + ...validateVisitors, + ...nestedHydrateVisitors, + ...captureVisitors, + }) + + return { + captured: capturedNames ? [...capturedNames].sort() : [], + nestedBoundaryCount, + hasNestedInteraction, + } +} + +function getHydrateImport(ast: t.File) { + let hydrateImport: HydrationImport | undefined + + for (const node of ast.program.body) { + if (!t.isImportDeclaration(node)) continue + if (hydrateImportSources.has(node.source.value)) { + for (const specifier of node.specifiers) { + if ( + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported, { name: 'Hydrate' }) + ) { + hydrateImport = { + hydrateLocalName: specifier.local.name, + source: node.source.value, + interactionLocalNames: + hydrateImport?.interactionLocalNames ?? new Set(), + } + } + } + continue + } + + if ( + node.source.value !== '@tanstack/react-start/hydration' && + node.source.value !== '@tanstack/solid-start/hydration' + ) { + continue + } + + for (const specifier of node.specifiers) { + if ( + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported, { name: 'interaction' }) + ) { + hydrateImport ??= { + hydrateLocalName: '', + source: '', + interactionLocalNames: new Set(), + } + hydrateImport.interactionLocalNames.add(specifier.local.name) + } + } + } + + return hydrateImport?.hydrateLocalName ? hydrateImport : undefined +} + +function getOrAddInteractionImport( + programPath: babel.NodePath, + source: string, +) { + const importSource = `${source}/hydration` + for (const node of programPath.node.body) { + if (!t.isImportDeclaration(node) || node.source.value !== importSource) { + continue + } + for (const specifier of node.specifiers) { + if ( + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported, { name: 'interaction' }) + ) { + return specifier.local + } + } + const interactionIdent = + programPath.scope.generateUidIdentifier('interaction') + node.specifiers.push( + t.importSpecifier(interactionIdent, t.identifier('interaction')), + ) + return interactionIdent + } + + const interactionIdent = + programPath.scope.generateUidIdentifier('interaction') + programPath.unshiftContainer( + 'body', + t.importDeclaration( + [t.importSpecifier(interactionIdent, t.identifier('interaction'))], + t.stringLiteral(importSource), + ), + ) + return interactionIdent +} + +function addClientImports( + programPath: babel.NodePath, + framework: CompileStartFrameworkOptions, +) { + const lazyIdent = + programPath.scope.generateUidIdentifier('lazyRouteComponent') + programPath.unshiftContainer('body', [ + t.importDeclaration( + [t.importSpecifier(lazyIdent, t.identifier('lazyRouteComponent'))], + t.stringLiteral(`@tanstack/${framework}-router`), + ), + ]) + return lazyIdent +} + +function createReturnExpressionFromChildren( + children: Array, +) { + const meaningfulChildren = children.filter( + (child) => !(t.isJSXText(child) && child.value.trim() === ''), + ) + + if (meaningfulChildren.length === 0) return t.nullLiteral() + if (meaningfulChildren.length === 1) { + const child = meaningfulChildren[0]! + if (t.isJSXExpressionContainer(child)) { + return t.isJSXEmptyExpression(child.expression) + ? t.nullLiteral() + : child.expression + } + if (t.isJSXElement(child) || t.isJSXFragment(child)) return child + if (t.isJSXText(child)) return t.stringLiteral(child.value) + } + + return t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), children) +} + +function transformHydrateAst(options: { + ast: t.File + code: string + id: string + root: string + env: 'client' | 'server' + framework: CompileStartFrameworkOptions + indexOffset?: number +}) { + if (!options.code.includes('Hydrate')) return null + + const hydrateImport = getHydrateImport(options.ast) + if (!hydrateImport) return null + const { hydrateLocalName: localName } = hydrateImport + const sourceId = cleanId(options.id) + const getBoundaryId = createBoundaryId(options.root, sourceId) + + let nextBoundaryIndex = options.indexOffset ?? 0 + const state = { modified: false } + let lazyIdent: t.Identifier | undefined + let interactionIdent: t.Identifier | undefined + + babel.traverse(options.ast, { + Program(programPath) { + programPath.traverse({ + JSXElement(path) { + if (getJSXElementName(path.node) !== localName) return + + if (options.env === 'server') { + if (stripJSXAttribute(path, 'fallback')) { + state.modified = true + } + if (stripJSXAttribute(path, 'prefetch')) { + state.modified = true + } + } + + const split = getBooleanProp(path.node.openingElement, 'split') + if (split === false) return + + const boundaryInspection = inspectSplitBoundary({ + code: options.code, + path, + validate: true, + collectCaptured: options.env === 'client', + ...(options.env === 'client' + ? { + nestedHydrate: { + localName, + interactionLocalNames: hydrateImport.interactionLocalNames, + }, + } + : {}), + }) + + const whenExpression = getWhenExpression(path.node.openingElement) + const index = nextBoundaryIndex + const needsDelegatedInteraction = + options.env === 'client' && + boundaryInspection.hasNestedInteraction && + !isInteractionCall( + whenExpression, + hydrateImport.interactionLocalNames, + ) + nextBoundaryIndex += 1 + boundaryInspection.nestedBoundaryCount + const id = getBoundaryId(index) + const exportName = `H${index}` + const needsPreloadProp = mayHavePrefetchProp(path.node.openingElement) + + upsertHydrateId(path.node.openingElement, id) + if (needsDelegatedInteraction) { + interactionIdent ??= getOrAddInteractionImport( + programPath, + hydrateImport.source, + ) + path.node.openingElement.attributes.push( + t.jsxAttribute( + t.jsxIdentifier('g'), + t.jsxExpressionContainer( + t.callExpression(interactionIdent, []), + ), + ), + ) + } + state.modified = true + + if (options.env === 'server') return + + if (!lazyIdent) { + lazyIdent = addClientImports(programPath, options.framework) + } + + const componentIdent = + programPath.scope.generateUidIdentifier(exportName) + const declarations = [ + t.variableDeclarator( + componentIdent, + t.callExpression(lazyIdent, [ + t.arrowFunctionExpression( + [], + t.callExpression(t.import(), [ + t.stringLiteral(createHydrateImportId(sourceId, id)), + ]), + ), + t.stringLiteral(exportName), + ]), + ), + ] + + let preloadIdent: t.Identifier | undefined + if (needsPreloadProp) { + preloadIdent = programPath.scope.generateUidIdentifier( + `${exportName}_preload`, + ) + declarations.push( + t.variableDeclarator( + preloadIdent, + t.memberExpression(componentIdent, t.identifier('preload')), + ), + ) + } + + programPath.unshiftContainer('body', [ + t.variableDeclaration('const', declarations), + ]) + if (preloadIdent) { + path.node.openingElement.attributes.push( + t.jsxAttribute( + t.jsxIdentifier('p'), + t.jsxExpressionContainer(preloadIdent), + ), + ) + } + + path.node.children = [ + t.jsxText('\n'), + t.jsxExpressionContainer( + t.jsxElement( + t.jsxOpeningElement( + t.jsxIdentifier(componentIdent.name), + boundaryInspection.captured.map((name) => + t.jsxAttribute( + t.jsxIdentifier(name), + t.jsxExpressionContainer(t.identifier(name)), + ), + ), + true, + ), + null, + [], + true, + ), + ), + t.jsxText('\n'), + ] + path.skip() + }, + }) + }, + }) + + if (!state.modified) return null + + return true +} + +function loadHydrateVirtualModule(options: { + id: string + root: string + code: string +}) { + const { sourceId, splitId, boundaryIndex } = parseHydrateVirtualId(options.id) + if (!splitId || boundaryIndex < 0) return null + const getBoundaryId = createBoundaryId(options.root, sourceId) + + const ast = parseAst({ code: options.code, sourceFilename: sourceId }) + const hydrateImport = getHydrateImport(ast) + if (!hydrateImport) return null + const { hydrateLocalName: localName } = hydrateImport + + let target: t.JSXElement | undefined + let targetIndex = -1 + let targetCaptured: Array = [] + let index = 0 + + babel.traverse(ast, { + JSXElement(path) { + if (getJSXElementName(path.node) !== localName) return + const split = getBooleanProp(path.node.openingElement, 'split') + if (split === false) return + + if (index === boundaryIndex) { + const id = getBoundaryId(index) + if (id !== splitId) { + path.stop() + return + } + targetCaptured = inspectSplitBoundary({ + code: options.code, + path, + collectCaptured: true, + }).captured + target = t.cloneNode(path.node, true) + targetIndex = index + path.stop() + return + } + index++ + }, + }) + + if (!target || targetIndex < 0) return null + + const children = target.children + const exportName = `H${targetIndex}` + const refIdents = findReferencedIdentifiers(ast) + + removeModuleLevelBindings(ast, new Set(['Route'])) + const localBindings = new Set() + for (const node of ast.program.body) { + collectLocalBindingsFromStatement(node, localBindings) + } + + const keepBindings = new Set() + const returnExpression = createReturnExpressionFromChildren(children) + for (const name of collectIdentifiersFromNode(returnExpression)) { + if (localBindings.has(name)) { + keepBindings.add(name) + } + } + + if (keepBindings.size > 0) { + expandTransitively( + keepBindings, + buildDependencyGraph(buildDeclarationMap(ast), localBindings), + ) + } + + retainModuleLevelDeclarations(ast, keepBindings) + unwrapExportedDeclarations(ast) + + ast.program.body.push( + t.exportNamedDeclaration( + t.functionDeclaration( + t.identifier(exportName), + targetCaptured.length > 0 + ? [ + t.objectPattern( + targetCaptured.map((name) => + t.objectProperty( + t.identifier(name), + t.identifier(name), + false, + true, + ), + ), + ), + ] + : [], + t.blockStatement([t.returnStatement(returnExpression)]), + ), + ), + ) + + deadCodeElimination(ast, refIdents) + stripUnreferencedTopLevelExpressionStatements(ast) + + const result = generateFromAst(ast, { + sourceMaps: true, + sourceFileName: options.id, + filename: options.id, + }) + return result +} + +export function createHydrateCompilerPlugin(): StartCompilerPlugin { + type SourceEntry = { + code: string + virtualModules: Map + } + + const sourcesByEnvironment = new Map>() + + const getEnvironmentSources = (envName: string) => { + let sources = sourcesByEnvironment.get(envName) + if (!sources) { + sources = new Map() + sourcesByEnvironment.set(envName, sources) + } + return sources + } + + const setSource = (envName: string, id: string, code: string) => { + const sourceId = cleanId(id) + const sources = getEnvironmentSources(envName) + const existing = sources.get(sourceId) + if (existing?.code === code) { + return existing + } + + const entry = { + code, + virtualModules: new Map(), + } + sources.set(sourceId, entry) + return entry + } + + const getSourceEntry = (envName: string, id: string) => + sourcesByEnvironment.get(envName)?.get(cleanId(id)) + + const deleteSource = (envName: string, id: string) => { + sourcesByEnvironment.get(envName)?.delete(cleanId(id)) + } + + return { + name: 'tanstack-start-core:hydrate', + detect: HYDRATE_DETECTION_PATTERN, + virtualModuleIdPattern: new RegExp(`[?&]${tssHydrate}=`), + transformAst(context) { + const virtualModule = parseHydrateVirtualId(context.id) + const indexOffset = + virtualModule.boundaryIndex < 0 + ? undefined + : virtualModule.boundaryIndex + 1 + const result = transformHydrateAst({ + ast: context.ast, + code: context.code, + id: context.id, + root: context.root, + env: context.env, + framework: context.framework, + indexOffset, + }) + + if (result && virtualModule.boundaryIndex < 0) { + setSource(context.envName, context.id, context.code) + } + + return !!result + }, + loadVirtualModule(context) { + const virtualModule = parseHydrateVirtualId(context.id) + if (!virtualModule.splitId || virtualModule.boundaryIndex < 0) { + return null + } + + const sourceEntry = + context.code === undefined + ? getSourceEntry(context.envName, virtualModule.sourceId) + : setSource(context.envName, virtualModule.sourceId, context.code) + + if (!sourceEntry) { + throw new MissingHydrateSourceError(context.id) + } + + if (sourceEntry.virtualModules.has(context.id)) { + return sourceEntry.virtualModules.get(context.id)! + } + + const result = loadHydrateVirtualModule({ + code: sourceEntry.code, + id: context.id, + root: context.root, + }) + sourceEntry.virtualModules.set(context.id, result) + return result + }, + invalidateModule(context) { + deleteSource(context.envName, context.id) + }, + } +} diff --git a/packages/start-plugin-core/src/hydration-constants.ts b/packages/start-plugin-core/src/hydration-constants.ts new file mode 100644 index 00000000000..b79c20ffd3d --- /dev/null +++ b/packages/start-plugin-core/src/hydration-constants.ts @@ -0,0 +1 @@ +export const tssHydrate = 'tss-hydrate' diff --git a/packages/start-plugin-core/src/rsbuild/normalized-client-build.ts b/packages/start-plugin-core/src/rsbuild/normalized-client-build.ts index b349b68040d..60244fdca98 100644 --- a/packages/start-plugin-core/src/rsbuild/normalized-client-build.ts +++ b/packages/start-plugin-core/src/rsbuild/normalized-client-build.ts @@ -1,4 +1,5 @@ import { tsrSplit } from '@tanstack/router-plugin' +import { tssHydrate } from '../hydration-constants' import { getCssAssetSource } from '../start-manifest-plugin/inlineCss' import { RSBUILD_ENVIRONMENT_NAMES } from './planning' import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core' @@ -57,6 +58,39 @@ function getRouteFilePathsFromModules( return routeFilePaths ?? [] } +function getHydrationIdsFromModules( + modules: Array, +): Array { + let hydrationIds: Array | undefined + let seen: Set | undefined + + for (const mod of modules) { + const identifier = mod.identifier() + const lastBangIndex = identifier.lastIndexOf('!') + const resourcePart = + lastBangIndex >= 0 ? identifier.slice(lastBangIndex + 1) : identifier + + const queryIndex = resourcePart.indexOf('?') + if (queryIndex < 0) continue + + const query = resourcePart.slice(queryIndex + 1) + if (!query.includes(tssHydrate)) continue + + const hydrationId = new URLSearchParams(query).get(tssHydrate) + if (!hydrationId || seen?.has(hydrationId)) continue + + if (!hydrationIds || !seen) { + hydrationIds = [] + seen = new Set() + } + + hydrationIds.push(hydrationId) + seen.add(hydrationId) + } + + return hydrationIds ?? [] +} + /** * Returns true for Rspack/webpack HMR runtime chunks that should never be * surfaced to the Start manifest. These files are emitted on every rebuild @@ -186,6 +220,7 @@ export function normalizeRspackClientBuild( for (const chunk of compilation.chunks) { const modules = compilation.chunkGraph.getChunkModules(chunk) const routeFilePaths = getRouteFilePathsFromModules(modules) + const hydrationIds = getHydrationIdsFromModules(modules) const cssFiles: Array = [] const seenCssFiles = new Set() @@ -242,6 +277,7 @@ export function normalizeRspackClientBuild( dynamicImports, css: [], routeFilePaths, + hydrationIds, } chunksByFileName.set(file, normalizedChunk) diff --git a/packages/start-plugin-core/src/rsbuild/start-compiler-host.ts b/packages/start-plugin-core/src/rsbuild/start-compiler-host.ts index 066d5b28676..d0ef77e34bb 100644 --- a/packages/start-plugin-core/src/rsbuild/start-compiler-host.ts +++ b/packages/start-plugin-core/src/rsbuild/start-compiler-host.ts @@ -5,15 +5,19 @@ import { detectKindsInCode } from '../start-compiler/compiler' import { getTransformCodeFilterForEnv } from '../start-compiler/config' import { createStartCompiler, + loadCompilerVirtualModule, matchesCodeFilters, mergeServerFnsById, } from '../start-compiler/host' import { cleanId } from '../start-compiler/utils' +import { createHydrateCompilerPlugin } from '../hydrate-when-transform' import { RSBUILD_ENVIRONMENT_NAMES } from './planning' import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core' import type { CompileStartFrameworkOptions, StartCompilerImportTransform, + StartCompilerPlugin, + StartCompilerTransformResult, } from '../types' import type { DevServerFnModuleSpecifierEncoder, @@ -27,7 +31,7 @@ type RsbuildTransformContext = Parameters< type RsbuildInputFileSystem = NonNullable /** - * Rsbuild dev server fn ref strategy: uses file:// URLs for absolute paths. + * Rsbuild dev server fn ref when: uses file:// URLs for absolute paths. * These are directly importable by Node's ESM VM runner without any bundler * path conventions (unlike Vite's /@id/ prefix). */ @@ -40,6 +44,7 @@ export interface StartCompilerHostOptions { providerEnvName: string generateFunctionId?: GenerateFunctionIdFnOptional compilerTransforms?: Array | undefined + compilerPlugins?: Array | undefined serverFnProviderModuleDirectives?: ReadonlyArray | undefined serverFnsById?: Record onServerFnsByIdChange?: () => void @@ -69,6 +74,10 @@ export function registerStartCompilerTransforms( mergeServerFnsById(serverFnsById, d) opts.onServerFnsByIdChange?.() } + const compilerPlugins = [ + createHydrateCompilerPlugin(), + ...(opts.compilerPlugins ?? []), + ] const isDev = api.context.action === 'dev' const mode = isDev ? 'dev' : 'build' @@ -83,9 +92,12 @@ export function registerStartCompilerTransforms( // Pre-compute code filter patterns per environment type const codeFilters: Record<'client' | 'server', Array> = { - client: getTransformCodeFilterForEnv('client'), + client: getTransformCodeFilterForEnv('client', { + compilerPlugins, + }), server: getTransformCodeFilterForEnv('server', { compilerTransforms: opts.compilerTransforms, + compilerPlugins, }), } @@ -109,17 +121,36 @@ export function registerStartCompilerTransforms( async (ctx: RsbuildTransformContext) => { return transformContextStorage.run(ctx, async () => { const code = ctx.code + let nextCode = code + let previousResult: { + code: string + map: StartCompilerTransformResult['map'] + } | null = null const id = ctx.resourcePath + (ctx.resourceQuery || '') + const root = getRoot() + + const virtualResult = loadCompilerVirtualModule(compilerPlugins, { + code, + id, + root, + env: env.type, + envName: env.name, + }) + if (virtualResult) { + nextCode = virtualResult.code + previousResult = { + code: virtualResult.code, + map: virtualResult.map ?? null, + } + } // Quick string-level check: does this file contain any patterns for this env? - if (!matchesCodeFilters(code, envCodeFilters)) { - return code + if (!matchesCodeFilters(nextCode, envCodeFilters)) { + return previousResult ?? nextCode } let compiler = compilers.get(env.name) if (!compiler) { - const root = getRoot() - compiler = createStartCompiler({ env: env.type, envName: env.name, @@ -129,6 +160,7 @@ export function registerStartCompilerTransforms( providerEnvName: opts.providerEnvName, generateFunctionId: opts.generateFunctionId, compilerTransforms, + compilerPlugins, serverFnProviderModuleDirectives, onServerFnsById, getKnownServerFns: () => serverFnsById, @@ -197,19 +229,23 @@ export function registerStartCompilerTransforms( compilers.set(env.name, compiler) } - const detectedKinds = detectKindsInCode(code, env.type, { + const detectedKinds = detectKindsInCode(nextCode, env.type, { compilerTransforms, }) - const result = await compiler.compile({ id, code, detectedKinds }) + const result = await compiler.compile({ + id, + code: nextCode, + detectedKinds, + }) - if (!result) { - return code + if (result) { + return { + code: result.code, + map: result.map ?? null, + } } - return { - code: result.code, - map: result.map ?? null, - } + return previousResult ?? nextCode }) }, ) diff --git a/packages/start-plugin-core/src/start-compiler/compiler.ts b/packages/start-plugin-core/src/start-compiler/compiler.ts index 9a950c65e2d..144f0dd61e1 100644 --- a/packages/start-plugin-core/src/start-compiler/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler/compiler.ts @@ -3,6 +3,7 @@ import crypto from 'node:crypto' import * as t from '@babel/types' import { deadCodeElimination, + extractModuleInfoFromAst, findReferencedIdentifiers, generateFromAst, parseAst, @@ -21,26 +22,24 @@ import type { RewriteCandidate, ServerFn, } from './types' +import type { ModuleInfoBinding } from '@tanstack/router-utils' import type { CompileStartFrameworkOptions, StartCompilerEnvironment, StartCompilerImportTransform, + StartCompilerPlugin, + StartCompilerTransformResult, } from '../types' -type Binding = - | { - type: 'import' - source: string - importedName: string - resolvedKind?: Kind - } - | { - type: 'var' - init: t.Expression | null - resolvedKind?: Kind - } +type Binding = ModuleInfoBinding & { + resolvedKind?: Kind +} type Kind = 'None' | `Root` | `Builder` | LookupKind +type ParsedAst = ReturnType +type StartCompilerAstPlugin = StartCompilerPlugin & { + transformAst: NonNullable +} export type BuiltInLookupKind = | 'ServerFn' @@ -84,11 +83,28 @@ export function isCompilerTransformEnabledForEnv( transform: StartCompilerImportTransform, env: StartCompilerEnvironment, ): boolean { - if (!transform.environment) return true - if (Array.isArray(transform.environment)) { - return transform.environment.includes(env) + return isStartCompilerEnvironmentEnabled(transform.environment, env) +} + +export function isStartCompilerPluginEnabledForEnv( + plugin: StartCompilerPlugin, + env: StartCompilerEnvironment, +): boolean { + return isStartCompilerEnvironmentEnabled(plugin.environment, env) +} + +function isStartCompilerEnvironmentEnabled( + environment: + | StartCompilerEnvironment + | Array + | undefined, + env: StartCompilerEnvironment, +): boolean { + if (!environment) return true + if (Array.isArray(environment)) { + return environment.includes(env) } - return transform.environment === env + return environment === env } const BuiltInLookupSetup: Record< @@ -217,13 +233,16 @@ export function detectKindsInCode( const validForEnv = getLookupKindsForEnv(env, opts) for (const kind of AllBuiltInLookupKinds) { - if (validForEnv.has(kind) && KindDetectionPatterns[kind].test(code)) { + const pattern = KindDetectionPatterns[kind] + pattern.lastIndex = 0 + if (validForEnv.has(kind) && pattern.test(code)) { detected.add(kind) } } for (const transform of opts?.compilerTransforms ?? []) { if (!isCompilerTransformEnabledForEnv(transform, env)) continue + transform.detect.lastIndex = 0 if (transform.detect.test(code)) { detected.add(getExternalLookupKind(transform)) } @@ -456,6 +475,7 @@ export class StartCompiler { StartCompilerImportTransform >() private externalLookupSetup = new Map() + private compilerPlugins: Array private externalDirectCallKindsBySource = new Map< string, Map @@ -509,6 +529,7 @@ export class StartCompiler { */ onServerFnsById?: (d: Record) => void compilerTransforms?: Array | undefined + compilerPlugins?: Array | undefined serverFnProviderModuleDirectives?: ReadonlyArray | undefined /** * Returns the currently known server functions from previous builds. @@ -519,6 +540,10 @@ export class StartCompiler { }, ) { this.validLookupKinds = options.lookupKinds + this.compilerPlugins = (options.compilerPlugins ?? []).filter((plugin) => + isStartCompilerPluginEnabledForEnv(plugin, options.env), + ) + for (const transform of options.compilerTransforms ?? []) { const kind = getExternalLookupKind(transform) if (!this.validLookupKinds.has(kind)) continue @@ -764,100 +789,13 @@ export class StartCompiler { ast: ReturnType, id: string, ): ModuleInfo { - const bindings = new Map() - const exports = new Map() - const reExportAllSources: Array = [] - - // we are only interested in top-level bindings, hence we don't traverse the AST - // instead we only iterate over the program body - for (const node of ast.program.body) { - if (t.isImportDeclaration(node)) { - const source = node.source.value - for (const s of node.specifiers) { - if (t.isImportSpecifier(s)) { - const importedName = t.isIdentifier(s.imported) - ? s.imported.name - : s.imported.value - bindings.set(s.local.name, { type: 'import', source, importedName }) - } else if (t.isImportDefaultSpecifier(s)) { - bindings.set(s.local.name, { - type: 'import', - source, - importedName: 'default', - }) - } else if (t.isImportNamespaceSpecifier(s)) { - bindings.set(s.local.name, { - type: 'import', - source, - importedName: '*', - }) - } - } - } else if (t.isVariableDeclaration(node)) { - for (const decl of node.declarations) { - if (t.isIdentifier(decl.id)) { - bindings.set(decl.id.name, { - type: 'var', - init: decl.init ?? null, - }) - } - } - } else if (t.isExportNamedDeclaration(node)) { - // export const foo = ... - if (node.declaration) { - if (t.isVariableDeclaration(node.declaration)) { - for (const d of node.declaration.declarations) { - if (t.isIdentifier(d.id)) { - exports.set(d.id.name, d.id.name) - bindings.set(d.id.name, { type: 'var', init: d.init ?? null }) - } - } - } - } - for (const sp of node.specifiers) { - if (t.isExportNamespaceSpecifier(sp)) { - exports.set(sp.exported.name, sp.exported.name) - } - // export { local as exported } - else if (t.isExportSpecifier(sp)) { - const local = sp.local.name - const exported = t.isIdentifier(sp.exported) - ? sp.exported.name - : sp.exported.value - exports.set(exported, local) - - // When re-exporting from another module (export { foo } from './module'), - // create an import binding so the server function can be resolved - if (node.source) { - bindings.set(local, { - type: 'import', - source: node.source.value, - importedName: local, - }) - } - } - } - } else if (t.isExportDefaultDeclaration(node)) { - const d = node.declaration - if (t.isIdentifier(d)) { - exports.set('default', d.name) - } else { - const synth = '__default_export__' - bindings.set(synth, { type: 'var', init: d as t.Expression }) - exports.set('default', synth) - } - } else if (t.isExportAllDeclaration(node)) { - // Handle `export * from './module'` syntax - // Track the source so we can look up exports from it when needed - reExportAllSources.push(node.source.value) - } - } + const extracted = extractModuleInfoFromAst(ast) const info: ModuleInfo = { id, - bindings, - exports, - reExportAllSources, + bindings: new Map(extracted.bindings), + exports: extracted.exports, + reExportAllSources: extracted.reExportAllSources, } this.moduleCache.set(id, info) return info @@ -878,13 +816,30 @@ export class StartCompiler { } public invalidateModule(id: string) { - const normalizedId = cleanId(id) - let hasCachedModule = false + return this.invalidateModules([id]).size > 0 + } + + public invalidateModules(ids: Iterable): Set { + const normalizedIds = new Set() + + for (const id of ids) { + normalizedIds.add(cleanId(id)) + + for (const plugin of this.compilerPlugins) { + plugin.invalidateModule?.({ id, envName: this.options.envName }) + } + } + + const deletedModuleIds = new Set() + if (normalizedIds.size === 0) { + return deletedModuleIds + } for (const moduleId of Array.from(this.moduleCache.keys())) { - if (cleanId(moduleId) === normalizedId) { + const normalizedModuleId = cleanId(moduleId) + if (normalizedIds.has(normalizedModuleId)) { this.moduleCache.delete(moduleId) - hasCachedModule = true + deletedModuleIds.add(normalizedModuleId) } } @@ -904,14 +859,20 @@ export class StartCompiler { this.resolveIdCache.clear() this.exportResolutionCache.clear() - return hasCachedModule + return deletedModuleIds } - public async getTransitiveImporters(id: string): Promise> { + public async getTransitiveImporters( + ids: string | Iterable, + ): Promise> { const discoveredImporters = new Set() - const pendingTargets = [cleanId(id)] + const pendingTargets = + typeof ids === 'string' + ? [cleanId(ids)] + : Array.from(ids, (id) => cleanId(id)) const visitedTargets = new Set() const resolveCache = new Map>() + const importersByTarget = new Map>() const resolveSource = (source: string, importer: string) => { const cacheKey = `${importer}::${source}` @@ -925,49 +886,51 @@ export class StartCompiler { return resolved } - while (pendingTargets.length > 0) { - const targetId = pendingTargets.pop()! - - if (visitedTargets.has(targetId)) { - continue - } + await Promise.all( + Array.from(this.moduleCache.values()).map(async (moduleInfo) => { + if (this.knownRootImports.has(moduleInfo.id)) { + return + } - visitedTargets.add(targetId) + const moduleId = cleanId(moduleInfo.id) + const importSources = new Set(moduleInfo.reExportAllSources) - const importerIds = await Promise.all( - Array.from(this.moduleCache.values()).map(async (moduleInfo) => { - if (this.knownRootImports.has(moduleInfo.id)) { - return null + for (const binding of moduleInfo.bindings.values()) { + if (binding.type === 'import') { + importSources.add(binding.source) } + } - const moduleId = cleanId(moduleInfo.id) - - if (moduleId === targetId) { - return null - } + await Promise.all( + Array.from(importSources, async (source) => { + const resolved = await resolveSource(source, moduleInfo.id) + if (!resolved) return - const importSources = new Set(moduleInfo.reExportAllSources) + const targetId = cleanId(resolved) + if (targetId === moduleId) return - for (const binding of moduleInfo.bindings.values()) { - if (binding.type === 'import') { - importSources.add(binding.source) + let importers = importersByTarget.get(targetId) + if (!importers) { + importers = new Set() + importersByTarget.set(targetId, importers) } - } + importers.add(moduleId) + }), + ) + }), + ) - for (const source of importSources) { - const resolved = await resolveSource(source, moduleInfo.id) + while (pendingTargets.length > 0) { + const targetId = pendingTargets.pop()! - if (resolved && cleanId(resolved) === targetId) { - return moduleId - } - } + if (visitedTargets.has(targetId)) { + continue + } - return null - }), - ) + visitedTargets.add(targetId) - for (const importerId of importerIds) { - if (!importerId || discoveredImporters.has(importerId)) { + for (const importerId of importersByTarget.get(targetId) ?? []) { + if (discoveredImporters.has(importerId)) { continue } @@ -1000,408 +963,435 @@ export class StartCompiler { ? new Set([...detectedKinds].filter((k) => this.validLookupKinds.has(k))) : this.validLookupKinds - // Early exit if no kinds to process - if (fileKinds.size === 0) { - return null - } - - const hasExternalKinds = hasExternalLookupKinds(fileKinds) - const checkDirectCalls = - hasBuiltInDirectCallKinds(fileKinds) || - (fileKinds.has('ServerFn') && - !hasExternalKinds && - hasBuiltInDirectCallKinds(this.validLookupKinds)) - // Optimization: ServerFn is always a top-level declaration (must be assigned to a variable). - // If the file only has ServerFn, we can skip full AST traversal and only visit - // the specific top-level declarations that have candidates. - const canUseFastPath = areAllKindsTopLevelOnly(fileKinds) - + const astTransformPlugins = this.getAstTransformPluginsForCode(code) // Always parse and extract module info upfront. // This ensures the module is cached for import resolution even if no candidates are found. - const { ast } = this.ingestModule({ code, id, parserFilename }) - - // Single-pass traversal to: - // 1. Collect candidate paths (only candidates, not all CallExpressions) - // 2. Build a map for looking up paths of nested calls in method chains - const candidatePaths: Array = [] - // Map for nested chain lookup - only populated for CallExpressions that are - // part of a method chain (callee.object is a CallExpression) - const chainCallPaths = new Map< - t.CallExpression, - babel.NodePath - >() - - // JSX candidates (e.g., ) - const jsxCandidatePaths: Array> = [] - const checkJSX = needsJSXDetection(fileKinds, this.externalLookupSetup) - // Get module info that was just cached by ingestModule - const moduleInfo = this.moduleCache.get(id)! - const externalDirectCallCandidates = this.getExternalDirectCallCandidates( - fileKinds, - moduleInfo, - ) - const checkExternalDirectCalls = hasExternalDirectCallCandidates( - externalDirectCallCandidates, - ) + const ast = this.ingestModule({ code, id, parserFilename }).ast + let astHasChanges = false - if (canUseFastPath) { - // Fast path: only visit top-level statements that have potential candidates + builtInTransforms: { + // Early exit if no built-in or import transforms need this file. + if (fileKinds.size === 0) { + break builtInTransforms + } + + const hasExternalKinds = hasExternalLookupKinds(fileKinds) + const checkDirectCalls = + hasBuiltInDirectCallKinds(fileKinds) || + (fileKinds.has('ServerFn') && + !hasExternalKinds && + hasBuiltInDirectCallKinds(this.validLookupKinds)) + // Optimization: ServerFn is always a top-level declaration (must be assigned to a variable). + // If the file only has ServerFn, we can skip full AST traversal and only visit + // the specific top-level declarations that have candidates. + const canUseFastPath = areAllKindsTopLevelOnly(fileKinds) + + // Single-pass traversal to: + // 1. Collect candidate paths (only candidates, not all CallExpressions) + // 2. Build a map for looking up paths of nested calls in method chains + const candidatePaths: Array = [] + // Map for nested chain lookup - only populated for CallExpressions that are + // part of a method chain (callee.object is a CallExpression) + const chainCallPaths = new Map< + t.CallExpression, + babel.NodePath + >() + + // JSX candidates (e.g., ) + const jsxCandidatePaths: Array> = [] + const checkJSX = needsJSXDetection(fileKinds, this.externalLookupSetup) + // Get module info that was just cached by ingestModule + const moduleInfo = this.moduleCache.get(id)! + const externalDirectCallCandidates = this.getExternalDirectCallCandidates( + fileKinds, + moduleInfo, + ) + const checkExternalDirectCalls = hasExternalDirectCallCandidates( + externalDirectCallCandidates, + ) - // Collect indices of top-level statements that contain candidates - const candidateIndices: Array = [] - for (let i = 0; i < ast.program.body.length; i++) { - const node = ast.program.body[i]! - let declarations: Array | undefined + if (canUseFastPath) { + // Fast path: only visit top-level statements that have potential candidates - if (t.isVariableDeclaration(node)) { - declarations = node.declarations - } else if (t.isExportNamedDeclaration(node) && node.declaration) { - if (t.isVariableDeclaration(node.declaration)) { - declarations = node.declaration.declarations - } - } + // Collect indices of top-level statements that contain candidates + const candidateIndices: Array = [] + for (let i = 0; i < ast.program.body.length; i++) { + const node = ast.program.body[i]! + let declarations: Array | undefined - if (declarations) { - for (const decl of declarations) { - if (decl.init && t.isCallExpression(decl.init)) { - if ( - isMethodChainCandidate(decl.init, fileKinds) || - (checkDirectCalls && - isTopLevelDirectCallCandidateNode(decl.init)) - ) { - candidateIndices.push(i) - break // Only need to mark this statement once - } + if (t.isVariableDeclaration(node)) { + declarations = node.declarations + } else if (t.isExportNamedDeclaration(node) && node.declaration) { + if (t.isVariableDeclaration(node.declaration)) { + declarations = node.declaration.declarations } } - } - } - - // Early exit: no potential candidates found at top level - if (candidateIndices.length === 0) { - return null - } - // Targeted traversal: only visit the specific statements that have candidates - // This is much faster than traversing the entire AST - babel.traverse(ast, { - Program(programPath) { - const bodyPaths = programPath.get('body') - for (const idx of candidateIndices) { - const stmtPath = bodyPaths[idx] - if (!stmtPath) continue - - // Traverse only this statement's subtree - stmtPath.traverse({ - CallExpression(path) { - const node = path.node - const parent = path.parent - - // Check if this call is part of a larger chain (inner call) + if (declarations) { + for (const decl of declarations) { + if (decl.init && t.isCallExpression(decl.init)) { if ( - t.isMemberExpression(parent) && - t.isCallExpression(path.parentPath.parent) + isMethodChainCandidate(decl.init, fileKinds) || + (checkDirectCalls && + isTopLevelDirectCallCandidateNode(decl.init)) ) { - chainCallPaths.set(node, path) - return + candidateIndices.push(i) + break // Only need to mark this statement once } + } + } + } + } - // Method chain pattern - if (isMethodChainCandidate(node, fileKinds)) { - candidatePaths.push({ path }) - return - } + // Early exit: no potential candidates found at top level + if (candidateIndices.length === 0) { + break builtInTransforms + } - if (checkExternalDirectCalls) { - const kind = getExternalDirectCallCandidateKind( - path, - externalDirectCallCandidates, - ) - if (kind) { - candidatePaths.push({ path, kind }) + // Targeted traversal: only visit the specific statements that have candidates + // This is much faster than traversing the entire AST + babel.traverse(ast, { + Program(programPath) { + const bodyPaths = programPath.get('body') + for (const idx of candidateIndices) { + const stmtPath = bodyPaths[idx] + if (!stmtPath) continue + + // Traverse only this statement's subtree + stmtPath.traverse({ + CallExpression(path) { + const node = path.node + const parent = path.parent + + // Check if this call is part of a larger chain (inner call) + if ( + t.isMemberExpression(parent) && + t.isCallExpression(path.parentPath.parent) + ) { + chainCallPaths.set(node, path) return } - } - if (isTopLevelDirectCallCandidate(path)) { - candidatePaths.push({ path }) - } - }, - }) - } - // Stop traversal after processing Program - programPath.stop() - }, - }) - } else { - // Normal path: full traversal for non-fast-path kinds - babel.traverse(ast, { - CallExpression: (path) => { - const node = path.node - const parent = path.parent - - // Check if this call is part of a larger chain (inner call) - // If so, store it for method chain lookup but don't treat as candidate - if ( - t.isMemberExpression(parent) && - t.isCallExpression(path.parentPath.parent) - ) { - // This is an inner call in a chain - store for later lookup - chainCallPaths.set(node, path) - return - } + // Method chain pattern + if (isMethodChainCandidate(node, fileKinds)) { + candidatePaths.push({ path }) + return + } - // Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.) - if (isMethodChainCandidate(node, fileKinds)) { - candidatePaths.push({ path }) - return - } + if (checkExternalDirectCalls) { + const kind = getExternalDirectCallCandidateKind( + path, + externalDirectCallCandidates, + ) + if (kind) { + candidatePaths.push({ path, kind }) + return + } + } - // External direct-call transforms are import-bound. Direct imports - // already identify the transform kind, so skip async import tracing. - if (checkExternalDirectCalls) { - const kind = getExternalDirectCallCandidateKind( - path, - externalDirectCallCandidates, - ) - if (kind) { - candidatePaths.push({ path, kind }) + if (isTopLevelDirectCallCandidate(path)) { + candidatePaths.push({ path }) + } + }, + }) + } + // Stop traversal after processing Program + programPath.stop() + }, + }) + } else { + // Normal path: full traversal for non-fast-path kinds + babel.traverse(ast, { + CallExpression: (path) => { + const node = path.node + const parent = path.parent + + // Check if this call is part of a larger chain (inner call) + // If so, store it for method chain lookup but don't treat as candidate + if ( + t.isMemberExpression(parent) && + t.isCallExpression(path.parentPath.parent) + ) { + // This is an inner call in a chain - store for later lookup + chainCallPaths.set(node, path) return } - } - if (checkDirectCalls && isTopLevelDirectCallCandidate(path)) { - candidatePaths.push({ path }) - return - } + // Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.) + if (isMethodChainCandidate(node, fileKinds)) { + candidatePaths.push({ path }) + return + } - // Pattern 2: Direct call pattern - if (checkDirectCalls) { - if ( - isNestedDirectCallCandidate( - node, - fileKinds, - this.externalLookupSetup, + // External direct-call transforms are import-bound. Direct imports + // already identify the transform kind, so skip async import tracing. + if (checkExternalDirectCalls) { + const kind = getExternalDirectCallCandidateKind( + path, + externalDirectCallCandidates, ) - ) { + if (kind) { + candidatePaths.push({ path, kind }) + return + } + } + + if (checkDirectCalls && isTopLevelDirectCallCandidate(path)) { candidatePaths.push({ path }) return } - } - }, - // Pattern 3: JSX element pattern (e.g., ) - // Collect JSX elements where the component is imported from a known package - // and resolves to a JSX kind (e.g., ClientOnly from @tanstack/react-router) - JSXElement: (path) => { - if (!checkJSX) return - - const openingElement = path.node.openingElement - const nameNode = openingElement.name - // Only handle simple identifier names (not namespaced or member expressions) - if (!t.isJSXIdentifier(nameNode)) return + // Pattern 2: Direct call pattern + if (checkDirectCalls) { + if ( + isNestedDirectCallCandidate( + node, + fileKinds, + this.externalLookupSetup, + ) + ) { + candidatePaths.push({ path }) + return + } + } + }, + // Pattern 3: JSX element pattern (e.g., ) + // Collect JSX elements where the component is imported from a known package + // and resolves to a JSX kind (e.g., ClientOnly from @tanstack/react-router) + JSXElement: (path) => { + if (!checkJSX) return - const componentName = nameNode.name - const binding = moduleInfo.bindings.get(componentName) + const openingElement = path.node.openingElement + const nameNode = openingElement.name - // Must be an import binding from a known package - if (!binding || binding.type !== 'import') return + // Only handle simple identifier names (not namespaced or member expressions) + if (!t.isJSXIdentifier(nameNode)) return - // Verify the import source is a known TanStack router package - const knownExports = this.knownRootImports.get(binding.source) - if (!knownExports) return + const componentName = nameNode.name + const binding = moduleInfo.bindings.get(componentName) - // Verify the imported name resolves to a JSX kind (e.g., ClientOnlyJSX) - const kind = knownExports.get(binding.importedName) - if (kind !== 'ClientOnlyJSX') return + // Must be an import binding from a known package + if (!binding || binding.type !== 'import') return - jsxCandidatePaths.push(path) - }, - }) - } + // Verify the import source is a known TanStack router package + const knownExports = this.knownRootImports.get(binding.source) + if (!knownExports) return - if (candidatePaths.length === 0 && jsxCandidatePaths.length === 0) { - return null - } + // Verify the imported name resolves to a JSX kind (e.g., ClientOnlyJSX) + const kind = knownExports.get(binding.importedName) + if (kind !== 'ClientOnlyJSX') return - // Resolve only candidates whose import scan did not already prove the kind. - const resolvedCandidates: Array<{ - path: babel.NodePath - kind: Kind - }> = [] - const unresolvedCandidates: Array = [] - - for (const candidate of candidatePaths) { - if (candidate.kind) { - resolvedCandidates.push({ - path: candidate.path, - kind: candidate.kind, + jsxCandidatePaths.push(path) + }, }) - } else { - unresolvedCandidates.push(candidate) } - } - if (unresolvedCandidates.length > 0) { - resolvedCandidates.push( - ...(await Promise.all( - unresolvedCandidates.map(async (candidate) => ({ - path: candidate.path, - kind: await this.resolveExprKind(candidate.path.node, id), - })), - )), - ) - } + if (candidatePaths.length === 0 && jsxCandidatePaths.length === 0) { + break builtInTransforms + } - // Filter to valid candidates - const validCandidates = resolvedCandidates.filter(({ path, kind }) => { - if ( - !this.validLookupKinds.has(kind as Exclude) - ) { - return false + // Resolve only candidates whose import scan did not already prove the kind. + const resolvedCandidates: Array<{ + path: babel.NodePath + kind: Kind + }> = [] + const unresolvedCandidates: Array = [] + + for (const candidate of candidatePaths) { + if (candidate.kind) { + resolvedCandidates.push({ + path: candidate.path, + kind: candidate.kind, + }) + } else { + unresolvedCandidates.push(candidate) + } } - if ( - isLookupKind(kind) && - kind !== 'ClientOnlyJSX' && - !isMethodChainCandidate(path.node, fileKinds) - ) { - return isDirectCallCandidateForKind(kind, this.externalLookupSetup) + if (unresolvedCandidates.length > 0) { + resolvedCandidates.push( + ...(await Promise.all( + unresolvedCandidates.map(async (candidate) => ({ + path: candidate.path, + kind: await this.resolveExprKind(candidate.path.node, id), + })), + )), + ) } - return true - }) as Array<{ - path: babel.NodePath - kind: Exclude - }> + // Filter to valid candidates + const validCandidates = resolvedCandidates.filter(({ path, kind }) => { + if ( + !this.validLookupKinds.has( + kind as Exclude, + ) + ) { + return false + } - if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) { - return null - } + if ( + isLookupKind(kind) && + kind !== 'ClientOnlyJSX' && + !isMethodChainCandidate(path.node, fileKinds) + ) { + return isDirectCallCandidateForKind(kind, this.externalLookupSetup) + } - // Process valid candidates to collect method chains - const pathsToRewrite: Array<{ - path: babel.NodePath - kind: Exclude - methodChain: MethodChainPaths - }> = [] - - for (const { path, kind } of validCandidates) { - const node = path.node - - // Collect method chain paths by walking DOWN from root through the chain - const methodChain: MethodChainPaths = { - middleware: null, - inputValidator: null, - handler: null, - server: null, - client: null, - } + return true + }) as Array<{ + path: babel.NodePath + kind: Exclude + }> - // Walk down the call chain using nodes, look up paths from map - let currentNode: t.CallExpression = node - let currentPath: babel.NodePath = path + if (validCandidates.length === 0 && jsxCandidatePaths.length === 0) { + break builtInTransforms + } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (true) { - const callee = currentNode.callee - if (!t.isMemberExpression(callee)) { - break + // Process valid candidates to collect method chains + const pathsToRewrite: Array<{ + path: babel.NodePath + kind: Exclude + methodChain: MethodChainPaths + }> = [] + + for (const { path, kind } of validCandidates) { + const node = path.node + + // Collect method chain paths by walking DOWN from root through the chain + const methodChain: MethodChainPaths = { + middleware: null, + inputValidator: null, + handler: null, + server: null, + client: null, } - // Record method chain path if it's a known method - if (t.isIdentifier(callee.property)) { - const name = callee.property.name as keyof MethodChainPaths - if (name in methodChain) { - // Get first argument path - const args = currentPath.get('arguments') - const firstArgPath = - Array.isArray(args) && args.length > 0 ? (args[0] ?? null) : null - methodChain[name] = { - callPath: currentPath, - firstArgPath, + // Walk down the call chain using nodes, look up paths from map + let currentNode: t.CallExpression = node + let currentPath: babel.NodePath = path + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + const callee = currentNode.callee + if (!t.isMemberExpression(callee)) { + break + } + + // Record method chain path if it's a known method + if (t.isIdentifier(callee.property)) { + const name = callee.property.name as keyof MethodChainPaths + if (name in methodChain) { + // Get first argument path + const args = currentPath.get('arguments') + const firstArgPath = + Array.isArray(args) && args.length > 0 + ? (args[0] ?? null) + : null + methodChain[name] = { + callPath: currentPath, + firstArgPath, + } } } - } - // Move to the inner call (the object of the member expression) - if (!t.isCallExpression(callee.object)) { - break - } - currentNode = callee.object - // Look up path from chain map, or use candidate path if not found - const nextPath = chainCallPaths.get(currentNode) - if (!nextPath) { - break + // Move to the inner call (the object of the member expression) + if (!t.isCallExpression(callee.object)) { + break + } + currentNode = callee.object + // Look up path from chain map, or use candidate path if not found + const nextPath = chainCallPaths.get(currentNode) + if (!nextPath) { + break + } + currentPath = nextPath } - currentPath = nextPath + + pathsToRewrite.push({ path, kind, methodChain }) } - pathsToRewrite.push({ path, kind, methodChain }) - } + const refIdents = findReferencedIdentifiers(ast) - const refIdents = findReferencedIdentifiers(ast) + const context: CompilationContext = { + ast, + id, + code, + env: this.options.env, + envName: this.options.envName, + mode: this.mode, + root: this.options.root, + framework: this.options.framework, + providerEnvName: this.options.providerEnvName, + types: t, + parseExpression: (expressionCode) => + babel.template.expression(expressionCode, { + placeholderPattern: false, + })() as t.Expression, + + generateFunctionId: (opts) => this.generateFunctionId(opts), + getKnownServerFns: this.options.getKnownServerFns, + serverFnProviderModuleDirectives: + this.options.serverFnProviderModuleDirectives, + onServerFnsById: this.options.onServerFnsById, + } - const context: CompilationContext = { - ast, - id, - code, - env: this.options.env, - envName: this.options.envName, - mode: this.mode, - root: this.options.root, - framework: this.options.framework, - providerEnvName: this.options.providerEnvName, - types: t, - parseExpression: (expressionCode) => - babel.template.expression(expressionCode, { - placeholderPattern: false, - })() as t.Expression, - - generateFunctionId: (opts) => this.generateFunctionId(opts), - getKnownServerFns: this.options.getKnownServerFns, - serverFnProviderModuleDirectives: - this.options.serverFnProviderModuleDirectives, - onServerFnsById: this.options.onServerFnsById, - } + // Group candidates by kind for batch processing + const candidatesByKind = new Map< + Exclude, + Array + >() + + for (const { path: candidatePath, kind, methodChain } of pathsToRewrite) { + const candidate: RewriteCandidate = { path: candidatePath, methodChain } + const existing = candidatesByKind.get(kind) + if (existing) { + existing.push(candidate) + } else { + candidatesByKind.set(kind, [candidate]) + } + } - // Group candidates by kind for batch processing - const candidatesByKind = new Map< - Exclude, - Array - >() + // External transforms run before built-ins by default so they can augment + // user handlers before server function extraction clones provider bodies. + this.runExternalTransforms('pre', candidatesByKind, context) - for (const { path: candidatePath, kind, methodChain } of pathsToRewrite) { - const candidate: RewriteCandidate = { path: candidatePath, methodChain } - const existing = candidatesByKind.get(kind) - if (existing) { - existing.push(candidate) - } else { - candidatesByKind.set(kind, [candidate]) + for (const kind of BuiltInKindHandlerOrder) { + const candidates = candidatesByKind.get(kind) + if (!candidates) continue + const handler = BuiltInKindHandlers[kind] + handler(candidates, context, kind) } - } - // External transforms run before built-ins by default so they can augment - // user handlers before server function extraction clones provider bodies. - this.runExternalTransforms('pre', candidatesByKind, context) + this.runExternalTransforms('post', candidatesByKind, context) - for (const kind of BuiltInKindHandlerOrder) { - const candidates = candidatesByKind.get(kind) - if (!candidates) continue - const handler = BuiltInKindHandlers[kind] - handler(candidates, context, kind) - } + // Handle JSX candidates (e.g., ) + // Validation was already done during traversal - just call the handler + for (const jsxPath of jsxCandidatePaths) { + handleClientOnlyJSX(jsxPath, { env: 'server' }) + } - this.runExternalTransforms('post', candidatesByKind, context) + deadCodeElimination(ast, refIdents) + astHasChanges = true + } - // Handle JSX candidates (e.g., ) - // Validation was already done during traversal - just call the handler - for (const jsxPath of jsxCandidatePaths) { - handleClientOnlyJSX(jsxPath, { env: 'server' }) + if (astTransformPlugins.length > 0) { + astHasChanges = + this.runAstTransforms({ + ast, + code, + id, + transforms: astTransformPlugins, + }) || astHasChanges } - deadCodeElimination(ast, refIdents) + return astHasChanges ? this.generateResultFromAst(ast, code, id) : null + } + private generateResultFromAst( + ast: ParsedAst, + sourceCode: string, + id: string, + ): StartCompilerTransformResult { const result = generateFromAst(ast, { sourceMaps: true, sourceFileName: id, @@ -1409,17 +1399,66 @@ export class StartCompiler { }) // @babel/generator does not populate sourcesContent because it only has - // the AST, not the original text. Without this, Vite's composed - // sourcemap omits the original source, causing downstream consumers - // (e.g. import-protection snippet display) to fall back to the shorter - // compiled output and fail to resolve original line numbers. + // the AST, not the original text. Without this, Vite's composed sourcemap + // omits the original source, causing downstream consumers to fall back to + // the compiled output and fail to resolve original line numbers. if (result.map) { - result.map.sourcesContent = [code] + result.map.sourcesContent = [sourceCode] } return result } + private getAstTransformPluginsForCode( + code: string, + ): Array { + return this.compilerPlugins.filter( + (plugin): plugin is StartCompilerAstPlugin => { + if (!plugin.transformAst) return false + if (!plugin.detect) return true + plugin.detect.lastIndex = 0 + return plugin.detect.test(code) + }, + ) + } + + private runAstTransforms({ + ast, + code, + id, + transforms, + }: { + ast: ParsedAst + code: string + id: string + transforms: Array + }): boolean { + let modified = false + + for (const plugin of transforms) { + const context = { + ast, + code, + id, + env: this.options.env, + envName: this.options.envName, + mode: this.mode, + root: this.options.root, + framework: this.options.framework, + providerEnvName: this.options.providerEnvName, + types: t, + parseExpression: (expressionCode: string) => + babel.template.expression(expressionCode, { + placeholderPattern: false, + })() as t.Expression, + } + + modified = plugin.transformAst(context) || modified + } + + return modified + } + private runExternalTransforms( order: 'pre' | 'post', candidatesByKind: Map< diff --git a/packages/start-plugin-core/src/start-compiler/config.ts b/packages/start-plugin-core/src/start-compiler/config.ts index e9d7d95e5e8..387343bad9c 100644 --- a/packages/start-plugin-core/src/start-compiler/config.ts +++ b/packages/start-plugin-core/src/start-compiler/config.ts @@ -3,17 +3,20 @@ import { getExternalLookupKind, getLookupKindsForEnv, isCompilerTransformEnabledForEnv, + isStartCompilerPluginEnabledForEnv, } from './compiler' import type { BuiltInLookupKind, LookupConfig } from './compiler' import type { CompileStartFrameworkOptions, StartCompilerImportTransform, + StartCompilerPlugin, } from '../types' export function getTransformCodeFilterForEnv( env: 'client' | 'server', opts?: { compilerTransforms?: Array | undefined + compilerPlugins?: Array | undefined }, ): Array { const validKinds = getLookupKindsForEnv(env, opts) @@ -33,6 +36,12 @@ export function getTransformCodeFilterForEnv( } } + for (const plugin of opts?.compilerPlugins ?? []) { + if (plugin.detect && isStartCompilerPluginEnabledForEnv(plugin, env)) { + patterns.push(plugin.detect) + } + } + return patterns } diff --git a/packages/start-plugin-core/src/start-compiler/host.ts b/packages/start-plugin-core/src/start-compiler/host.ts index e1da89d7f28..c225347deac 100644 --- a/packages/start-plugin-core/src/start-compiler/host.ts +++ b/packages/start-plugin-core/src/start-compiler/host.ts @@ -3,6 +3,9 @@ import { getLookupConfigurationsForEnv } from './config' import type { CompileStartFrameworkOptions, StartCompilerImportTransform, + StartCompilerPlugin, + StartCompilerTransformResult, + StartCompilerVirtualModuleContext, } from '../types' import type { DevServerFnModuleSpecifierEncoder, @@ -19,6 +22,7 @@ export interface CreateStartCompilerOptions { mode: 'dev' | 'build' generateFunctionId?: GenerateFunctionIdFnOptional compilerTransforms?: Array | undefined + compilerPlugins?: Array | undefined serverFnProviderModuleDirectives?: ReadonlyArray | undefined onServerFnsById?: (d: Record) => void getKnownServerFns: () => Record @@ -48,6 +52,7 @@ export function createStartCompiler( generateFunctionId: options.generateFunctionId, onServerFnsById: options.onServerFnsById, compilerTransforms: options.compilerTransforms, + compilerPlugins: options.compilerPlugins, serverFnProviderModuleDirectives: options.serverFnProviderModuleDirectives, getKnownServerFns: options.getKnownServerFns, devServerFnModuleSpecifierEncoder: options.encodeModuleSpecifierInDev, @@ -81,6 +86,7 @@ export function matchesCodeFilters( filters: ReadonlyArray, ): boolean { for (const pattern of filters) { + pattern.lastIndex = 0 if (pattern.test(code)) { return true } @@ -88,3 +94,43 @@ export function matchesCodeFilters( return false } + +export function createCompilerVirtualModuleIdPattern( + compilerPlugins: ReadonlyArray, +) { + const patterns = compilerPlugins + .map((plugin) => plugin.virtualModuleIdPattern) + .filter((pattern): pattern is RegExp => !!pattern) + + if (patterns.length === 0) { + return undefined + } + + return new RegExp( + patterns.map((pattern) => `(?:${pattern.source})`).join('|'), + ) +} + +export function loadCompilerVirtualModule( + compilerPlugins: ReadonlyArray, + context: StartCompilerVirtualModuleContext, +): StartCompilerTransformResult | null { + for (const compilerPlugin of compilerPlugins) { + const pattern = compilerPlugin.virtualModuleIdPattern + if (!pattern) { + continue + } + + pattern.lastIndex = 0 + if (!pattern.test(context.id)) { + continue + } + + const result = compilerPlugin.loadVirtualModule?.(context) + if (result) { + return result + } + } + + return null +} diff --git a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts index 1c52d124eb3..13733491fdd 100644 --- a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts +++ b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts @@ -462,10 +462,35 @@ export function buildRouteManifestRoutes(options: { getChunkCssAssets, getChunkPreloads: options.assetResolvers.getChunkPreloads, }) + + if (routeId !== rootRouteId) { + mergeReachableHydrationChunkData({ + route: targetRoute, + chunk, + chunksByFileName: options.chunksByFileName, + getChunkCssAssets, + }) + } } } const rootRoute = (routes[rootRouteId] = routes[rootRouteId] || {}) + const rootRouteTreeRoute = options.routeTreeRoutes[rootRouteId] + const rootRouteChunks = rootRouteTreeRoute?.filePath + ? options.routeChunksByFilePath.get(rootRouteTreeRoute.filePath) + : undefined + + if (rootRouteChunks) { + for (const chunk of rootRouteChunks) { + mergeReachableHydrationChunkData({ + route: rootRoute, + chunk, + chunksByFileName: options.chunksByFileName, + getChunkCssAssets, + }) + } + } + mergeRouteChunkData({ route: rootRoute, chunk: options.entryChunk, @@ -495,6 +520,54 @@ export function buildRouteManifestRoutes(options: { return routes } +function mergeReachableHydrationChunkData(options: { + route: RouteTreeRoute + chunk: NormalizedClientChunk + chunksByFileName: ReadonlyMap + getChunkCssAssets: (chunk: NormalizedClientChunk) => Array +}) { + const visitedStaticChunks = new Set() + const mergedHydrationChunks = new Set() + + const mergeHydrationChunk = (chunk: NormalizedClientChunk) => { + if (mergedHydrationChunks.has(chunk.fileName)) return + mergedHydrationChunks.add(chunk.fileName) + + options.route.assets = appendUniqueAssets( + options.route.assets, + options.getChunkCssAssets(chunk), + ) + + for (const dynamicImport of chunk.dynamicImports) { + const dynamicChunk = options.chunksByFileName.get(dynamicImport) + if (dynamicChunk?.hydrationIds.length) { + mergeHydrationChunk(dynamicChunk) + } + } + } + + const visitStaticChunk = (chunk: NormalizedClientChunk) => { + if (visitedStaticChunks.has(chunk.fileName)) return + visitedStaticChunks.add(chunk.fileName) + + for (const importedFileName of chunk.imports) { + const importedChunk = options.chunksByFileName.get(importedFileName) + if (importedChunk) { + visitStaticChunk(importedChunk) + } + } + + for (const dynamicImport of chunk.dynamicImports) { + const dynamicChunk = options.chunksByFileName.get(dynamicImport) + if (dynamicChunk?.hydrationIds.length) { + mergeHydrationChunk(dynamicChunk) + } + } + } + + visitStaticChunk(options.chunk) +} + export { getRouteFilePathsFromModuleIds, normalizeViteClientBuild, diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts index bc76726875d..f0acdbfe35c 100644 --- a/packages/start-plugin-core/src/types.ts +++ b/packages/start-plugin-core/src/types.ts @@ -1,5 +1,6 @@ import type * as babel from '@babel/core' import type * as t from '@babel/types' +import type { GeneratorResult } from '@tanstack/router-utils' import type { TanStackStartOutputConfig } from './schema' export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' @@ -62,6 +63,36 @@ export interface StartCompilerImportTransform { ) => void } +export interface StartCompilerTransformResult { + code: string + map?: GeneratorResult['map'] | null +} + +export interface StartCompilerVirtualModuleContext { + readonly id: string + readonly root: string + readonly env: StartCompilerEnvironment + readonly envName: string + readonly code?: string +} + +export interface StartCompilerPlugin { + name: string + environment?: + | StartCompilerEnvironment + | Array + | undefined + detect?: RegExp | undefined + virtualModuleIdPattern?: RegExp | undefined + transformAst?: ( + context: StartCompilerTransformContext, + ) => boolean | null | undefined + loadVirtualModule?: ( + context: StartCompilerVirtualModuleContext, + ) => StartCompilerTransformResult | null + invalidateModule?: (context: { id: string; envName: string }) => void +} + export interface NormalizedBasePaths { publicBase: string assetBase: { @@ -82,6 +113,7 @@ export interface NormalizedClientChunk { dynamicImports: Array css: Array routeFilePaths: Array + hydrationIds: Array } export interface NormalizedClientBuild { diff --git a/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts index 3130ca47c15..ef40689a02a 100644 --- a/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts +++ b/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts @@ -8,12 +8,18 @@ import { import { detectKindsInCode } from '../../start-compiler/compiler' import { getTransformCodeFilterForEnv } from '../../start-compiler/config' import { + createCompilerVirtualModuleIdPattern, createStartCompiler, + loadCompilerVirtualModule, mergeServerFnsById, } from '../../start-compiler/host' import { generateServerFnResolverModule } from '../../start-compiler/server-fn-resolver-module' import { cleanId } from '../../start-compiler/utils' import { createVirtualModule } from '../createVirtualModule' +import { + MissingHydrateSourceError, + createHydrateCompilerPlugin, +} from '../../hydrate-when-transform' import { resolveViteId } from '../../utils' import { createViteDevServerFnModuleSpecifierEncoder, @@ -23,12 +29,13 @@ import { mergeHotUpdateModules } from './hot-update' import type { CompileStartFrameworkOptions, StartCompilerImportTransform, + StartCompilerPlugin, } from '../../types' import type { GenerateFunctionIdFnOptional, ServerFn, } from '../../start-compiler/types' -import type { EnvironmentModuleNode, PluginOption } from 'vite' +import type { Environment, EnvironmentModuleNode, PluginOption } from 'vite' // Re-export from shared constants for backwards compatibility export { SERVER_FN_LOOKUP } @@ -46,6 +53,32 @@ type ModuleInvalidationEnvironment = { } } +type ViteModuleLoadOptions = { + devId?: string + load: (options: { id: string }) => Promise<{ code?: string | null } | null> + error: (message: string) => never +} + +async function loadViteModuleFromEnvironment( + environment: Environment, + id: string, + opts: ViteModuleLoadOptions, +): Promise { + if (environment.mode === 'build') { + const loaded = await opts.load({ id }) + return loaded?.code ?? '' + } + + if (environment.mode === 'dev') { + await environment.transformRequest(opts.devId ?? id) + return undefined + } + + opts.error( + `could not load module ${id}: unknown environment mode ${environment.mode}`, + ) +} + function invalidateMatchingFileModules( environment: ModuleInvalidationEnvironment, ids: Iterable, @@ -104,6 +137,25 @@ function invalidateServerFnLookupModules( ) } +function invalidateCompilerVirtualModules( + environment: ModuleInvalidationEnvironment, + ids: Iterable, + pattern: RegExp | undefined, +) { + if (!pattern) { + return [] + } + + return invalidateMatchingFileModules(environment, ids, (fileModule) => { + if (!fileModule.id) { + return false + } + + pattern.lastIndex = 0 + return pattern.test(fileModule.id) + }) +} + function getServerFnProviderIds(ids: Iterable) { const providerIds = new Set() @@ -170,6 +222,7 @@ export interface StartCompilerPluginOptions { */ generateFunctionId?: GenerateFunctionIdFnOptional compilerTransforms?: Array | undefined + compilerPlugins?: Array | undefined serverFnProviderModuleDirectives?: ReadonlyArray | undefined /** * The Vite environment name for the server function provider. @@ -181,6 +234,15 @@ export function startCompilerPlugin( opts: StartCompilerPluginOptions, ): PluginOption { const compilers = new Map>() + const compilerPlugins = [ + createHydrateCompilerPlugin(), + ...(opts.compilerPlugins ?? []), + ] + const compilerVirtualModuleIdPattern = + createCompilerVirtualModuleIdPattern(compilerPlugins) + const environmentByName = new Map( + opts.environments.map((environment) => [environment.name, environment]), + ) // Shared registry of server functions across all environments const serverFnsById: Record = {} @@ -218,6 +280,7 @@ export function startCompilerPlugin( // Derive transform code filter from KindDetectionPatterns (single source of truth) const transformCodeFilter = getTransformCodeFilterForEnv(environment.type, { compilerTransforms, + compilerPlugins, }) return { name: `tanstack-start-core::server-fn:${environment.name}`, @@ -254,6 +317,7 @@ export function startCompilerPlugin( providerEnvName: opts.providerEnvName, generateFunctionId: opts.generateFunctionId, compilerTransforms, + compilerPlugins, serverFnProviderModuleDirectives, onServerFnsById, getKnownServerFns: () => serverFnsById, @@ -262,23 +326,18 @@ export function startCompilerPlugin( ? createViteDevServerFnModuleSpecifierEncoder(root) : undefined, loadModule: async (id: string) => { - if (mode === 'build') { - const loaded = await this.load({ id }) - const code = loaded.code ?? '' - + const code = await loadViteModuleFromEnvironment( + this.environment, + id, + { + load: (options) => this.load(options), + error: (message) => this.error(message), + devId: `${id}?${SERVER_FN_LOOKUP}`, + }, + ) + if (code !== undefined) { compiler!.ingestModule({ code, id }) - return } - - if (this.environment.mode !== 'dev') { - this.error( - `could not load module ${id}: unknown environment mode ${this.environment.mode}`, - ) - } - - await this.environment.transformRequest( - `${id}?${SERVER_FN_LOOKUP}`, - ) }, resolveId: async (source: string, importer?: string) => { @@ -307,6 +366,7 @@ export function startCompilerPlugin( code, detectedKinds, }) + return result }, }, @@ -316,17 +376,22 @@ export function startCompilerPlugin( const idsToInvalidate = new Set() const transitiveCompilerImportersToInvalidate = new Set() const importerModulesToInvalidate = new Set() + const changedIds: Array = [] ctx.modules.forEach((m) => { if (m.id) { idsToInvalidate.add(m.id) - const deleted = compiler?.invalidateModule(m.id) + changedIds.push(m.id) + } + }) - if (deleted) { + const deletedIds = compiler?.invalidateModules(changedIds) ?? new Set() + + ctx.modules.forEach((m) => { + if (m.id) { + if (deletedIds.has(cleanId(m.id))) { transitiveCompilerImportersToInvalidate.add(cleanId(m.id)) - } - if (deleted) { m.importers.forEach((importer) => { if (importer.id) { idsToInvalidate.add(importer.id) @@ -341,54 +406,54 @@ export function startCompilerPlugin( }) const finishHotUpdate = async () => { - if (environment.type === 'server' && compiler) { - const pendingImporters = [ - ...transitiveCompilerImportersToInvalidate, - ] - const seenImporters = new Set(pendingImporters) - - while (pendingImporters.length > 0) { - const importerId = pendingImporters.pop()! - const nestedImporters = - await compiler.getTransitiveImporters(importerId) - - for (const nestedImporterId of nestedImporters) { - if (seenImporters.has(nestedImporterId)) { - continue - } - - seenImporters.add(nestedImporterId) - pendingImporters.push(nestedImporterId) - } + if ( + environment.type === 'server' && + compiler && + transitiveCompilerImportersToInvalidate.size > 0 + ) { + const seenImporters = new Set( + transitiveCompilerImportersToInvalidate, + ) + const nestedImporters = + await compiler.getTransitiveImporters(seenImporters) + + for (const nestedImporterId of nestedImporters) { + seenImporters.add(nestedImporterId) } for (const importerId of seenImporters) { idsToInvalidate.add(importerId) - compiler.invalidateModule(importerId) } + compiler.invalidateModules(seenImporters) } invalidateModuleNodes(this.environment, importerModulesToInvalidate) invalidateServerFnLookupModules(this.environment, idsToInvalidate) + const compilerVirtualModules = invalidateCompilerVirtualModules( + this.environment, + idsToInvalidate, + compilerVirtualModuleIdPattern, + ) if (environment.type !== 'server') { - return + return mergeHotUpdateModules(ctx.modules, compilerVirtualModules) } invalidateModuleNodes(this.environment, ctx.modules) const providerIdsToInvalidate = getServerFnProviderIds(idsToInvalidate) - for (const providerId of providerIdsToInvalidate) { - compiler?.invalidateModule(providerId) - } + compiler?.invalidateModules(providerIdsToInvalidate) const providerModules = invalidateServerFnProviderModules( this.environment, [...idsToInvalidate, ...providerIdsToInvalidate], ) - return mergeHotUpdateModules(ctx.modules, providerModules) + return mergeHotUpdateModules(ctx.modules, [ + ...compilerVirtualModules, + ...providerModules, + ]) } return finishHotUpdate() @@ -415,6 +480,45 @@ export function startCompilerPlugin( }, }, }, + { + name: 'tanstack-start-core:compiler-virtual-module', + enforce: 'pre', + load: { + filter: { + id: compilerVirtualModuleIdPattern ?? /$^/, + }, + async handler(id) { + const environment = environmentByName.get(this.environment.name) + if (!environment || !compilerVirtualModuleIdPattern) { + return null + } + + const loadVirtualModule = () => + loadCompilerVirtualModule(compilerPlugins, { + id, + root, + env: environment.type, + envName: this.environment.name, + }) + + try { + return loadVirtualModule() + } catch (error) { + if (!(error instanceof MissingHydrateSourceError)) { + throw error + } + } + + const sourceId = cleanId(id) + await loadViteModuleFromEnvironment(this.environment, sourceId, { + load: (options) => this.load(options), + error: (message) => this.error(message), + }) + + return loadVirtualModule() + }, + }, + }, // Validate server function ID in dev mode { name: 'tanstack-start-core:validate-server-fn-id', @@ -443,7 +547,7 @@ export function startCompilerPlugin( typeof decoded.file === 'string' && typeof decoded.export === 'string' ) { - // Use the Vite strategy to decode the module specifier + // Use the Vite when to decode the module specifier // back to the original source file path. const sourceFile = decodeViteDevServerModuleSpecifier( decoded.file, diff --git a/packages/start-plugin-core/src/vite/start-manifest-plugin/normalized-client-build.ts b/packages/start-plugin-core/src/vite/start-manifest-plugin/normalized-client-build.ts index 8ac7915ff0c..c14609bc6a4 100644 --- a/packages/start-plugin-core/src/vite/start-manifest-plugin/normalized-client-build.ts +++ b/packages/start-plugin-core/src/vite/start-manifest-plugin/normalized-client-build.ts @@ -1,4 +1,5 @@ import { tsrSplit } from '@tanstack/router-plugin' +import { tssHydrate } from '../../hydration-constants' import { getCssAssetSource } from '../../start-manifest-plugin/inlineCss' import type { Rollup } from 'vite' import type { NormalizedClientBuild, NormalizedClientChunk } from '../../types' @@ -13,6 +14,7 @@ export function normalizeViteClientChunk( dynamicImports: chunk.dynamicImports, css: Array.from(chunk.viteMetadata?.importedCss ?? []), routeFilePaths: getRouteFilePathsFromModuleIds(chunk.moduleIds), + hydrationIds: getHydrationIdsFromModuleIds(chunk.moduleIds), } } @@ -147,3 +149,38 @@ export function getRouteFilePathsFromModuleIds(moduleIds: Array) { return routeFilePaths ?? [] } + +export function getHydrationIdsFromModuleIds(moduleIds: Array) { + let hydrationIds: Array | undefined + let seen: Set | undefined + + for (const moduleId of moduleIds) { + const queryIndex = moduleId.indexOf('?') + + if (queryIndex < 0) { + continue + } + + const query = moduleId.slice(queryIndex + 1) + + if (!query.includes(tssHydrate)) { + continue + } + + const hydrationId = new URLSearchParams(query).get(tssHydrate) + + if (!hydrationId || seen?.has(hydrationId)) { + continue + } + + if (hydrationIds === undefined || seen === undefined) { + hydrationIds = [] + seen = new Set() + } + + hydrationIds.push(hydrationId) + seen.add(hydrationId) + } + + return hydrationIds ?? [] +} diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts index 58a91e3e1d0..c7df31b2c8c 100644 --- a/packages/start-plugin-core/tests/compiler.test.ts +++ b/packages/start-plugin-core/tests/compiler.test.ts @@ -185,6 +185,29 @@ describe('detectKindsInCode', () => { new Set(['ClientOnlyFn']), ) }) + + test('resets external transform regex state between detections', () => { + const compilerTransforms: Array = [ + { + name: 'global-detect', + environment: 'server', + imports: [{ libName: '@example/runtime', rootExport: 'renderThing' }], + detect: /\brenderThing\b/g, + transform: () => {}, + }, + ] + const code = ` + import { renderThing } from '@example/runtime' + renderThing() + ` + + expect(detectKindsInCode(code, 'server', { compilerTransforms })).toEqual( + new Set(['External:global-detect']), + ) + expect(detectKindsInCode(code, 'server', { compilerTransforms })).toEqual( + new Set(['External:global-detect']), + ) + }) }) describe('detects multiple kinds in same file', () => { @@ -611,6 +634,53 @@ test('ingestModule handles empty code gracefully', () => { }).not.toThrow() }) +test('compile caches modules without detected candidates for transitive importer traversal', async () => { + const compiler = new StartCompiler({ + env: 'client', + ...getDefaultTestOptions('client'), + lookupKinds: new Set(['ServerFn']), + lookupConfigurations: [], + getKnownServerFns: () => ({}), + loadModule: async () => {}, + resolveId: async (source, importer) => { + if (source === './leaf' && importer === '/src/plain.ts') { + return '/src/leaf.ts' + } + + if (source === './plain' && importer === '/src/parent.ts') { + return '/src/plain.ts' + } + + return null + }, + }) + + await compiler.compile({ + id: '/src/plain.ts', + code: ` + import { value } from './leaf' + export const plain = value + `, + detectedKinds: new Set(), + }) + + await compiler.compile({ + id: '/src/parent.ts', + code: ` + import { plain } from './plain' + export const parent = plain + `, + detectedKinds: new Set(), + }) + + expect(await compiler.getTransitiveImporters('/src/leaf.ts')).toEqual( + new Set(['/src/plain.ts', '/src/parent.ts']), + ) + expect(await compiler.getTransitiveImporters(['/src/leaf.ts'])).toEqual( + new Set(['/src/plain.ts', '/src/parent.ts']), + ) +}) + describe('calling result of createServerOnlyFn/createClientOnlyFn', () => { // This tests the fix for https://github.com/TanStack/router/issues/6643 // When a file has both createServerFn and createServerOnlyFn, and the result diff --git a/packages/start-plugin-core/tests/hydrate-when-transform.test.ts b/packages/start-plugin-core/tests/hydrate-when-transform.test.ts new file mode 100644 index 00000000000..13e1d909bd1 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrate-when-transform.test.ts @@ -0,0 +1,618 @@ +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import * as t from '@babel/types' +import { generateFromAst, parseAst } from '@tanstack/router-utils' +import { describe, expect, test } from 'vitest' +import { createHydrateCompilerPlugin } from '../src/hydrate-when-transform' +import type { CompileStartFrameworkOptions } from '../src/types' + +const root = '/repo' +const id = '/repo/src/routes/about.tsx' + +type HydrateBoundary = { + id: string + exportName: string + index: number +} + +function getHydrateBoundariesFromCode(code: string): Array { + const boundaries: Array = [] + const hydrateImportPattern = + /import\("([^"]*[?&]tss-hydrate=[^"]*)"\),\s*"([^"]+)"/g + let match: RegExpExecArray | null + + while ((match = hydrateImportPattern.exec(code))) { + const importId = match[1]! + const queryIndex = importId.indexOf('?') + const params = new URLSearchParams(importId.slice(queryIndex + 1)) + const boundaryId = params.get('tss-hydrate') + const separatorIndex = boundaryId?.indexOf('_') ?? -1 + const index = + boundaryId && separatorIndex > 0 + ? Number.parseInt(boundaryId.slice(0, separatorIndex), 36) + : Number.NaN + + if (boundaryId && Number.isInteger(index)) { + boundaries.push({ + id: boundaryId, + exportName: match[2]!, + index, + }) + } + } + + return boundaries.sort((a, b) => a.index - b.index) +} + +function virtualHydrateId( + file: string, + boundary: Pick, +) { + const params = new URLSearchParams() + params.set('tss-hydrate', boundary.id) + return `${file}?${params.toString()}` +} + +function withSourceHash(id: string, sourceHash: string) { + const separatorIndex = id.indexOf('_') + return `${id.slice(0, separatorIndex + 1)}${sourceHash}` +} + +function compileHydrate(options: { + code: string + id: string + root: string + env: 'client' | 'server' + envName?: string + framework?: CompileStartFrameworkOptions + plugin?: ReturnType +}) { + const plugin = options.plugin ?? createHydrateCompilerPlugin() + const envName = options.envName ?? options.env + const ast = parseAst({ code: options.code, sourceFilename: options.id }) + const result = plugin.transformAst?.({ + ast, + code: options.code, + id: options.id, + root: options.root, + env: options.env, + envName, + mode: 'dev', + framework: options.framework ?? 'react', + providerEnvName: 'ssr', + types: t, + parseExpression: (expressionCode) => t.identifier(expressionCode), + }) + if (!result) return null + + const generated = generateFromAst(ast, { + sourceMaps: true, + sourceFileName: options.id, + filename: options.id, + }) + + return { + code: generated.code, + map: generated.map, + boundaries: getHydrateBoundariesFromCode(generated.code), + plugin, + } +} + +function loadVirtualHydrateModule(options: { + code: string + id: string + root: string + envName?: string +}) { + const plugin = createHydrateCompilerPlugin() + return plugin.loadVirtualModule?.({ + code: options.code, + id: options.id, + root: options.root, + env: 'client', + envName: options.envName ?? 'client', + }) +} + +describe('Hydrate compiler transform', () => { + test('splits Hydrate children behind a lazy import', () => { + const result = compileHydrate({ + code: ` + import { Hydrate } from '@tanstack/react-start' + import { visible } from '@tanstack/react-start/hydration' + + export function Page() { + return ( + + + + ) + } + + function Widget(props: { title: string }) { + return

{props.title}

+ } + `, + id, + root, + env: 'client', + }) + + expect(result?.code).toContain('lazyRouteComponent') + expect(result?.code).toContain('tss-hydrate=') + expect(result?.code).not.toContain('tss-hydrate-index') + expect(result?.code).not.toContain('createElement') + expect(result?.code).toContain('h=') + expect(result?.code).not.toContain('p=') + }) + + test('uses the Solid Router import source for Solid Hydrate boundaries', () => { + const result = compileHydrate({ + code: ` + import { Hydrate } from '@tanstack/solid-start' + import { visible } from '@tanstack/solid-start/hydration' + + export function Page() { + return ( + + + + ) + } + + function Widget(props: { title: string }) { + return

{props.title}

+ } + `, + id, + root, + env: 'client', + framework: 'solid', + }) + + expect(result?.code).toContain( + 'lazyRouteComponent } from "@tanstack/solid-router"', + ) + expect(result?.code).toContain('tss-hydrate=') + expect(result?.code).toContain('h=') + expect(result?.code).not.toContain('p=') + }) + + test('rejects function-as-children unless the boundary opts out of splitting', () => { + expect(() => + compileHydrate({ + code: ` + import { Hydrate } from '@tanstack/react-start' + import { idle } from '@tanstack/react-start/hydration' + + export function Page() { + return ( + + {() =>

child

} +
+ ) + } + `, + id, + root, + env: 'client', + }), + ).toThrow(/function-as-children/) + }) + + test('rejects hook calls that would be moved into an extracted component', () => { + expect(() => + compileHydrate({ + code: ` + import { Hydrate } from '@tanstack/react-start' + import { idle } from '@tanstack/react-start/hydration' + + function useThing() { + return 'thing' + } + + export function Page() { + return ( + +

{useThing()}

+
+ ) + } + `, + id, + root, + env: 'client', + }), + ).toThrow(/hooks/) + }) + + test('strips Hydrate fallback from the server transform', () => { + const result = compileHydrate({ + code: ` + import { Hydrate } from '@tanstack/react-start' + import { idle, visible } from '@tanstack/react-start/hydration' + + const spreadProps = { + when: visible(), + fallback:
bound
, + } + + export function Page() { + return ( + <> + fallback
} + > + + + inline} + > + + + spread, + }} + > + + + + + + + ) + } + + function Widget(props: { title: string }) { + return

{props.title}

+ } + `, + id, + root, + env: 'server', + }) + + expect(result?.code).not.toContain('fallback=') + expect(result?.code).not.toContain('fallback:') + expect(result?.code).not.toContain('server-fallback') + expect(result?.code).not.toContain('server-inline') + expect(result?.code).not.toContain('server-spread') + expect(result?.code).not.toContain('server-bound-spread') + expect(result?.code).toContain('h=') + expect(result?.code).toContain('') + expect(result?.code).toContain('') + expect(result?.code).toContain('') + expect(result?.code).toContain('') + }) + + test('supports nested Hydrate boundaries in extracted virtual modules', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const code = ` + import { Hydrate } from '@tanstack/react-start' + import { interaction, visible } from '@tanstack/react-start/hydration' + + const unused = 'remove me' + + export function Page() { + return ( + +
+ + + +
+
+ ) + } + ` + const firstPass = compileHydrate({ + code, + id: file, + root: dir, + env: 'client', + }) + expect(firstPass?.boundaries).toHaveLength(1) + + const virtualId = virtualHydrateId(file, firstPass!.boundaries[0]!) + const virtualModule = loadVirtualHydrateModule({ + code, + id: virtualId, + root: dir, + }) + expect(virtualModule?.code).not.toContain('remove me') + + const boundaryIndex = firstPass!.boundaries[0]!.index + const nestedPass = compileHydrate({ + code: virtualModule!.code, + id: virtualId, + root: dir, + env: 'client', + }) + + expect(boundaryIndex).toBe(0) + expect(nestedPass?.code).toContain('H1') + expect(nestedPass?.code).toContain('tss-hydrate=') + }) + + test('keeps sibling boundary ids stable after nested boundaries', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const code = ` + import { Hydrate } from '@tanstack/react-start' + import { idle, interaction, visible } from '@tanstack/react-start/hydration' + + export function Page() { + return ( + <> + +
+ + + +
+
+ +

Sibling

+
+ + ) + } + ` + const firstPass = compileHydrate({ + code, + id: file, + root: dir, + env: 'client', + }) + expect( + firstPass?.boundaries.map((boundary) => boundary.exportName), + ).toEqual(['H0', 'H2']) + + const siblingBoundary = firstPass!.boundaries[1]! + const virtualId = virtualHydrateId(file, siblingBoundary) + const boundaryIndex = siblingBoundary.index + const virtualModule = loadVirtualHydrateModule({ + code, + id: virtualId, + root: dir, + }) + const parentVirtualId = virtualHydrateId(file, firstPass!.boundaries[0]!) + const parentVirtualModule = loadVirtualHydrateModule({ + code, + id: parentVirtualId, + root: dir, + }) + const nestedPass = compileHydrate({ + code: parentVirtualModule!.code, + id: parentVirtualId, + root: dir, + env: 'client', + }) + const serverPass = compileHydrate({ + code, + id: file, + root: dir, + env: 'server', + }) + + expect(boundaryIndex).toBe(2) + expect(virtualModule?.code).toContain('Sibling') + expect(virtualModule?.code).not.toContain('Nested') + expect(nestedPass?.boundaries[0]?.exportName).toBe('H1') + expect(serverPass?.code).toContain(firstPass!.boundaries[0]!.id) + expect(serverPass?.code).toContain(nestedPass!.boundaries[0]!.id) + expect(serverPass?.code).toContain(siblingBoundary.id) + }) + + test('loads virtual modules from supplied bundler code instead of the filesystem', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const oldCode = ` + import { Hydrate } from '@tanstack/react-start' + import { visible } from '@tanstack/react-start/hydration' + + export function Page() { + return

Old

+ } + ` + const nextCode = oldCode.replace('Old', 'New') + const firstPass = compileHydrate({ + code: oldCode, + id: file, + root: dir, + env: 'client', + }) + const virtualModule = loadVirtualHydrateModule({ + code: nextCode, + id: virtualHydrateId(file, firstPass!.boundaries[0]!), + root: dir, + }) + + expect(virtualModule?.code).toContain('New') + expect(virtualModule?.code).not.toContain('Old') + }) + + test('captures identifiers used by shorthand objects and computed members', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const code = ` + import { Hydrate } from '@tanstack/react-start' + import { visible } from '@tanstack/react-start/hydration' + + export function Page() { + const key = 'name' + const items = { name: 'Ada' } + return ( + + + + ) + } + + function Widget(props: { data: { key: string; value: string } }) { + return

{props.data.value}

+ } + ` + const firstPass = compileHydrate({ + code, + id: file, + root: dir, + env: 'client', + }) + const virtualModule = loadVirtualHydrateModule({ + code, + id: virtualHydrateId(file, firstPass!.boundaries[0]!), + root: dir, + }) + + expect(virtualModule?.code).toContain('export function H0({') + expect(virtualModule?.code).toContain('items') + expect(virtualModule?.code).toContain('key') + expect(virtualModule?.code).toContain('items[key]') + }) + + test('unwraps exported declarations needed by extracted virtual modules', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const code = ` + import { createFileRoute } from '@tanstack/react-router' + import { Hydrate } from '@tanstack/react-start' + import { visible } from '@tanstack/react-start/hydration' + + export const Route = createFileRoute('/test')({ + component: Page, + }) + + export const label = 'Ada' + + export const Widget = () => { + return

{label}

+ } + + function Page() { + return + } + ` + const firstPass = compileHydrate({ + code, + id: file, + root: dir, + env: 'client', + }) + const virtualModule = loadVirtualHydrateModule({ + code, + id: virtualHydrateId(file, firstPass!.boundaries[0]!), + root: dir, + }) + + expect(virtualModule?.code).toContain('const Widget') + expect(virtualModule?.code).toContain('const label') + expect(virtualModule?.code).toContain('') + expect(virtualModule?.code).not.toContain('export const Widget') + expect(virtualModule?.code).not.toContain('createFileRoute') + expect(virtualModule?.code).not.toContain('const Route') + }) + + test('invalidates cached Hydrate source for virtual module loads', () => { + const dir = mkdtempSync(join(tmpdir(), 'hydrate-when-')) + const file = join(dir, 'route.tsx') + const oldCode = ` + import { Hydrate } from '@tanstack/react-start' + import { visible } from '@tanstack/react-start/hydration' + + export function Page() { + return

Old

+ } + ` + const nextCode = oldCode.replace('Old', 'New') + const envName = 'client' + const plugin = createHydrateCompilerPlugin() + const transformed = compileHydrate({ + code: oldCode, + id: file, + root: dir, + env: 'client', + envName, + plugin, + }) + const virtualId = virtualHydrateId(file, transformed!.boundaries[0]!) + + expect( + transformed!.plugin.loadVirtualModule?.({ + id: virtualId, + root: dir, + env: 'client', + envName, + })?.code, + ).toContain('Old') + + transformed!.plugin.invalidateModule?.({ id: file, envName }) + expect(() => + transformed!.plugin.loadVirtualModule?.({ + id: virtualId, + root: dir, + env: 'client', + envName, + }), + ).toThrow(/Missing Hydrate source/) + + const updated = compileHydrate({ + code: nextCode, + id: file, + root: dir, + env: 'client', + envName, + plugin, + }) + + expect( + updated!.plugin.loadVirtualModule?.({ + id: virtualId, + root: dir, + env: 'client', + envName, + })?.code, + ).toContain('New') + }) + + test('rejects virtual module ids whose source hash does not match', () => { + const code = ` + import { Hydrate } from '@tanstack/react-start' + import { idle, visible } from '@tanstack/react-start/hydration' + + export function Page() { + return ( + <> +

First

+

Second

+ + ) + } + ` + const firstPass = compileHydrate({ + code, + id, + root, + env: 'client', + }) + const mismatchedId = virtualHydrateId(id, { + id: withSourceHash(firstPass!.boundaries[0]!.id, 'mismatch'), + index: firstPass!.boundaries[0]!.index, + }) + + expect( + new URLSearchParams(mismatchedId.split('?')[1]).get('tss-hydrate'), + ).toMatch(/_mismatch$/) + expect( + loadVirtualHydrateModule({ code, id: mismatchedId, root }), + ).toBeNull() + }) +}) diff --git a/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenFunctionChild.tsx b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenFunctionChild.tsx new file mode 100644 index 00000000000..a2560a61977 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenFunctionChild.tsx @@ -0,0 +1,6 @@ +import { Hydrate } from '@tanstack/react-start' +import { idle } from '@tanstack/react-start/hydration' + +export function Page() { + return {() =>

child

}
+} diff --git a/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenHookCall.tsx b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenHookCall.tsx new file mode 100644 index 00000000000..1e89bb7303e --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenHookCall.tsx @@ -0,0 +1,14 @@ +import { Hydrate } from '@tanstack/react-start' +import { idle } from '@tanstack/react-start/hydration' + +function useThing() { + return 'thing' +} + +export function Page() { + return ( + +

{useThing()}

+
+ ) +} diff --git a/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenSuperCapture.tsx b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenSuperCapture.tsx new file mode 100644 index 00000000000..63b91b02390 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenSuperCapture.tsx @@ -0,0 +1,16 @@ +import { Hydrate } from '@tanstack/react-start' +import { idle } from '@tanstack/react-start/hydration' + +class Base { + title = 'super' +} + +export class Page extends Base { + render() { + return ( + +

{super.title}

+
+ ) + } +} diff --git a/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenThisCapture.tsx b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenThisCapture.tsx new file mode 100644 index 00000000000..5f7b3df6062 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/error-files/hydrateWhenThisCapture.tsx @@ -0,0 +1,14 @@ +import { Hydrate } from '@tanstack/react-start' +import { idle } from '@tanstack/react-start/hydration' + +export class Page { + title = 'this' + + render() { + return ( + +

{this.title}

+
+ ) + } +} diff --git a/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts b/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts new file mode 100644 index 00000000000..2d121539fee --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/hydrateWhen.test.ts @@ -0,0 +1,241 @@ +import { readFile, readdir } from 'node:fs/promises' +import path from 'node:path' +import * as t from '@babel/types' +import { generateFromAst, parseAst } from '@tanstack/router-utils' +import { describe, expect, test } from 'vitest' +import { createHydrateCompilerPlugin } from '../../src/hydrate-when-transform' + +const fixtureRoot = path.resolve(import.meta.dirname, './test-files') +const errorRoot = path.resolve(import.meta.dirname, './error-files') + +function fixtureId(filename: string) { + return path.join(fixtureRoot, filename) +} + +function normalizeSnapshotCode(code: string) { + return code.split(fixtureRoot).join('') +} + +async function readFixture(filename: string) { + return await readFile(fixtureId(filename), 'utf8') +} + +async function getFilenames(dirname: string) { + return (await readdir(dirname)) + .filter((filename) => filename.endsWith('.tsx')) + .sort() +} + +type HydrateBoundary = { + id: string + exportName: string + index: number +} + +function getHydrateBoundariesFromCode(code: string): Array { + const boundaries: Array = [] + const hydrateImportPattern = + /import\("([^"]*[?&]tss-hydrate=[^"]*)"\),\s*"([^"]+)"/g + let match: RegExpExecArray | null + + while ((match = hydrateImportPattern.exec(code))) { + const importId = match[1]! + const queryIndex = importId.indexOf('?') + const params = new URLSearchParams(importId.slice(queryIndex + 1)) + const boundaryId = params.get('tss-hydrate') + const separatorIndex = boundaryId?.indexOf('_') ?? -1 + const index = + boundaryId && separatorIndex > 0 + ? Number.parseInt(boundaryId.slice(0, separatorIndex), 36) + : Number.NaN + + if (boundaryId && Number.isInteger(index)) { + boundaries.push({ + id: boundaryId, + exportName: match[2]!, + index, + }) + } + } + + return boundaries.sort((a, b) => a.index - b.index) +} + +function compile(opts: { + env: 'client' | 'server' + code: string + id: string + root?: string +}) { + const options = { + ...opts, + root: opts.root ?? fixtureRoot, + } + const plugin = createHydrateCompilerPlugin() + const ast = parseAst({ code: options.code, sourceFilename: options.id }) + const result = plugin.transformAst?.({ + ast, + code: options.code, + id: options.id, + root: options.root, + env: options.env, + envName: options.env, + mode: 'dev', + framework: 'react', + providerEnvName: 'ssr', + types: t, + parseExpression: (expressionCode) => t.identifier(expressionCode), + }) + if (!result) return null + + const generated = generateFromAst(ast, { + sourceMaps: true, + sourceFileName: options.id, + filename: options.id, + }) + + return { + code: generated.code, + map: generated.map, + boundaries: getHydrateBoundariesFromCode(generated.code), + } +} + +function loadVirtualHydrateModule(options: { + code: string + id: string + root: string +}) { + return createHydrateCompilerPlugin().loadVirtualModule?.({ + code: options.code, + id: options.id, + root: options.root, + env: 'client', + envName: 'client', + }) +} + +function virtualHydrateId( + file: string, + boundary: Pick, +) { + const params = new URLSearchParams() + params.set('tss-hydrate', boundary.id) + return `${file}?${params.toString()}` +} + +describe('Hydrate compiler transform fixtures', async () => { + const filenames = await getFilenames(fixtureRoot) + + describe.each(filenames)('should handle "%s"', async (filename) => { + const code = await readFixture(filename) + const id = fixtureId(filename) + + test(`should compile ${filename} for client`, async () => { + const result = compile({ env: 'client', code, id }) + + await expect( + normalizeSnapshotCode(result?.code ?? 'no-transform'), + ).toMatchFileSnapshot(`./snapshots/client/${filename}`) + }) + + test(`should compile ${filename} for server`, async () => { + const result = compile({ env: 'server', code, id }) + + await expect( + normalizeSnapshotCode(result?.code ?? 'no-transform'), + ).toMatchFileSnapshot(`./snapshots/server/${filename}`) + }) + }) + + test('should extract virtual modules and keep nested ids stable', async () => { + const filename = 'hydrateWhenNested.tsx' + const code = await readFixture(filename) + const id = fixtureId(filename) + const firstPass = compile({ env: 'client', code, id }) + + expect( + firstPass?.boundaries.map((boundary) => boundary.exportName), + ).toEqual(['H0', 'H2']) + + for (const boundary of firstPass!.boundaries) { + const virtualId = virtualHydrateId(id, boundary) + const loaded = loadVirtualHydrateModule({ + code, + id: virtualId, + root: fixtureRoot, + }) + + await expect( + normalizeSnapshotCode(loaded?.code ?? 'no-virtual-module'), + ).toMatchFileSnapshot( + `./snapshots/virtual/${filename}.${boundary.exportName}.tsx`, + ) + } + + const parentBoundary = firstPass!.boundaries[0]! + const parentVirtualId = virtualHydrateId(id, parentBoundary) + const parentVirtualModule = loadVirtualHydrateModule({ + code, + id: parentVirtualId, + root: fixtureRoot, + }) + const nestedPass = compile({ + code: parentVirtualModule!.code, + id: parentVirtualId, + env: 'client', + }) + + await expect( + normalizeSnapshotCode(nestedPass?.code ?? 'no-transform'), + ).toMatchFileSnapshot(`./snapshots/virtual/${filename}.H0.client.tsx`) + }) +}) + +describe('Hydrate compiler extraction errors', async () => { + const errorCases = [ + { + filename: 'hydrateWhenFunctionChild.tsx', + message: /function-as-children/, + }, + { + filename: 'hydrateWhenHookCall.tsx', + message: /hooks/, + }, + { + filename: 'hydrateWhenThisCapture.tsx', + message: /captures this/, + }, + { + filename: 'hydrateWhenSuperCapture.tsx', + message: /captures super/, + }, + ] as const + + describe.each(errorCases)('$filename', async ({ filename, message }) => { + const code = await readFile(path.join(errorRoot, filename), 'utf8') + const id = path.join(errorRoot, filename) + + test('should reject unsafe client extraction', () => { + expect(() => + compile({ + code, + id, + root: errorRoot, + env: 'client', + }), + ).toThrow(message) + }) + + test('should reject unsafe server extraction', () => { + expect(() => + compile({ + code, + id, + root: errorRoot, + env: 'server', + }), + ).toThrow(message) + }) + }) +}) diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenBasic.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenBasic.tsx new file mode 100644 index 00000000000..76e29377048 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenBasic.tsx @@ -0,0 +1,20 @@ +const _H = _lazyRouteComponent(() => import("/hydrateWhenBasic.tsx?tss-hydrate=0_3cf0187f82"), "H0"), + _H0_preload = _H.preload; +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate } from '@tanstack/react-start'; +import { idle, visible } from '@tanstack/react-start/hydration'; +import { Chart, FallbackPane } from './widgets'; +import { formatValue } from './format'; +const chartTitle = formatValue('Revenue'); +export function Page() { + return
+ } h="0_3cf0187f82" p={_H0_preload}> + {<_H />} + +
; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenMultiple.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenMultiple.tsx new file mode 100644 index 00000000000..b84b38d61c6 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenMultiple.tsx @@ -0,0 +1,28 @@ +const _H3 = _lazyRouteComponent(() => import("/hydrateWhenMultiple.tsx?tss-hydrate=2_21aa371e0f"), "H2"); +const _H2 = _lazyRouteComponent(() => import("/hydrateWhenMultiple.tsx?tss-hydrate=1_21aa371e0f"), "H1"); +const _H = _lazyRouteComponent(() => import("/hydrateWhenMultiple.tsx?tss-hydrate=0_21aa371e0f"), "H0"); +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate } from '@tanstack/react-start'; +import { load, media, visible } from '@tanstack/react-start/hydration'; +function Summary() { + return
Summary
; +} +function Comments() { + return
Comments
; +} +function Footer() { + return
Footer
; +} +export function Page() { + return <> + + {<_H />} + + + {<_H2 />} + + + {<_H3 />} + + ; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNested.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNested.tsx new file mode 100644 index 00000000000..25e563955d8 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNested.tsx @@ -0,0 +1,25 @@ +const _H2 = _lazyRouteComponent(() => import("/hydrateWhenNested.tsx?tss-hydrate=2_466696e41d"), "H2"); +const _H = _lazyRouteComponent(() => import("/hydrateWhenNested.tsx?tss-hydrate=0_466696e41d"), "H0"); +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate } from '@tanstack/react-start'; +import { idle, interaction, visible } from '@tanstack/react-start/hydration'; +const unused = 'remove me from virtual modules'; +function Outer() { + return
Outer
; +} +function NestedButton() { + return ; +} +function Sibling() { + return ; +} +export function Page() { + return <> + + {<_H />} + + + {<_H2 />} + + ; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNever.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNever.tsx new file mode 100644 index 00000000000..4afd39a67ab --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNever.tsx @@ -0,0 +1,12 @@ +const _H = _lazyRouteComponent(() => import("/hydrateWhenNever.tsx?tss-hydrate=0_b752509d76"), "H0"); +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate } from '@tanstack/react-start'; +import { never } from '@tanstack/react-start/hydration'; +function StaticMarketingBlock() { + return
Static marketing
; +} +export function Page() { + return + {<_H />} + ; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNoImport.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNoImport.tsx new file mode 100644 index 00000000000..b35c36ab1b3 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNoImport.tsx @@ -0,0 +1 @@ +no-transform \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNotFromTanstack.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNotFromTanstack.tsx new file mode 100644 index 00000000000..b35c36ab1b3 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenNotFromTanstack.tsx @@ -0,0 +1 @@ +no-transform \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenObjectFallback.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenObjectFallback.tsx new file mode 100644 index 00000000000..87c116af39a --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenObjectFallback.tsx @@ -0,0 +1,33 @@ +const _H3 = _lazyRouteComponent(() => import("/hydrateWhenObjectFallback.tsx?tss-hydrate=2_7f4dc3aa80"), "H2"), + _H2_preload = _H3.preload; +const _H2 = _lazyRouteComponent(() => import("/hydrateWhenObjectFallback.tsx?tss-hydrate=1_7f4dc3aa80"), "H1"), + _H1_preload = _H2.preload; +const _H = _lazyRouteComponent(() => import("/hydrateWhenObjectFallback.tsx?tss-hydrate=0_7f4dc3aa80"), "H0"); +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate } from '@tanstack/react-start'; +import { idle, visible } from '@tanstack/react-start/hydration'; +const spreadProps = { + when: visible(), + fallback:
Bound
+}; +function Widget(props: { + title: string; +}) { + return

{props.title}

; +} +export function Page() { + return <> + Direct} h="0_7f4dc3aa80"> + {<_H />} + + Inline + }} h="1_7f4dc3aa80" p={_H1_preload}> + {<_H2 />} + + + {<_H3 />} + + ; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenRenamed.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenRenamed.tsx new file mode 100644 index 00000000000..1faec915b71 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenRenamed.tsx @@ -0,0 +1,14 @@ +const _H = _lazyRouteComponent(() => import("/hydrateWhenRenamed.tsx?tss-hydrate=0_f555ef3ac2"), "H0"); +import { lazyRouteComponent as _lazyRouteComponent } from "@tanstack/react-router"; +import { Hydrate as HW } from '@tanstack/react-start'; +import { interaction } from '@tanstack/react-start/hydration'; +function SearchBox() { + return ; +} +export function Page() { + return + {<_H />} + ; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalse.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalse.tsx new file mode 100644 index 00000000000..b35c36ab1b3 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalse.tsx @@ -0,0 +1 @@ +no-transform \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalseFunctionChild.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalseFunctionChild.tsx new file mode 100644 index 00000000000..b35c36ab1b3 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenSplitFalseFunctionChild.tsx @@ -0,0 +1 @@ +no-transform \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenWrongImportName.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenWrongImportName.tsx new file mode 100644 index 00000000000..b35c36ab1b3 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/client/hydrateWhenWrongImportName.tsx @@ -0,0 +1 @@ +no-transform \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenBasic.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenBasic.tsx new file mode 100644 index 00000000000..3c95cf357ef --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenBasic.tsx @@ -0,0 +1,15 @@ +import { Hydrate } from '@tanstack/react-start'; +import { visible } from '@tanstack/react-start/hydration'; +import { Chart } from './widgets'; +import { formatValue } from './format'; +const chartTitle = formatValue('Revenue'); +export function Page() { + return
+ + + +
; +} \ No newline at end of file diff --git a/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenMultiple.tsx b/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenMultiple.tsx new file mode 100644 index 00000000000..6959b25b3e7 --- /dev/null +++ b/packages/start-plugin-core/tests/hydrateWhen/snapshots/server/hydrateWhenMultiple.tsx @@ -0,0 +1,24 @@ +import { Hydrate } from '@tanstack/react-start'; +import { load, media, visible } from '@tanstack/react-start/hydration'; +function Summary() { + return
Summary
; +} +function Comments() { + return
Comments
; +} +function Footer() { + return
Footer
; +} +export function Page() { + return <> + + + + + + + +