Skip to content
Merged
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
20 changes: 20 additions & 0 deletions e2e/vue-start/selective-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
node_modules
package-lock.json
yarn.lock

.DS_Store
.cache
.env
.vercel
.output
/build/
/api/
/server/build
/public/build# Sentry Config File
.env.sentry-build-plugin
Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix comment placement.

The comment is concatenated with the path on line 13. This should be on a separate line for clarity:

-/public/build# Sentry Config File
+/public/build
+# Sentry Config File
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/public/build# Sentry Config File
.env.sentry-build-plugin
/public/build
# Sentry Config File
.env.sentry-build-plugin
🤖 Prompt for AI Agents
In e2e/vue-start/selective-ssr/.gitignore around lines 13 to 14, the inline
comment is concatenated with the path; separate the comment and the path onto
two lines by placing the comment (“# Sentry Config File”) on its own line and
the file path (.env.sentry-build-plugin) on the following line so the comment is
not appended to the entry.

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

count.txt
4 changes: 4 additions & 0 deletions e2e/vue-start/selective-ssr/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts
33 changes: 33 additions & 0 deletions e2e/vue-start/selective-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "tanstack-vue-start-e2e-selective-ssr",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/vue-router": "workspace:^",
"@tanstack/vue-start": "workspace:^",
"vue": "^3.5.25",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@tanstack/router-e2e-utils": "workspace:^",
"postcss": "^8.5.1",
"srvx": "^0.8.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.2",
"vite-tsconfig-paths": "^5.1.4",
"vue-tsc": "^3.1.8"
}
}
34 changes: 34 additions & 0 deletions e2e/vue-start/selective-ssr/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
},

webServer: {
command: `VITE_SERVER_PORT=${PORT} pnpm build && NODE_ENV=production PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
5 changes: 5 additions & 0 deletions e2e/vue-start/selective-ssr/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
112 changes: 112 additions & 0 deletions e2e/vue-start/selective-ssr/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as PostsRouteImport } from './routes/posts'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'

const PostsRoute = PostsRouteImport.update({
id: '/posts',
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/posts': typeof PostsRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/posts': typeof PostsRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/posts': typeof PostsRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/posts' | '/posts/$postId'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/posts' | '/posts/$postId'
id: '__root__' | '/' | '/posts' | '/posts/$postId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
PostsRoute: typeof PostsRouteWithChildren
}

declare module '@tanstack/vue-router' {
interface FileRoutesByPath {
'/posts': {
id: '/posts'
path: '/posts'
fullPath: '/posts'
preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
fullPath: '/posts/$postId'
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
}
}

interface PostsRouteChildren {
PostsPostIdRoute: typeof PostsPostIdRoute
}

const PostsRouteChildren: PostsRouteChildren = {
PostsPostIdRoute: PostsPostIdRoute,
}

const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
PostsRoute: PostsRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

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

export function getRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true,
})

return router
}

declare module '@tanstack/vue-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
157 changes: 157 additions & 0 deletions e2e/vue-start/selective-ssr/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/// <reference types="vite/client" />
import {
Body,
ClientOnly,
HeadContent,
Html,
Link,
Outlet,
Scripts,
createRootRoute,
useRouterState,
} from '@tanstack/vue-router'
import { z } from 'zod'
import { ssrSchema } from '~/search'
import appCss from '~/styles/app.css?url'

export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Selective SSR E2E Test',
},
],
links: [{ rel: 'stylesheet', href: appCss }],
}),
validateSearch: z.object({ root: ssrSchema }),
ssr: ({ search }) => {
if (typeof window !== 'undefined') {
const error = `ssr() for ${Route.id} should not be called on the client`
console.error(error)
throw new Error(error)
}
if (search.status === 'success') {
return search.value.root?.ssr
}
},
beforeLoad: ({ search }) => {
console.log(
`beforeLoad for ${Route.id} called on the ${typeof window !== 'undefined' ? 'client' : 'server'}`,
)
if (
search.root?.expected?.data === 'client' &&
typeof window === 'undefined'
) {
const error = `Expected beforeLoad for ${Route.id} to be executed on the client, but it is running on the server`
console.error(error)
throw new Error(error)
}
return {
root: typeof window === 'undefined' ? 'server' : 'client',
search,
}
},
loader: ({ context }) => {
console.log(
`loader for ${Route.id} called on the ${typeof window !== 'undefined' ? 'client' : 'server'}`,
)

if (
context.search.root?.expected?.data === 'client' &&
typeof window === 'undefined'
) {
const error = `Expected loader for ${Route.id} to be executed on the client, but it is running on the server`
console.error(error)
throw new Error(error)
}
return { root: typeof window === 'undefined' ? 'server' : 'client' }
},
shellComponent: RootDocument,
component: () => {
const search = Route.useSearch()
if (
typeof window === 'undefined' &&
search.value.root?.expected?.render === 'client-only'
) {
const error = `Expected component for ${Route.id} to be executed on the client, but it is running on the server`
console.error(error)
throw new Error(error)
}
const loaderData = Route.useLoaderData()
const context = Route.useRouteContext()
return (
<div data-testid="root-container">
<h2 data-testid="root-heading">root</h2>
<div>
ssr: <b>{JSON.stringify(search.value.root?.ssr ?? 'undefined')}</b>
</div>
<div>
expected data location execution:{' '}
<b data-testid="root-data-expected">
{search.value.root?.expected?.data}
</b>
</div>
<div>
loader: <b data-testid="root-loader">{loaderData.value.root}</b>
</div>
<div>
context: <b data-testid="root-context">{context.value.root}</b>
</div>
<hr />
<Outlet />
</div>
)
},
})

function RootDocument(_: unknown, { slots }: { slots: any }) {
const routerState = useRouterState({
select: (state) => ({
isLoading: state.isLoading,
status: state.status,
}),
})
return (
<Html>
<head>
<HeadContent />
</head>
<Body>
<div class="p-2 flex gap-2 text-lg">
<h1>Selective SSR E2E Test</h1>
<Link
to="/"
activeProps={{
class: 'font-bold',
}}
>
Home
</Link>
</div>
<hr />
<ClientOnly>
<div>
router isLoading:{' '}
<b data-testid="router-isLoading">
{routerState.value.isLoading ? 'true' : 'false'}
</b>
</div>
<div>
router status:{' '}
<b data-testid="router-status">{routerState.value.status}</b>
</div>
</ClientOnly>
<hr />
{slots.default?.()}
<Scripts />
</Body>
</Html>
)
}
Loading
Loading