Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/plain-tools-work.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions e2e/octane-start/basic/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build]
command = "pnpm run build:netlify"
publish = "dist/client"
51 changes: 51 additions & 0 deletions e2e/octane-start/basic/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
}
}
42 changes: 42 additions & 0 deletions e2e/octane-start/basic/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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'] },
},
],
})
177 changes: 177 additions & 0 deletions e2e/octane-start/basic/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -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<FileRouteTypes>()

import type { getRouter } from './router.ts'
import type { createStart } from '@tanstack/octane-start'
declare module '@tanstack/octane-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
9 changes: 9 additions & 0 deletions e2e/octane-start/basic/src/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRouter } from '@tanstack/octane-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
})
}
62 changes: 62 additions & 0 deletions e2e/octane-start/basic/src/routes/__root.tsrx
Original file line number Diff line number Diff line change
@@ -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 }) @{
<Html lang="en">
<Head>
<HeadContent />
</Head>
<Body>
{props.children}
<Scripts />
</Body>
</Html>
}

function RootLayout() @{
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/deferred">Deferred</Link>
</nav>
<main>
<Outlet />
</main>
</>
}

function RouteError(props) @{
<section data-testid="route-error">
<h1>{'Route error'}</h1>
<p>{props.error instanceof Error ? props.error.message : String(props.error)}</p>
</section>
}

function NotFound() @{
<section data-testid="not-found">
<h1>{'Not found'}</h1>
</section>
}
11 changes: 11 additions & 0 deletions e2e/octane-start/basic/src/routes/about.tsrx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/octane-router';

export const Route = createFileRoute('/about')({
component: About,
});

function About() @{
<section>
<h1 data-testid="about-heading">{'Client navigation works'}</h1>
</section>
}
21 changes: 21 additions & 0 deletions e2e/octane-start/basic/src/routes/deferred-error.tsrx
Original file line number Diff line number Diff line change
@@ -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();

<section>
<h1>{'Deferred rejection'}</h1>
<Await promise={data.value} fallback={<p>{'Pending rejection'}</p>}>
{(value) => <p>{String(value)}</p>}
</Await>
</section>
}
Loading
Loading