diff --git a/.changeset/plain-tools-work.md b/.changeset/plain-tools-work.md new file mode 100644 index 0000000000..cf59929b11 --- /dev/null +++ b/.changeset/plain-tools-work.md @@ -0,0 +1,12 @@ +--- +'@tanstack/octane-router': patch +'@tanstack/octane-start': patch +'@tanstack/octane-start-client': patch +'@tanstack/octane-start-server': patch +'@tanstack/router-cli': patch +'@tanstack/router-generator': patch +'@tanstack/router-plugin': patch +'@tanstack/start-plugin-core': patch +--- + +Add first-class Octane Router and TanStack Start support, including `.tsrx` file routes, code splitting, SSR, streaming hydration, and Vite integration. diff --git a/e2e/octane-start/basic/netlify.toml b/e2e/octane-start/basic/netlify.toml new file mode 100644 index 0000000000..c1e3d8492a --- /dev/null +++ b/e2e/octane-start/basic/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "pnpm run build:netlify" + publish = "dist/client" diff --git a/e2e/octane-start/basic/package.json b/e2e/octane-start/basic/package.json new file mode 100644 index 0000000000..f3bebabfe0 --- /dev/null +++ b/e2e/octane-start/basic/package.json @@ -0,0 +1,51 @@ +{ + "name": "tanstack-octane-start-e2e-basic", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "build:cloudflare": "vite build --config vite.config.cloudflare.ts", + "build:netlify": "vite build --config vite.config.netlify.ts", + "start": "node .output/server/index.mjs", + "test:platform-builds": "pnpm run build:cloudflare && pnpm run build:netlify", + "test:e2e:local": "pnpm build && playwright test --project=chromium", + "test:e2e:hmr:local": "E2E_DEV=true playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/octane-router": "workspace:^", + "@tanstack/octane-start": "workspace:^", + "octane": "catalog:" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.29.0", + "@netlify/vite-plugin-tanstack-start": "^1.1.4", + "@playwright/test": "^1.61.0", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/node": "^22.10.2", + "nitro": "^3.0.260311-beta", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "wrangler": "^4.74.0" + }, + "nx": { + "metadata": { + "playwrightModes": [ + { + "toolchain": "vite", + "mode": "ssr" + }, + { + "toolchain": "vite", + "mode": "ssr", + "name": "hmr", + "env": { + "E2E_DEV": "true" + } + } + ] + } + } +} diff --git a/e2e/octane-start/basic/playwright.config.ts b/e2e/octane-start/basic/playwright.config.ts new file mode 100644 index 0000000000..5df20a28cd --- /dev/null +++ b/e2e/octane-start/basic/playwright.config.ts @@ -0,0 +1,42 @@ +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 e2ePortKey = process.env.E2E_PORT_KEY ?? packageJson.name +const distDir = process.env.E2E_DIST_DIR ?? '.output' +const isDev = process.env.E2E_DEV === 'true' + +if (process.env.TEST_WORKER_INDEX === undefined) { + fs.rmSync(`port-${e2ePortKey}.txt`, { force: true }) +} + +const port = await getTestServerPort(e2ePortKey) +const baseURL = `http://127.0.0.1:${port}` + +export default defineConfig({ + testDir: './tests', + testMatch: isDev ? 'hmr.spec.ts' : 'basic.spec.ts', + workers: 1, + reporter: [['line']], + use: { baseURL }, + webServer: { + command: isDev + ? `pnpm dev:e2e --port ${port} --strictPort` + : `PORT=${port} node ${distDir}/server/index.mjs`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + env: isDev + ? { E2E_DEV: 'true' } + : { + NODE_ENV: 'production', + }, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/octane-start/basic/src/routeTree.gen.ts b/e2e/octane-start/basic/src/routeTree.gen.ts new file mode 100644 index 0000000000..7f33b8c6bf --- /dev/null +++ b/e2e/octane-start/basic/src/routeTree.gen.ts @@ -0,0 +1,177 @@ +/* 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.tsrx' +import { Route as ServerFunctionRouteImport } from './routes/server-function.tsrx' +import { Route as RedirectRouteImport } from './routes/redirect.tsrx' +import { Route as DeferredErrorRouteImport } from './routes/deferred-error.tsrx' +import { Route as DeferredRouteImport } from './routes/deferred.tsrx' +import { Route as AboutRouteImport } from './routes/about.tsrx' +import { Route as IndexRouteImport } from './routes/index.tsrx' + +const ServerFunctionRoute = ServerFunctionRouteImport.update({ + id: '/server-function', + path: '/server-function', + getParentRoute: () => rootRouteImport, +} as any) +const RedirectRoute = RedirectRouteImport.update({ + id: '/redirect', + path: '/redirect', + getParentRoute: () => rootRouteImport, +} as any) +const DeferredErrorRoute = DeferredErrorRouteImport.update({ + id: '/deferred-error', + path: '/deferred-error', + getParentRoute: () => rootRouteImport, +} as any) +const DeferredRoute = DeferredRouteImport.update({ + id: '/deferred', + path: '/deferred', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/deferred': typeof DeferredRoute + '/deferred-error': typeof DeferredErrorRoute + '/redirect': typeof RedirectRoute + '/server-function': typeof ServerFunctionRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/deferred': typeof DeferredRoute + '/deferred-error': typeof DeferredErrorRoute + '/redirect': typeof RedirectRoute + '/server-function': typeof ServerFunctionRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/deferred': typeof DeferredRoute + '/deferred-error': typeof DeferredErrorRoute + '/redirect': typeof RedirectRoute + '/server-function': typeof ServerFunctionRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/about' + | '/deferred' + | '/deferred-error' + | '/redirect' + | '/server-function' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/about' + | '/deferred' + | '/deferred-error' + | '/redirect' + | '/server-function' + id: + | '__root__' + | '/' + | '/about' + | '/deferred' + | '/deferred-error' + | '/redirect' + | '/server-function' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + DeferredRoute: typeof DeferredRoute + DeferredErrorRoute: typeof DeferredErrorRoute + RedirectRoute: typeof RedirectRoute + ServerFunctionRoute: typeof ServerFunctionRoute +} + +declare module '@tanstack/octane-router' { + interface FileRoutesByPath { + '/server-function': { + id: '/server-function' + path: '/server-function' + fullPath: '/server-function' + preLoaderRoute: typeof ServerFunctionRouteImport + parentRoute: typeof rootRouteImport + } + '/redirect': { + id: '/redirect' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectRouteImport + parentRoute: typeof rootRouteImport + } + '/deferred-error': { + id: '/deferred-error' + path: '/deferred-error' + fullPath: '/deferred-error' + preLoaderRoute: typeof DeferredErrorRouteImport + parentRoute: typeof rootRouteImport + } + '/deferred': { + id: '/deferred' + path: '/deferred' + fullPath: '/deferred' + preLoaderRoute: typeof DeferredRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, + DeferredRoute: DeferredRoute, + DeferredErrorRoute: DeferredErrorRoute, + RedirectRoute: RedirectRoute, + ServerFunctionRoute: ServerFunctionRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.ts' +import type { createStart } from '@tanstack/octane-start' +declare module '@tanstack/octane-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/octane-start/basic/src/router.ts b/e2e/octane-start/basic/src/router.ts new file mode 100644 index 0000000000..c47ed5df50 --- /dev/null +++ b/e2e/octane-start/basic/src/router.ts @@ -0,0 +1,9 @@ +import { createRouter } from '@tanstack/octane-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }) +} diff --git a/e2e/octane-start/basic/src/routes/__root.tsrx b/e2e/octane-start/basic/src/routes/__root.tsrx new file mode 100644 index 0000000000..6a9414a587 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/__root.tsrx @@ -0,0 +1,62 @@ +import { + Body, + Head, + HeadContent, + Html, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/octane-router'; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'Octane Start E2E' }, + ], + }), + shellComponent: RootDocument, + component: RootLayout, + errorComponent: RouteError, + notFoundComponent: NotFound, +}); + +function RootDocument(props: { children?: unknown }) @{ + + + + + + {props.children} + + + +} + +function RootLayout() @{ + <> + +
+ +
+ +} + +function RouteError(props) @{ +
+

{'Route error'}

+

{props.error instanceof Error ? props.error.message : String(props.error)}

+
+} + +function NotFound() @{ +
+

{'Not found'}

+
+} diff --git a/e2e/octane-start/basic/src/routes/about.tsrx b/e2e/octane-start/basic/src/routes/about.tsrx new file mode 100644 index 0000000000..74ae8da864 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/about.tsrx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/octane-router'; + +export const Route = createFileRoute('/about')({ + component: About, +}); + +function About() @{ +
+

{'Client navigation works'}

+
+} diff --git a/e2e/octane-start/basic/src/routes/deferred-error.tsrx b/e2e/octane-start/basic/src/routes/deferred-error.tsrx new file mode 100644 index 0000000000..ad201e1605 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/deferred-error.tsrx @@ -0,0 +1,21 @@ +import { Await, createFileRoute } from '@tanstack/octane-router'; + +export const Route = createFileRoute('/deferred-error')({ + loader: () => ({ + value: new Promise((_resolve, reject) => { + setTimeout(() => reject(new Error('deferred boom')), 50); + }), + }), + component: DeferredErrorPage, +}); + +function DeferredErrorPage() @{ + const data = Route.useLoaderData(); + +
+

{'Deferred rejection'}

+ {'Pending rejection'}

}> + {(value) =>

{String(value)}

} +
+
+} diff --git a/e2e/octane-start/basic/src/routes/deferred.tsrx b/e2e/octane-start/basic/src/routes/deferred.tsrx new file mode 100644 index 0000000000..c3defdeb5c --- /dev/null +++ b/e2e/octane-start/basic/src/routes/deferred.tsrx @@ -0,0 +1,28 @@ +import { Await, createFileRoute } from '@tanstack/octane-router'; + +export const Route = createFileRoute('/deferred')({ + loader: () => ({ + value: new Promise((resolve) => { + setTimeout(() => { + resolve({ + date: new Date('2026-07-16T22:00:00.000Z'), + map: new Map([['state', 'streamed']]), + }); + }, 750); + }), + }), + component: DeferredPage, +}); + +function DeferredPage() @{ + const data = Route.useLoaderData(); + +
+

{'Deferred loader data'}

+ {'Pending'}

}> + {(value) =>

+ {value.date.toISOString() + ' ' + value.map.get('state')} +

} +
+
+} diff --git a/e2e/octane-start/basic/src/routes/index.tsrx b/e2e/octane-start/basic/src/routes/index.tsrx new file mode 100644 index 0000000000..a51791b182 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/index.tsrx @@ -0,0 +1,41 @@ +import { useState } from 'octane'; +import { createFileRoute } from '@tanstack/octane-router'; + +const loaderRuns = new Map(); +const loaderEnvironment = typeof document === 'undefined' ? 'server' : 'client'; + +function getHeading() { + return 'Octane + TanStack Start'; +} + +export const Route = createFileRoute('/')({ + loader: ({ location }) => { + const runs = (loaderRuns.get(location.href) ?? 0) + 1; + loaderRuns.set(location.href, runs); + + return { + date: new Date('2026-07-16T12:34:56.000Z'), + map: new Map([ + ['framework', 'octane'], + ['pipeline', 'start'], + ]), + run: `${loaderEnvironment} ${runs}`, + }; + }, + component: Home, +}); + +function Home() @{ + const data = Route.useLoaderData(); + const [count, setCount] = useState(0); + +
+

{getHeading()}

+

{'loader ran on ' + data.run}

+

{data.date.toISOString()}

+

{data.map.get('framework') + ' ' + data.map.get('pipeline')}

+ +
+} diff --git a/e2e/octane-start/basic/src/routes/redirect.tsrx b/e2e/octane-start/basic/src/routes/redirect.tsrx new file mode 100644 index 0000000000..62384a7531 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/redirect.tsrx @@ -0,0 +1,7 @@ +import { createFileRoute, redirect } from '@tanstack/octane-router'; + +export const Route = createFileRoute('/redirect')({ + beforeLoad: () => { + throw redirect({ to: '/about' }); + }, +}); diff --git a/e2e/octane-start/basic/src/routes/server-function.tsrx b/e2e/octane-start/basic/src/routes/server-function.tsrx new file mode 100644 index 0000000000..eeff50cde1 --- /dev/null +++ b/e2e/octane-start/basic/src/routes/server-function.tsrx @@ -0,0 +1,39 @@ +import { useState } from 'octane'; +import { createFileRoute } from '@tanstack/octane-router'; +import { createServerFn } from '@tanstack/octane-start'; + +const roundTripServerValue = createServerFn({ method: 'POST' }) + .validator((data: { timestamp: Date; labels: Map }) => data) + .handler(async ({ data }) => ({ + timestamp: new Date(data.timestamp.getTime() + 1000), + labels: new Map([...data.labels, ['runtime', 'server']]), + })); + +export const Route = createFileRoute('/server-function')({ + component: ServerFunctionPage, +}); + +function ServerFunctionPage() @{ + const [result, setResult] = useState('idle'); + + const callServer = async () => { + const value = await roundTripServerValue({ + data: { + timestamp: new Date('2026-07-17T12:00:00.000Z'), + labels: new Map([['framework', 'octane']]), + }, + }); + + setResult( + `${value.timestamp.toISOString()} ${value.labels.get('framework')} ${value.labels.get('runtime')}`, + ); + }; + +
+

{'Server function'}

+ +

{result}

+
+} diff --git a/e2e/octane-start/basic/src/tsrx.d.ts b/e2e/octane-start/basic/src/tsrx.d.ts new file mode 100644 index 0000000000..c95fcf1499 --- /dev/null +++ b/e2e/octane-start/basic/src/tsrx.d.ts @@ -0,0 +1 @@ +declare module '*.tsrx' diff --git a/e2e/octane-start/basic/tests/basic.spec.ts b/e2e/octane-start/basic/tests/basic.spec.ts new file mode 100644 index 0000000000..23b7132fa7 --- /dev/null +++ b/e2e/octane-start/basic/tests/basic.spec.ts @@ -0,0 +1,149 @@ +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' + +test('renders a full Start document and hydrates Octane', async ({ + page, + request, +}) => { + const hydrationErrors: Array = [] + page.on('console', (message) => { + if (message.type() === 'error' || message.type() === 'warning') { + hydrationErrors.push(message.text()) + } + }) + page.on('pageerror', (error) => hydrationErrors.push(error.message)) + + const response = await request.get('/?probe=document') + expect(response.status()).toBe(200) + const html = await response.text() + expect(html).toContain('') + expect(html).toContain('') + expect(html).toContain('id="__app"') + expect(html).toContain('Octane + TanStack Start') + expect(html).toContain('loader ran on server') + + await page.goto('/?probe=hydrate') + await expect(page).toHaveTitle('Octane Start E2E') + await expect(page.locator('#__app')).toHaveCount(1) + await expect(page.locator('#__app nav')).toHaveCount(1) + await expect(page.locator('#__app main')).toHaveCount(1) + await expect(page.getByTestId('loader-run')).toHaveText( + 'loader ran on server 1', + ) + await expect(page.getByTestId('date-value')).toHaveText( + '2026-07-16T12:34:56.000Z', + ) + await expect(page.getByTestId('map-value')).toHaveText('octane start') + + await page.getByTestId('counter').click() + await expect(page.getByTestId('counter')).toHaveText('count 1') + expect(hydrationErrors).toEqual([]) +}) + +test('navigates without replacing the document', async ({ page }) => { + await page.goto('/') + await page.getByRole('link', { name: 'About' }).click() + await expect(page.getByTestId('about-heading')).toBeVisible() + await expect + .poll(() => + page.evaluate(() => performance.getEntriesByType('navigation').length), + ) + .toBe(1) +}) + +test('streams deferred values through the Start serializer', async ({ + page, +}) => { + await page.goto('/deferred') + await expect(page.getByTestId('deferred-value')).toHaveCount(1) + await expect(page.getByTestId('deferred-value')).toHaveText( + '2026-07-16T22:00:00.000Z streamed', + ) +}) + +test('round-trips typed values through a server function', async ({ page }) => { + await page.goto('/server-function') + + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/_serverFn/') && + response.request().method() === 'POST', + ) + await page.getByTestId('call-server-function').click() + + const response = await responsePromise + expect(response.status()).toBe(200) + await expect(page.getByTestId('server-function-result')).toHaveText( + '2026-07-17T12:00:01.000Z octane server', + ) +}) + +test('flushes the deferred fallback before the value resolves', async ({ + baseURL, +}) => { + const response = await fetch(`${baseURL}/deferred?probe=stream`, { + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/127.0.0.0 Safari/537.36', + }, + }) + expect(response.status).toBe(200) + expect(response.body).not.toBeNull() + + const reader = response.body!.getReader() + const decoder = new TextDecoder() + let shell = '' + + const shellResult = await Promise.race([ + (async () => { + while (!shell.includes('deferred-pending')) { + const result = await reader.read() + if (result.done) { + throw new Error('The response ended before the deferred fallback') + } + shell += decoder.decode(result.value, { stream: true }) + } + return shell + })(), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('The deferred fallback was not flushed promptly')) + }, 500) + }), + ]) + + expect(shellResult).not.toContain('2026-07-16T22:00:00.000Z streamed') + + let tail = '' + while (true) { + const result = await reader.read() + if (result.done) { + tail += decoder.decode() + break + } + tail += decoder.decode(result.value, { stream: true }) + } + + expect(shellResult + tail).toContain('2026-07-16T22:00:00.000Z streamed') +}) + +test('routes deferred rejections through the route error boundary', async ({ + page, +}) => { + await page.goto('/deferred-error') + await expect(page.getByTestId('route-error')).toContainText('deferred boom') +}) + +test('preserves redirect and not-found response status', async ({ + request, +}) => { + const redirectResponse = await request.get('/redirect', { + maxRedirects: 0, + }) + expect([301, 302, 307, 308]).toContain(redirectResponse.status()) + expect(redirectResponse.headers().location).toBe('/about') + + const missingResponse = await request.get('/missing') + expect(missingResponse.status()).toBe(404) + expect(await missingResponse.text()).toContain('Not found') +}) diff --git a/e2e/octane-start/basic/tests/hmr.spec.ts b/e2e/octane-start/basic/tests/hmr.spec.ts new file mode 100644 index 0000000000..8439acfc30 --- /dev/null +++ b/e2e/octane-start/basic/tests/hmr.spec.ts @@ -0,0 +1,55 @@ +import path from 'node:path' +import { expect } from '@playwright/test' +import { createHmrFileEditor, test } from '@tanstack/router-e2e-utils' + +const baselineHeading = 'Octane + TanStack Start' +const updatedHeading = 'Octane + TanStack Start HMR' +const routeEditor = createHmrFileEditor({ + files: { + index: path.join(process.cwd(), 'src/routes/index.tsrx'), + }, + normalizeSource: (_fileKey, source) => + source.split(updatedHeading).join(baselineHeading), +}) + +test.afterAll(async () => { + await routeEditor.capturePromise + await routeEditor.restoreFiles() +}) + +test('hot updates a code-split tsrx route without losing state', async ({ + page, +}) => { + const hydrationErrors: Array = [] + page.on('console', (message) => { + if (message.type() === 'error' || message.type() === 'warning') { + hydrationErrors.push(message.text()) + } + }) + page.on('pageerror', (error) => hydrationErrors.push(error.message)) + + await routeEditor.capturePromise + await page.goto('/?probe=hmr') + await expect(page.getByTestId('home-heading')).toHaveText(baselineHeading) + + await page.getByTestId('counter').click() + await expect(page.getByTestId('counter')).toHaveText('count 1') + + try { + await routeEditor.replaceText('index', baselineHeading, updatedHeading) + await expect(page.getByTestId('home-heading')).toHaveText(updatedHeading, { + timeout: 20_000, + }) + await expect(page.getByTestId('counter')).toHaveText('count 1') + } finally { + await routeEditor.restoreFiles() + } + + await expect(page.getByTestId('home-heading')).toHaveText(baselineHeading, { + timeout: 20_000, + }) + await expect(page.getByTestId('counter')).toHaveText('count 1') + await expect(page.locator('#__app nav')).toHaveCount(1) + await expect(page.locator('#__app main')).toHaveCount(1) + expect(hydrationErrors).toEqual([]) +}) diff --git a/e2e/octane-start/basic/tsconfig.json b/e2e/octane-start/basic/tsconfig.json new file mode 100644 index 0000000000..e7e65f9b05 --- /dev/null +++ b/e2e/octane-start/basic/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["**/*.ts", "**/*.d.ts"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2024"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2024", + "types": ["node"], + "noEmit": true + } +} diff --git a/e2e/octane-start/basic/vite.config.cloudflare.ts b/e2e/octane-start/basic/vite.config.cloudflare.ts new file mode 100644 index 0000000000..a57522277c --- /dev/null +++ b/e2e/octane-start/basic/vite.config.cloudflare.ts @@ -0,0 +1,10 @@ +import { cloudflare } from '@cloudflare/vite-plugin' +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/octane-start/plugin/vite' + +export default defineConfig({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' }, inspectorPort: false }), + tanstackStart(), + ], +}) diff --git a/e2e/octane-start/basic/vite.config.netlify.ts b/e2e/octane-start/basic/vite.config.netlify.ts new file mode 100644 index 0000000000..a72352017e --- /dev/null +++ b/e2e/octane-start/basic/vite.config.netlify.ts @@ -0,0 +1,7 @@ +import netlify from '@netlify/vite-plugin-tanstack-start' +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/octane-start/plugin/vite' + +export default defineConfig({ + plugins: [netlify(), tanstackStart()], +}) diff --git a/e2e/octane-start/basic/vite.config.ts b/e2e/octane-start/basic/vite.config.ts new file mode 100644 index 0000000000..241a00f193 --- /dev/null +++ b/e2e/octane-start/basic/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import { nitro } from 'nitro/vite' +import { tanstackStart } from '@tanstack/octane-start/plugin/vite' + +const outDir = process.env.E2E_DIST_DIR + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tanstackStart(), + nitro( + outDir + ? { + output: { + dir: outDir, + serverDir: `${outDir}/server`, + publicDir: `${outDir}/public`, + }, + } + : {}, + ), + ], +}) diff --git a/e2e/octane-start/basic/wrangler.jsonc b/e2e/octane-start/basic/wrangler.jsonc new file mode 100644 index 0000000000..d78374a0d4 --- /dev/null +++ b/e2e/octane-start/basic/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "tanstack-octane-start-e2e-basic", + "compatibility_date": "2025-09-24", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/octane-start/server-entry", +} diff --git a/labeler-config.yml b/labeler-config.yml index c1ad49328e..2a8f354377 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -13,6 +13,18 @@ 'package: nitro-v2-vite-plugin': - changed-files: - any-glob-to-any-file: 'packages/nitro-v2-vite-plugin/**/*' +'package: octane-router': + - changed-files: + - any-glob-to-any-file: 'packages/octane-router/**/*' +'package: octane-start': + - changed-files: + - any-glob-to-any-file: 'packages/octane-start/**/*' +'package: octane-start-client': + - changed-files: + - any-glob-to-any-file: 'packages/octane-start-client/**/*' +'package: octane-start-server': + - changed-files: + - any-glob-to-any-file: 'packages/octane-start-server/**/*' 'package: react-router': - changed-files: - any-glob-to-any-file: 'packages/react-router/**/*' diff --git a/package.json b/package.json index cdc2bf2bc8..ba6825b917 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,9 @@ "cleanup-empty-packages": "node scripts/cleanup-empty-packages.mjs", "test:docs": "node scripts/verify-links.ts", "ts:symbol-references": "node scripts/ts-symbol-references.mjs", - "vite-ecosystem-ci:build": "nx run-many --targets=build --projects=@tanstack/router-plugin,@tanstack/start-plugin-core,@tanstack/react-start,@tanstack/react-start-client,@tanstack/react-start-server --skipRemoteCache", + "vite-ecosystem-ci:build": "nx run-many --targets=build --projects=@tanstack/router-plugin,@tanstack/start-plugin-core,@tanstack/react-start,@tanstack/react-start-client,@tanstack/react-start-server,@tanstack/octane-router,@tanstack/octane-start,@tanstack/octane-start-client,@tanstack/octane-start-server --skipRemoteCache", "vite-ecosystem-ci:before-test": "pnpm exec playwright install chromium", - "vite-ecosystem-ci:test": "nx run-many --targets=test:unit --projects=@tanstack/router-plugin,@tanstack/start-plugin-core,@tanstack/react-start-client --skipRemoteCache && nx run-many --target=test:e2e --projects=tanstack-router-e2e-react-basic-file-based,tanstack-router-e2e-react-basic-file-based-code-splitting,tanstack-react-start-e2e-basic,tanstack-vue-start-e2e-basic,tanstack-solid-start-e2e-basic --skipRemoteCache" + "vite-ecosystem-ci:test": "nx run-many --targets=test:unit --projects=@tanstack/router-plugin,@tanstack/start-plugin-core,@tanstack/react-start-client,@tanstack/octane-router,@tanstack/octane-start,@tanstack/octane-start-client,@tanstack/octane-start-server --skipRemoteCache && nx run-many --target=test:e2e --projects=tanstack-router-e2e-react-basic-file-based,tanstack-router-e2e-react-basic-file-based-code-splitting,tanstack-react-start-e2e-basic,tanstack-vue-start-e2e-basic,tanstack-solid-start-e2e-basic,tanstack-octane-start-e2e-basic --skipRemoteCache && nx run tanstack-octane-start-e2e-basic:test:platform-builds --skipRemoteCache" }, "nx": { "includedScripts": [ diff --git a/packages/octane-router/CHANGELOG.md b/packages/octane-router/CHANGELOG.md new file mode 100644 index 0000000000..cd2e364d9e --- /dev/null +++ b/packages/octane-router/CHANGELOG.md @@ -0,0 +1,12 @@ +# @tanstack/octane-router + +## 1.170.18 + +### Patch Changes + +- Add the official TanStack Router binding for Octane, including typed code- and + file-based routes, client hydration, streaming SSR, head and script management, + and Octane route generation support. + +- Updated dependencies: + - @tanstack/router-core@1.171.15 diff --git a/packages/octane-router/README.md b/packages/octane-router/README.md new file mode 100644 index 0000000000..ac72b3e017 --- /dev/null +++ b/packages/octane-router/README.md @@ -0,0 +1,101 @@ +# TanStack Router for Octane + +Official TanStack Router bindings for the +[Octane](https://github.com/octanejs/octane) renderer. + +```sh +pnpm add @tanstack/octane-router octane +``` + +The package re-exports `@tanstack/router-core` and supplies the Octane-specific +components, hooks, hydration, document primitives, and streaming SSR handlers. +It is published as raw source so the Octane Vite compiler can produce the +correct client or server output for each build environment. + +## Router + +```ts +import { createRoot } from 'octane' +import { + Link, + Outlet, + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/octane-router' + +function RootLayout() @{ + + +} + +function Home() @{ +

Home

+} + +const rootRoute = createRootRoute({ component: RootLayout }) +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: Home, +}) + +const router = createRouter({ + routeTree: rootRoute.addChildren([indexRoute]), +}) + +createRoot(document.getElementById('__app')!).render(RouterProvider, { router }) +``` + +Code-based and generated file-based routes share the same typed APIs as the +other TanStack Router bindings, including route-bound hooks, loaders, search +validation, navigation blocking, lazy routes, and link preloading. + +## Start and SSR + +TanStack Start uses the `@tanstack/octane-router/ssr/server` and +`@tanstack/octane-router/ssr/client` entries. The root route owns the document: + +```ts +import { + Body, + Head, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/octane-router' + +function RootDocument(props: { children?: unknown }) @{ + + + + + + {props.children} + + + +} + +function RootLayout() @{ + +} + +export const Route = createRootRoute({ + shellComponent: RootDocument, + component: RootLayout, +}) +``` + +`Body` emits the server document's `#__app` hydration boundary. On the client, +`Html` and `Body` flatten into that existing boundary while `Head` portals its +children into `document.head`. + +Visit [tanstack.com/router](https://tanstack.com/router) for Router concepts and +API documentation. diff --git a/packages/octane-router/eslint.config.ts b/packages/octane-router/eslint.config.ts new file mode 100644 index 0000000000..6f42530e18 --- /dev/null +++ b/packages/octane-router/eslint.config.ts @@ -0,0 +1,19 @@ +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + ignores: [ + 'tests/_fixtures/**', + 'tests/conformance/**', + 'tests/_helpers.ts', + ], + }, + { + files: ['src/**/*.ts', 'tests/**/*.ts'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/triple-slash-reference': 'off', + }, + }, +] diff --git a/packages/octane-router/package.json b/packages/octane-router/package.json new file mode 100644 index 0000000000..562a16ddde --- /dev/null +++ b/packages/octane-router/package.json @@ -0,0 +1,85 @@ +{ + "name": "@tanstack/octane-router", + "version": "1.170.18", + "description": "Modern and scalable routing for Octane applications", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/router.git", + "directory": "packages/octane-router" + }, + "homepage": "https://tanstack.com/router", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "location", + "router", + "routing", + "async", + "async router", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test:eslint": "eslint", + "test:types": "tsc --noEmit", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:build": "publint --strict && attw --pack . --profile esm-only --ignore-rules no-resolution internal-resolution-error", + "build": "tsc --noEmit -p tsconfig.build.json" + }, + "type": "module", + "types": "src/index.ts", + "main": "src/index.ts", + "module": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./history": "./src/history.ts", + "./ssr/server": { + "types": "./src/ssr/server.ts", + "default": "./src/ssr/server.ts" + }, + "./ssr/client": { + "types": "./src/ssr/client.ts", + "default": "./src/ssr/client.ts" + }, + "./generator-plugin": { + "types": "./src/generator-plugin.d.ts", + "default": "./src/generator-plugin.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "src", + "README.md" + ], + "engines": { + "node": ">=22" + }, + "octane": { + "hookSlots": { + "manual": [ + "src" + ] + } + }, + "dependencies": { + "@tanstack/history": "workspace:*", + "@tanstack/router-core": "workspace:*", + "@tanstack/store": "^0.9.3", + "isbot": "^5.1.22" + }, + "devDependencies": { + "@types/node": ">=22", + "octane": "catalog:", + "vite": "*" + }, + "peerDependencies": { + "octane": ">=0.1.8" + } +} diff --git a/packages/octane-router/src/Asset.tsrx b/packages/octane-router/src/Asset.tsrx new file mode 100644 index 0000000000..6200750fee --- /dev/null +++ b/packages/octane-router/src/Asset.tsrx @@ -0,0 +1,116 @@ +import { createElement, useLayoutEffect } from 'octane'; +import { isServer } from '@tanstack/router-core/isServer'; +import { useRouter } from './context.ts'; +import { useHydrated } from './ClientOnly.tsrx'; + +const MANAGED_KEY_ATTR = 'data-tsr-managed-key'; + +function attributeName(name) { + switch (name) { + case 'charSet': + return 'charset'; + case 'className': + return 'class'; + case 'crossOrigin': + return 'crossorigin'; + case 'httpEquiv': + return 'http-equiv'; + default: + return name; + } +} + +function setAttributes(element, attrs) { + if (!attrs) { + return; + } + for (const [name, value] of Object.entries(attrs)) { + if ( + name === 'suppressHydrationWarning' || + value === undefined || + value === null || + value === false + ) { + continue; + } + element.setAttribute(attributeName(name), value === true ? '' : String(value)); + } +} + +function findManagedElement(parent, key) { + for (const element of parent.querySelectorAll(`[${MANAGED_KEY_ATTR}]`)) { + if (element.getAttribute(MANAGED_KEY_ATTR) === key) { + return element; + } + } + return undefined; +} + +function mountAsset(props) { + const parent = props.target === 'head' ? document.head : document.body; + const existing = findManagedElement(parent, props.assetKey); + if (existing) { + return () => existing.remove(); + } + + const element = document.createElement(props.tag); + element.setAttribute(MANAGED_KEY_ATTR, props.assetKey); + setAttributes(element, props.attrs); + if (typeof props.children === 'string') { + element.textContent = props.children; + } + parent.appendChild(element); + return () => element.remove(); +} + +export function Asset(props) @{ + const router = useRouter(); + const server = isServer ?? router.isServer; + const hydrated = useHydrated(); + const renderElement = server || props.target === 'body' && !hydrated; + + if (!server) { + useLayoutEffect(() => mountAsset(props), [ + props.assetKey, + props.attrs, + props.children, + props.tag, + props.target, + ]); + } + + @if (renderElement) { + @switch (props.tag) { + @case 'title': { + + {(props.children ?? '') as string} + + } + @case 'meta': { + + } + @case 'link': { + + } + @case 'style': { + {createElement('style', { + ...props.attrs, + 'data-tsr-managed-key': props.assetKey, + dangerouslySetInnerHTML: { __html: props.children ?? '' }, + })} + } + @case 'script': { + ') + }) + + it('emits the pre-hydration scroll restoration script when enabled', async () => { + const router = makeSsrRouter({ scrollRestoration: true }) + attachRouterServerSsrUtils({ router, manifest: undefined }) + await router.load() + await router.serverSsr.dehydrate() + const script = getScrollRestorationScriptForRouter(router) + + const response = await renderRouterToString({ + router, + responseHeaders: new Headers({ 'content-type': 'text/html' }), + App: RouterServer, + }) + + expect(script).toBeTruthy() + expect(await response.text()).toContain(script) + }) + + it.each([false, 'data-only'] as const)( + 'does not render route UI when ssr is %s', + async (routeSsr) => { + const router = makeSsrRouter({ routeSsr }) + attachRouterServerSsrUtils({ router, manifest: undefined }) + await router.load() + await router.serverSsr.dehydrate() + + const response = await renderRouterToString({ + router, + responseHeaders: new Headers({ 'content-type': 'text/html' }), + App: RouterServer, + }) + + expect(await response.text()).not.toContain('Rendered on the server') + }, + ) + + it('places shell styles inside the route-owned head when streaming', async () => { + const router = makeSsrRouter() + attachRouterServerSsrUtils({ router, manifest: undefined }) + await router.load() + await router.serverSsr.dehydrate() + + const response = await renderRouterToStream({ + request: new Request('http://localhost/', { + headers: { 'user-agent': 'Mozilla/5.0' }, + }), + router, + responseHeaders: new Headers({ 'content-type': 'text/html' }), + App: RouterServer, + }) + const html = await response.text() + const doctype = html.indexOf('') + const document = html.indexOf('') + + expect(doctype).toBe(0) + expect(document).toBeGreaterThan(doctype) + expect(head).toBeGreaterThan(document) + expect(style).toBeGreaterThan(head) + expect(headClose).toBeGreaterThan(style) + expect(html.slice(0, document)).not.toContain('', + ), + ) + controller.close() + }, + }) + + const reader = relocateLeadingOctaneStylesToHead(source).getReader() + const shellResult = await reader.read() + const shell = decoder.decode(shellResult.value) + + expect(shell).toContain( + '', + ) + }) + + it('propagates cancellation through document normalization', async () => { + const encoder = new TextEncoder() + const reason = new Error('client disconnected') + let markCancelled!: (reason: unknown) => void + const cancelled = new Promise((resolve) => { + markCancelled = resolve + }) + const source = new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode( + '', + ), + ) + }, + cancel(cancelReason) { + markCancelled(cancelReason) + }, + }) + const reader = relocateLeadingOctaneStylesToHead(source).getReader() + + await reader.read() + await reader.cancel(reason) + + expect(await cancelled).toBe(reason) + }) +}) diff --git a/packages/octane-router/tests/externalHydration.test.ts b/packages/octane-router/tests/externalHydration.test.ts new file mode 100644 index 0000000000..a42dbb4c07 --- /dev/null +++ b/packages/octane-router/tests/externalHydration.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest' +import { toExternalHydrationThenable } from '../src/externalHydration' + +const EXTERNAL_HYDRATION_PROMISE = Symbol.for( + 'octane.external-hydration-promise', +) + +describe('external hydration thenables', () => { + it('returns a stable wrapper without mutating a frozen promise', async () => { + const promise = Object.freeze(Promise.resolve('ready')) + + const first = toExternalHydrationThenable(promise) + const second = toExternalHydrationThenable(promise) + + expect(first).toBe(second) + expect(first).not.toBe(promise) + expect( + (first as PromiseLike & Record)[ + EXTERNAL_HYDRATION_PROMISE + ], + ).toBe(true) + expect(await first).toBe('ready') + expect(Object.getOwnPropertySymbols(promise)).not.toContain( + EXTERNAL_HYDRATION_PROMISE, + ) + }) + + it('preserves synchronous fulfilled and rejected metadata', () => { + const value = { date: new Date('2026-07-16T12:00:00.000Z') } + const fulfilled = Object.freeze( + Object.assign(Promise.resolve(value), { + status: 'fulfilled' as const, + value, + }), + ) + const reason = new Error('denied') + const rejected = Object.freeze( + Object.assign(Promise.resolve(undefined), { + status: 'rejected' as const, + reason, + }), + ) + + const fulfilledWrapper = toExternalHydrationThenable(fulfilled) as any + const rejectedWrapper = toExternalHydrationThenable(rejected) as any + + expect(fulfilledWrapper.status).toBe('fulfilled') + expect(fulfilledWrapper.value).toBe(value) + expect(rejectedWrapper.status).toBe('rejected') + expect(rejectedWrapper.reason).toBe(reason) + }) + + it('does not expose incompatible status conventions to Octane', async () => { + const promise = Object.freeze( + Object.assign(Promise.resolve('ready'), { + status: 'resolved' as const, + value: 'ready', + }), + ) + const wrapper = toExternalHydrationThenable(promise) as any + + expect(wrapper.status).toBeUndefined() + expect(await wrapper).toBe('ready') + }) + + it('isolates Octane tracking metadata from the source thenable', () => { + const promise = Object.freeze(Promise.resolve('ready')) + const wrapper = toExternalHydrationThenable(promise) as any + const reason = new Error('local') + + wrapper.status = 'rejected' + wrapper.reason = reason + + expect(wrapper.status).toBe('rejected') + expect(wrapper.reason).toBe(reason) + expect((promise as any).status).toBeUndefined() + expect((promise as any).reason).toBeUndefined() + }) + + it('tolerates hostile metadata accessors', async () => { + const thenable = { + get status(): never { + throw new Error('status is private') + }, + then( + onfulfilled?: + | ((value: string) => TFulfilled | PromiseLike) + | null, + onrejected?: + | ((reason: any) => TRejected | PromiseLike) + | null, + ): PromiseLike { + return Promise.resolve('ready').then(onfulfilled, onrejected) + }, + } + const wrapper = toExternalHydrationThenable(thenable) as any + + expect(wrapper.status).toBeUndefined() + expect(await wrapper).toBe('ready') + }) +}) diff --git a/packages/octane-router/tests/generator-plugin.test.ts b/packages/octane-router/tests/generator-plugin.test.ts new file mode 100644 index 0000000000..e5f04716bf --- /dev/null +++ b/packages/octane-router/tests/generator-plugin.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { maskOctaneRouteSource } from '../src/generator-plugin' + +describe('maskOctaneRouteSource', () => { + it('preserves offsets while masking Octane component templates', () => { + const source = `import { createFileRoute } from '@tanstack/octane-router' + +export const Route = createFileRoute('/wrong')({ component: Page }) + +function Page() @{ + const value = \`value: \${JSON.stringify({ nested: true })}\` + @if (/\\}/.test(value)) { +
{value as string}
+ } +} +` + + const masked = maskOctaneRouteSource(source) + + expect(masked).toHaveLength(source.length) + expect(masked.indexOf("createFileRoute('/wrong')")).toBe( + source.indexOf("createFileRoute('/wrong')"), + ) + expect(masked).toContain('function Page() {') + expect(masked).not.toContain('@if') + expect(masked.split('\n')).toHaveLength(source.split('\n').length) + }) + + it('does not mask lookalikes in comments or strings', () => { + const source = `const string = '@{ untouched }' +// @{ untouched } +/* @{ untouched } */ +export const Route = createFileRoute('/')({}) +` + + expect(maskOctaneRouteSource(source)).toBe(source) + }) + + it('rejects an unterminated template', () => { + expect(() => maskOctaneRouteSource('function Page() @{
')).toThrow() + }) +}) diff --git a/packages/octane-router/tests/router.test-d.ts b/packages/octane-router/tests/router.test-d.ts new file mode 100644 index 0000000000..351c2765d7 --- /dev/null +++ b/packages/octane-router/tests/router.test-d.ts @@ -0,0 +1,91 @@ +import { expectTypeOf } from 'vitest' +import { + createFileRoute, + createLink, + createRootRoute, + createRootRouteWithContext, + createRoute, + createRouter, + linkOptions, +} from '../src' +import type { ComponentBody } from 'octane' +import type { AnyRoute } from '@tanstack/router-core' + +const contextualRoot = createRootRouteWithContext<{ userId: string }>()({ + loader: ({ context }) => ({ owner: context.userId }), +}) + +const projectRoute = createRoute({ + getParentRoute: () => contextualRoot, + path: 'projects/$projectId', + loader: ({ params, context }) => ({ + projectId: params.projectId, + owner: context.userId, + }), +}) + +const routeTree = contextualRoot.addChildren([projectRoute]) +const _router = createRouter({ + routeTree, + context: { userId: 'tanner' }, +}) + +expectTypeOf(projectRoute.id).toEqualTypeOf<'/projects/$projectId'>() +expectTypeOf(projectRoute.fullPath).toEqualTypeOf<'/projects/$projectId'>() +expectTypeOf(projectRoute.useParams()).toEqualTypeOf<{ + projectId: string +}>() +expectTypeOf(projectRoute.useLoaderData()).toEqualTypeOf<{ + projectId: string + owner: string +}>() +expectTypeOf(contextualRoot.useRouteContext()).toEqualTypeOf<{ + userId: string +}>() + +const reusableLink = linkOptions< + { + from: '/projects/$projectId' + to: '/projects/$projectId' + params: { projectId: string } + }, + typeof _router +>({ + from: '/projects/$projectId', + to: '/projects/$projectId', + params: { projectId: 'router' }, +}) +expectTypeOf(reusableLink.params).toEqualTypeOf<{ projectId: string }>() + +const DesignLink: ComponentBody<{ + variant: 'primary' + href?: string + children?: unknown +}> = () => {} +const CustomLink = createLink(DesignLink) +CustomLink({ + variant: 'primary', + to: '/', +}) + +const _fileRoot = createRootRoute() + +declare module '@tanstack/router-core' { + interface FileRoutesByPath { + '/posts/$postId': { + preLoaderRoute: AnyRoute + parentRoute: typeof _fileRoot + id: '/posts/$postId' + fullPath: '/posts/$postId' + path: 'posts/$postId' + } + } +} + +const postRoute = createFileRoute('/posts/$postId')({ + loader: ({ params }) => params.postId, +}) + +expectTypeOf(postRoute.id).toEqualTypeOf<'/posts/$postId'>() +expectTypeOf(postRoute.path).toEqualTypeOf<'posts/$postId'>() +expectTypeOf(postRoute.types.loaderData).toEqualTypeOf() diff --git a/packages/octane-router/tests/setup.ts b/packages/octane-router/tests/setup.ts new file mode 100644 index 0000000000..85979921ac --- /dev/null +++ b/packages/octane-router/tests/setup.ts @@ -0,0 +1,7 @@ +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'scrollTo', { + configurable: true, + writable: true, + value: () => {}, + }) +} diff --git a/packages/octane-router/tsconfig.build.json b/packages/octane-router/tsconfig.build.json new file mode 100644 index 0000000000..ea97605aae --- /dev/null +++ b/packages/octane-router/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "Bundler", + "noEmit": true + }, + "include": ["src"] +} diff --git a/packages/octane-router/tsconfig.json b/packages/octane-router/tsconfig.json new file mode 100644 index 0000000000..8a870f3aeb --- /dev/null +++ b/packages/octane-router/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": [ + "src", + "tests/**/*.test-d.ts", + "tests/externalHydration.test.ts", + "tests/generator-plugin.test.ts", + "tests/setup.ts", + "vite.config.ts", + "eslint.config.ts" + ] +} diff --git a/packages/octane-router/vite.config.ts b/packages/octane-router/vite.config.ts new file mode 100644 index 0000000000..c050f9b3db --- /dev/null +++ b/packages/octane-router/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' +import { octane } from 'octane/compiler/vite' +import packageJson from './package.json' + +export default defineConfig({ + plugins: [octane()], + ...(process.env.VITEST && { + resolve: { + conditions: ['development'], + }, + }), + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + typecheck: { enabled: true }, + }, +}) diff --git a/packages/octane-start-client/CHANGELOG.md b/packages/octane-start-client/CHANGELOG.md new file mode 100644 index 0000000000..65baceac78 --- /dev/null +++ b/packages/octane-start-client/CHANGELOG.md @@ -0,0 +1 @@ +# @tanstack/octane-start-client diff --git a/packages/octane-start-client/README.md b/packages/octane-start-client/README.md new file mode 100644 index 0000000000..cb5cfaef41 --- /dev/null +++ b/packages/octane-start-client/README.md @@ -0,0 +1,3 @@ +# TanStack Octane Start - Client + +This package is an internal dependency of [`@tanstack/octane-start`](https://www.npmjs.com/package/@tanstack/octane-start) and is not intended to be installed directly. diff --git a/packages/octane-start-client/eslint.config.ts b/packages/octane-start-client/eslint.config.ts new file mode 100644 index 0000000000..d1557ca862 --- /dev/null +++ b/packages/octane-start-client/eslint.config.ts @@ -0,0 +1,3 @@ +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/octane-start-client/octane-compiler.d.ts b/packages/octane-start-client/octane-compiler.d.ts new file mode 100644 index 0000000000..9fd5fc221d --- /dev/null +++ b/packages/octane-start-client/octane-compiler.d.ts @@ -0,0 +1,5 @@ +declare module 'octane/compiler/vite' { + import type { Plugin } from 'vite' + + export function octane(options?: unknown): Plugin +} diff --git a/packages/octane-start-client/package.json b/packages/octane-start-client/package.json new file mode 100644 index 0000000000..81a990380f --- /dev/null +++ b/packages/octane-start-client/package.json @@ -0,0 +1,74 @@ +{ + "name": "@tanstack/octane-start-client", + "version": "1.168.16", + "description": "Client runtime for TanStack Start applications built with Octane", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/router.git", + "directory": "packages/octane-start-client" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "location", + "router", + "routing", + "async", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:unit:dev": "vitest --watch", + "test:eslint": "eslint ./src", + "test:types": "tsc --noEmit", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=22.12.0" + }, + "octane": { + "hookSlots": { + "manual": [ + "dist" + ] + } + }, + "dependencies": { + "@tanstack/octane-router": "workspace:*", + "@tanstack/router-core": "workspace:*", + "@tanstack/start-client-core": "workspace:*" + }, + "devDependencies": { + "@types/node": ">=22", + "octane": "catalog:", + "vite": "*" + }, + "peerDependencies": { + "octane": ">=0.1.8" + } +} diff --git a/packages/octane-start-client/src/StartClient.ts b/packages/octane-start-client/src/StartClient.ts new file mode 100644 index 0000000000..218cc329c1 --- /dev/null +++ b/packages/octane-start-client/src/StartClient.ts @@ -0,0 +1,21 @@ +import { HYDRATION_RANGE_BOUNDARY, createElement, useEffect } from 'octane' +import { RouterProvider } from '@tanstack/octane-router' +import type { AnyRouter } from '@tanstack/router-core' + +export interface StartClientProps { + router: AnyRouter +} + +export function StartClient({ router }: StartClientProps) { + useEffect(() => { + window.$_TSR?.h() + }, []) + + return createElement(RouterProvider, { router }) +} + +;( + StartClient as typeof StartClient & { + [HYDRATION_RANGE_BOUNDARY]: 'passthrough' + } +)[HYDRATION_RANGE_BOUNDARY] = 'passthrough' diff --git a/packages/octane-start-client/src/hydrateStart.ts b/packages/octane-start-client/src/hydrateStart.ts new file mode 100644 index 0000000000..a1fca5f585 --- /dev/null +++ b/packages/octane-start-client/src/hydrateStart.ts @@ -0,0 +1,53 @@ +import { hydrateStart as coreHydrateStart } from '@tanstack/start-client-core/client' +import type { AnyRouter } from '@tanstack/router-core' + +type StoreSubscription = { + unsubscribe: () => void +} + +type SubscribableStore = { + get: () => TValue + subscribe: (listener: () => void) => StoreSubscription +} + +/** + * Octane batches router store writes in a transition. The core hydration + * promise can therefore settle before the hydrated matches are observable to + * the first render. Wait for that commit so hydrateRoot never adopts the + * server document with an empty match tree. + */ +export function waitForRouterMatches(router: AnyRouter): Promise { + const matchesId = router.stores.matchesId as unknown as SubscribableStore< + Array + > + + if (matchesId.get().length > 0) { + return Promise.resolve() + } + + return new Promise((resolve) => { + let resolved = false + + const finish = () => { + if (resolved || matchesId.get().length === 0) { + return + } + + resolved = true + subscription.unsubscribe() + resolve() + } + + const subscription = matchesId.subscribe(finish) + + // Close the race where the transition commits between the initial read and + // listener registration without synchronously notifying this listener. + finish() + }) +} + +export async function hydrateStart(): Promise { + const router = await coreHydrateStart() + await waitForRouterMatches(router) + return router +} diff --git a/packages/octane-start-client/src/index.ts b/packages/octane-start-client/src/index.ts new file mode 100644 index 0000000000..9e883adf54 --- /dev/null +++ b/packages/octane-start-client/src/index.ts @@ -0,0 +1,3 @@ +export { StartClient } from './StartClient' +export type { StartClientProps } from './StartClient' +export { hydrateStart } from './hydrateStart' diff --git a/packages/octane-start-client/src/tests/StartClient.test.ts b/packages/octane-start-client/src/tests/StartClient.test.ts new file mode 100644 index 0000000000..8a288aa062 --- /dev/null +++ b/packages/octane-start-client/src/tests/StartClient.test.ts @@ -0,0 +1,58 @@ +import { + HYDRATION_RANGE_BOUNDARY, + createRoot, + drainPassiveEffects, + flushSync, + useEffect, +} from 'octane' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { AnyRouter } from '@tanstack/router-core' + +afterEach(() => { + vi.doUnmock('@tanstack/octane-router') + vi.resetModules() +}) + +describe('StartClient', () => { + it('signals router hydration after descendant passive effects', async () => { + const order: Array = [] + + function RouterProvider() { + useEffect(() => { + order.push('child') + }, []) + } + + vi.doMock('@tanstack/octane-router', () => ({ RouterProvider })) + const { StartClient } = await import('../StartClient') + + expect( + ( + StartClient as typeof StartClient & { + [HYDRATION_RANGE_BOUNDARY]?: string + } + )[HYDRATION_RANGE_BOUNDARY], + ).toBe('passthrough') + + const previousTsr = window.$_TSR + window.$_TSR = { + h: () => { + order.push('router') + }, + } as typeof window.$_TSR + + const container = document.createElement('div') + const root = createRoot(container) + + try { + root.render(StartClient, { router: {} as AnyRouter }) + flushSync(() => {}) + drainPassiveEffects() + + expect(order).toEqual(['child', 'router']) + } finally { + root.unmount() + window.$_TSR = previousTsr + } + }) +}) diff --git a/packages/octane-start-client/src/tests/hydrateStart.test.ts b/packages/octane-start-client/src/tests/hydrateStart.test.ts new file mode 100644 index 0000000000..78eb4e771d --- /dev/null +++ b/packages/octane-start-client/src/tests/hydrateStart.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { hydrateStart, waitForRouterMatches } from '../hydrateStart' +import type { AnyRouter } from '@tanstack/router-core' + +const { coreHydrateStart } = vi.hoisted(() => ({ + coreHydrateStart: vi.fn<() => Promise>(), +})) + +vi.mock('@tanstack/start-client-core/client', () => ({ + hydrateStart: coreHydrateStart, +})) + +type TestMatchesStore = { + get: () => Array + subscribe: (listener: () => void) => { unsubscribe: () => void } + commit: (value: Array, notify?: boolean) => void + subscriberCount: () => number +} + +function createMatchesStore( + initialValue: Array = [], +): TestMatchesStore { + let value = initialValue + const listeners = new Set<() => void>() + + return { + get: () => value, + subscribe(listener) { + listeners.add(listener) + return { + unsubscribe: () => listeners.delete(listener), + } + }, + commit(nextValue, notify = true) { + value = nextValue + if (notify) { + listeners.forEach((listener) => listener()) + } + }, + subscriberCount: () => listeners.size, + } +} + +function createRouterWithMatchesStore(store: TestMatchesStore): AnyRouter { + return { + stores: { + matchesId: store, + }, + } as unknown as AnyRouter +} + +describe('Octane Start hydration readiness', () => { + beforeEach(() => { + coreHydrateStart.mockReset() + }) + + it('waits for the transitioned match commit before resolving hydration', async () => { + const store = createMatchesStore() + const router = createRouterWithMatchesStore(store) + coreHydrateStart.mockResolvedValue(router) + + let hydrated = false + const promise = hydrateStart().then((result) => { + hydrated = true + return result + }) + + await Promise.resolve() + await Promise.resolve() + + expect(hydrated).toBe(false) + expect(store.subscriberCount()).toBe(1) + + store.commit(['__root__']) + + await expect(promise).resolves.toBe(router) + expect(store.subscriberCount()).toBe(0) + }) + + it('resolves immediately when hydration already committed matches', async () => { + const store = createMatchesStore(['__root__']) + const router = createRouterWithMatchesStore(store) + + await expect(waitForRouterMatches(router)).resolves.toBeUndefined() + expect(store.subscriberCount()).toBe(0) + }) + + it('closes a commit race between the initial read and subscription', async () => { + const store = createMatchesStore() + const subscribe = store.subscribe + store.subscribe = (listener) => { + store.commit(['__root__'], false) + return subscribe(listener) + } + const router = createRouterWithMatchesStore(store) + + await expect(waitForRouterMatches(router)).resolves.toBeUndefined() + expect(store.subscriberCount()).toBe(0) + }) +}) diff --git a/packages/octane-start-client/tsconfig.build.json b/packages/octane-start-client/tsconfig.build.json new file mode 100644 index 0000000000..92be18587f --- /dev/null +++ b/packages/octane-start-client/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/octane-start-client/tsconfig.json b/packages/octane-start-client/tsconfig.json new file mode 100644 index 0000000000..51fcc049cd --- /dev/null +++ b/packages/octane-start-client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"] + }, + "include": ["src", "vite.config.ts", "octane-compiler.d.ts"] +} diff --git a/packages/octane-start-client/vite.config.ts b/packages/octane-start-client/vite.config.ts new file mode 100644 index 0000000000..8c7b3d7a87 --- /dev/null +++ b/packages/octane-start-client/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import { octane } from 'octane/compiler/vite' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [octane()], + 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.ts', + cjs: false, + }), +) diff --git a/packages/octane-start-server/CHANGELOG.md b/packages/octane-start-server/CHANGELOG.md new file mode 100644 index 0000000000..56704ba171 --- /dev/null +++ b/packages/octane-start-server/CHANGELOG.md @@ -0,0 +1 @@ +# @tanstack/octane-start-server diff --git a/packages/octane-start-server/README.md b/packages/octane-start-server/README.md new file mode 100644 index 0000000000..e45dc1a207 --- /dev/null +++ b/packages/octane-start-server/README.md @@ -0,0 +1,3 @@ +# TanStack Octane Start - Server + +This package is an internal dependency of [`@tanstack/octane-start`](https://www.npmjs.com/package/@tanstack/octane-start) and is not intended to be installed directly. diff --git a/packages/octane-start-server/eslint.config.ts b/packages/octane-start-server/eslint.config.ts new file mode 100644 index 0000000000..d1557ca862 --- /dev/null +++ b/packages/octane-start-server/eslint.config.ts @@ -0,0 +1,3 @@ +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/octane-start-server/package.json b/packages/octane-start-server/package.json new file mode 100644 index 0000000000..bc8cda2b11 --- /dev/null +++ b/packages/octane-start-server/package.json @@ -0,0 +1,66 @@ +{ + "name": "@tanstack/octane-start-server", + "version": "1.167.22", + "description": "Server runtime for TanStack Start applications built with Octane", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/router.git", + "directory": "packages/octane-start-server" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "location", + "router", + "routing", + "async", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit", + "test:unit": "vitest", + "test:eslint": "eslint ./src", + "test:types": "tsc --noEmit", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=22.12.0" + }, + "dependencies": { + "@tanstack/octane-router": "workspace:*", + "@tanstack/router-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*" + }, + "devDependencies": { + "@types/node": ">=22", + "octane": "catalog:", + "vite": "*" + }, + "peerDependencies": { + "octane": ">=0.1.8" + } +} diff --git a/packages/octane-start-server/src/StartServer.ts b/packages/octane-start-server/src/StartServer.ts new file mode 100644 index 0000000000..0b9ff9c3e3 --- /dev/null +++ b/packages/octane-start-server/src/StartServer.ts @@ -0,0 +1,18 @@ +import { createElement } from 'octane/server' +import { RouterProvider } from '@tanstack/octane-router' +import type { ElementDescriptor } from 'octane' +import type { AnyRouter } from '@tanstack/router-core' + +type ServerRenderable = Parameters[0] + +export interface StartServerProps { + router: AnyRouter +} + +export function StartServer({ + router, +}: StartServerProps): ElementDescriptor { + return createElement(RouterProvider as unknown as ServerRenderable, { + router, + }) as unknown as ElementDescriptor +} diff --git a/packages/octane-start-server/src/defaultRenderHandler.ts b/packages/octane-start-server/src/defaultRenderHandler.ts new file mode 100644 index 0000000000..e3233784d7 --- /dev/null +++ b/packages/octane-start-server/src/defaultRenderHandler.ts @@ -0,0 +1,12 @@ +import { defineHandlerCallback } from '@tanstack/start-server-core' +import { renderRouterToString } from '@tanstack/octane-router/ssr/server' +import { StartServer } from './StartServer' + +export const defaultRenderHandler = defineHandlerCallback( + ({ router, responseHeaders }) => + renderRouterToString({ + router, + responseHeaders, + App: StartServer, + }), +) diff --git a/packages/octane-start-server/src/defaultStreamHandler.ts b/packages/octane-start-server/src/defaultStreamHandler.ts new file mode 100644 index 0000000000..f4809423ea --- /dev/null +++ b/packages/octane-start-server/src/defaultStreamHandler.ts @@ -0,0 +1,13 @@ +import { defineHandlerCallback } from '@tanstack/start-server-core' +import { renderRouterToStream } from '@tanstack/octane-router/ssr/server' +import { StartServer } from './StartServer' + +export const defaultStreamHandler = defineHandlerCallback( + async ({ request, router, responseHeaders }) => + await renderRouterToStream({ + request, + router, + responseHeaders, + App: StartServer, + }), +) diff --git a/packages/octane-start-server/src/index.ts b/packages/octane-start-server/src/index.ts new file mode 100644 index 0000000000..90293bfacf --- /dev/null +++ b/packages/octane-start-server/src/index.ts @@ -0,0 +1,9 @@ +export { StartServer } from './StartServer' +export type { StartServerProps } from './StartServer' +export { defaultStreamHandler } from './defaultStreamHandler' +export { defaultRenderHandler } from './defaultRenderHandler' +export type { + RequestHandler, + RequestOptions, +} from '@tanstack/start-server-core' +export * from '@tanstack/start-server-core' diff --git a/packages/octane-start-server/src/tests/StartServer.test.ts b/packages/octane-start-server/src/tests/StartServer.test.ts new file mode 100644 index 0000000000..9701621d84 --- /dev/null +++ b/packages/octane-start-server/src/tests/StartServer.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it, vi } from 'vitest' +import { StartServer } from '../StartServer' +import type { AnyRouter } from '@tanstack/router-core' + +const { RouterProvider } = vi.hoisted(() => ({ + RouterProvider: () => undefined, +})) + +vi.mock('@tanstack/octane-router', () => ({ RouterProvider })) + +describe('StartServer', () => { + it('passes the request router to the Octane router provider', () => { + const router = {} as AnyRouter + const element = StartServer({ router }) + + expect(element.type).toBe(RouterProvider) + expect(element.props).toEqual({ router }) + }) +}) diff --git a/packages/octane-start-server/tsconfig.build.json b/packages/octane-start-server/tsconfig.build.json new file mode 100644 index 0000000000..92be18587f --- /dev/null +++ b/packages/octane-start-server/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/octane-start-server/tsconfig.json b/packages/octane-start-server/tsconfig.json new file mode 100644 index 0000000000..2e58ce654d --- /dev/null +++ b/packages/octane-start-server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/octane-start-server/vite.config.ts b/packages/octane-start-server/vite.config.ts new file mode 100644 index 0000000000..0e1b2c24c1 --- /dev/null +++ b/packages/octane-start-server/vite.config.ts @@ -0,0 +1,22 @@ +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: 'node', + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + tsconfigPath: './tsconfig.build.json', + srcDir: './src', + entry: './src/index.ts', + cjs: false, + }), +) diff --git a/packages/octane-start/CHANGELOG.md b/packages/octane-start/CHANGELOG.md new file mode 100644 index 0000000000..278f58bbef --- /dev/null +++ b/packages/octane-start/CHANGELOG.md @@ -0,0 +1 @@ +# @tanstack/octane-start diff --git a/packages/octane-start/README.md b/packages/octane-start/README.md new file mode 100644 index 0000000000..1272a1c8d8 --- /dev/null +++ b/packages/octane-start/README.md @@ -0,0 +1,5 @@ +# TanStack Octane Start + +SSR, streaming, server functions, server routes, and bundling for Octane, powered by [TanStack Router](https://tanstack.com/router) and Vite. + +Visit [tanstack.com/start](https://tanstack.com/start) for TanStack Start documentation. diff --git a/packages/octane-start/eslint.config.ts b/packages/octane-start/eslint.config.ts new file mode 100644 index 0000000000..d1557ca862 --- /dev/null +++ b/packages/octane-start/eslint.config.ts @@ -0,0 +1,3 @@ +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/octane-start/package.json b/packages/octane-start/package.json new file mode 100644 index 0000000000..7efe2b1ed0 --- /dev/null +++ b/packages/octane-start/package.json @@ -0,0 +1,132 @@ +{ + "name": "@tanstack/octane-start", + "version": "1.168.28", + "description": "Full-stack application framework for Octane built on TanStack Router", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/router.git", + "directory": "packages/octane-start" + }, + "homepage": "https://tanstack.com/start", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "location", + "router", + "routing", + "async", + "typescript" + ], + "scripts": { + "clean": "rimraf ./dist && rimraf ./coverage", + "test": "pnpm test:eslint && pnpm test:types && pnpm test:unit && pnpm test:build", + "test:eslint": "eslint ./src", + "test:types": "tsc --noEmit", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", + "test:build": "publint --strict && attw --ignore-rules no-resolution --pack .", + "build": "vite build && vite build -c vite.config.server-entry.ts" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./client": { + "import": { + "types": "./dist/esm/client.d.ts", + "default": "./dist/esm/client.js" + } + }, + "./client-rpc": { + "import": { + "types": "./dist/esm/client-rpc.d.ts", + "default": "./dist/esm/client-rpc.js" + } + }, + "./server-rpc": { + "import": { + "types": "./dist/esm/server-rpc.d.ts", + "default": "./dist/esm/server-rpc.js" + } + }, + "./ssr-rpc": { + "import": { + "types": "./dist/esm/ssr-rpc.d.ts", + "default": "./dist/esm/ssr-rpc.js" + } + }, + "./server": { + "import": { + "types": "./dist/esm/server.d.ts", + "default": "./dist/esm/server.js" + } + }, + "./plugin/vite": { + "import": { + "types": "./dist/esm/plugin/vite.d.ts", + "default": "./dist/esm/plugin/vite.js" + } + }, + "./server-entry": { + "import": { + "types": "./dist/default-entry/esm/server.d.ts", + "default": "./dist/default-entry/esm/server.js" + } + }, + "./server-only": { + "import": { + "types": "./dist/esm/server-only.d.ts", + "default": "./dist/esm/server-only.js" + } + }, + "./client-only": { + "import": { + "types": "./dist/esm/client-only.d.ts", + "default": "./dist/esm/client-only.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=22.12.0" + }, + "dependencies": { + "@tanstack/octane-router": "workspace:*", + "@tanstack/octane-start-client": "workspace:*", + "@tanstack/octane-start-server": "workspace:*", + "@tanstack/start-client-core": "workspace:*", + "@tanstack/start-plugin-core": "workspace:*", + "@tanstack/start-server-core": "workspace:*", + "pathe": "^2.0.3" + }, + "devDependencies": { + "@tanstack/router-utils": "workspace:*", + "@types/node": ">=20", + "octane": "catalog:", + "vite": "*" + }, + "peerDependencies": { + "octane": ">=0.1.8", + "vite": ">=7.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } +} diff --git a/packages/octane-start/src/client-only.ts b/packages/octane-start/src/client-only.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/packages/octane-start/src/client-only.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/octane-start/src/client-rpc.ts b/packages/octane-start/src/client-rpc.ts new file mode 100644 index 0000000000..ca68ae47e9 --- /dev/null +++ b/packages/octane-start/src/client-rpc.ts @@ -0,0 +1 @@ +export * from '@tanstack/start-client-core/client-rpc' diff --git a/packages/octane-start/src/client.ts b/packages/octane-start/src/client.ts new file mode 100644 index 0000000000..3e3253cd34 --- /dev/null +++ b/packages/octane-start/src/client.ts @@ -0,0 +1 @@ +export * from '@tanstack/octane-start-client' diff --git a/packages/octane-start/src/default-entry/client.ts b/packages/octane-start/src/default-entry/client.ts new file mode 100644 index 0000000000..9eecfb17eb --- /dev/null +++ b/packages/octane-start/src/default-entry/client.ts @@ -0,0 +1,12 @@ +import { hydrateRoot } from 'octane' +import { StartClient, hydrateStart } from '@tanstack/octane-start/client' + +hydrateStart().then((router) => { + const container = document.getElementById('__app') + + if (!container) { + throw new Error('TanStack Start could not find the Octane hydration root.') + } + + hydrateRoot(container, StartClient, { router }) +}) diff --git a/packages/octane-start/src/default-entry/server.ts b/packages/octane-start/src/default-entry/server.ts new file mode 100644 index 0000000000..c7b7fec209 --- /dev/null +++ b/packages/octane-start/src/default-entry/server.ts @@ -0,0 +1,20 @@ +import { + createStartHandler, + defaultStreamHandler, +} from '@tanstack/octane-start/server' +import type { Register } from '@tanstack/octane-router' +import type { RequestHandler } from '@tanstack/octane-start/server' + +const fetch = createStartHandler(defaultStreamHandler) + +export type ServerEntry = { fetch: RequestHandler } + +export function createServerEntry(entry: ServerEntry): ServerEntry { + return { + async fetch(...args) { + return await entry.fetch(...args) + }, + } +} + +export default createServerEntry({ fetch }) diff --git a/packages/octane-start/src/default-entry/start.ts b/packages/octane-start/src/default-entry/start.ts new file mode 100644 index 0000000000..087859454d --- /dev/null +++ b/packages/octane-start/src/default-entry/start.ts @@ -0,0 +1 @@ +export const startInstance = undefined diff --git a/packages/octane-start/src/index.ts b/packages/octane-start/src/index.ts new file mode 100644 index 0000000000..512b378017 --- /dev/null +++ b/packages/octane-start/src/index.ts @@ -0,0 +1,11 @@ +export { useServerFn } from './useServerFn' +export * from '@tanstack/start-client-core' +export { + createClientOnlyFn, + createCsrfMiddleware, + createIsomorphicFn, + createMiddleware, + createServerFn, + createServerOnlyFn, + createStart, +} from '@tanstack/start-client-core' diff --git a/packages/octane-start/src/octane-compiler.d.ts b/packages/octane-start/src/octane-compiler.d.ts new file mode 100644 index 0000000000..9fd5fc221d --- /dev/null +++ b/packages/octane-start/src/octane-compiler.d.ts @@ -0,0 +1,5 @@ +declare module 'octane/compiler/vite' { + import type { Plugin } from 'vite' + + export function octane(options?: unknown): Plugin +} diff --git a/packages/octane-start/src/plugin/shared.ts b/packages/octane-start/src/plugin/shared.ts new file mode 100644 index 0000000000..83560e70b4 --- /dev/null +++ b/packages/octane-start/src/plugin/shared.ts @@ -0,0 +1,17 @@ +import { fileURLToPath } from 'node:url' +import path from 'pathe' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const defaultEntryDir = path.resolve( + currentDir, + '..', + '..', + 'plugin', + 'default-entry', +) + +export const octaneStartDefaultEntryPaths = { + client: path.resolve(defaultEntryDir, 'client.ts'), + server: path.resolve(defaultEntryDir, 'server.ts'), + start: path.resolve(defaultEntryDir, 'start.ts'), +} diff --git a/packages/octane-start/src/plugin/validate-options.ts b/packages/octane-start/src/plugin/validate-options.ts new file mode 100644 index 0000000000..92080377d3 --- /dev/null +++ b/packages/octane-start/src/plugin/validate-options.ts @@ -0,0 +1,9 @@ +export function validateOctaneCompilerOptions( + octaneOptions: object | undefined, +): void { + if (octaneOptions && 'ssr' in octaneOptions) { + throw new Error( + '`octane.ssr` is not supported by TanStack Start. Start compiles its client and server environments separately and selects the correct Octane compilation mode for each one.', + ) + } +} diff --git a/packages/octane-start/src/plugin/vite.ts b/packages/octane-start/src/plugin/vite.ts new file mode 100644 index 0000000000..88e21743f4 --- /dev/null +++ b/packages/octane-start/src/plugin/vite.ts @@ -0,0 +1,97 @@ +import { + START_ENVIRONMENT_NAMES, + tanStackStartVite, +} from '@tanstack/start-plugin-core/vite' +import { octaneRouteGeneratorPlugin } from '@tanstack/octane-router/generator-plugin' +import { octane } from 'octane/compiler/vite' +import { octaneStartDefaultEntryPaths } from './shared' +import { validateOctaneCompilerOptions } from './validate-options' +import type { + TanStackStartViteInputConfig, + TanStackStartVitePluginCoreOptions, +} from '@tanstack/start-plugin-core/vite' +import type { PluginOption } from 'vite' + +export interface OctaneRendererDescriptor { + module: string + target?: 'dom' | 'universal' + server?: 'render' | 'client-only' | 'unsupported' + intrinsics?: string + text?: 'reject' | 'ignore' | 'host' + capabilities?: ReadonlyArray +} + +export interface OctaneRendererBoundary { + ownerRenderer: string + childRenderer: string + prop: string + server?: 'omit-child' +} + +export interface OctaneRendererRule { + include: string | ReadonlyArray + exclude?: string | ReadonlyArray + renderer: string +} + +export interface OctaneRendererConfig { + default?: string + registry?: Record + rules?: ReadonlyArray + boundaries?: Record> +} + +export interface OctaneCompilerOptions { + exclude?: ReadonlyArray + hmr?: boolean + profile?: boolean + renderers?: OctaneRendererConfig +} + +export type TanStackOctaneStartViteInputConfig = + TanStackStartViteInputConfig & { + octane?: OctaneCompilerOptions + } + +export function tanstackStart( + options?: TanStackOctaneStartViteInputConfig, +): Array { + const { octane: octaneOptions, ...startOptions } = options ?? {} + validateOctaneCompilerOptions(octaneOptions) + + const corePluginOptions: TanStackStartVitePluginCoreOptions = { + framework: 'octane', + defaultEntryPaths: octaneStartDefaultEntryPaths, + providerEnvironmentName: START_ENVIRONMENT_NAMES.server, + ssrIsProvider: true, + ssrResolverStrategy: { + type: 'default', + }, + routerGeneratorPlugins: [octaneRouteGeneratorPlugin()], + } + + return [ + octane(octaneOptions), + { + name: 'tanstack-octane-start:config', + configEnvironment(environmentName, options) { + return { + optimizeDeps: + environmentName === START_ENVIRONMENT_NAMES.client || + (environmentName === START_ENVIRONMENT_NAMES.server && + options.optimizeDeps?.noDiscovery === false) + ? { + exclude: [ + '@tanstack/octane-start', + '@tanstack/octane-router', + '@tanstack/start-static-server-functions', + 'octane', + ], + } + : undefined, + } + }, + }, + tanStackStartVite(corePluginOptions, startOptions), + ] +} diff --git a/packages/octane-start/src/server-only.ts b/packages/octane-start/src/server-only.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/packages/octane-start/src/server-only.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/octane-start/src/server-rpc.ts b/packages/octane-start/src/server-rpc.ts new file mode 100644 index 0000000000..76cee4dbc0 --- /dev/null +++ b/packages/octane-start/src/server-rpc.ts @@ -0,0 +1 @@ +export { createServerRpc } from '@tanstack/start-server-core/createServerRpc' diff --git a/packages/octane-start/src/server.ts b/packages/octane-start/src/server.ts new file mode 100644 index 0000000000..80d3631b61 --- /dev/null +++ b/packages/octane-start/src/server.ts @@ -0,0 +1 @@ +export * from '@tanstack/octane-start-server' diff --git a/packages/octane-start/src/ssr-rpc.ts b/packages/octane-start/src/ssr-rpc.ts new file mode 100644 index 0000000000..568429b567 --- /dev/null +++ b/packages/octane-start/src/ssr-rpc.ts @@ -0,0 +1 @@ +export { createSsrRpc } from '@tanstack/start-server-core/createSsrRpc' diff --git a/packages/octane-start/src/useServerFn.ts b/packages/octane-start/src/useServerFn.ts new file mode 100644 index 0000000000..20afb05f94 --- /dev/null +++ b/packages/octane-start/src/useServerFn.ts @@ -0,0 +1,26 @@ +import { isRedirect, useRouter } from '@tanstack/octane-router' + +export function useServerFn) => Promise>( + serverFn: T, +): (...args: Parameters) => ReturnType { + const router = useRouter() + + return (async (...args: Array) => { + try { + const response = await serverFn(...args) + + if (isRedirect(response)) { + throw response + } + + return response + } catch (error) { + if (isRedirect(error)) { + error.options._fromLocation = router.stores.location.get() + return router.navigate(router.resolveRedirect(error).options) + } + + throw error + } + }) as (...args: Parameters) => ReturnType +} diff --git a/packages/octane-start/tests/plugin.test.ts b/packages/octane-start/tests/plugin.test.ts new file mode 100644 index 0000000000..64845e9b98 --- /dev/null +++ b/packages/octane-start/tests/plugin.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { validateOctaneCompilerOptions } from '../src/plugin/validate-options' +import type { OctaneCompilerOptions } from '../src/plugin/vite' + +describe('Octane compiler options', () => { + it('accepts readonly renderer presets', () => { + const options = { + renderers: { + registry: { + three: { + module: '@octanejs/three/renderer', + target: 'universal', + server: 'client-only', + capabilities: ['visibility', 'portal'], + }, + }, + rules: [{ include: '**/*.three.tsrx', renderer: 'three' }], + }, + } as const satisfies OctaneCompilerOptions + + expect(options.renderers.rules[0].renderer).toBe('three') + }) + + it('rejects a forced Octane SSR compilation mode', () => { + const options: OctaneCompilerOptions = { + // @ts-expect-error Start owns the per-environment SSR mode. + ssr: true, + } + + expect(() => validateOctaneCompilerOptions(options)).toThrowError( + '`octane.ssr` is not supported by TanStack Start', + ) + }) +}) diff --git a/packages/octane-start/tsconfig.build.json b/packages/octane-start/tsconfig.build.json new file mode 100644 index 0000000000..92be18587f --- /dev/null +++ b/packages/octane-start/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/octane-start/tsconfig.json b/packages/octane-start/tsconfig.json new file mode 100644 index 0000000000..b477484474 --- /dev/null +++ b/packages/octane-start/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "paths": { + "@tanstack/octane-start/client": ["./src/client.ts"] + }, + "types": ["node"] + }, + "include": ["src", "tests", "vite.config.ts", "vite.config.server-entry.ts"] +} diff --git a/packages/octane-start/tsconfig.server-entry.json b/packages/octane-start/tsconfig.server-entry.json new file mode 100644 index 0000000000..86be8a7a6e --- /dev/null +++ b/packages/octane-start/tsconfig.server-entry.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "paths": { + "@tanstack/octane-start/server": ["./src/server.ts"] + } + }, + "include": ["src/default-entry"] +} diff --git a/packages/octane-start/vite.config.server-entry.ts b/packages/octane-start/vite.config.server-entry.ts new file mode 100644 index 0000000000..4980908c1c --- /dev/null +++ b/packages/octane-start/vite.config.server-entry.ts @@ -0,0 +1,17 @@ +import { tanstackViteConfig } from '@tanstack/vite-config' +import { mergeConfig, defineConfig } from 'vitest/config' + +const config = defineConfig({}) + +export default mergeConfig( + config, + tanstackViteConfig({ + tsconfigPath: './tsconfig.server-entry.json', + srcDir: './src/default-entry', + exclude: ['./src/default-entry/client.ts'], + entry: ['./src/default-entry/server.ts'], + externalDeps: ['@tanstack/octane-start/server'], + outDir: './dist/default-entry', + cjs: false, + }), +) diff --git a/packages/octane-start/vite.config.ts b/packages/octane-start/vite.config.ts new file mode 100644 index 0000000000..b99f5eda9d --- /dev/null +++ b/packages/octane-start/vite.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import { copyFilesPlugin } from '@tanstack/router-utils' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [ + copyFilesPlugin({ + pattern: ['*.ts', '!*.d.ts'], + fromDir: 'src/default-entry', + toDir: 'dist/plugin/default-entry', + }), + ], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'node', + typecheck: { enabled: true }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + tsconfigPath: './tsconfig.build.json', + srcDir: './src', + exclude: ['./src/default-entry'], + entry: [ + './src/index.ts', + './src/client.ts', + './src/client-rpc.ts', + './src/ssr-rpc.ts', + './src/server-rpc.ts', + './src/server.ts', + './src/plugin/vite.ts', + './src/server-only.ts', + './src/client-only.ts', + ], + externalDeps: [ + '@tanstack/octane-start-client', + '@tanstack/octane-start-server', + ], + cjs: false, + }), +) diff --git a/packages/router-cli/package.json b/packages/router-cli/package.json index 7bff76b1b8..863cce0b2e 100644 --- a/packages/router-cli/package.json +++ b/packages/router-cli/package.json @@ -26,6 +26,8 @@ "scripts": { "clean": "rimraf ./dist && rimraf ./coverage", "test:eslint": "eslint ./src", + "test:unit": "vitest", + "test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests", "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", diff --git a/packages/router-cli/src/create-generator.ts b/packages/router-cli/src/create-generator.ts new file mode 100644 index 0000000000..7b89abd1c0 --- /dev/null +++ b/packages/router-cli/src/create-generator.ts @@ -0,0 +1,56 @@ +import path from 'node:path' +import { createRequire } from 'node:module' +import { pathToFileURL } from 'node:url' +import { Generator } from '@tanstack/router-generator' +import type { Config, GeneratorPlugin } from '@tanstack/router-generator' + +type OctaneGeneratorPluginModule = { + octaneRouteGeneratorPlugin?: () => GeneratorPlugin +} + +export async function createGenerator(config: Config, root: string) { + const frameworkPlugins = await getFrameworkGeneratorPlugins(config, root) + + return new Generator({ + config: { + ...config, + plugins: [...frameworkPlugins, ...(config.plugins ?? [])], + }, + root, + }) +} + +async function getFrameworkGeneratorPlugins( + config: Config, + root: string, +): Promise> { + if (config.target !== 'octane') { + return [] + } + + const requireFromProject = createRequire(path.resolve(root, 'package.json')) + let pluginPath: string + + try { + pluginPath = requireFromProject.resolve( + '@tanstack/octane-router/generator-plugin', + ) + } catch (cause) { + throw new Error( + 'The Octane router target requires `@tanstack/octane-router` to be installed in the project.', + { cause }, + ) + } + + const pluginModule = (await import( + /* @vite-ignore */ pathToFileURL(pluginPath).href + )) as OctaneGeneratorPluginModule + + if (typeof pluginModule.octaneRouteGeneratorPlugin !== 'function') { + throw new Error( + '`@tanstack/octane-router/generator-plugin` does not export `octaneRouteGeneratorPlugin`.', + ) + } + + return [pluginModule.octaneRouteGeneratorPlugin()] +} diff --git a/packages/router-cli/src/generate.ts b/packages/router-cli/src/generate.ts index 158de37b86..8936ecaf04 100644 --- a/packages/router-cli/src/generate.ts +++ b/packages/router-cli/src/generate.ts @@ -1,12 +1,9 @@ -import { Generator } from '@tanstack/router-generator' +import { createGenerator } from './create-generator' import type { Config } from '@tanstack/router-generator' export async function generate(config: Config, root: string) { try { - const generator = new Generator({ - config, - root, - }) + const generator = await createGenerator(config, root) await generator.run() process.exit(0) } catch (err) { diff --git a/packages/router-cli/src/watch.ts b/packages/router-cli/src/watch.ts index bf165a374b..a1afe26e23 100644 --- a/packages/router-cli/src/watch.ts +++ b/packages/router-cli/src/watch.ts @@ -1,9 +1,6 @@ import chokidar from 'chokidar' -import { - Generator, - getConfig, - resolveConfigPath, -} from '@tanstack/router-generator' +import { getConfig, resolveConfigPath } from '@tanstack/router-generator' +import { createGenerator } from './create-generator' import type { FileEventType } from '@tanstack/router-generator' export function watch(root: string) { @@ -14,11 +11,11 @@ export function watch(root: string) { let watcher = new chokidar.FSWatcher({}) - const generatorWatcher = () => { + const generatorWatcher = async () => { const config = getConfig() - const generator = new Generator({ config, root }) + const generator = await createGenerator(config, root) - watcher.close() + await watcher.close() console.info(`TSR: Watching routes (${config.routesDirectory})...`) watcher = chokidar.watch(config.routesDirectory) @@ -56,6 +53,13 @@ export function watch(root: string) { }) } - configWatcher.on('ready', generatorWatcher) - configWatcher.on('change', generatorWatcher) + const reloadGenerator = () => { + void generatorWatcher().catch((err) => { + console.error(err) + console.info() + }) + } + + configWatcher.on('ready', reloadGenerator) + configWatcher.on('change', reloadGenerator) } diff --git a/packages/router-cli/tests/octane.test.ts b/packages/router-cli/tests/octane.test.ts new file mode 100644 index 0000000000..0c01ed4d0a --- /dev/null +++ b/packages/router-cli/tests/octane.test.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { getConfig } from '@tanstack/router-generator' +import { createGenerator } from '../src/create-generator' + +const tempRoots: Array = [] + +afterEach(async () => { + await Promise.all( + tempRoots.splice(0).map((root) => + fs.rm(root, { + recursive: true, + force: true, + }), + ), + ) +}) + +describe('Octane route generation', () => { + it('loads the Octane source adapter from the project', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'tsr-cli-octane-')) + tempRoots.push(root) + + const routesDirectory = path.join(root, 'src/routes') + const packageDirectory = path.join( + root, + 'node_modules/@tanstack/octane-router', + ) + await fs.mkdir(routesDirectory, { recursive: true }) + await fs.mkdir(path.dirname(packageDirectory), { recursive: true }) + await fs.writeFile(path.join(root, 'package.json'), '{"type":"module"}') + await fs.symlink( + path.resolve(import.meta.dirname, '../../octane-router'), + packageDirectory, + 'dir', + ) + + await fs.writeFile( + path.join(routesDirectory, '__root.tsrx'), + [ + "import { createRootRoute } from '@tanstack/octane-router'", + 'export const Route = createRootRoute({})', + ].join('\n'), + ) + await fs.writeFile( + path.join(routesDirectory, 'index.tsrx'), + [ + "import { createFileRoute } from '@tanstack/octane-router'", + "export const Route = createFileRoute('/stale')({ component: Page })", + 'function Page() @{
Octane
}', + ].join('\n'), + ) + + const config = getConfig({ + target: 'octane', + routesDirectory, + generatedRouteTree: path.join(root, 'src/routeTree.gen.ts'), + tmpDir: path.join(root, '.tanstack/tmp'), + disableLogging: true, + }) + const generator = await createGenerator(config, root) + + await generator.run() + + const routeSource = await fs.readFile( + path.join(routesDirectory, 'index.tsrx'), + 'utf8', + ) + const routeTree = await fs.readFile(config.generatedRouteTree, 'utf8') + + expect(routeSource).toContain("createFileRoute('/')") + expect(routeSource).toContain('function Page() @{
Octane
}') + expect(routeTree).toContain("'./routes/index.tsrx'") + }) +}) diff --git a/packages/router-cli/tsconfig.json b/packages/router-cli/tsconfig.json index 09d7037393..aed76c01b9 100644 --- a/packages/router-cli/tsconfig.json +++ b/packages/router-cli/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src", "vite.config.ts"], + "include": ["src", "tests", "vite.config.ts"], "compilerOptions": { "types": ["node"] } diff --git a/packages/router-cli/vite.config.ts b/packages/router-cli/vite.config.ts index f418b4e943..3c014d8c76 100644 --- a/packages/router-cli/vite.config.ts +++ b/packages/router-cli/vite.config.ts @@ -1,7 +1,16 @@ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' +import packageJson from './package.json' -const config = defineConfig({}) +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'node', + typecheck: { enabled: true }, + }, +}) export default mergeConfig( config, diff --git a/packages/router-generator/src/config.ts b/packages/router-generator/src/config.ts index 5c719183db..8b69bd9822 100644 --- a/packages/router-generator/src/config.ts +++ b/packages/router-generator/src/config.ts @@ -19,8 +19,10 @@ export type TokenMatcherJson = string | z.infer export type TokenMatcher = z.infer +export const routerFrameworks = ['react', 'solid', 'vue', 'octane'] as const + export const baseConfigSchema = z.object({ - target: z.enum(['react', 'solid', 'vue']).optional().default('react'), + target: z.enum(routerFrameworks).optional().default('react'), virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(), routeFilePrefix: z.string().optional(), routeFileIgnorePrefix: z.string().optional().default('-'), @@ -102,6 +104,7 @@ export function getConfig( const exists = existsSync(configFilePathJson) let config: Config + let rawConfig: Partial if (exists) { // Parse file config (allows JSON regex-object form) @@ -113,11 +116,20 @@ export function getConfig( ...fileConfigRaw, ...inlineConfig, } + rawConfig = merged config = configSchema.parse(merged) } else { + rawConfig = inlineConfig config = configSchema.parse(inlineConfig) } + // Vite does not resolve `.tsrx` from extensionless imports. Keep Octane's + // generated route tree runnable without requiring every app to repeat this + // target-specific setting, while still honoring an explicit override. + if (config.target === 'octane' && rawConfig.addExtensions === undefined) { + config.addExtensions = true + } + // If typescript is disabled, make sure the generated route tree is a .js file if (config.disableTypes) { config.generatedRouteTree = config.generatedRouteTree.replace( diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts index 09e7fc6c25..6c045c2579 100644 --- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts @@ -33,7 +33,8 @@ export interface TokenRegexBundle { routeTokenSegmentRegex: RegExp } -const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx|vue)/ +const disallowedRouteGroupConfiguration = + /\(([^)]+)\)\.(ts|js|tsx|jsx|vue|tsrx)$/ const virtualConfigFileRegExp = /__virtual\.[mc]?[jt]s$/ export function isVirtualConfigFile(fileName: string): boolean { @@ -170,7 +171,7 @@ export async function getRouteNodes( if (dirent.isDirectory()) { await recurse(relativePath) - } else if (fullPath.match(/\.(tsx|ts|jsx|js|vue)$/)) { + } else if (fullPath.match(/\.(tsx|ts|jsx|js|vue|tsrx)$/)) { const filePath = replaceBackslash(path.join(dir, dirent.name)) const filePathNoExt = removeExt(filePath) const { @@ -217,10 +218,11 @@ export async function getRouteNodes( routeType = 'pathless_layout' } - // Only show deprecation warning for .tsx/.ts files, not .vue files + // Only show deprecation warnings for route-module files, not Vue SFCs. // Vue files using .component.vue is the Vue-native way const isVueFile = filePath.endsWith('.vue') if (!isVueFile) { + const extension = path.extname(filePath) ;( [ ['component', 'component'], @@ -232,7 +234,7 @@ export async function getRouteNodes( ).forEach(([matcher, type]) => { if (routeType === matcher) { logger.warn( - `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`, + `WARNING: The \`.${type}${extension}\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy${extension}\` suffix instead.`, ) } }) diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts index 4203f2b44f..8a807f2582 100644 --- a/packages/router-generator/src/generator.ts +++ b/packages/router-generator/src/generator.ts @@ -45,6 +45,7 @@ import { transform } from './transform/transform' import { validateRouteParams } from './validate-route-params' import type { GeneratorPlugin } from './plugin/types' import type { TargetTemplate } from './template' +import type { TransformOptions } from './transform/types' import type { FsRouteType, GetRouteNodesResult, @@ -72,6 +73,17 @@ interface fs { chown: (filePath: string, uid: number, gid: number) => Promise } +function getRootRouteFileExtension(config: Config): string { + switch (config.target) { + case 'vue': + return 'vue' + case 'octane': + return 'tsrx' + default: + return config.disableTypes ? 'js' : 'tsx' + } +} + function getRoutePathSegmentMetadataForPath( node: RouteNode, routePath: string, @@ -303,6 +315,47 @@ export class Generator { ) } + private async fillRouteTemplate( + node: RouteNode, + template: string, + values: Parameters[2], + ): Promise { + return fillTemplate(this.config, template, values, async (source) => { + let result = source + let wasFormatted = false + for (const plugin of this.plugins) { + const pluginResult = await plugin.formatRoute?.({ + source: result, + node, + }) + if (pluginResult !== undefined) { + result = pluginResult + wasFormatted = true + } + } + return wasFormatted ? result : format(source, this.config) + }) + } + + private async getParseSource( + opts: Pick, + ): Promise { + let result = opts.source + let wasTransformed = false + for (const plugin of this.plugins) { + const pluginResult = await plugin.transformRouteSource?.({ + source: result, + filename: opts.filename ?? opts.node.fullPath, + node: opts.node, + }) + if (pluginResult !== undefined) { + result = pluginResult + wasTransformed = true + } + } + return wasTransformed ? result : undefined + } + public async run(event?: GeneratorEvent): Promise { if ( event && @@ -410,7 +463,8 @@ export class Generator { if (rootRouteNode === undefined) { let errorMessage = `rootRouteNode must not be undefined. Make sure you've added your root route into the route-tree.` if (!this.config.virtualRouteConfig) { - errorMessage += `\nMake sure that you add a "${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}" file to your routes directory.\nAdd the file in: "${this.config.routesDirectory}/${rootPathId}.${this.config.disableTypes ? 'js' : 'tsx'}"` + const rootRouteExtension = getRootRouteFileExtension(this.config) + errorMessage += `\nMake sure that you add a "${rootPathId}.${rootRouteExtension}" file to your routes directory.\nAdd the file in: "${this.config.routesDirectory}/${rootPathId}.${rootRouteExtension}"` } throw new Error(errorMessage) } @@ -1049,8 +1103,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved const tLazyRouteTemplate = this.targetTemplate.lazyRoute // Check by default check if the user has a specific lazy route template // If not, check if the user has a route template and use that instead - updatedCacheEntry.fileContent = await fillTemplate( - this.config, + updatedCacheEntry.fileContent = await this.fillRouteTemplate( + node, (this.config.customScaffolding?.lazyRouteTemplate || this.config.customScaffolding?.routeTemplate) ?? tLazyRouteTemplate.template(), @@ -1078,8 +1132,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved ).every((d) => d !== node._fsRouteType) ) { const tRouteTemplate = this.targetTemplate.route - updatedCacheEntry.fileContent = await fillTemplate( - this.config, + updatedCacheEntry.fileContent = await this.fillRouteTemplate( + node, this.config.customScaffolding?.routeTemplate ?? tRouteTemplate.template(), { @@ -1101,7 +1155,7 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved if (!isVueFile) { // transform the file - const transformResult = await transform({ + const transformOptions = { source: updatedCacheEntry.fileContent, filename: node.fullPath, ctx: { @@ -1110,6 +1164,10 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved lazy: node._fsRouteType === 'lazy', }, node, + } satisfies TransformOptions + const transformResult = await transform({ + ...transformOptions, + parseSource: await this.getParseSource(transformOptions), }) if (transformResult.result === 'no-route-export') { @@ -1360,8 +1418,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved // scaffold the root route if (!rootNodeFile.fileContent) { const rootTemplate = this.targetTemplate.rootRoute - const rootRouteContent = await fillTemplate( - this.config, + const rootRouteContent = await this.fillRouteTemplate( + node, rootTemplate.template(), { tsrImports: rootTemplate.imports.tsrImports(), diff --git a/packages/router-generator/src/index.ts b/packages/router-generator/src/index.ts index 9a4f8a17ff..e4d9a3add8 100644 --- a/packages/router-generator/src/index.ts +++ b/packages/router-generator/src/index.ts @@ -9,7 +9,11 @@ export type { Config, BaseConfig } from './config' export { Generator } from './generator' export type { FileEventType, FileEvent, GeneratorEvent } from './generator' -export type { GeneratorPlugin } from './plugin/types' +export type { + FormatRouteOptions, + GeneratorPlugin, + TransformRouteSourceOptions, +} from './plugin/types' export { capitalize, @@ -45,4 +49,8 @@ export { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getR export { rootPathId } from './filesystem/physical/rootPathId' -export type { TransformContext, TransformOptions } from './transform/types' +export type { + TransformContext, + TransformOptions, + TransformResult, +} from './transform/types' diff --git a/packages/router-generator/src/plugin/types.ts b/packages/router-generator/src/plugin/types.ts index 57d210b40b..d8952194b4 100644 --- a/packages/router-generator/src/plugin/types.ts +++ b/packages/router-generator/src/plugin/types.ts @@ -1,6 +1,17 @@ import type { HandleNodeAccumulator, RouteNode } from '../types' import type { Generator } from '../generator' +export interface TransformRouteSourceOptions { + source: string + filename: string + node: RouteNode +} + +export interface FormatRouteOptions { + source: string + node: RouteNode +} + export interface GeneratorPlugin { name: string init?: (opts: { generator: Generator }) => void @@ -15,4 +26,23 @@ export interface GeneratorPlugin { node: RouteNode prevNode: RouteNode | undefined }) => void + + /** + * Converts framework-specific route syntax into position-preserving + * TypeScript/JSX for route analysis. Hooks run in plugin order and each + * returned value is passed to the next hook. The final source must have the + * same length as the authored source. It is never emitted. + */ + transformRouteSource?: ( + opts: TransformRouteSourceOptions, + ) => string | void | Promise + + /** + * Formats a newly scaffolded route. Hooks run in plugin order and each + * returned value is passed to the next hook. The built-in TypeScript + * formatter is used only when no plugin handles the route. + */ + formatRoute?: ( + opts: FormatRouteOptions, + ) => string | void | Promise } diff --git a/packages/router-generator/src/template.ts b/packages/router-generator/src/template.ts index 11ca968933..69df162301 100644 --- a/packages/router-generator/src/template.ts +++ b/packages/router-generator/src/template.ts @@ -7,12 +7,13 @@ export function fillTemplate( config: Config, template: string, values: Record, + formatter?: (source: string) => string | Promise, ) { const replaced = template.replace( /%%(\w+)%%/g, (_, key) => values[key as TemplateTag] || '', ) - return format(replaced, config) + return formatter ? formatter(replaced) : format(replaced, config) } export type TargetTemplate = { @@ -216,6 +217,58 @@ export function getTargetTemplate(config: Config): TargetTemplate { }, }, } + case 'octane': + return { + fullPkg: '@tanstack/octane-router', + subPkg: 'octane-router', + rootRoute: { + template: () => + [ + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RootComponent\n }%%tsrExportEnd%%\n\n', + 'function RootComponent() @{\n
Hello "%%tsrPath%%"!
\n}\n', + ].join(''), + imports: { + tsrImports: () => + "import { Outlet, createRootRoute } from '@tanstack/octane-router';", + tsrExportStart: () => 'export const Route = createRootRoute(', + tsrExportEnd: () => ');', + }, + }, + route: { + template: () => + [ + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', + 'function RouteComponent() @{\n
Hello "%%tsrPath%%"!
\n}\n', + ].join(''), + imports: { + tsrImports: () => + "import { createFileRoute } from '@tanstack/octane-router';", + tsrExportStart: (routePath) => + `export const Route = createFileRoute(${serializeRoutePath(routePath)})(`, + tsrExportEnd: () => ');', + }, + }, + lazyRoute: { + template: () => + [ + '%%tsrImports%%', + '\n\n', + '%%tsrExportStart%%{\n component: RouteComponent\n }%%tsrExportEnd%%\n\n', + 'function RouteComponent() @{\n
Hello "%%tsrPath%%"!
\n}\n', + ].join(''), + imports: { + tsrImports: () => + "import { createLazyFileRoute } from '@tanstack/octane-router';", + tsrExportStart: (routePath) => + `export const Route = createLazyFileRoute(${serializeRoutePath(routePath)})(`, + tsrExportEnd: () => ');', + }, + }, + } default: throw new Error(`router-generator: Unknown target type: ${target}`) } diff --git a/packages/router-generator/src/transform/transform.ts b/packages/router-generator/src/transform/transform.ts index 9fcad87948..368a66d4b0 100644 --- a/packages/router-generator/src/transform/transform.ts +++ b/packages/router-generator/src/transform/transform.ts @@ -48,13 +48,23 @@ type RouteCallAnalysis = { export function transform({ ctx, source, + parseSource, filename, node, }: TransformOptions): TransformResult { + if (parseSource !== undefined && parseSource.length !== source.length) { + return { + result: 'error', + error: new Error( + `expected parseSource for ${filename ?? ctx.routeId} to preserve the original source length`, + ), + } + } + let ast: ReturnType try { - ast = parseAst({ code: source, filename }) + ast = parseAst({ code: parseSource ?? source, filename }) } catch (error) { return { result: 'error', diff --git a/packages/router-generator/src/transform/types.ts b/packages/router-generator/src/transform/types.ts index 4a8fecaada..1e90ff878d 100644 --- a/packages/router-generator/src/transform/types.ts +++ b/packages/router-generator/src/transform/types.ts @@ -3,6 +3,12 @@ import type { Config } from '../config' export interface TransformOptions { source: string + /** + * An optional, position-preserving version of `source` that can be parsed as + * TypeScript/JSX. AST ranges are applied to the original source, so this must + * have exactly the same length as `source`. + */ + parseSource?: string filename?: string ctx: TransformContext node: RouteNode diff --git a/packages/router-generator/tests/octane-target.test.ts b/packages/router-generator/tests/octane-target.test.ts new file mode 100644 index 0000000000..05bfb7ed83 --- /dev/null +++ b/packages/router-generator/tests/octane-target.test.ts @@ -0,0 +1,128 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { Generator, getConfig } from '../src' +import type { GeneratorPlugin } from '../src' + +const tempRoots: Array = [] + +afterEach(async () => { + await Promise.all( + tempRoots.splice(0).map((root) => + fs.rm(root, { + recursive: true, + force: true, + }), + ), + ) +}) + +function maskTsrxComponentBodies(source: string) { + return source.replace(/@\{[\s\S]*?\}/g, (componentBody) => { + const inner = componentBody.slice(2, -1).replaceAll(/[^\n\r]/g, ' ') + return ` {${inner}}` + }) +} + +describe('octane target', () => { + it('discovers, scaffolds, and updates .tsrx route modules through framework hooks', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'tsr-octane-')) + tempRoots.push(root) + const routesDirectory = path.join(root, 'src/routes') + await fs.mkdir(routesDirectory, { recursive: true }) + + await fs.writeFile( + path.join(routesDirectory, '__root.tsrx'), + [ + "import { createRootRoute } from '@tanstack/octane-router'", + 'export const Route = createRootRoute({})', + ].join('\n'), + ) + await fs.writeFile( + path.join(routesDirectory, 'index.tsrx'), + [ + "import { createFileRoute } from '@tanstack/octane-router'", + "export const Route = createFileRoute('/stale')({ component: Page })", + 'function Page() @{
Index
}', + ].join('\n'), + ) + await fs.writeFile(path.join(routesDirectory, 'about.tsrx'), '') + + const hookSources: Array = [] + const sourcePlugin: GeneratorPlugin = { + name: 'test-octane-source', + transformRouteSource: ({ source }) => maskTsrxComponentBodies(source), + formatRoute: ({ source }) => source, + } + const userPlugin: GeneratorPlugin = { + name: 'test-user-plugin', + transformRouteSource: ({ source }) => { + hookSources.push(source) + }, + formatRoute: ({ source }) => `${source}// user-formatted\n`, + } + const config = getConfig({ + target: 'octane', + routesDirectory, + generatedRouteTree: path.join(root, 'src/routeTree.gen.ts'), + tmpDir: path.join(root, '.tanstack/tmp'), + disableLogging: true, + plugins: [sourcePlugin, userPlugin], + }) + + await new Generator({ config, root }).run() + + const indexRoute = await fs.readFile( + path.join(routesDirectory, 'index.tsrx'), + 'utf8', + ) + const aboutRoute = await fs.readFile( + path.join(routesDirectory, 'about.tsrx'), + 'utf8', + ) + const routeTree = await fs.readFile(config.generatedRouteTree, 'utf8') + + expect(indexRoute).toContain("createFileRoute('/')") + expect(indexRoute).toContain('function Page() @{
Index
}') + expect(aboutRoute).toContain( + "import { createFileRoute } from '@tanstack/octane-router';", + ) + expect(aboutRoute).toContain('function RouteComponent() @{') + expect(aboutRoute).toContain('// user-formatted') + expect(routeTree).toContain("declare module '@tanstack/octane-router'") + expect(routeTree).toContain("'./routes/about.tsrx'") + expect(hookSources.length).toBeGreaterThan(0) + expect(hookSources.every((source) => !source.includes('@{'))).toBe(true) + }) + + it('honors an explicit extension override', () => { + expect(getConfig({ target: 'octane' }).addExtensions).toBe(true) + expect( + getConfig({ target: 'octane', addExtensions: false }).addExtensions, + ).toBe(false) + for (const target of ['react', 'solid', 'vue'] as const) { + expect(getConfig({ target }).addExtensions).toBe(false) + } + }) + + it('reports the authored Octane root extension when types are disabled', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'tsr-octane-root-')) + tempRoots.push(root) + const routesDirectory = path.join(root, 'src/routes') + await fs.mkdir(routesDirectory, { recursive: true }) + + const config = getConfig({ + target: 'octane', + routesDirectory, + generatedRouteTree: path.join(root, 'src/routeTree.gen.js'), + tmpDir: path.join(root, '.tanstack/tmp'), + disableLogging: true, + disableTypes: true, + }) + + await expect(new Generator({ config, root }).run()).rejects.toThrow( + '__root.tsrx', + ) + }) +}) diff --git a/packages/router-generator/tests/template.test.ts b/packages/router-generator/tests/template.test.ts index 745604b4ec..66a4fff6be 100644 --- a/packages/router-generator/tests/template.test.ts +++ b/packages/router-generator/tests/template.test.ts @@ -34,4 +34,17 @@ describe('template', () => { `export const Route = createLazyFileRoute(${JSON.stringify('/say-"hi"')})(`, ) }) + + it('uses first-class Octane package names and TSRX templates', () => { + const template = getTargetTemplate({ target: 'octane' } as never) + + expect(template.fullPkg).toBe('@tanstack/octane-router') + expect(template.route.imports.tsrImports()).toContain( + "from '@tanstack/octane-router'", + ) + expect(template.route.imports.tsrExportStart(`/say-"hi"`)).toBe( + `export const Route = createFileRoute(${JSON.stringify('/say-"hi"')})(`, + ) + expect(template.route.template()).toContain('function RouteComponent() @{') + }) }) diff --git a/packages/router-generator/tests/transform.test.ts b/packages/router-generator/tests/transform.test.ts index 8a53f3278e..f6b1779be7 100644 --- a/packages/router-generator/tests/transform.test.ts +++ b/packages/router-generator/tests/transform.test.ts @@ -14,6 +14,60 @@ function makeNode( } describe('transform', () => { + it('uses position-preserving framework source for parsing but emits raw source', async () => { + const componentBody = '@{
Hello
}' + const parseableBody = `{${' '.repeat(componentBody.length - 2)}}` + const source = [ + "import { createFileRoute } from '@tanstack/octane-router'", + '', + "export const Route = createFileRoute('/old')({ component: Component })", + '', + `function Component() ${componentBody}`, + ].join('\n') + const parseSource = source.replace(componentBody, parseableBody) + + const result = await transform({ + source, + parseSource, + filename: 'route.tsrx', + ctx: { + target: 'octane', + routeId: '/new', + lazy: false, + }, + node: makeNode(), + }) + + expect(result.result).toBe('modified') + if (result.result !== 'modified') { + throw new Error(`expected modified result, got ${result.result}`) + } + expect(result.output).toContain("createFileRoute('/new')") + expect(result.output).toContain(componentBody) + }) + + it('rejects framework parse source that changes source positions', async () => { + const result = await transform({ + source: "export const Route = createFileRoute('/old')({})", + parseSource: "export const Route = createFileRoute('/old')({}) ", + filename: 'route.tsrx', + ctx: { + target: 'octane', + routeId: '/new', + lazy: false, + }, + node: makeNode(), + }) + + expect(result.result).toBe('error') + if (result.result !== 'error') { + throw new Error(`expected error result, got ${result.result}`) + } + expect(String(result.error)).toContain( + 'preserve the original source length', + ) + }) + it('does not treat root route exports as missing Route exports', async () => { const result = await transform({ source: [ diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index 9411165630..a9cccff5b9 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -27,6 +27,7 @@ import { getFrameworkOptions } from './framework-options' import type { CompileCodeSplitReferenceRouteOptions, ReferenceRouteCompilerPlugin, + VirtualRouteCompilerPlugin, } from './plugins' import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' import type { CodeSplitGroupings, SplitRouteIdentNodes } from '../constants' @@ -44,6 +45,41 @@ export { removeBindingsTransitivelyDependingOn, } from '@tanstack/router-utils' +function hashRouteHmrSignature(code: string): string { + let hash = 0xcbf29ce484222325n + for (let index = 0; index < code.length; index++) { + hash ^= BigInt(code.charCodeAt(index)) + hash = BigInt.asUintN(64, hash * 0x100000001b3n) + } + return hash.toString(16).padStart(16, '0') +} + +function getRouteHmrSignatureSource(ast: t.File, filename: string): string { + const signatureAst = t.cloneNode(ast, true) + const localBindings = new Set() + + for (const node of signatureAst.program.body) { + collectLocalBindingsFromStatement(node, localBindings) + } + + const declarationMap = buildDeclarationMap(signatureAst) + const dependencyGraph = buildDependencyGraph(declarationMap, localBindings) + const keepBindings = new Set(['Route']) + expandTransitively(keepBindings, dependencyGraph) + + retainModuleLevelDeclarations(signatureAst, keepBindings) + unwrapExportedDeclarations(signatureAst) + signatureAst.program.body = signatureAst.program.body.filter( + (statement) => !t.isImportDeclaration(statement), + ) + + return generateFromAst(signatureAst, { + sourceMaps: false, + sourceFileName: filename, + filename, + }).code +} + export function removeBindingsDependingOnRoute( bindings: Set, dependencyGraph: Map>, @@ -450,14 +486,6 @@ export function compileCodeSplitReferenceRoute( } }) - programPath.pushContainer( - 'body', - createRouteHmrStatement(stableRouteOptionKeys, { - hmrStyle: opts.hmrStyle ?? 'vite', - targetFramework: opts.targetFramework, - routeId: opts.hmrRouteId, - }), - ) modified = true hmrAdded = true } @@ -856,6 +884,18 @@ export function compileCodeSplitReferenceRoute( } } + if (hmrAdded) { + const signatureSource = getRouteHmrSignatureSource(ast, opts.filename) + ast.program.body.push( + ...createRouteHmrStatement(stableRouteOptionKeys, { + hmrStyle: opts.hmrStyle ?? 'vite', + targetFramework: opts.targetFramework, + routeId: opts.hmrRouteId, + routeSignature: hashRouteHmrSignature(signatureSource), + }), + ) + } + const result = generateFromAst(ast, { sourceMaps: true, sourceFileName: opts.filename, @@ -879,6 +919,7 @@ export function compileCodeSplitVirtualRoute( splitTargets: Array filename: string sharedBindings?: Set + compilerPlugins?: Array }, ): GeneratorResult { const ast = parseAst(opts) @@ -1145,6 +1186,17 @@ export function compileCodeSplitVirtualRoute( }) } + for (const plugin of opts.compilerPlugins ?? []) { + const pluginExporterIdent = plugin.onExportSplitRouteProperty?.({ + programPath, + splitNodeMeta: splitMeta, + localExporterIdent: splitMeta.localExporterIdent, + }) + if (pluginExporterIdent) { + splitMeta.localExporterIdent = pluginExporterIdent.name + } + } + // Export the node programPath.pushContainer('body', [ t.exportNamedDeclaration(null, [ diff --git a/packages/router-plugin/src/core/code-splitter/framework-options.ts b/packages/router-plugin/src/core/code-splitter/framework-options.ts index 0a2a9e8230..31b2297773 100644 --- a/packages/router-plugin/src/core/code-splitter/framework-options.ts +++ b/packages/router-plugin/src/core/code-splitter/framework-options.ts @@ -1,3 +1,5 @@ +import type { Config } from '../config' + type FrameworkOptions = { package: string idents: { @@ -7,45 +9,43 @@ type FrameworkOptions = { } } -export function getFrameworkOptions(framework: string): FrameworkOptions { - let frameworkOptions: FrameworkOptions - - switch (framework) { - case 'react': - frameworkOptions = { - package: '@tanstack/react-router', - idents: { - createFileRoute: 'createFileRoute', - lazyFn: 'lazyFn', - lazyRouteComponent: 'lazyRouteComponent', - }, - } - break - case 'solid': - frameworkOptions = { - package: '@tanstack/solid-router', - idents: { - createFileRoute: 'createFileRoute', - lazyFn: 'lazyFn', - lazyRouteComponent: 'lazyRouteComponent', - }, - } - break - case 'vue': - frameworkOptions = { - package: '@tanstack/vue-router', - idents: { - createFileRoute: 'createFileRoute', - lazyFn: 'lazyFn', - lazyRouteComponent: 'lazyRouteComponent', - }, - } - break - default: - throw new Error( - `[getFrameworkOptions] - Unsupported framework: ${framework}`, - ) - } +const frameworkOptions = { + react: { + package: '@tanstack/react-router', + idents: { + createFileRoute: 'createFileRoute', + lazyFn: 'lazyFn', + lazyRouteComponent: 'lazyRouteComponent', + }, + }, + solid: { + package: '@tanstack/solid-router', + idents: { + createFileRoute: 'createFileRoute', + lazyFn: 'lazyFn', + lazyRouteComponent: 'lazyRouteComponent', + }, + }, + vue: { + package: '@tanstack/vue-router', + idents: { + createFileRoute: 'createFileRoute', + lazyFn: 'lazyFn', + lazyRouteComponent: 'lazyRouteComponent', + }, + }, + octane: { + package: '@tanstack/octane-router', + idents: { + createFileRoute: 'createFileRoute', + lazyFn: 'lazyFn', + lazyRouteComponent: 'lazyRouteComponent', + }, + }, +} satisfies Record - return frameworkOptions +export function getFrameworkOptions( + framework: Config['target'], +): FrameworkOptions { + return frameworkOptions[framework] } diff --git a/packages/router-plugin/src/core/code-splitter/plugins.ts b/packages/router-plugin/src/core/code-splitter/plugins.ts index ebf1f4aa30..d528135982 100644 --- a/packages/router-plugin/src/core/code-splitter/plugins.ts +++ b/packages/router-plugin/src/core/code-splitter/plugins.ts @@ -56,3 +56,16 @@ export type ReferenceRouteCompilerPlugin = { ctx: ReferenceRouteSplitPropertyCompilerPluginContext, ) => void | t.Expression } + +export type VirtualRouteCompilerPluginContext = { + programPath: babel.NodePath + splitNodeMeta: SplitNodeMeta + localExporterIdent: string +} + +export type VirtualRouteCompilerPlugin = { + name: string + onExportSplitRouteProperty?: ( + ctx: VirtualRouteCompilerPluginContext, + ) => void | t.Identifier +} diff --git a/packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts b/packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts index de8195853a..2db402f9d1 100644 --- a/packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts +++ b/packages/router-plugin/src/core/code-splitter/plugins/framework-plugins.ts @@ -1,7 +1,12 @@ import { createReactRefreshIgnoredRouteExportsPlugin } from './react-refresh-ignored-route-exports' import { createReactRefreshRouteComponentsPlugin } from './react-refresh-route-components' -import { createReactStableHmrSplitRouteComponentsPlugin } from './react-stable-hmr-split-route-components' -import type { ReferenceRouteCompilerPlugin } from '../plugins' +import { createStableHmrSplitRouteComponentsPlugin } from './react-stable-hmr-split-route-components' +import { createOctaneSplitRouteComponentsPlugin } from './octane-split-route-components' +import { createOctaneHmrSplitRouteComponentsPlugin } from './octane-hmr-split-route-components' +import type { + ReferenceRouteCompilerPlugin, + VirtualRouteCompilerPlugin, +} from '../plugins' import type { Config, HmrStyle } from '../../config' export function getReferenceRouteCompilerPlugins(opts: { @@ -18,12 +23,40 @@ export function getReferenceRouteCompilerPlugins(opts: { ? [createReactRefreshIgnoredRouteExportsPlugin()] : []), createReactRefreshRouteComponentsPlugin(), - createReactStableHmrSplitRouteComponentsPlugin({ hmrStyle }), + createStableHmrSplitRouteComponentsPlugin({ hmrStyle }), ] } return undefined } + case 'octane': { + return [ + createOctaneSplitRouteComponentsPlugin(), + ...(opts.addHmr + ? [ + createStableHmrSplitRouteComponentsPlugin({ + hmrStyle: opts.hmrStyle ?? 'vite', + }), + ] + : []), + ] + } default: return undefined } } + +export function getVirtualRouteCompilerPlugins(opts: { + targetFramework: Config['target'] + addHmr?: boolean + hmrStyle?: HmrStyle +}): Array | undefined { + if (opts.targetFramework !== 'octane' || !opts.addHmr) { + return undefined + } + + return [ + createOctaneHmrSplitRouteComponentsPlugin({ + hmrStyle: opts.hmrStyle ?? 'vite', + }), + ] +} diff --git a/packages/router-plugin/src/core/code-splitter/plugins/octane-hmr-split-route-components.ts b/packages/router-plugin/src/core/code-splitter/plugins/octane-hmr-split-route-components.ts new file mode 100644 index 0000000000..8845cd0fda --- /dev/null +++ b/packages/router-plugin/src/core/code-splitter/plugins/octane-hmr-split-route-components.ts @@ -0,0 +1,132 @@ +import * as template from '@babel/template' +import * as t from '@babel/types' +import { getUniqueProgramIdentifier } from '../../utils' +import type { HmrStyle } from '../../config' +import type { VirtualRouteCompilerPlugin } from '../plugins' + +const buildViteHmrStatements = template.statements( + ` + const %%candidateIdent%% = %%hmrIdent%%(%%componentIdent%%) + const %%hotIdent%% = import.meta.hot + const %%hotDataIdent%% = %%hotIdent%% ? (%%hotIdent%%.data ??= {}) : undefined + let %%stableIdent%% = %%candidateIdent%% + if (%%hotDataIdent%%?.[%%hotDataKey%%]) { + %%hotDataIdent%%[%%hotDataKey%%][%%hmrSymbolIdent%%].update(%%candidateIdent%%) + %%stableIdent%% = %%hotDataIdent%%[%%hotDataKey%%] + } + %%stableIdent%%.$$singleRoot = %%componentIdent%%.$$singleRoot + try { %%stableIdent%%.__oct_loc = %%componentIdent%%.__oct_loc } catch {} + if (%%hotIdent%%) { + %%hotDataIdent%%[%%hotDataKey%%] = %%stableIdent%% + import.meta.hot.accept() + } + `, + { syntacticPlaceholders: true }, +) + +const buildWebpackHmrStatements = template.statements( + ` + const %%candidateIdent%% = %%hmrIdent%%(%%componentIdent%%) + const %%hotIdent%% = import.meta.webpackHot + let %%stableIdent%% = %%candidateIdent%% + if (%%hotIdent%%?.data?.[%%hotDataKey%%]) { + %%hotIdent%%.data[%%hotDataKey%%][%%hmrSymbolIdent%%].update(%%candidateIdent%%) + %%stableIdent%% = %%hotIdent%%.data[%%hotDataKey%%] + } + %%stableIdent%%.$$singleRoot = %%componentIdent%%.$$singleRoot + try { %%stableIdent%%.__oct_loc = %%componentIdent%%.__oct_loc } catch {} + if (%%hotIdent%%) { + %%hotIdent%%.dispose((data) => { + data[%%hotDataKey%%] = %%stableIdent%% + }) + %%hotIdent%%.accept() + } + `, + { syntacticPlaceholders: true }, +) + +function capitalizeIdentifier(value: string) { + return value[0]!.toUpperCase() + value.slice(1) +} + +export function createOctaneHmrSplitRouteComponentsPlugin(opts: { + hmrStyle: HmrStyle +}): VirtualRouteCompilerPlugin { + let hmrIdent: t.Identifier | undefined + let hmrSymbolIdent: t.Identifier | undefined + + return { + name: 'octane-hmr-split-route-components', + onExportSplitRouteProperty(ctx) { + if (ctx.splitNodeMeta.splitStrategy !== 'lazyRouteComponent') { + return + } + + if (!hmrIdent || !hmrSymbolIdent) { + hmrIdent = getUniqueProgramIdentifier(ctx.programPath, 'TSROctaneHmr') + hmrSymbolIdent = getUniqueProgramIdentifier( + ctx.programPath, + 'TSROctaneHmrSymbol', + ) + ctx.programPath.unshiftContainer( + 'body', + t.importDeclaration( + [ + t.importSpecifier(hmrIdent, t.identifier('hmr')), + t.importSpecifier(hmrSymbolIdent, t.identifier('HMR')), + ], + t.stringLiteral('octane'), + ), + ) + } + + const exportName = ctx.splitNodeMeta.exporterIdent + const name = capitalizeIdentifier(exportName) + const componentIdent = t.identifier(ctx.localExporterIdent) + const candidateIdent = getUniqueProgramIdentifier( + ctx.programPath, + `TSROctane${name}Candidate`, + ) + const stableIdent = getUniqueProgramIdentifier( + ctx.programPath, + `TSROctane${name}`, + ) + const hotIdent = getUniqueProgramIdentifier( + ctx.programPath, + `TSROctane${name}Hot`, + ) + const hotDataIdent = getUniqueProgramIdentifier( + ctx.programPath, + `TSROctane${name}HotData`, + ) + const hotDataKey = t.stringLiteral( + `tsr-octane-split-component:${exportName}`, + ) + + const statements = + opts.hmrStyle === 'webpack' + ? buildWebpackHmrStatements({ + candidateIdent, + componentIdent, + hmrIdent, + hmrSymbolIdent, + hotDataKey, + hotIdent, + stableIdent, + }) + : buildViteHmrStatements({ + candidateIdent, + componentIdent, + hmrIdent, + hmrSymbolIdent, + hotDataIdent, + hotDataKey, + hotIdent, + stableIdent, + }) + + ctx.programPath.pushContainer('body', statements) + return stableIdent + }, + } +} diff --git a/packages/router-plugin/src/core/code-splitter/plugins/octane-split-route-components.ts b/packages/router-plugin/src/core/code-splitter/plugins/octane-split-route-components.ts new file mode 100644 index 0000000000..33e4db67ea --- /dev/null +++ b/packages/router-plugin/src/core/code-splitter/plugins/octane-split-route-components.ts @@ -0,0 +1,119 @@ +import * as t from '@babel/types' +import type { ReferenceRouteCompilerPlugin } from '../plugins' + +function isStaticMember( + node: t.Node | null | undefined, + objectName: string, + propertyName: string, +): node is t.MemberExpression { + return ( + t.isMemberExpression(node) && + !node.computed && + t.isIdentifier(node.object, { name: objectName }) && + t.isIdentifier(node.property, { name: propertyName }) + ) +} + +function isOctaneSingleRootStamp( + statement: t.Statement, + componentName: string, +) { + if (!t.isExpressionStatement(statement)) { + return false + } + + const expression = statement.expression + return ( + t.isAssignmentExpression(expression, { operator: '=' }) && + isStaticMember(expression.left, componentName, '$$singleRoot') && + t.isBooleanLiteral(expression.right, { value: true }) + ) +} + +function isOctaneWarmPlan(statement: t.Statement, componentName: string) { + if (!t.isExpressionStatement(statement)) { + return false + } + + const expression = statement.expression + return ( + t.isAssignmentExpression(expression, { operator: '=' }) && + isStaticMember(expression.left, componentName, '__warm') && + t.isArrowFunctionExpression(expression.right) && + !expression.right.async && + expression.right.params.length === 1 && + t.isIdentifier(expression.right.params[0], { name: '__wp' }) + ) +} + +function isOctaneLocationStamp(statement: t.Statement, componentName: string) { + if ( + !t.isTryStatement(statement) || + statement.finalizer || + !statement.handler || + statement.handler.param || + statement.block.body.length !== 1 || + statement.handler.body.body.length !== 0 || + !statement.handler.body.innerComments?.some( + (comment) => comment.value.trim() === 'frozen component', + ) + ) { + return false + } + + const [bodyStatement] = statement.block.body + if (!t.isExpressionStatement(bodyStatement)) { + return false + } + + const expression = bodyStatement.expression + if ( + !t.isAssignmentExpression(expression, { operator: '=' }) || + !isStaticMember(expression.left, componentName, '__oct_loc') + ) { + return false + } + + return ( + (t.isStringLiteral(expression.right) && + /:\d+:\d+$/.test(expression.right.value)) || + (t.isMemberExpression(expression.right) && + !expression.right.computed && + t.isIdentifier(expression.right.property, { name: '__oct_loc' })) + ) +} + +function isOctaneCompilerCompanion( + statement: t.Statement, + componentName: string, +) { + return ( + isOctaneSingleRootStamp(statement, componentName) || + isOctaneWarmPlan(statement, componentName) || + isOctaneLocationStamp(statement, componentName) + ) +} + +/** + * Octane emits component metadata as adjacent top-level statements. Once the + * component binding moves to a virtual route module, those statements must + * move with it instead of evaluating against a missing binding in the + * reference module. + */ +export function createOctaneSplitRouteComponentsPlugin(): ReferenceRouteCompilerPlugin { + return { + name: 'octane-split-route-components', + onSplitRouteProperty(ctx) { + if (!t.isIdentifier(ctx.prop.value)) { + return + } + + const componentName = ctx.prop.value.name + for (const statementPath of ctx.programPath.get('body')) { + if (isOctaneCompilerCompanion(statementPath.node, componentName)) { + statementPath.remove() + } + } + }, + } +} diff --git a/packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts b/packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts index 99448f452a..daf834de76 100644 --- a/packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts +++ b/packages/router-plugin/src/core/code-splitter/plugins/react-stable-hmr-split-route-components.ts @@ -34,7 +34,7 @@ function hotExpressionAstFor(hmrStyle: HmrStyle): t.Expression { ) } -export function createReactStableHmrSplitRouteComponentsPlugin(opts: { +export function createStableHmrSplitRouteComponentsPlugin(opts: { hmrStyle: HmrStyle }): ReferenceRouteCompilerPlugin { return { diff --git a/packages/router-plugin/src/core/framework-plugin-order.ts b/packages/router-plugin/src/core/framework-plugin-order.ts new file mode 100644 index 0000000000..1736b12a2e --- /dev/null +++ b/packages/router-plugin/src/core/framework-plugin-order.ts @@ -0,0 +1,121 @@ +import type { Config } from './config' + +type TransformationPluginInfo = { + pluginNames: Array + pkg: string + usage: string + order: 'before-router' | 'after-router' + required?: boolean +} + +/** + * Framework source transformation plugins and their required ordering relative + * to route analysis. + */ +const TRANSFORMATION_PLUGINS_BY_FRAMEWORK: Partial< + Record> +> = { + react: [ + { + // Babel-based React plugin + pluginNames: ['vite:react-babel', 'vite:react-refresh'], + pkg: '@vitejs/plugin-react', + usage: 'react()', + order: 'after-router', + }, + { + // SWC-based React plugin + pluginNames: ['vite:react-swc', 'vite:react-swc:resolve-runtime'], + pkg: '@vitejs/plugin-react-swc', + usage: 'reactSwc()', + order: 'after-router', + }, + { + // OXC-based React plugin (deprecated but should still be handled) + pluginNames: ['vite:react-oxc:config', 'vite:react-oxc:refresh-runtime'], + pkg: '@vitejs/plugin-react-oxc', + usage: 'reactOxc()', + order: 'after-router', + }, + ], + solid: [ + { + pluginNames: ['solid'], + pkg: 'vite-plugin-solid', + usage: 'solid()', + order: 'after-router', + }, + ], + octane: [ + { + pluginNames: ['octane'], + pkg: 'octane/compiler/vite', + usage: 'octane()', + order: 'before-router', + required: true, + }, + ], +} + +export function validateFrameworkPluginOrder(opts: { + framework: Config['target'] + plugins: ReadonlyArray<{ name: string }> + routerPluginName: string +}): void { + const routerPluginIndex = opts.plugins.findIndex( + (plugin) => plugin.name === opts.routerPluginName, + ) + if (routerPluginIndex === -1) { + return + } + + const frameworkPlugins = TRANSFORMATION_PLUGINS_BY_FRAMEWORK[opts.framework] + if (!frameworkPlugins) { + return + } + + for (const transformPlugin of frameworkPlugins) { + const transformPluginIndex = opts.plugins.findIndex((plugin) => + transformPlugin.pluginNames.includes(plugin.name), + ) + + if (transformPluginIndex === -1) { + if (transformPlugin.required) { + throw new Error( + `Plugin setup error: '${transformPlugin.pkg}' is required for the '${opts.framework}' target.\n\n` + + `Please update your Vite config:\n\n` + + ` plugins: [\n` + + ` ${transformPlugin.usage},\n` + + ` tanstackRouter(),\n` + + ` ]\n`, + ) + } + continue + } + + const shouldComeBeforeRouter = transformPlugin.order === 'before-router' + const isMisordered = shouldComeBeforeRouter + ? transformPluginIndex > routerPluginIndex + : transformPluginIndex < routerPluginIndex + + if (!isMisordered) { + continue + } + + const firstPlugin = shouldComeBeforeRouter + ? transformPlugin.usage + : 'tanstackRouter()' + const secondPlugin = shouldComeBeforeRouter + ? 'tanstackRouter()' + : transformPlugin.usage + throw new Error( + `Plugin order error: '${transformPlugin.pkg}' is placed ${shouldComeBeforeRouter ? 'after' : 'before'} '@tanstack/router-plugin'.\n\n` + + `${shouldComeBeforeRouter ? 'This framework compiler must lower its source syntax before TanStack Router analyzes route modules.' : 'The TanStack Router plugin must analyze route modules before JSX transformation plugins.'}\n\n` + + `Please update your Vite config:\n\n` + + ` plugins: [\n` + + ` ${firstPlugin},\n` + + ` ${secondPlugin},\n` + + ` ]\n`, + ) + } +} diff --git a/packages/router-plugin/src/core/hmr/handle-route-update.ts b/packages/router-plugin/src/core/hmr/handle-route-update.ts index c0325b06e9..444b54d5e9 100644 --- a/packages/router-plugin/src/core/hmr/handle-route-update.ts +++ b/packages/router-plugin/src/core/hmr/handle-route-update.ts @@ -46,6 +46,7 @@ type AnyRouteMatchWithPrivateProps = AnyRouteMatch & { function handleRouteUpdate( routeId: string, newRoute: AnyRouteWithPrivateProps, + shouldApplyRouteUpdate = true, ) { const router = window.__TSR_ROUTER__ as AnyRouterWithPrivateMaps const oldRoute = router.routesById[routeId] as @@ -56,6 +57,11 @@ function handleRouteUpdate( return } + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute) + return + } + // Generated route-tree options are not present on the freshly imported route // module, but they must stay on the live route before rebuilding indexes. const generatedRouteOptionKeys = new Set(['id', 'path', 'getParentRoute']) diff --git a/packages/router-plugin/src/core/hmr/select-adapter.ts b/packages/router-plugin/src/core/hmr/select-adapter.ts index 60395a1534..d77ac5e540 100644 --- a/packages/router-plugin/src/core/hmr/select-adapter.ts +++ b/packages/router-plugin/src/core/hmr/select-adapter.ts @@ -7,6 +7,7 @@ export type CreateRouteHmrStatementOpts = { hmrStyle: HmrStyle targetFramework: Config['target'] routeId?: string + routeSignature?: string } /** @@ -24,9 +25,12 @@ export function createRouteHmrStatement( return createWebpackHmrStatement(stableRouteOptionKeys, { targetFramework: opts.targetFramework, routeId, + routeSignature: opts.routeSignature, }) } return createViteHmrStatement(stableRouteOptionKeys, { + targetFramework: opts.targetFramework, routeId, + routeSignature: opts.routeSignature, }) } diff --git a/packages/router-plugin/src/core/hmr/vite-adapter.ts b/packages/router-plugin/src/core/hmr/vite-adapter.ts index ba8d42a80a..929515a17b 100644 --- a/packages/router-plugin/src/core/hmr/vite-adapter.ts +++ b/packages/router-plugin/src/core/hmr/vite-adapter.ts @@ -1,26 +1,41 @@ import * as template from '@babel/template' import { getHandleRouteUpdateCode } from './handle-route-update' +import type { Config } from '../config' import type * as t from '@babel/types' /** * Emits HMR accept code for Vite / native ESM HMR: `import.meta.hot.accept` * with a callback that receives the freshly re-imported module. * - * `targetFramework` is currently unused — Vite's framework-specific fast-refresh - * plugins handle component body patching via their own accept boundaries — but - * we take it for API symmetry with `createWebpackHmrStatement`. + * Framework-specific component runtimes still own component body patching. + * The route signature only suppresses a redundant data invalidation when an + * Octane update changed extracted component code but not the route definition. */ export function createViteHmrStatement( stableRouteOptionKeys: Array, opts: { + targetFramework: Config['target'] routeId?: string - } = {}, + routeSignature?: string + }, ): Array { const handleRouteUpdateCode = getHandleRouteUpdateCode(stableRouteOptionKeys) // The replacement Route object can be uninitialized; keep a generated id as // fallback for the existing router route we need to patch. const routeIdFallback = typeof opts.routeId === 'string' ? JSON.stringify(opts.routeId) : 'Route.id' + const routeSignature = + typeof opts.routeSignature === 'string' + ? JSON.stringify(opts.routeSignature) + : 'undefined' + const shouldInvalidateCurrentRoute = + opts.targetFramework === 'octane' + ? `typeof routeSignature !== 'string' || previousRouteSignature !== routeSignature` + : 'true' + const shouldInvalidateNextRoute = + opts.targetFramework === 'octane' + ? `typeof routeSignature !== 'string' || nextRouteSignature !== routeSignature` + : 'true' return [ template.statement( @@ -29,6 +44,12 @@ if (import.meta.hot) { const hot = import.meta.hot const hotData = hot.data ??= {} const handleRouteUpdate = ${handleRouteUpdateCode} + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature') + const routeSignature = ${routeSignature} + const previousRouteSignature = hotData['tsr-route-signature'] + const shouldInvalidateCurrentRoute = ${shouldInvalidateCurrentRoute} + Route[routeSignatureKey] = routeSignature + hotData['tsr-route-signature'] = routeSignature const initialRouteId = ${routeIdFallback} ?? hotData['tsr-route-id'] if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId @@ -38,10 +59,10 @@ if (import.meta.hot) { ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route) + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute) hotData['tsr-route-update-handled'] = Route } - hot.accept((newModule) => { + import.meta.hot.accept((newModule) => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? ${routeIdFallback} if (routeId) { @@ -51,7 +72,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled'] return } - handleRouteUpdate(routeId, newModule.Route) + const nextRouteSignature = newModule.Route[routeSignatureKey] + const shouldInvalidateNextRoute = ${shouldInvalidateNextRoute} + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute) } }) } diff --git a/packages/router-plugin/src/core/hmr/webpack-adapter.ts b/packages/router-plugin/src/core/hmr/webpack-adapter.ts index eaf34ccfb1..bb75166837 100644 --- a/packages/router-plugin/src/core/hmr/webpack-adapter.ts +++ b/packages/router-plugin/src/core/hmr/webpack-adapter.ts @@ -20,6 +20,7 @@ export function createWebpackHmrStatement( opts: { targetFramework: Config['target'] routeId?: string + routeSignature?: string }, ): Array { const handleRouteUpdateCode = getHandleRouteUpdateCode(stableRouteOptionKeys) @@ -27,6 +28,14 @@ export function createWebpackHmrStatement( typeof opts.routeId === 'string' ? JSON.stringify(opts.routeId) : 'undefined' + const routeSignature = + typeof opts.routeSignature === 'string' + ? JSON.stringify(opts.routeSignature) + : 'undefined' + const shouldInvalidateRoute = + opts.targetFramework === 'octane' + ? `typeof routeSignature !== 'string' || previousRouteSignature !== routeSignature` + : 'true' // React-only: route modules aren't React Refresh "boundaries" (they export // a non-component `Route`), so the bundler's react-refresh runtime won't @@ -69,6 +78,10 @@ export function createWebpackHmrStatement( if (import.meta.webpackHot) { const hot = import.meta.webpackHot const hotData = hot.data ??= {} + const routeSignature = ${routeSignature} + const previousRouteSignature = hotData['tsr-route-signature'] + const shouldInvalidateRoute = ${shouldInvalidateRoute} + hotData['tsr-route-signature'] = routeSignature const routeId = hotData['tsr-route-id'] ?? Route.id ?? (Route.isRoot ? '__root__' : ${staticRouteIdLiteral}) if (routeId) { hotData['tsr-route-id'] = routeId @@ -78,12 +91,13 @@ if (import.meta.webpackHot) { ? window.__TSR_ROUTER__?.routesById?.[routeId] : undefined if (routeId && existingRoute && existingRoute !== Route) { - (${handleRouteUpdateCode})(routeId, Route)${reactRefreshCall} + (${handleRouteUpdateCode})(routeId, Route, shouldInvalidateRoute)${reactRefreshCall} } hot.dispose((data) => { if (routeId) { data['tsr-route-id'] = routeId } + data['tsr-route-signature'] = routeSignature }) hot.accept() } 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 4b6af091cc..0bbc14f647 100644 --- a/packages/router-plugin/src/core/router-code-splitter-plugin.ts +++ b/packages/router-plugin/src/core/router-code-splitter-plugin.ts @@ -13,7 +13,10 @@ import { computeSharedBindings, detectCodeSplitGroupingsFromRoute, } from './code-splitter/compilers' -import { getReferenceRouteCompilerPlugins } from './code-splitter/plugins/framework-plugins' +import { + getReferenceRouteCompilerPlugins, + getVirtualRouteCompilerPlugins, +} from './code-splitter/plugins/framework-plugins' import { defaultCodeSplitGroupings, splitRouteIdentNodes, @@ -22,6 +25,7 @@ import { } from './constants' import { debug, normalizePath, routeFactoryCallCodeFilter } from './utils' import { createRouterPluginContext } from './router-plugin-context' +import { validateFrameworkPluginOrder } from './framework-plugin-order' import type { CodeSplitGroupings, SplitRouteIdentNodes } from './constants' import type { GetRoutesByFileMapResultValue } from '@tanstack/router-generator' import type { Config } from './config' @@ -34,49 +38,6 @@ import type { const CODE_SPLITTER_PLUGIN_NAME = 'tanstack-router:code-splitter:compile-reference-file' -type TransformationPluginInfo = { - pluginNames: Array - pkg: string - usage: string -} - -/** - * JSX transformation plugins grouped by framework. - * These plugins must come AFTER the TanStack Router plugin in the Vite config. - */ -const TRANSFORMATION_PLUGINS_BY_FRAMEWORK: Record< - string, - Array -> = { - react: [ - { - // Babel-based React plugin - pluginNames: ['vite:react-babel', 'vite:react-refresh'], - pkg: '@vitejs/plugin-react', - usage: 'react()', - }, - { - // SWC-based React plugin - pluginNames: ['vite:react-swc', 'vite:react-swc:resolve-runtime'], - pkg: '@vitejs/plugin-react-swc', - usage: 'reactSwc()', - }, - { - // OXC-based React plugin (deprecated but should still be handled) - pluginNames: ['vite:react-oxc:config', 'vite:react-oxc:refresh-runtime'], - pkg: '@vitejs/plugin-react-oxc', - usage: 'reactOxc()', - }, - ], - solid: [ - { - pluginNames: ['solid'], - pkg: 'vite-plugin-solid', - usage: 'solid()', - }, - ], -} - export function createRouterCodeSplitterPlugin( options: Partial Config)> | undefined = {}, routerPluginContext: RouterPluginContext, @@ -105,6 +66,9 @@ export function createRouterCodeSplitterPlugin( const getShouldSplitFn = () => { return userConfig.codeSplittingOptions?.splitBehavior } + const shouldAddHmr = () => { + return (userConfig.codeSplittingOptions?.addHmr ?? true) && !isProduction + } const handleCompilingReferenceFile = ( code: string, @@ -159,8 +123,7 @@ export function createRouterCodeSplitterPlugin( sharedBindingsMap.delete(id) } - const addHmr = - (userConfig.codeSplittingOptions?.addHmr ?? true) && !isProduction + const addHmr = shouldAddHmr() const hmrStyle = userConfig.plugin?.hmr?.style ?? 'vite' const compiledReferenceRoute = compileCodeSplitReferenceRoute({ @@ -232,6 +195,11 @@ export function createRouterCodeSplitterPlugin( filename: id, splitTargets: grouping, sharedBindings: resolvedSharedBindings, + compilerPlugins: getVirtualRouteCompilerPlugins({ + targetFramework: userConfig.target, + addHmr: shouldAddHmr(), + hmrStyle: userConfig.plugin?.hmr?.style ?? 'vite', + }), }) if (debug) { @@ -252,7 +220,7 @@ export function createRouterCodeSplitterPlugin( id: { exclude: [tsrSplit, tsrShared], // this is necessary for webpack / rspack to avoid matching .html files - include: /\.(m|c)?(j|t)sx?$/, + include: [/\.(m|c)?(j|t)sx?$/, /\.tsrx(?:$|\?)/], }, code: { include: routeFactoryCallCodeFilter, @@ -279,37 +247,11 @@ export function createRouterCodeSplitterPlugin( ROOT = config.root initUserConfig() - // Validate plugin order - router must come before JSX transformation plugins - const routerPluginIndex = config.plugins.findIndex( - (p) => p.name === CODE_SPLITTER_PLUGIN_NAME, - ) - - if (routerPluginIndex === -1) return - - const frameworkPlugins = - TRANSFORMATION_PLUGINS_BY_FRAMEWORK[userConfig.target] - if (!frameworkPlugins) return - - for (const transformPlugin of frameworkPlugins) { - const transformPluginIndex = config.plugins.findIndex((p) => - transformPlugin.pluginNames.includes(p.name), - ) - - if ( - transformPluginIndex !== -1 && - transformPluginIndex < routerPluginIndex - ) { - throw new Error( - `Plugin order error: '${transformPlugin.pkg}' is placed before '@tanstack/router-plugin'.\n\n` + - `The TanStack Router plugin must come BEFORE JSX transformation plugins.\n\n` + - `Please update your Vite config:\n\n` + - ` plugins: [\n` + - ` tanstackRouter(),\n` + - ` ${transformPlugin.usage},\n` + - ` ]\n`, - ) - } - } + validateFrameworkPluginOrder({ + framework: userConfig.target, + plugins: config.plugins, + routerPluginName: CODE_SPLITTER_PLUGIN_NAME, + }) }, applyToEnvironment(environment) { if (userConfig.plugin?.vite?.environmentName) { diff --git a/packages/router-plugin/src/core/router-generator-plugin.ts b/packages/router-plugin/src/core/router-generator-plugin.ts index 46dda8c581..cba31f137f 100644 --- a/packages/router-plugin/src/core/router-generator-plugin.ts +++ b/packages/router-plugin/src/core/router-generator-plugin.ts @@ -2,6 +2,7 @@ import { isAbsolute, join, normalize } from 'node:path' import { Generator, resolveConfigPath } from '@tanstack/router-generator' import { getConfig } from './config' import { createRouterPluginContext } from './router-plugin-context' +import { validateFrameworkPluginOrder } from './framework-plugin-order' import type { GeneratorEvent } from '@tanstack/router-generator' import type { FSWatcher } from 'chokidar' @@ -79,6 +80,13 @@ export function createRouterGeneratorPlugin( vite: { async configResolved(config) { initConfigAndGenerator({ root: config.root }) + if (userConfig.target === 'octane') { + validateFrameworkPluginOrder({ + framework: userConfig.target, + plugins: config.plugins, + routerPluginName: 'tanstack:router-generator', + }) + } await generate() }, }, diff --git a/packages/router-plugin/src/core/router-hmr-plugin.ts b/packages/router-plugin/src/core/router-hmr-plugin.ts index 75dfde11f8..bc29bb8ea0 100644 --- a/packages/router-plugin/src/core/router-hmr-plugin.ts +++ b/packages/router-plugin/src/core/router-hmr-plugin.ts @@ -5,6 +5,7 @@ import { createRouteHmrStatement } from './hmr' import { debug, normalizePath, routeFactoryCallCodeFilter } from './utils' import { getConfig } from './config' import { createRouterPluginContext } from './router-plugin-context' +import { validateFrameworkPluginOrder } from './framework-plugin-order' import type { UnpluginFactory } from 'unplugin' import type { Config } from './config' import type { RouterPluginContext } from './router-plugin-context' @@ -33,7 +34,7 @@ export function createRouterHmrPlugin( transform: { filter: { // this is necessary for webpack / rspack to avoid matching .html files - id: /\.(m|c)?(j|t)sx?$/, + id: [/\.(m|c)?(j|t)sx?$/, /\.tsrx(?:$|\?)/], code: { include: routeFactoryCallCodeFilter, }, @@ -101,6 +102,11 @@ export function createRouterHmrPlugin( configResolved(config) { ROOT = config.root userConfig = resolveUserConfig() + validateFrameworkPluginOrder({ + framework: userConfig.target, + plugins: config.plugins, + routerPluginName: 'tanstack-router:hmr', + }) }, applyToEnvironment(environment) { if (userConfig.plugin?.vite?.environmentName) { diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx index a463d9ab22..151ff595ad 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@true.tsx @@ -38,13 +38,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -168,16 +173,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "0f796970d75aa160"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -187,7 +198,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@webpack-hot.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@webpack-hot.tsx index 44908e32a9..447250a670 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@webpack-hot.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/arrow-function@webpack-hot.tsx @@ -18,19 +18,28 @@ export const Route = createFileRoute('/posts')({ if (import.meta.webpackHot) { const hot = import.meta.webpackHot; const hotData = hot.data ??= {}; + const routeSignature = "e1ab40bd20014cf5"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateRoute = true; + hotData['tsr-route-signature'] = routeSignature; const routeId = hotData['tsr-route-id'] ?? Route.id ?? (Route.isRoot ? '__root__' : undefined); if (routeId) { hotData['tsr-route-id'] = routeId; } const existingRoute = typeof window !== 'undefined' && routeId ? window.__TSR_ROUTER__?.routesById?.[routeId] : undefined; if (routeId && existingRoute && existingRoute !== Route) { - (function handleRouteUpdate(routeId, newRoute) { + (function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -153,7 +162,7 @@ if (import.meta.webpackHot) { ...(match.__routeContext ?? {}) }; } - })(routeId, Route); + })(routeId, Route, shouldInvalidateRoute); try { const tsrReactRefreshUtils = typeof __react_refresh_utils__ !== 'undefined' ? __react_refresh_utils__ : undefined; const tsrEnqueueUpdate = tsrReactRefreshUtils && typeof tsrReactRefreshUtils.enqueueUpdate === 'function' ? tsrReactRefreshUtils.enqueueUpdate : undefined; @@ -166,6 +175,7 @@ if (import.meta.webpackHot) { if (routeId) { data['tsr-route-id'] = routeId; } + data['tsr-route-signature'] = routeSignature; }); hot.accept(); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx index 9ef182f4b7..f8f64c3902 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute-inline-component@true.tsx @@ -27,13 +27,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -157,16 +162,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "b9facbfa635605cd"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -176,7 +187,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx index 9789370efc..01f5e5040f 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRoute@true.tsx @@ -29,13 +29,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -159,16 +164,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "70736afe8140c16d"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -178,7 +189,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx index 6c7073a8f3..0a6fb0d1ee 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/createRootRouteWithContext-type-args@true.tsx @@ -32,13 +32,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -162,16 +167,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "63414667545e0197"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -181,7 +192,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx index bee793d372..c30860a325 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/explicit-undefined-component@true.tsx @@ -26,13 +26,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -156,16 +161,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "7cf18acca7129808"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -175,7 +186,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx index 75b457b778..71e1a49bd3 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/function-declaration@true.tsx @@ -38,13 +38,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -168,16 +173,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "b7bf411a73660b73"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -187,7 +198,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx index 7f28b7b3d3..7c3fd0982f 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/multi-component@true.tsx @@ -48,13 +48,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -178,16 +183,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "39d9e4424d7cc6bc"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -197,7 +208,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx index 6bee992948..6cecbfe5d4 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/react/string-literal-keys@true.tsx @@ -48,13 +48,18 @@ export function TSRFastRefreshAnchor() { if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -178,16 +183,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "eb0f0a62fcbaa584"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -197,7 +208,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx index 2d1be7e32b..c36be131d6 100644 --- a/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx +++ b/packages/router-plugin/tests/add-hmr/snapshots/solid/arrow-function@true.tsx @@ -9,13 +9,18 @@ export const Route = createFileRoute('/posts')({ if (import.meta.hot) { const hot = import.meta.hot; const hotData = hot.data ??= {}; - const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute) { + const handleRouteUpdate = function handleRouteUpdate(routeId, newRoute, shouldApplyRouteUpdate = true) { const router = window.__TSR_ROUTER__; const oldRoute = router.routesById[routeId]; if (!oldRoute) { return; } ; + if (!shouldApplyRouteUpdate) { + syncHotRouteExport(oldRoute); + return; + } + ; const generatedRouteOptionKeys = new Set(["id", "path", "getParentRoute"]); const generatedRouteOptions = {}; generatedRouteOptionKeys.forEach(key => { @@ -139,16 +144,22 @@ if (import.meta.hot) { }; } }; + const routeSignatureKey = Symbol.for('tanstack.router.hmr.route-signature'); + const routeSignature = "dc01523d36e3d894"; + const previousRouteSignature = hotData['tsr-route-signature']; + const shouldInvalidateCurrentRoute = true; + Route[routeSignatureKey] = routeSignature; + hotData['tsr-route-signature'] = routeSignature; const initialRouteId = Route.id ?? hotData['tsr-route-id']; if (initialRouteId) { hotData['tsr-route-id'] = initialRouteId; } const existingRoute = typeof window !== 'undefined' && initialRouteId ? window.__TSR_ROUTER__?.routesById?.[initialRouteId] : undefined; if (initialRouteId && existingRoute && existingRoute !== Route) { - handleRouteUpdate(initialRouteId, Route); + handleRouteUpdate(initialRouteId, Route, shouldInvalidateCurrentRoute); hotData['tsr-route-update-handled'] = Route; } - hot.accept(newModule => { + import.meta.hot.accept(newModule => { if (Route && newModule && newModule.Route) { const routeId = hotData['tsr-route-id'] ?? Route.id; if (routeId) { @@ -158,7 +169,9 @@ if (import.meta.hot) { delete hotData['tsr-route-update-handled']; return; } - handleRouteUpdate(routeId, newModule.Route); + const nextRouteSignature = newModule.Route[routeSignatureKey]; + const shouldInvalidateNextRoute = true; + handleRouteUpdate(routeId, newModule.Route, shouldInvalidateNextRoute); } }); } \ No newline at end of file diff --git a/packages/router-plugin/tests/code-splitter.test.ts b/packages/router-plugin/tests/code-splitter.test.ts index ae64e2778b..ef48d54ed6 100644 --- a/packages/router-plugin/tests/code-splitter.test.ts +++ b/packages/router-plugin/tests/code-splitter.test.ts @@ -165,6 +165,32 @@ describe('code-splitter works', () => { }) }) +describe('Octane code splitting', () => { + it('uses the official Octane router runtime after TSRX has been lowered', () => { + const code = [ + "import { createFileRoute } from '@tanstack/octane-router'", + 'function RouteComponent() {}', + "export const Route = createFileRoute('/octane')({", + ' component: RouteComponent,', + '})', + ].join('\n') + + const result = compileCodeSplitReferenceRoute({ + code, + filename: 'octane.tsrx', + id: 'octane.tsrx', + addHmr: false, + codeSplitGroupings: defaultCodeSplitGroupings, + targetFramework: 'octane', + }) + + expect(result?.code).toContain( + "import { lazyRouteComponent } from '@tanstack/octane-router'", + ) + expect(result?.code).toContain("import('octane.tsrx?tsr-split=component')") + }) +}) + describe('computeSharedBindings fast paths', () => { it('returns empty when only one split group is present (default groupings)', () => { const code = ` diff --git a/packages/router-plugin/tests/octane-code-splitter.test.ts b/packages/router-plugin/tests/octane-code-splitter.test.ts new file mode 100644 index 0000000000..95be55d74a --- /dev/null +++ b/packages/router-plugin/tests/octane-code-splitter.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from 'vitest' +import { + compileCodeSplitReferenceRoute, + compileCodeSplitVirtualRoute, +} from '../src/core/code-splitter/compilers' +import { + getReferenceRouteCompilerPlugins, + getVirtualRouteCompilerPlugins, +} from '../src/core/code-splitter/plugins/framework-plugins' +import type { CodeSplitGroupings } from '../src/core/constants' + +const groupings: CodeSplitGroupings = [['component'], ['errorComponent']] + +const code = ` +import { createFileRoute } from '@tanstack/octane-router' +import { warmChild } from 'octane' + +export const Route = createFileRoute('/posts')({ + component: PostsPage, + errorComponent: PostsError, +}) + +function PostsPage() {} +PostsPage.$$singleRoot = true +PostsPage.__warm = (__wp) => { const props = __wp; warmChild(Child, props) } +try { PostsPage.__oct_loc = 'posts.tsrx:8:0' } catch { /* frozen component */ } + +function PostsError() {} +PostsError.$$singleRoot = true +PostsError.__warm = (__wp) => { const props = __wp; warmChild(ErrorChild, props) } +try { PostsError.__oct_loc = PostsPage.__oct_loc } catch { /* frozen component */ } + +PostsPage.$$singleRoot = false +PostsPage.__warm = userWarmPlan +PostsPage['__oct_loc'] = userLocation +PostsError.__warm = function (props) { userWarmChild(ErrorChild, props) } +try { PostsError.__oct_loc = 'user-location' } catch {} +PostsError.userMetadata = true +observe(PostsPage) +` + +function compileReference() { + return compileCodeSplitReferenceRoute({ + code, + filename: 'posts.tsrx', + id: 'posts.tsrx', + addHmr: false, + codeSplitGroupings: groupings, + targetFramework: 'octane', + compilerPlugins: getReferenceRouteCompilerPlugins({ + targetFramework: 'octane', + addHmr: false, + }), + })!.code +} + +describe('Octane code-split component companions', () => { + it('removes compiler metadata for every extracted component', () => { + const reference = compileReference() + + expect(reference).not.toContain('PostsPage.$$singleRoot = true') + expect(reference).not.toContain('PostsPage.__warm = __wp =>') + expect(reference).not.toContain("PostsPage.__oct_loc = 'posts.tsrx:8:0'") + expect(reference).not.toContain('PostsError.$$singleRoot = true') + expect(reference).not.toContain('PostsError.__warm = __wp =>') + expect(reference).not.toContain( + 'PostsError.__oct_loc = PostsPage.__oct_loc', + ) + }) + + it('retains compiler metadata in each virtual component module', () => { + const component = compileCodeSplitVirtualRoute({ + code, + filename: 'posts.tsrx?tsr-split=component', + splitTargets: ['component'], + }).code + const errorComponent = compileCodeSplitVirtualRoute({ + code, + filename: 'posts.tsrx?tsr-split=errorComponent', + splitTargets: ['errorComponent'], + }).code + + expect(component).toContain('PostsPage.$$singleRoot = true') + expect(component).toContain('PostsPage.__warm = __wp =>') + expect(component).toContain("PostsPage.__oct_loc = 'posts.tsrx:8:0'") + expect(errorComponent).toContain('PostsError.$$singleRoot = true') + expect(errorComponent).toContain('PostsError.__warm = __wp =>') + expect(errorComponent).toContain( + 'PostsError.__oct_loc = PostsPage.__oct_loc', + ) + }) + + it('preserves component and lazy wrapper identity across Vite updates', () => { + const reference = compileCodeSplitReferenceRoute({ + code, + filename: 'posts.tsrx', + id: 'posts.tsrx', + addHmr: true, + hmrStyle: 'vite', + codeSplitGroupings: groupings, + targetFramework: 'octane', + compilerPlugins: getReferenceRouteCompilerPlugins({ + targetFramework: 'octane', + addHmr: true, + hmrStyle: 'vite', + }), + })!.code + const component = compileCodeSplitVirtualRoute({ + code, + filename: 'posts.tsrx?tsr-split=component', + splitTargets: ['component'], + compilerPlugins: getVirtualRouteCompilerPlugins({ + targetFramework: 'octane', + addHmr: true, + hmrStyle: 'vite', + }), + }).code + + expect(reference).toContain('tsr-split-component:component') + expect(reference).toContain('const TSRSplitComponent =') + expect(component).toContain("from 'octane'") + expect(component).toContain('import.meta.hot.accept()') + expect(component).toContain( + 'TSROctaneComponentHotData["tsr-octane-split-component:component"][TSROctaneHmrSymbol].update(TSROctaneComponentCandidate)', + ) + expect(component).toContain( + 'TSROctaneComponentHotData["tsr-octane-split-component:component"] = TSROctaneComponent', + ) + expect(component).toContain('TSROctaneComponent.$$singleRoot') + expect(component).toContain('export { TSROctaneComponent as component }') + }) + + it('invalidates routes for route dependencies but not split component dependencies', () => { + const source = ` +import { createFileRoute } from '@tanstack/octane-router' + +const loaderValue = 'loader one' +const componentValue = 'component one' + +export const Route = createFileRoute('/posts')({ + loader: () => loaderValue, + component: PostsPage, +}) + +function PostsPage() { + return componentValue +} +` + const compileSignature = (input: string) => { + const reference = compileCodeSplitReferenceRoute({ + code: input, + filename: 'posts.tsrx', + id: 'posts.tsrx', + addHmr: true, + hmrStyle: 'vite', + codeSplitGroupings: [['component']], + targetFramework: 'octane', + compilerPlugins: getReferenceRouteCompilerPlugins({ + targetFramework: 'octane', + addHmr: true, + hmrStyle: 'vite', + }), + })!.code + const match = reference.match(/const routeSignature = "([^"]+)"/) + + expect(match).not.toBeNull() + return match![1] + } + + const initialSignature = compileSignature(source) + expect( + compileSignature(source.replace('component one', 'component two')), + ).toBe(initialSignature) + expect( + compileSignature(source.replace('loader one', 'loader two')), + ).not.toBe(initialSignature) + }) + + it('hands the canonical component wrapper across webpack updates', () => { + const component = compileCodeSplitVirtualRoute({ + code, + filename: 'posts.tsrx?tsr-split=component', + splitTargets: ['component'], + compilerPlugins: getVirtualRouteCompilerPlugins({ + targetFramework: 'octane', + addHmr: true, + hmrStyle: 'webpack', + }), + }).code + + expect(component).toContain('import.meta.webpackHot') + expect(component).toContain('tsr-octane-split-component:component') + expect(component).toContain('TSROctaneComponentHot.dispose') + expect(component).toContain('TSROctaneComponentHot.accept()') + expect(component).not.toContain('import.meta.hot') + }) + + it('does not remove user-authored component expressions', () => { + const reference = compileReference() + + expect(reference).toContain('PostsPage.$$singleRoot = false') + expect(reference).toContain('PostsPage.__warm = userWarmPlan') + expect(reference).toContain("PostsPage['__oct_loc'] = userLocation") + expect(reference).toContain('PostsError.__warm = function') + expect(reference).toContain("PostsError.__oct_loc = 'user-location'") + expect(reference).toContain('PostsError.userMetadata = true') + expect(reference).toContain('observe(PostsPage)') + }) +}) diff --git a/packages/router-plugin/tests/router-plugin-context.test.ts b/packages/router-plugin/tests/router-plugin-context.test.ts index 40664c50a8..383a4a0794 100644 --- a/packages/router-plugin/tests/router-plugin-context.test.ts +++ b/packages/router-plugin/tests/router-plugin-context.test.ts @@ -23,7 +23,10 @@ function getReferencePlugin( return plugin } -async function configurePlugin(plugin: UnpluginOptions) { +async function configurePlugin( + plugin: UnpluginOptions, + pluginNames: Array = [referencePluginName], +) { const hook = plugin.vite?.configResolved if (!hook) { return @@ -31,7 +34,7 @@ async function configurePlugin(plugin: UnpluginOptions) { const config = { root: process.cwd(), - plugins: [{ name: referencePluginName }], + plugins: pluginNames.map((name) => ({ name })), } as never if (typeof hook === 'function') { @@ -77,6 +80,71 @@ function countProgramHotDeclarations(code: string) { } describe('router plugin context', () => { + it.each([ + ['react', 'vite:react-babel'], + ['solid', 'solid'], + ] as const)( + 'keeps the router before the %s source transform', + async (target, frameworkPluginName) => { + const context = createRouterPluginContext() + const splitter = getReferencePlugin( + createRouterCodeSplitterPlugin({ target }, context), + ) + + await expect( + configurePlugin(splitter, [frameworkPluginName, referencePluginName]), + ).rejects.toThrow("is placed before '@tanstack/router-plugin'") + await expect( + configurePlugin(splitter, [referencePluginName, frameworkPluginName]), + ).resolves.toBeUndefined() + }, + ) + + it('requires the Octane compiler to lower TSRX before route analysis', async () => { + const context = createRouterPluginContext() + const splitter = getReferencePlugin( + createRouterCodeSplitterPlugin({ target: 'octane' }, context), + ) + + await expect(configurePlugin(splitter)).rejects.toThrow( + "'octane/compiler/vite' is required for the 'octane' target", + ) + await expect( + configurePlugin(splitter, [referencePluginName, 'octane']), + ).rejects.toThrow( + "'octane/compiler/vite' is placed after '@tanstack/router-plugin'", + ) + }) + + it('accepts the Octane compiler before route analysis', async () => { + const context = createRouterPluginContext() + const splitter = getReferencePlugin( + createRouterCodeSplitterPlugin({ target: 'octane' }, context), + ) + + await expect( + configurePlugin(splitter, ['octane', referencePluginName]), + ).resolves.toBeUndefined() + }) + + it('requires the Octane compiler when code splitting is disabled', async () => { + const context = createRouterPluginContext() + const hmrPlugin = createRouterHmrPlugin({ target: 'octane' }, context) + const hmr = Array.isArray(hmrPlugin) ? hmrPlugin[0]! : hmrPlugin + + await expect(configurePlugin(hmr, ['tanstack-router:hmr'])).rejects.toThrow( + "'octane/compiler/vite' is required for the 'octane' target", + ) + await expect( + configurePlugin(hmr, ['tanstack-router:hmr', 'octane']), + ).rejects.toThrow( + "'octane/compiler/vite' is placed after '@tanstack/router-plugin'", + ) + await expect( + configurePlugin(hmr, ['octane', 'tanstack-router:hmr']), + ).resolves.toBeUndefined() + }) + it('keeps multiple code-splitter instances isolated by explicit context', async () => { const routeFile = normalizePath( path.join(process.cwd(), 'src/routes-a/owned.tsx'), @@ -178,4 +246,32 @@ export const Route = createFileRoute('/owned')({ expect(hmrCode).not.toBeNull() expect(hmrCode).toContain('TSRFastRefreshAnchor') }) + + it('adds Octane HMR handling to compiled .tsrx routes', async () => { + const routeFile = normalizePath( + path.join(process.cwd(), 'src/routes/owned.tsrx'), + ) + const routeCode = ` +import { createFileRoute } from '@tanstack/octane-router' + +function Component() {} + +export const Route = createFileRoute('/owned')({ + component: Component, +}) +` + const context = createRouterPluginContext() + context.routesByFile.set(routeFile, { routeId: '/owned' }) + const hmrPlugin = createRouterHmrPlugin({ target: 'octane' }, context) + const hmrResult = await transformReferenceRoute( + Array.isArray(hmrPlugin) ? hmrPlugin[0]! : hmrPlugin, + routeCode, + routeFile, + ) + const hmrCode = getCode(hmrResult) + + expect(hmrCode).not.toBeNull() + expect(hmrCode).toContain('hot.accept') + expect(hmrCode).toContain("from '@tanstack/octane-router'") + }) }) diff --git a/packages/start-plugin-core/src/constants.ts b/packages/start-plugin-core/src/constants.ts index d31c9e8246..ba6cb67898 100644 --- a/packages/start-plugin-core/src/constants.ts +++ b/packages/start-plugin-core/src/constants.ts @@ -28,7 +28,7 @@ export const DEV_CLIENT_ENTRY = 'virtual:tanstack-start-dev-client-entry' export const SERVER_FN_LOOKUP = 'server-fn-module-lookup' // matches -// .ts, .tsx, .cts, .mts, .js, .jsx, .cjs, .mjs +// .ts, .tsx, .tsrx, .cts, .mts, .js, .jsx, .cjs, .mjs // with optional query params after // but not .json -export const TRANSFORM_ID_REGEX = [/\.[cm]?[tj]sx?($|\?)/] +export const TRANSFORM_ID_REGEX = [/\.[cm]?[tj]sx?($|\?)/, /\.tsrx($|\?)/] diff --git a/packages/start-plugin-core/src/import-protection/constants.ts b/packages/start-plugin-core/src/import-protection/constants.ts index 54bb55fdab..77f14e0a8e 100644 --- a/packages/start-plugin-core/src/import-protection/constants.ts +++ b/packages/start-plugin-core/src/import-protection/constants.ts @@ -19,6 +19,7 @@ export const IMPORT_PROTECTION_DEBUG_FILTER = export const KNOWN_SOURCE_EXTENSIONS = new Set([ '.ts', '.tsx', + '.tsrx', '.mts', '.cts', '.js', diff --git a/packages/start-plugin-core/src/import-protection/defaults.ts b/packages/start-plugin-core/src/import-protection/defaults.ts index 40bdffd630..a2a7b5431f 100644 --- a/packages/start-plugin-core/src/import-protection/defaults.ts +++ b/packages/start-plugin-core/src/import-protection/defaults.ts @@ -1,3 +1,4 @@ +import { startFrameworks } from '../types' import type { ImportProtectionEnvRules } from '../schema' import type { Pattern } from './utils' @@ -6,16 +7,14 @@ export interface DefaultImportProtectionRules { server: Required } -const frameworks = ['react', 'solid', 'vue'] as const - /** * Returns the default import protection rules. * - * All three framework variants are always included so that, e.g., a React + * All framework variants are always included so that, e.g., a React * project also denies `@tanstack/solid-start/server` imports. */ export function getDefaultImportProtectionRules(): DefaultImportProtectionRules { - const clientSpecifiers: Array = frameworks.map( + const clientSpecifiers: Array = startFrameworks.map( (fw) => `@tanstack/${fw}-start/server`, ) @@ -41,7 +40,11 @@ export function getMarkerSpecifiers(): { clientOnly: Array } { return { - serverOnly: frameworks.map((fw) => `@tanstack/${fw}-start/server-only`), - clientOnly: frameworks.map((fw) => `@tanstack/${fw}-start/client-only`), + serverOnly: startFrameworks.map( + (fw) => `@tanstack/${fw}-start/server-only`, + ), + clientOnly: startFrameworks.map( + (fw) => `@tanstack/${fw}-start/client-only`, + ), } } diff --git a/packages/start-plugin-core/src/resolve-entries.ts b/packages/start-plugin-core/src/resolve-entries.ts index dd28815fa0..26862743f3 100644 --- a/packages/start-plugin-core/src/resolve-entries.ts +++ b/packages/start-plugin-core/src/resolve-entries.ts @@ -11,7 +11,7 @@ function resolveModule(opts: ResolveModuleOptions): string | undefined { } return resolveModulePath(baseName, { from: opts.from, - extensions: ['.ts', '.js', '.mts', '.mjs', '.tsx', '.jsx'], + extensions: ['.ts', '.js', '.mts', '.mjs', '.tsx', '.jsx', '.tsrx'], try: true, }) } diff --git a/packages/start-plugin-core/src/rsbuild/import-protection.ts b/packages/start-plugin-core/src/rsbuild/import-protection.ts index e3d63ba04d..869603f3c0 100644 --- a/packages/start-plugin-core/src/rsbuild/import-protection.ts +++ b/packages/start-plugin-core/src/rsbuild/import-protection.ts @@ -566,6 +566,7 @@ function getModuleFile(module: RspackModule): string { const IMPORT_PROTECTION_PARSEABLE_EXTENSIONS = new Set([ '.ts', '.tsx', + '.tsrx', '.mts', '.cts', '.js', diff --git a/packages/start-plugin-core/src/rsbuild/start-router-plugin.ts b/packages/start-plugin-core/src/rsbuild/start-router-plugin.ts index 40dfb9e42a..ae590211eb 100644 --- a/packages/start-plugin-core/src/rsbuild/start-router-plugin.ts +++ b/packages/start-plugin-core/src/rsbuild/start-router-plugin.ts @@ -6,6 +6,7 @@ import { } from '@tanstack/router-plugin/rspack' import { routesManifestPlugin } from '../start-router-plugin/generator-plugins/routes-manifest-plugin' import { prerenderRoutesPlugin } from '../start-router-plugin/generator-plugins/prerender-routes-plugin' +import { composeGeneratorPlugins } from '../start-router-plugin/generator-plugins/compose-generator-plugins' import { buildRouteTreeFileFooterFromConfig } from '../start-router-plugin/route-tree-footer' import { RSBUILD_ENVIRONMENT_NAMES } from './planning' import type { RsbuildPluginAPI } from '@rsbuild/core' @@ -50,12 +51,16 @@ export function registerRouterPlugins( corePluginOpts: opts.corePluginOpts, }) }, - plugins: [ - routesManifestPlugin(), - ...(opts.startPluginOpts.prerender?.enabled === true - ? [prerenderRoutesPlugin()] - : []), - ], + plugins: composeGeneratorPlugins({ + frameworkPlugins: opts.corePluginOpts.routerGeneratorPlugins, + userPlugins: routerConfig.plugins, + builtInPlugins: [ + routesManifestPlugin(), + ...(opts.startPluginOpts.prerender?.enabled === true + ? [prerenderRoutesPlugin()] + : []), + ], + }), }, routerPluginContext, ) diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index ab0bb9660a..0a9e6d1c21 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -85,6 +85,7 @@ export function parseStartConfig( ...getConfig( { ...options.router, + target: corePluginOpts.framework, routesDirectory, generatedRouteTree, }, diff --git a/packages/start-plugin-core/src/start-router-plugin/generator-plugins/compose-generator-plugins.ts b/packages/start-plugin-core/src/start-router-plugin/generator-plugins/compose-generator-plugins.ts new file mode 100644 index 0000000000..a39e0f9d1f --- /dev/null +++ b/packages/start-plugin-core/src/start-router-plugin/generator-plugins/compose-generator-plugins.ts @@ -0,0 +1,13 @@ +import type { GeneratorPlugin } from '@tanstack/router-generator' + +export function composeGeneratorPlugins(opts: { + frameworkPlugins?: ReadonlyArray | undefined + userPlugins?: ReadonlyArray | undefined + builtInPlugins: ReadonlyArray +}): Array { + return [ + ...(opts.frameworkPlugins ?? []), + ...(opts.userPlugins ?? []), + ...opts.builtInPlugins, + ] +} diff --git a/packages/start-plugin-core/src/types.ts b/packages/start-plugin-core/src/types.ts index db02759842..f95c7504b2 100644 --- a/packages/start-plugin-core/src/types.ts +++ b/packages/start-plugin-core/src/types.ts @@ -1,9 +1,12 @@ import type * as babel from '@babel/core' import type * as t from '@babel/types' import type { GeneratorResult } from '@tanstack/router-utils' +import type { GeneratorPlugin } from '@tanstack/router-generator' import type { TanStackStartOutputConfig } from './schema' -export type CompileStartFrameworkOptions = 'react' | 'solid' | 'vue' +export const startFrameworks = ['react', 'solid', 'vue', 'octane'] as const + +export type CompileStartFrameworkOptions = (typeof startFrameworks)[number] export type ServerFnLookupAccess = { origin: 'client' } | { origin: 'server' } @@ -137,6 +140,8 @@ export interface TanStackStartCoreOptions { serializationAdapters?: Array | undefined compilerTransforms?: Array | undefined serverFnProviderModuleDirectives?: ReadonlyArray | undefined + /** Framework-owned route source adapters and generator extensions. */ + routerGeneratorPlugins?: ReadonlyArray | undefined } export interface ResolvedStartConfig { diff --git a/packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts b/packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts index e4f06893a7..984fcdd2b0 100644 --- a/packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts +++ b/packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts @@ -8,6 +8,7 @@ import { normalizePath } from 'vite' import { VITE_ENVIRONMENT_NAMES } from '../../constants' import { routesManifestPlugin } from '../../start-router-plugin/generator-plugins/routes-manifest-plugin' import { prerenderRoutesPlugin } from '../../start-router-plugin/generator-plugins/prerender-routes-plugin' +import { composeGeneratorPlugins } from '../../start-router-plugin/generator-plugins/compose-generator-plugins' import { buildRouteTreeFileFooterFromConfig } from '../../start-router-plugin/route-tree-footer' import { pruneServerOnlySubtrees } from '../../start-router-plugin/pruneServerOnlySubtrees' import { SERVER_PROP } from '../../start-router-plugin/constants' @@ -146,10 +147,18 @@ export function tanStackStartRouter( clientTreePlugin, tanstackRouterGenerator(() => { const routerConfig = getConfig().startConfig.router - const plugins = [clientTreeGeneratorPlugin, routesManifestPlugin()] + const builtInPlugins: Array = [ + clientTreeGeneratorPlugin, + routesManifestPlugin(), + ] if (startPluginOpts.prerender?.enabled === true) { - plugins.push(prerenderRoutesPlugin()) + builtInPlugins.push(prerenderRoutesPlugin()) } + const plugins = composeGeneratorPlugins({ + frameworkPlugins: corePluginOpts.routerGeneratorPlugins, + userPlugins: routerConfig.plugins, + builtInPlugins, + }) return { ...routerConfig, target: corePluginOpts.framework, diff --git a/packages/start-plugin-core/tests/importProtection/defaults.test.ts b/packages/start-plugin-core/tests/importProtection/defaults.test.ts index 53e33ea902..3ccd304718 100644 --- a/packages/start-plugin-core/tests/importProtection/defaults.test.ts +++ b/packages/start-plugin-core/tests/importProtection/defaults.test.ts @@ -13,6 +13,7 @@ describe('getDefaultImportProtectionRules', () => { '@tanstack/react-start/server', '@tanstack/solid-start/server', '@tanstack/vue-start/server', + '@tanstack/octane-start/server', ]), ) @@ -59,10 +60,12 @@ describe('getMarkerSpecifiers', () => { test('includes all frameworks', () => { const markers = getMarkerSpecifiers() - expect(markers.serverOnly.length).toBe(3) - expect(markers.clientOnly.length).toBe(3) + expect(markers.serverOnly.length).toBe(4) + expect(markers.clientOnly.length).toBe(4) expect(markers.serverOnly).toContain('@tanstack/solid-start/server-only') expect(markers.clientOnly).toContain('@tanstack/vue-start/client-only') + expect(markers.serverOnly).toContain('@tanstack/octane-start/server-only') + expect(markers.clientOnly).toContain('@tanstack/octane-start/client-only') }) }) diff --git a/packages/start-plugin-core/tests/importProtection/utils.test.ts b/packages/start-plugin-core/tests/importProtection/utils.test.ts index 459601036f..93caf02238 100644 --- a/packages/start-plugin-core/tests/importProtection/utils.test.ts +++ b/packages/start-plugin-core/tests/importProtection/utils.test.ts @@ -76,6 +76,7 @@ describe('withoutKnownExtension', () => { test('removes known source extension', () => { expect(withoutKnownExtension('/a/b.ts')).toBe('/a/b') expect(withoutKnownExtension('/a/b.tsx')).toBe('/a/b') + expect(withoutKnownExtension('/a/b.tsrx')).toBe('/a/b') }) test('keeps unknown extension', () => { diff --git a/packages/start-plugin-core/tests/octane-framework.test.ts b/packages/start-plugin-core/tests/octane-framework.test.ts new file mode 100644 index 0000000000..7975b0c7ed --- /dev/null +++ b/packages/start-plugin-core/tests/octane-framework.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { TRANSFORM_ID_REGEX } from '../src/constants' +import { parseStartConfig } from '../src/schema' +import { getLookupConfigurationsForEnv } from '../src/start-compiler/config' +import { composeGeneratorPlugins } from '../src/start-router-plugin/generator-plugins/compose-generator-plugins' +import { startFrameworks } from '../src/types' +import type { GeneratorPlugin } from '@tanstack/router-generator' + +describe('Octane framework configuration', () => { + it('is a supported Start compiler framework', () => { + expect(startFrameworks).toContain('octane') + + const clientLookups = getLookupConfigurationsForEnv('client', 'octane') + const serverLookups = getLookupConfigurationsForEnv('server', 'octane') + + expect(clientLookups).toContainEqual( + expect.objectContaining({ + libName: '@tanstack/octane-start', + rootExport: 'createServerFn', + }), + ) + expect(serverLookups).toContainEqual( + expect.objectContaining({ + libName: '@tanstack/octane-router', + rootExport: 'ClientOnly', + }), + ) + }) + + it('runs Start transforms on compiled .tsrx modules', () => { + expect( + TRANSFORM_ID_REGEX.some((pattern) => pattern.test('route.tsrx')), + ).toBe(true) + expect( + TRANSFORM_ID_REGEX.some((pattern) => + pattern.test('route.tsrx?tsr-split=component'), + ), + ).toBe(true) + }) + + it('applies target-specific router defaults before resolving Start config', () => { + const root = process.cwd() + + expect( + parseStartConfig({}, { framework: 'octane' }, root).router.addExtensions, + ).toBe(true) + expect( + parseStartConfig( + { router: { addExtensions: false } }, + { framework: 'octane' }, + root, + ).router.addExtensions, + ).toBe(false) + + for (const framework of ['react', 'solid', 'vue'] as const) { + expect( + parseStartConfig({}, { framework }, root).router.addExtensions, + ).toBe(false) + } + }) + + it('runs required framework adapters before user and built-in plugins', () => { + const plugin = (name: string): GeneratorPlugin => ({ name }) + + expect( + composeGeneratorPlugins({ + frameworkPlugins: [plugin('framework')], + userPlugins: [plugin('user')], + builtInPlugins: [plugin('built-in')], + }).map(({ name }) => name), + ).toEqual(['framework', 'user', 'built-in']) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a91aaea20f..87e4a8aaef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,7 @@ overrides: '@tanstack/history': workspace:* '@tanstack/router-core': workspace:* '@tanstack/react-router': workspace:* + '@tanstack/octane-router': workspace:* '@tanstack/router-cli': workspace:* '@tanstack/router-devtools': workspace:* '@tanstack/router-devtools-core': workspace:^ @@ -49,6 +50,9 @@ overrides: '@tanstack/vue-start': workspace:* '@tanstack/vue-start-client': workspace:* '@tanstack/vue-start-server': workspace:* + '@tanstack/octane-start': workspace:* + '@tanstack/octane-start-client': workspace:* + '@tanstack/octane-start-server': workspace:* '@tanstack/start-plugin-core': workspace:* '@tanstack/start-client-core': workspace:* '@tanstack/start-server-core': workspace:* @@ -59,6 +63,7 @@ overrides: '@tanstack/router-utils': workspace:* '@tanstack/start-static-server-functions': workspace:* '@tanstack/nitro-v2-vite-plugin': workspace:* + octane: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane importers: @@ -314,7 +319,7 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) benchmarks/memory/client: dependencies: @@ -369,7 +374,7 @@ importers: version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) jsdom: specifier: 29.1.1 - version: 29.1.1(@noble/hashes@2.0.1) + version: 29.1.1(@noble/hashes@2.2.0) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -381,7 +386,7 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) benchmarks/memory/server: dependencies: @@ -442,7 +447,7 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) benchmarks/ssr: dependencies: @@ -500,7 +505,7 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) e2e/e2e-utils: devDependencies: @@ -545,7 +550,47 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + + e2e/octane-start/basic: + dependencies: + '@tanstack/octane-router': + specifier: workspace:* + version: link:../../../packages/octane-router + '@tanstack/octane-start': + specifier: workspace:* + version: link:../../../packages/octane-start + octane: + specifier: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane + version: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1) + devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.29.0 + version: 1.29.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(workerd@1.20260317.1)(wrangler@4.75.0) + '@netlify/vite-plugin-tanstack-start': + specifier: ^1.1.4 + version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@playwright/test': + specifier: ^1.61.0 + version: 1.61.1 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + nitro: + specifier: ^3.0.260311-beta + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + wrangler: + specifier: ^4.74.0 + version: 4.75.0 e2e/react-router/basepath-file-based: dependencies: @@ -1990,7 +2035,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) sass: specifier: ^1.97.2 version: 1.97.2 @@ -2219,7 +2264,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) srvx: specifier: ^0.11.9 version: 0.11.12 @@ -2317,7 +2362,7 @@ importers: version: 9.2.1 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -3353,7 +3398,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -9068,7 +9113,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -9810,7 +9855,7 @@ importers: version: 0.5.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.1.18 version: 4.2.2 @@ -11559,7 +11604,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -11771,7 +11816,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -12586,6 +12631,112 @@ importers: specifier: ^8.0.14 version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + packages/octane-router: + dependencies: + '@tanstack/history': + specifier: workspace:* + version: link:../history + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/store': + specifier: ^0.9.3 + version: 0.9.3 + isbot: + specifier: ^5.1.22 + version: 5.1.28 + octane: + specifier: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane + version: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1) + devDependencies: + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + + packages/octane-start: + dependencies: + '@tanstack/octane-router': + specifier: workspace:* + version: link:../octane-router + '@tanstack/octane-start-client': + specifier: workspace:* + version: link:../octane-start-client + '@tanstack/octane-start-server': + specifier: workspace:* + version: link:../octane-start-server + '@tanstack/start-client-core': + specifier: workspace:* + version: link:../start-client-core + '@tanstack/start-plugin-core': + specifier: workspace:* + version: link:../start-plugin-core + '@tanstack/start-server-core': + specifier: workspace:* + version: link:../start-server-core + octane: + specifier: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane + version: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1) + pathe: + specifier: ^2.0.3 + version: 2.0.3 + devDependencies: + '@tanstack/router-utils': + specifier: workspace:* + version: link:../router-utils + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + + packages/octane-start-client: + dependencies: + '@tanstack/octane-router': + specifier: workspace:* + version: link:../octane-router + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/start-client-core': + specifier: workspace:* + version: link:../start-client-core + octane: + specifier: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane + version: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1) + devDependencies: + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + + packages/octane-start-server: + dependencies: + '@tanstack/octane-router': + specifier: workspace:* + version: link:../octane-router + '@tanstack/router-core': + specifier: workspace:* + version: link:../router-core + '@tanstack/start-server-core': + specifier: workspace:* + version: link:../start-server-core + octane: + specifier: github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane + version: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1) + devDependencies: + '@types/node': + specifier: 25.0.9 + version: 25.0.9 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + packages/react-router: dependencies: '@tanstack/history': @@ -15780,80 +15931,40 @@ packages: resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.4': - resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - '@img/sharp-darwin-arm64@0.34.5': resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.4': - resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.3': - resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.3': - resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} - cpu: [x64] - os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.3': - resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.3': - resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} - cpu: [arm] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.3': - resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] @@ -15866,61 +15977,30 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.3': - resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.3': - resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.3': - resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.3': - resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} - cpu: [x64] - os: [linux] - libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.4': - resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -15928,13 +16008,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.4': - resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -15942,13 +16015,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.4': - resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -15963,13 +16029,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.4': - resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -15977,13 +16036,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.4': - resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -15991,13 +16043,6 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.4': - resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -16005,13 +16050,6 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.4': - resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -16019,46 +16057,23 @@ packages: os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.4': - resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.4': - resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.4': - resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.4': - resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -16567,6 +16582,10 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -16797,9 +16816,6 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/types@0.115.0': - resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} - '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} @@ -17963,73 +17979,36 @@ packages: '@types/react-dom': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.0.2': resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.2': resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.2': resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.2': resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18037,13 +18016,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.2': resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18051,13 +18023,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18065,13 +18030,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18079,13 +18037,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18093,13 +18044,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.2': resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18107,46 +18051,23 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.2': resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.2': resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -19011,6 +18932,11 @@ packages: '@supabase/supabase-js@2.48.1': resolution: {integrity: sha512-VMD+CYk/KxfwGbI4fqwSUVA7CLr1izXpqfFerhnYPSi6LEKD8GoR4kuO5Cc8a+N43LnfSQwLJu4kVm2e4etEmA==} + '@sveltejs/acorn-typescript@1.0.11': + resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} + peerDependencies: + acorn: ^8.9.0 + '@swc-node/core@1.14.1': resolution: {integrity: sha512-jrt5GUaZUU6cmMS+WTJEvGvaB6j1YNKPHPzC2PUi2BjaFbtxURHj6641Az6xN7b665hNniAIdvjxWcRml5yCnw==} engines: {node: '>= 10'} @@ -19478,6 +19404,9 @@ packages: react-dom: ^19.2.3 typescript: '>=5.7.2' + '@tsrx/core@0.1.40': + resolution: {integrity: sha512-JLvoVchSw3qVgrMj6hEPqhF4iojAasENLuyohWkKoF9k6Sk3nFR3puBE2++iL67g2RuvhQ7sLfKQg2VUxcdGUQ==} + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -19535,6 +19464,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -20358,6 +20290,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -21288,14 +21225,6 @@ packages: srvx: optional: true - crossws@0.4.4: - resolution: {integrity: sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==} - peerDependencies: - srvx: '>=0.7.1' - peerDependenciesMeta: - srvx: - optional: true - crossws@0.4.5: resolution: {integrity: sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA==} peerDependencies: @@ -21603,6 +21532,9 @@ packages: dettle@1.0.5: resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + dexie@4.2.1: resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} @@ -22113,6 +22045,14 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@2.2.13: + resolution: {integrity: sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -22573,16 +22513,6 @@ packages: crossws: optional: true - h3@2.0.1-rc.16: - resolution: {integrity: sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag==} - engines: {node: '>=20.11.1'} - hasBin: true - peerDependencies: - crossws: ^0.4.1 - peerDependenciesMeta: - crossws: - optional: true - h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} @@ -24064,6 +23994,11 @@ packages: ocache@0.1.2: resolution: {integrity: sha512-lI34wjM7cahEdrq2I5obbF7MEdE97vULf6vNj6ZCzwEadzyXO1w7QOl2qzzG4IL8cyO7wDtXPj9CqW/aG3mn7g==} + octane@https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane: + resolution: {gitHosted: true, integrity: sha512-khNZ4UDfdTW6aBi178zyizpRQy6nyyrbJP5cdMcJUrPWhjqTFSN4e02TKftWUBJB2X/nZcT5MLbXScU6aElL6A==, path: /packages/octane, tarball: https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51} + version: 0.1.8 + engines: {node: '>=22'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -24958,11 +24893,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - rolldown@1.0.0-rc.9: - resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.0.2: resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -25197,10 +25127,6 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - sharp@0.34.4: - resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -27911,7 +27837,7 @@ snapshots: '@codspeed/core': 5.5.0(debug@4.4.3) tinybench: 2.9.0 vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) - vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) transitivePeerDependencies: - debug - supports-color @@ -28708,9 +28634,9 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 - '@exodus/bytes@1.15.1(@noble/hashes@2.0.1)': + '@exodus/bytes@1.15.1(@noble/hashes@2.2.0)': optionalDependencies: - '@noble/hashes': 2.0.1 + '@noble/hashes': 2.2.0 '@fastify/accept-negotiator@2.0.1': {} @@ -29111,108 +29037,56 @@ snapshots: '@img/colour@1.0.0': {} - '@img/sharp-darwin-arm64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.3 - optional: true - '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.3 - optional: true - '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.3': - optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.3': - optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.3': - optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.3': - optional: true - '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.3': - optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.3': - optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.3': - optional: true - '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.3': - optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.3': - optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.3 - optional: true - '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.3 - optional: true - '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.3 - optional: true - '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.2.4 @@ -29223,71 +29097,37 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.3 - optional: true - '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.3 - optional: true - '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.3 - optional: true - '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.4': - dependencies: - '@emnapi/runtime': 1.10.0 - optional: true - '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.10.0 optional: true - '@img/sharp-win32-arm64@0.34.4': - optional: true - '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.4': - optional: true - '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.4': - optional: true - '@img/sharp-win32-x64@0.34.5': optional: true @@ -30061,7 +29901,7 @@ snapshots: '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.56.0)': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 @@ -30106,6 +29946,8 @@ snapshots: '@noble/hashes@2.0.1': {} + '@noble/hashes@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -30254,8 +30096,6 @@ snapshots: '@oxc-minify/binding-win32-x64-msvc@0.110.0': optional: true - '@oxc-project/types@0.115.0': {} - '@oxc-project/types@0.132.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': @@ -31412,86 +31252,42 @@ snapshots: '@types/react': 19.2.9 '@types/react-dom': 19.2.3(@types/react@19.2.9) - '@rolldown/binding-android-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-android-arm64@1.0.2': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-darwin-arm64@1.0.2': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - optional: true - '@rolldown/binding-darwin-x64@1.0.2': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - optional: true - '@rolldown/binding-freebsd-x64@1.0.2': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': dependencies: '@emnapi/core': 1.10.0 @@ -31499,15 +31295,9 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true @@ -32281,7 +32071,7 @@ snapshots: '@supabase/node-fetch': 2.6.15 '@types/phoenix': 1.6.6 '@types/ws': 8.5.14 - ws: 8.18.3 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -32308,6 +32098,10 @@ snapshots: - bufferutil - utf-8-validate + '@sveltejs/acorn-typescript@1.0.11(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + '@swc-node/core@1.14.1(@swc/core@1.15.33(@swc/helpers@0.5.23))(@swc/types@0.1.26)': dependencies: '@swc/core': 1.15.33(@swc/helpers@0.5.23) @@ -32805,6 +32599,21 @@ snapshots: react-dom: 19.2.3(react@19.2.3) typescript: 6.0.2 + '@tsrx/core@0.1.40(@typescript-eslint/types@8.57.1)': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/hashes': 2.2.0 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + acorn: 8.16.0 + esrap: 2.2.13(@typescript-eslint/types@8.57.1) + is-reference: 3.0.3 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -32883,6 +32692,10 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.7': {} '@types/estree@1.0.8': {} @@ -33164,21 +32977,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.53.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@6.0.2) '@typescript-eslint/types': 8.53.0 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@6.0.2)': + '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@6.0.2) - '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 debug: 4.4.3 - typescript: 6.0.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -33258,14 +33071,14 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@6.0.2)': dependencies: typescript: 6.0.2 + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@6.0.2)': dependencies: typescript: 6.0.2 @@ -33402,21 +33215,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 - debug: 4.4.3 - minimatch: 9.0.9 - semver: 7.7.3 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.53.0(typescript@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.53.0(typescript@6.0.2) @@ -33432,6 +33230,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.2 + tinyglobby: 0.2.16 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.57.1(typescript@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.57.1(typescript@6.0.2) @@ -33596,15 +33409,15 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.0 '@rollup/pluginutils': 5.1.4(rollup@4.56.0) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 glob: 10.5.0 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 - picomatch: 4.0.3 + picomatch: 4.0.4 resolve-from: 5.0.0 transitivePeerDependencies: - encoding @@ -34252,6 +34065,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-import-phases@1.0.4(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -34264,6 +34081,8 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -34645,7 +34464,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) solid-js: 1.9.12 - vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vue: 3.5.25(typescript@6.0.2) transitivePeerDependencies: - '@cloudflare/workers-types' @@ -35249,14 +35068,9 @@ snapshots: optionalDependencies: srvx: 0.10.1 - crossws@0.4.4(srvx@0.11.15): - optionalDependencies: - srvx: 0.11.15 - crossws@0.4.5(srvx@0.11.15): optionalDependencies: srvx: 0.11.15 - optional: true css-loader@7.1.2(webpack@5.97.1): dependencies: @@ -35343,10 +35157,10 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 - data-urls@7.0.0(@noble/hashes@2.0.1): + data-urls@7.0.0(@noble/hashes@2.2.0): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) + whatwg-url: 16.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -35488,11 +35302,11 @@ snapshots: dependencies: node-source-walk: 7.0.1 - detective-postcss@7.0.1(postcss@8.5.8): + detective-postcss@7.0.1(postcss@8.5.15): dependencies: is-url: 1.2.4 - postcss: 8.5.8 - postcss-values-parser: 6.0.2(postcss@8.5.8) + postcss: 8.5.15 + postcss-values-parser: 6.0.2(postcss@8.5.15) detective-sass@6.0.1: dependencies: @@ -35508,7 +35322,7 @@ snapshots: detective-typescript@14.0.0(typescript@5.9.3): dependencies: - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) ast-module-types: 6.0.1 node-source-walk: 7.0.1 typescript: 5.9.3 @@ -35530,6 +35344,8 @@ snapshots: dettle@1.0.5: {} + devalue@5.8.1: {} + dexie@4.2.1: {} diff@8.0.2: {} @@ -35711,7 +35527,7 @@ snapshots: env-runner@0.1.6(miniflare@4.20260317.0): dependencies: - crossws: 0.4.4(srvx@0.11.15) + crossws: 0.4.5(srvx@0.11.15) httpxy: 0.3.1 srvx: 0.11.15 optionalDependencies: @@ -36247,6 +36063,12 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.2.13(@typescript-eslint/types@8.57.1): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.57.1 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -36823,13 +36645,6 @@ snapshots: optionalDependencies: crossws: 0.4.3(srvx@0.10.1) - h3@2.0.1-rc.16(crossws@0.4.4(srvx@0.11.15)): - dependencies: - rou3: 0.8.1 - srvx: 0.11.15 - optionalDependencies: - crossws: 0.4.4(srvx@0.11.15) - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): dependencies: rou3: 0.8.1 @@ -36906,9 +36721,9 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -37152,7 +36967,7 @@ snapshots: listhen: 1.9.0 ofetch: 1.5.1 pathe: 2.0.3 - sharp: 0.34.4 + sharp: 0.34.5 svgo: 4.0.0 ufo: 1.6.3 unstorage: 1.17.4(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2) @@ -37486,17 +37301,17 @@ snapshots: - supports-color - utf-8-validate - jsdom@29.1.1(@noble/hashes@2.0.1): + jsdom@29.1.1(@noble/hashes@2.2.0): dependencies: '@asamuzakjp/css-color': 5.1.11 '@asamuzakjp/dom-selector': 7.1.1 '@bramus/specificity': 2.4.2 '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) - '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) css-tree: 3.2.1 - data-urls: 7.0.0(@noble/hashes@2.0.1) + data-urls: 7.0.0(@noble/hashes@2.2.0) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.1 parse5: 8.0.1 @@ -37507,7 +37322,7 @@ snapshots: w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) + whatwg-url: 16.0.1(@noble/hashes@2.2.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -38169,19 +37984,19 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 - crossws: 0.4.4(srvx@0.11.15) + crossws: 0.4.5(srvx@0.11.15) db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) env-runner: 0.1.6(miniflare@4.20260317.0) - h3: 2.0.1-rc.16(crossws@0.4.4(srvx@0.11.15)) + h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) hookable: 6.1.0 nf3: 0.3.11 ocache: 0.1.2 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 - rolldown: 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + rolldown: 1.0.2 srvx: 0.11.15 unenv: 2.0.0-rc.24 unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) @@ -38200,8 +38015,6 @@ snapshots: - '@capacitor/preferences' - '@deno/kv' - '@electric-sql/pglite' - - '@emnapi/core' - - '@emnapi/runtime' - '@libsql/client' - '@netlify/blobs' - '@planetscale/database' @@ -38366,7 +38179,7 @@ snapshots: node-source-walk@7.0.1: dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 node-stream-zip@1.15.0: {} @@ -38570,6 +38383,14 @@ snapshots: dependencies: ohash: 2.0.11 + octane@https://codeload.github.com/tannerlinsley/octane/tar.gz/690053baee01b38f836c6b662cc2c0efe8403a51#path:/packages/octane(@typescript-eslint/types@8.57.1): + dependencies: + '@tsrx/core': 0.1.40(@typescript-eslint/types@8.57.1) + devalue: 5.8.1 + esrap: 2.2.13(@typescript-eslint/types@8.57.1) + transitivePeerDependencies: + - '@typescript-eslint/types' + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -39009,11 +38830,11 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss-values-parser@6.0.2(postcss@8.5.8): + postcss-values-parser@6.0.2(postcss@8.5.15): dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.5.8 + postcss: 8.5.15 quote-unquote: 1.0.0 postcss@8.5.15: @@ -39065,7 +38886,7 @@ snapshots: detective-amd: 6.0.1 detective-cjs: 6.0.1 detective-es6: 5.0.1 - detective-postcss: 7.0.1(postcss@8.5.8) + detective-postcss: 7.0.1(postcss@8.5.15) detective-sass: 6.0.1 detective-scss: 5.0.1 detective-stylus: 5.0.1 @@ -39073,7 +38894,7 @@ snapshots: detective-vue2: 2.2.0(typescript@5.9.3) module-definition: 6.0.1 node-source-walk: 7.0.1 - postcss: 8.5.8 + postcss: 8.5.15 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -39582,30 +39403,6 @@ snapshots: glob: 13.0.0 package-json-from-dist: 1.0.1 - rolldown@1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): - dependencies: - '@oxc-project/types': 0.115.0 - '@rolldown/pluginutils': 1.0.0-rc.9 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-x64': 1.0.0-rc.9 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - rolldown@1.0.2: dependencies: '@oxc-project/types': 0.132.0 @@ -39907,35 +39704,6 @@ snapshots: dependencies: kind-of: 6.0.3 - sharp@0.34.4: - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.8.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.4 - '@img/sharp-darwin-x64': 0.34.4 - '@img/sharp-libvips-darwin-arm64': 1.2.3 - '@img/sharp-libvips-darwin-x64': 1.2.3 - '@img/sharp-libvips-linux-arm': 1.2.3 - '@img/sharp-libvips-linux-arm64': 1.2.3 - '@img/sharp-libvips-linux-ppc64': 1.2.3 - '@img/sharp-libvips-linux-s390x': 1.2.3 - '@img/sharp-libvips-linux-x64': 1.2.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 - '@img/sharp-libvips-linuxmusl-x64': 1.2.3 - '@img/sharp-linux-arm': 0.34.4 - '@img/sharp-linux-arm64': 0.34.4 - '@img/sharp-linux-ppc64': 0.34.4 - '@img/sharp-linux-s390x': 0.34.4 - '@img/sharp-linux-x64': 0.34.4 - '@img/sharp-linuxmusl-arm64': 0.34.4 - '@img/sharp-linuxmusl-x64': 0.34.4 - '@img/sharp-wasm32': 0.34.4 - '@img/sharp-win32-arm64': 0.34.4 - '@img/sharp-win32-ia32': 0.34.4 - '@img/sharp-win32-x64': 0.34.4 - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -40279,7 +40047,7 @@ snapshots: dependencies: commander: 11.1.0 css-select: 5.1.0 - css-tree: 3.1.0 + css-tree: 3.2.1 css-what: 6.1.0 csso: 5.0.5 picocolors: 1.1.1 @@ -40387,7 +40155,7 @@ snapshots: terser@5.37.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -41031,7 +40799,7 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -41056,11 +40824,11 @@ snapshots: optionalDependencies: '@types/node': 25.0.9 '@vitest/ui': 4.1.4(vitest@4.1.4) - jsdom: 29.1.1(@noble/hashes@2.0.1) + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.2.0))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -41085,7 +40853,7 @@ snapshots: optionalDependencies: '@types/node': 25.0.9 '@vitest/ui': 4.1.4(vitest@4.1.4) - jsdom: 29.1.1(@noble/hashes@2.0.1) + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw @@ -41431,9 +41199,9 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 - whatwg-url@16.0.1(@noble/hashes@2.0.1): + whatwg-url@16.0.1(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aca73336f9..5a8b6ee46b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,6 +31,7 @@ packages: - 'e2e/react-start/*' - 'e2e/solid-start/*' - 'e2e/vue-start/*' + - 'e2e/octane-start/*' - 'e2e/eslint-plugin-start' catalog: @@ -46,6 +47,7 @@ catalog: '@tanstack/react-query': ^5.99.0 '@tanstack/solid-query': ^5.99.0 '@tanstack/vue-query': ^5.99.0 + octane: ^0.1.8 overrides: '@types/babel__traverse': '^7.28.0' @@ -66,6 +68,7 @@ overrides: '@tanstack/history': 'workspace:*' '@tanstack/router-core': 'workspace:*' '@tanstack/react-router': 'workspace:*' + '@tanstack/octane-router': 'workspace:*' '@tanstack/router-cli': 'workspace:*' '@tanstack/router-devtools': 'workspace:*' '@tanstack/router-devtools-core': 'workspace:^' @@ -92,6 +95,9 @@ overrides: '@tanstack/vue-start': 'workspace:*' '@tanstack/vue-start-client': 'workspace:*' '@tanstack/vue-start-server': 'workspace:*' + '@tanstack/octane-start': 'workspace:*' + '@tanstack/octane-start-client': 'workspace:*' + '@tanstack/octane-start-server': 'workspace:*' '@tanstack/start-plugin-core': 'workspace:*' '@tanstack/start-client-core': 'workspace:*' '@tanstack/start-server-core': 'workspace:*' @@ -102,6 +108,7 @@ overrides: '@tanstack/router-utils': 'workspace:*' '@tanstack/start-static-server-functions': 'workspace:*' '@tanstack/nitro-v2-vite-plugin': 'workspace:*' + octane: 'github:tannerlinsley/octane#690053baee01b38f836c6b662cc2c0efe8403a51&path:/packages/octane' allowBuilds: # root dependency