diff --git a/e2e/react-start/css-modules/.gitignore b/e2e/react-start/css-modules/.gitignore
new file mode 100644
index 00000000000..950326609ca
--- /dev/null
+++ b/e2e/react-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/react-start/css-modules/.prettierignore b/e2e/react-start/css-modules/.prettierignore
new file mode 100644
index 00000000000..083bdb7c4c6
--- /dev/null
+++ b/e2e/react-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/react-start/css-modules/package.json b/e2e/react-start/css-modules/package.json
new file mode 100644
index 00000000000..a52c3872eda
--- /dev/null
+++ b/e2e/react-start/css-modules/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-react-start-e2e-css-modules",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev --port $PORT",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+ "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+ "test:e2e:prod": "playwright test --project=chromium",
+ "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "srvx": "^0.10.0",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/css-modules/playwright.config.ts b/e2e/react-start/css-modules/playwright.config.ts
new file mode 100644
index 00000000000..7582bbbb531
--- /dev/null
+++ b/e2e/react-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ baseURL,
+ },
+
+ webServer: {
+ command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ env: {
+ VITE_NODE_ENV: 'test',
+ PORT: String(PORT),
+ },
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+})
diff --git a/e2e/react-start/css-modules/src/router.tsx b/e2e/react-start/css-modules/src/router.tsx
new file mode 100644
index 00000000000..83ea998b393
--- /dev/null
+++ b/e2e/react-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: false,
+ })
+
+ return router
+}
diff --git a/e2e/react-start/css-modules/src/routes/__root.tsx b/e2e/react-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 00000000000..d44c33b8d0d
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,60 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ { charSet: 'utf-8' },
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+ ],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/css-modules/src/routes/index.tsx b/e2e/react-start/css-modules/src/routes/index.tsx
new file mode 100644
index 00000000000..f7668802f1e
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/react-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
CSS Collection Test - Global CSS
+
This page tests that global CSS is collected and served during SSR.
+
+
+
+ Global CSS Applied
+
+
+ This container should have a blue background, white text, and rounded
+ corners even with JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/react-start/css-modules/src/routes/modules.tsx b/e2e/react-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 00000000000..3b812a5d4e0
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,28 @@
+///
+import { createFileRoute } from '@tanstack/react-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+ component: Modules,
+})
+
+function Modules() {
+ return (
+
+
CSS Collection Test - CSS Modules
+
+ This page tests that CSS modules are collected and served during SSR.
+
+
+
+
+ CSS Module Applied
+
+
+ This card should have a green theme with scoped class names even with
+ JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/react-start/css-modules/src/styles/card.module.css b/e2e/react-start/css-modules/src/styles/card.module.css
new file mode 100644
index 00000000000..fcbcb419aa9
--- /dev/null
+++ b/e2e/react-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+ background-color: #f0fdf4; /* green-50 */
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #166534; /* green-800 */
+ margin-bottom: 8px;
+}
+
+.content {
+ font-size: 14px;
+ color: #15803d; /* green-700 */
+}
diff --git a/e2e/react-start/css-modules/src/styles/global.css b/e2e/react-start/css-modules/src/styles/global.css
new file mode 100644
index 00000000000..5bef746780e
--- /dev/null
+++ b/e2e/react-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+ background-color: #3b82f6; /* blue-500 */
+ padding: 24px;
+ border-radius: 12px;
+ color: white;
+}
+
+.global-title {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 16px;
+}
+
+.global-description {
+ font-size: 16px;
+ opacity: 0.9;
+}
diff --git a/e2e/react-start/css-modules/tests/css.spec.ts b/e2e/react-start/css-modules/tests/css.spec.ts
new file mode 100644
index 00000000000..ec4955f9b24
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+ test.describe('with JavaScript disabled', () => {
+ test.use({ javaScriptEnabled: false })
+
+ test('global CSS is applied on initial page load', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Verify the CSS is applied by checking computed styles
+ // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ const padding = await element.evaluate(
+ (el) => getComputedStyle(el).padding,
+ )
+ expect(padding).toBe('24px')
+
+ const borderRadius = await element.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('12px')
+ })
+
+ test('CSS modules are applied on initial page load', async ({ page }) => {
+ await page.goto('/modules')
+
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+
+ // Verify class is scoped (hashed)
+ const className = await card.getAttribute('class')
+ expect(className).toBeTruthy()
+ expect(className).not.toBe('card')
+ // The class should contain some hash characters (CSS modules add a hash)
+ expect(className!.length).toBeGreaterThan(5)
+
+ // Verify computed styles from card.module.css
+ // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+ expect(padding).toBe('16px')
+
+ const borderRadius = await card.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('8px')
+ })
+
+ test('global CSS class names are NOT scoped', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Get the class attribute - it should be the plain class name (not hashed)
+ const className = await element.getAttribute('class')
+ expect(className).toBe('global-container')
+ })
+ })
+
+ test('styles persist after hydration', async ({ page }) => {
+ await page.goto('/')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const element = page.getByTestId('global-styled')
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+
+ test('CSS modules styles persist after hydration', async ({ page }) => {
+ await page.goto('/modules')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const card = page.getByTestId('module-card')
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+ })
+
+ test('styles work correctly after client-side navigation', async ({
+ page,
+ }) => {
+ // Start from home
+ await page.goto('/')
+ await page.waitForTimeout(1000)
+
+ // Verify initial styles
+ const globalElement = page.getByTestId('global-styled')
+ await expect(globalElement).toBeVisible()
+ let backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ // Navigate to modules page
+ await page.getByTestId('nav-modules').click()
+ await page.waitForURL('/modules')
+
+ // Verify CSS modules styles
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+ backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ // Navigate back to home
+ await page.getByTestId('nav-home').click()
+ await page.waitForURL('/')
+
+ // Verify global styles still work
+ await expect(globalElement).toBeVisible()
+ backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+})
diff --git a/e2e/react-start/css-modules/tests/setup/global.setup.ts b/e2e/react-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/css-modules/tests/setup/global.teardown.ts b/e2e/react-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/css-modules/tsconfig.json b/e2e/react-start/css-modules/tsconfig.json
new file mode 100644
index 00000000000..3a9fb7cd716
--- /dev/null
+++ b/e2e/react-start/css-modules/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true
+ }
+}
diff --git a/e2e/react-start/css-modules/vite.config.ts b/e2e/react-start/css-modules/vite.config.ts
new file mode 100644
index 00000000000..c2c28ae93b7
--- /dev/null
+++ b/e2e/react-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart(),
+ viteReact(),
+ ],
+})
diff --git a/e2e/solid-start/css-modules/.gitignore b/e2e/solid-start/css-modules/.gitignore
new file mode 100644
index 00000000000..950326609ca
--- /dev/null
+++ b/e2e/solid-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/solid-start/css-modules/.prettierignore b/e2e/solid-start/css-modules/.prettierignore
new file mode 100644
index 00000000000..083bdb7c4c6
--- /dev/null
+++ b/e2e/solid-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/solid-start/css-modules/package.json b/e2e/solid-start/css-modules/package.json
new file mode 100644
index 00000000000..27417100bf6
--- /dev/null
+++ b/e2e/solid-start/css-modules/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "tanstack-solid-start-e2e-css-modules",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev --port $PORT",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+ "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+ "test:e2e:prod": "playwright test --project=chromium",
+ "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+ },
+ "dependencies": {
+ "@tanstack/solid-router": "workspace:^",
+ "@tanstack/solid-start": "workspace:^",
+ "solid-js": "^1.9.10"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "srvx": "^0.10.0",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-plugin-solid": "^2.11.10",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/solid-start/css-modules/playwright.config.ts b/e2e/solid-start/css-modules/playwright.config.ts
new file mode 100644
index 00000000000..7582bbbb531
--- /dev/null
+++ b/e2e/solid-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ baseURL,
+ },
+
+ webServer: {
+ command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ env: {
+ VITE_NODE_ENV: 'test',
+ PORT: String(PORT),
+ },
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+})
diff --git a/e2e/solid-start/css-modules/src/router.tsx b/e2e/solid-start/css-modules/src/router.tsx
new file mode 100644
index 00000000000..aa5762b4f74
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: false,
+ })
+
+ return router
+}
diff --git a/e2e/solid-start/css-modules/src/routes/__root.tsx b/e2e/solid-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 00000000000..304d76fb332
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ { charSet: 'utf-8' },
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+ ],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/css-modules/src/routes/index.tsx b/e2e/solid-start/css-modules/src/routes/index.tsx
new file mode 100644
index 00000000000..3c7caf6a979
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
CSS Collection Test - Global CSS
+
This page tests that global CSS is collected and served during SSR.
+
+
+
+ Global CSS Applied
+
+
+ This container should have a blue background, white text, and rounded
+ corners even with JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/solid-start/css-modules/src/routes/modules.tsx b/e2e/solid-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 00000000000..34cd8d36eaa
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,28 @@
+///
+import { createFileRoute } from '@tanstack/solid-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+ component: Modules,
+})
+
+function Modules() {
+ return (
+
+
CSS Collection Test - CSS Modules
+
+ This page tests that CSS modules are collected and served during SSR.
+
+
+
+
+ CSS Module Applied
+
+
+ This card should have a green theme with scoped class names even with
+ JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/solid-start/css-modules/src/styles/card.module.css b/e2e/solid-start/css-modules/src/styles/card.module.css
new file mode 100644
index 00000000000..fcbcb419aa9
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+ background-color: #f0fdf4; /* green-50 */
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #166534; /* green-800 */
+ margin-bottom: 8px;
+}
+
+.content {
+ font-size: 14px;
+ color: #15803d; /* green-700 */
+}
diff --git a/e2e/solid-start/css-modules/src/styles/global.css b/e2e/solid-start/css-modules/src/styles/global.css
new file mode 100644
index 00000000000..5bef746780e
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+ background-color: #3b82f6; /* blue-500 */
+ padding: 24px;
+ border-radius: 12px;
+ color: white;
+}
+
+.global-title {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 16px;
+}
+
+.global-description {
+ font-size: 16px;
+ opacity: 0.9;
+}
diff --git a/e2e/solid-start/css-modules/tests/css.spec.ts b/e2e/solid-start/css-modules/tests/css.spec.ts
new file mode 100644
index 00000000000..ec4955f9b24
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+ test.describe('with JavaScript disabled', () => {
+ test.use({ javaScriptEnabled: false })
+
+ test('global CSS is applied on initial page load', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Verify the CSS is applied by checking computed styles
+ // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ const padding = await element.evaluate(
+ (el) => getComputedStyle(el).padding,
+ )
+ expect(padding).toBe('24px')
+
+ const borderRadius = await element.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('12px')
+ })
+
+ test('CSS modules are applied on initial page load', async ({ page }) => {
+ await page.goto('/modules')
+
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+
+ // Verify class is scoped (hashed)
+ const className = await card.getAttribute('class')
+ expect(className).toBeTruthy()
+ expect(className).not.toBe('card')
+ // The class should contain some hash characters (CSS modules add a hash)
+ expect(className!.length).toBeGreaterThan(5)
+
+ // Verify computed styles from card.module.css
+ // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+ expect(padding).toBe('16px')
+
+ const borderRadius = await card.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('8px')
+ })
+
+ test('global CSS class names are NOT scoped', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Get the class attribute - it should be the plain class name (not hashed)
+ const className = await element.getAttribute('class')
+ expect(className).toBe('global-container')
+ })
+ })
+
+ test('styles persist after hydration', async ({ page }) => {
+ await page.goto('/')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const element = page.getByTestId('global-styled')
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+
+ test('CSS modules styles persist after hydration', async ({ page }) => {
+ await page.goto('/modules')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const card = page.getByTestId('module-card')
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+ })
+
+ test('styles work correctly after client-side navigation', async ({
+ page,
+ }) => {
+ // Start from home
+ await page.goto('/')
+ await page.waitForTimeout(1000)
+
+ // Verify initial styles
+ const globalElement = page.getByTestId('global-styled')
+ await expect(globalElement).toBeVisible()
+ let backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ // Navigate to modules page
+ await page.getByTestId('nav-modules').click()
+ await page.waitForURL('/modules')
+
+ // Verify CSS modules styles
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+ backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ // Navigate back to home
+ await page.getByTestId('nav-home').click()
+ await page.waitForURL('/')
+
+ // Verify global styles still work
+ await expect(globalElement).toBeVisible()
+ backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+})
diff --git a/e2e/solid-start/css-modules/tests/setup/global.setup.ts b/e2e/solid-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/solid-start/css-modules/tests/setup/global.teardown.ts b/e2e/solid-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/solid-start/css-modules/tsconfig.json b/e2e/solid-start/css-modules/tsconfig.json
new file mode 100644
index 00000000000..ed8b73fa2dd
--- /dev/null
+++ b/e2e/solid-start/css-modules/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true,
+ "types": ["vite/client"]
+ }
+}
diff --git a/e2e/solid-start/css-modules/vite.config.ts b/e2e/solid-start/css-modules/vite.config.ts
new file mode 100644
index 00000000000..1a2219f4435
--- /dev/null
+++ b/e2e/solid-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart(),
+ viteSolid({ ssr: true }),
+ ],
+})
diff --git a/e2e/vue-start/css-modules/.gitignore b/e2e/vue-start/css-modules/.gitignore
new file mode 100644
index 00000000000..950326609ca
--- /dev/null
+++ b/e2e/vue-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/vue-start/css-modules/.prettierignore b/e2e/vue-start/css-modules/.prettierignore
new file mode 100644
index 00000000000..083bdb7c4c6
--- /dev/null
+++ b/e2e/vue-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/vue-start/css-modules/package.json b/e2e/vue-start/css-modules/package.json
new file mode 100644
index 00000000000..b2e267f9b56
--- /dev/null
+++ b/e2e/vue-start/css-modules/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "tanstack-vue-start-e2e-css-modules",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "dev:e2e": "vite dev --port $PORT",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+ "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+ "test:e2e:prod": "playwright test --project=chromium",
+ "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+ },
+ "dependencies": {
+ "@tanstack/vue-router": "workspace:^",
+ "@tanstack/vue-start": "workspace:^",
+ "vue": "^3.5.16"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@vitejs/plugin-vue-jsx": "^4.1.2",
+ "srvx": "^0.10.0",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/vue-start/css-modules/playwright.config.ts b/e2e/vue-start/css-modules/playwright.config.ts
new file mode 100644
index 00000000000..7582bbbb531
--- /dev/null
+++ b/e2e/vue-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+ reporter: [['line']],
+
+ globalSetup: './tests/setup/global.setup.ts',
+ globalTeardown: './tests/setup/global.teardown.ts',
+
+ use: {
+ baseURL,
+ },
+
+ webServer: {
+ command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ env: {
+ VITE_NODE_ENV: 'test',
+ PORT: String(PORT),
+ },
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+})
diff --git a/e2e/vue-start/css-modules/src/router.tsx b/e2e/vue-start/css-modules/src/router.tsx
new file mode 100644
index 00000000000..7394b7042e8
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+ const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreload: false,
+ })
+
+ return router
+}
diff --git a/e2e/vue-start/css-modules/src/routes/__root.tsx b/e2e/vue-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 00000000000..39c0110beea
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+import {
+ Body,
+ HeadContent,
+ Html,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ { charSet: 'utf-8' },
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+ ],
+ }),
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-start/css-modules/src/routes/index.tsx b/e2e/vue-start/css-modules/src/routes/index.tsx
new file mode 100644
index 00000000000..5c52a6e3427
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
CSS Collection Test - Global CSS
+
This page tests that global CSS is collected and served during SSR.
+
+
+
+ Global CSS Applied
+
+
+ This container should have a blue background, white text, and rounded
+ corners even with JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/vue-start/css-modules/src/routes/modules.tsx b/e2e/vue-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 00000000000..4c1a655ed8f
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,28 @@
+///
+import { createFileRoute } from '@tanstack/vue-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+ component: Modules,
+})
+
+function Modules() {
+ return (
+
+
CSS Collection Test - CSS Modules
+
+ This page tests that CSS modules are collected and served during SSR.
+
+
+
+
+ CSS Module Applied
+
+
+ This card should have a green theme with scoped class names even with
+ JavaScript disabled.
+
+
+
+ )
+}
diff --git a/e2e/vue-start/css-modules/src/styles/card.module.css b/e2e/vue-start/css-modules/src/styles/card.module.css
new file mode 100644
index 00000000000..fcbcb419aa9
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+ background-color: #f0fdf4; /* green-50 */
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #166534; /* green-800 */
+ margin-bottom: 8px;
+}
+
+.content {
+ font-size: 14px;
+ color: #15803d; /* green-700 */
+}
diff --git a/e2e/vue-start/css-modules/src/styles/global.css b/e2e/vue-start/css-modules/src/styles/global.css
new file mode 100644
index 00000000000..5bef746780e
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+ background-color: #3b82f6; /* blue-500 */
+ padding: 24px;
+ border-radius: 12px;
+ color: white;
+}
+
+.global-title {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 16px;
+}
+
+.global-description {
+ font-size: 16px;
+ opacity: 0.9;
+}
diff --git a/e2e/vue-start/css-modules/tests/css.spec.ts b/e2e/vue-start/css-modules/tests/css.spec.ts
new file mode 100644
index 00000000000..ec4955f9b24
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+ test.describe('with JavaScript disabled', () => {
+ test.use({ javaScriptEnabled: false })
+
+ test('global CSS is applied on initial page load', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Verify the CSS is applied by checking computed styles
+ // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ const padding = await element.evaluate(
+ (el) => getComputedStyle(el).padding,
+ )
+ expect(padding).toBe('24px')
+
+ const borderRadius = await element.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('12px')
+ })
+
+ test('CSS modules are applied on initial page load', async ({ page }) => {
+ await page.goto('/modules')
+
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+
+ // Verify class is scoped (hashed)
+ const className = await card.getAttribute('class')
+ expect(className).toBeTruthy()
+ expect(className).not.toBe('card')
+ // The class should contain some hash characters (CSS modules add a hash)
+ expect(className!.length).toBeGreaterThan(5)
+
+ // Verify computed styles from card.module.css
+ // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+ expect(padding).toBe('16px')
+
+ const borderRadius = await card.evaluate(
+ (el) => getComputedStyle(el).borderRadius,
+ )
+ expect(borderRadius).toBe('8px')
+ })
+
+ test('global CSS class names are NOT scoped', async ({ page }) => {
+ await page.goto('/')
+
+ const element = page.getByTestId('global-styled')
+ await expect(element).toBeVisible()
+
+ // Get the class attribute - it should be the plain class name (not hashed)
+ const className = await element.getAttribute('class')
+ expect(className).toBe('global-container')
+ })
+ })
+
+ test('styles persist after hydration', async ({ page }) => {
+ await page.goto('/')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const element = page.getByTestId('global-styled')
+ const backgroundColor = await element.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+
+ test('CSS modules styles persist after hydration', async ({ page }) => {
+ await page.goto('/modules')
+
+ // Wait for hydration
+ await page.waitForTimeout(1000)
+
+ const card = page.getByTestId('module-card')
+ const backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+ })
+
+ test('styles work correctly after client-side navigation', async ({
+ page,
+ }) => {
+ // Start from home
+ await page.goto('/')
+ await page.waitForTimeout(1000)
+
+ // Verify initial styles
+ const globalElement = page.getByTestId('global-styled')
+ await expect(globalElement).toBeVisible()
+ let backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+ // Navigate to modules page
+ await page.getByTestId('nav-modules').click()
+ await page.waitForURL('/modules')
+
+ // Verify CSS modules styles
+ const card = page.getByTestId('module-card')
+ await expect(card).toBeVisible()
+ backgroundColor = await card.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+ // Navigate back to home
+ await page.getByTestId('nav-home').click()
+ await page.waitForURL('/')
+
+ // Verify global styles still work
+ await expect(globalElement).toBeVisible()
+ backgroundColor = await globalElement.evaluate(
+ (el) => getComputedStyle(el).backgroundColor,
+ )
+ expect(backgroundColor).toBe('rgb(59, 130, 246)')
+ })
+})
diff --git a/e2e/vue-start/css-modules/tests/setup/global.setup.ts b/e2e/vue-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/vue-start/css-modules/tests/setup/global.teardown.ts b/e2e/vue-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/vue-start/css-modules/tsconfig.json b/e2e/vue-start/css-modules/tsconfig.json
new file mode 100644
index 00000000000..8bf3d33789b
--- /dev/null
+++ b/e2e/vue-start/css-modules/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "vue",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "ES2022",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./src/*"]
+ },
+ "noEmit": true,
+ "types": ["vite/client"]
+ }
+}
diff --git a/e2e/vue-start/css-modules/vite.config.ts b/e2e/vue-start/css-modules/vite.config.ts
new file mode 100644
index 00000000000..d7b96c1c2f8
--- /dev/null
+++ b/e2e/vue-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart(),
+ vueJsx(),
+ ],
+})
diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx
index 3a30c521dc5..8da3b5b279b 100644
--- a/packages/react-router/src/HeadContent.tsx
+++ b/packages/react-router/src/HeadContent.tsx
@@ -203,6 +203,39 @@ export const useTags = () => {
)
}
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+function DevStylesLink() {
+ const router = useRouter()
+
+ const routeIds = useRouterState({
+ select: (state) => state.matches.map((match) => match.routeId),
+ })
+
+ React.useEffect(() => {
+ // After hydration, remove the SSR-rendered dev styles link
+ document
+ .querySelectorAll('[data-tanstack-start-dev-styles]')
+ .forEach((el) => el.remove())
+ }, [])
+
+ // Build the same href on both server and client for hydration match
+ const href = `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`
+
+ return (
+
+ )
+}
+
/**
* Render route-managed head tags (title, meta, links, styles, head scripts).
* Place inside the document head of your app shell.
@@ -212,9 +245,14 @@ export function HeadContent() {
const tags = useTags()
const router = useRouter()
const nonce = router.options.ssr?.nonce
- return tags.map((tag) => (
-
- ))
+ return (
+ <>
+ {process.env.NODE_ENV !== 'production' && }
+ {tags.map((tag) => (
+
+ ))}
+ >
+ )
}
function uniqBy(arr: Array, fn: (item: T) => string) {
diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index 315b3e528e5..ddd11cf1a79 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -217,7 +217,7 @@ describe('ssr HeadContent', () => {
,
)
expect(html).toEqual(
- `Index`,
+ `Index`,
)
})
})
diff --git a/packages/solid-router/src/HeadContent.tsx b/packages/solid-router/src/HeadContent.tsx
index 32326976764..2d52ac88a7a 100644
--- a/packages/solid-router/src/HeadContent.tsx
+++ b/packages/solid-router/src/HeadContent.tsx
@@ -1,6 +1,6 @@
import * as Solid from 'solid-js'
import { MetaProvider } from '@solidjs/meta'
-import { For } from 'solid-js'
+import { For, onMount } from 'solid-js'
import { escapeHtml } from '@tanstack/router-core'
import { Asset } from './Asset'
import { useRouter } from './useRouter'
@@ -197,6 +197,31 @@ export const useTags = () => {
)
}
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+function DevStylesLink() {
+ const routeIds = useRouterState({
+ select: (state) => state.matches.map((match) => match.routeId),
+ })
+
+ onMount(() => {
+ // After hydration, remove the SSR-rendered dev styles link
+ document
+ .querySelectorAll('[data-tanstack-start-dev-styles]')
+ .forEach((el) => el.remove())
+ })
+
+ // Build the same href on both server and client for hydration match
+ const href = () =>
+ `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds().join(','))}`
+
+ return
+}
+
/**
* @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
* When using full document hydration (hydrating from ``), this component should be rendered in the ``
@@ -208,6 +233,7 @@ export function HeadContent() {
return (
+ {process.env.NODE_ENV !== 'production' && }
{(tag) => }
)
diff --git a/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts b/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts
new file mode 100644
index 00000000000..4c226ae425c
--- /dev/null
+++ b/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts
@@ -0,0 +1,176 @@
+/**
+ * CSS collection for dev mode.
+ * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.
+ */
+import type { ModuleNode, ViteDevServer } from 'vite'
+
+// CSS file extensions supported by Vite
+const CSS_FILE_REGEX =
+ /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
+// URL params that indicate CSS should not be injected (e.g., ?url, ?inline)
+const CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']
+
+function isCssFile(file: string): boolean {
+ return CSS_FILE_REGEX.test(file)
+}
+
+function hasCssSideEffectFreeParam(url: string): boolean {
+ const queryString = url.split('?')[1]
+ if (!queryString) return false
+
+ const params = new URLSearchParams(queryString)
+ return CSS_SIDE_EFFECT_FREE_PARAMS.some(
+ (param) =>
+ params.get(param) === '' &&
+ !url.includes(`?${param}=`) &&
+ !url.includes(`&${param}=`),
+ )
+}
+
+export interface CollectDevStylesOptions {
+ viteDevServer: ViteDevServer
+ entries: Array
+}
+
+/**
+ * Collect CSS content from the module graph starting from the given entry points.
+ */
+export async function collectDevStyles(
+ opts: CollectDevStylesOptions,
+): Promise {
+ const { viteDevServer, entries } = opts
+ const styles: Map = new Map()
+ const visited = new Set()
+
+ for (const entry of entries) {
+ const normalizedPath = entry.replace(/\\/g, '/')
+ let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
+
+ // If module isn't in the graph yet, request it to trigger transform
+ if (!node) {
+ try {
+ await viteDevServer.transformRequest(normalizedPath)
+ } catch (err) {
+ // Ignore - the module might not exist yet
+ }
+ node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
+ }
+
+ if (node) {
+ await crawlModuleForCss(viteDevServer, node, visited, styles)
+ }
+ }
+
+ if (styles.size === 0) return undefined
+
+ return Array.from(styles.entries())
+ .map(([fileName, css]) => {
+ const escapedFileName = fileName
+ .replace(/\/\*/g, '/\\*')
+ .replace(/\*\//g, '*\\/')
+ return `\n/* ${escapedFileName} */\n${css}`
+ })
+ .join('\n')
+}
+
+async function crawlModuleForCss(
+ vite: ViteDevServer,
+ node: ModuleNode,
+ visited: Set,
+ styles: Map,
+): Promise {
+ if (visited.has(node)) return
+ visited.add(node)
+
+ const branches: Array> = []
+
+ // Ensure the module has been transformed to populate its deps
+ // This is important for code-split modules that may not have been processed yet
+ if (!node.ssrTransformResult) {
+ try {
+ await vite.transformRequest(node.url, { ssr: true })
+ // Re-fetch the node to get updated state
+ const updatedNode = await vite.moduleGraph.getModuleByUrl(node.url)
+ if (updatedNode) {
+ node = updatedNode
+ }
+ } catch {
+ // Ignore transform errors - the module might not be transformable
+ }
+ }
+
+ // Check if this is a CSS file
+ if (
+ node.file &&
+ isCssFile(node.file) &&
+ !hasCssSideEffectFreeParam(node.url)
+ ) {
+ const css = await loadCssContent(vite, node)
+ if (css) {
+ styles.set(node.url, css)
+ }
+ }
+
+ // Crawl dependencies using ssrTransformResult.deps and importedModules
+ // We need both because:
+ // 1. ssrTransformResult.deps has resolved URLs for SSR dependencies
+ // 2. importedModules may contain CSS files and code-split modules not in SSR deps
+ const depsFromSsr = node.ssrTransformResult?.deps ?? []
+ const urlsToVisit = new Set(depsFromSsr)
+
+ // Check importedModules for CSS files and additional modules
+ for (const importedNode of node.importedModules) {
+ if (importedNode.file && isCssFile(importedNode.file)) {
+ // CSS files often don't appear in ssrTransformResult.deps, add them explicitly
+ branches.push(crawlModuleForCss(vite, importedNode, visited, styles))
+ } else if (!urlsToVisit.has(importedNode.url)) {
+ // Also add non-CSS imports that aren't in SSR deps (e.g., code-split modules)
+ urlsToVisit.add(importedNode.url)
+ }
+ }
+
+ for (const depUrl of urlsToVisit) {
+ branches.push(
+ (async () => {
+ const depNode = await vite.moduleGraph.getModuleByUrl(depUrl)
+ if (depNode) {
+ await crawlModuleForCss(vite, depNode, visited, styles)
+ }
+ })(),
+ )
+ }
+
+ await Promise.all(branches)
+}
+
+async function loadCssContent(
+ vite: ViteDevServer,
+ node: ModuleNode,
+): Promise {
+ // For ALL CSS files (including CSS modules), get the transformed content
+ // and extract __vite__css. Vite's transform puts the final CSS (with hashed
+ // class names for modules) into the __vite__css variable.
+ const transformResult = await vite.transformRequest(node.url)
+ if (!transformResult?.code) return undefined
+
+ // Extract CSS content from Vite's transformed module
+ return extractCssFromViteModule(transformResult.code)
+}
+
+/**
+ * Extract CSS string from Vite's transformed CSS module code.
+ * Vite wraps CSS content in a JS module with __vite__css variable.
+ */
+function extractCssFromViteModule(code: string): string | undefined {
+ // Match: const __vite__css = "..."
+ const match = code.match(/const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/)
+ if (match?.[1]) {
+ // Unescape the string
+ return match[1]
+ .replace(/\\n/g, '\n')
+ .replace(/\\t/g, '\t')
+ .replace(/\\"/g, '"')
+ .replace(/\\\\/g, '\\')
+ }
+ return undefined
+}
diff --git a/packages/start-plugin-core/src/dev-server-plugin/plugin.ts b/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
index fccbfb09de0..1fe8856310c 100644
--- a/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
@@ -4,13 +4,14 @@ import { NodeRequest, sendNodeResponse } from 'srvx/node'
import { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from '../constants'
import { resolveViteId } from '../utils'
import { extractHtmlScripts } from './extract-html-scripts'
+import { collectDevStyles } from './dev-styles'
import type { Connect, DevEnvironment, PluginOption } from 'vite'
-import type { TanStackStartOutputConfig } from '../schema'
+import type { GetConfigFn } from '../types'
export function devServerPlugin({
getConfig,
}: {
- getConfig: () => { startConfig: TanStackStartOutputConfig }
+ getConfig: GetConfigFn
}): PluginOption {
let isTest = false
@@ -75,6 +76,52 @@ export function devServerPlugin({
)
}
+ // Middleware to serve collected CSS for dev mode
+ // Security: Route IDs from query params are validated against TSS_ROUTES_MANIFEST.
+ // Only routes that exist in the manifest will have their CSS collected.
+ // Arbitrary file paths cannot be injected.
+ viteDevServer.middlewares.use(async (req, res, next) => {
+ const url = req.url ?? ''
+ if (!url.startsWith('/@tanstack-start/styles.css')) {
+ return next()
+ }
+
+ // Parse route IDs from query param
+ const urlObj = new URL(url, 'http://localhost')
+ const routesParam = urlObj.searchParams.get('routes')
+ const routeIds = routesParam ? routesParam.split(',') : []
+
+ // Build entries list from route file paths
+ const entries: Array = []
+
+ // Look up route file paths from manifest
+ // Only routes registered in the manifest are used - this prevents path injection
+ const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
+ | Record }>
+ | undefined
+
+ if (routesManifest && routeIds.length > 0) {
+ for (const routeId of routeIds) {
+ const route = routesManifest[routeId]
+ if (route?.filePath) {
+ entries.push(route.filePath)
+ }
+ }
+ }
+
+ const css =
+ entries.length > 0
+ ? await collectDevStyles({
+ viteDevServer,
+ entries,
+ })
+ : undefined
+
+ res.setHeader('Content-Type', 'text/css')
+ res.setHeader('Cache-Control', 'no-store')
+ res.end(css ?? '')
+ })
+
viteDevServer.middlewares.use(async (req, res) => {
// fix the request URL to match the original URL
// otherwise, the request URL will '/index.html'
diff --git a/packages/vue-router/src/HeadContent.tsx b/packages/vue-router/src/HeadContent.tsx
index 1baeef6a1fc..a7666d20a7d 100644
--- a/packages/vue-router/src/HeadContent.tsx
+++ b/packages/vue-router/src/HeadContent.tsx
@@ -6,6 +6,41 @@ import { useRouter } from './useRouter'
import { useRouterState } from './useRouterState'
import type { RouterManagedTag } from '@tanstack/router-core'
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+const DevStylesLink = Vue.defineComponent({
+ name: 'DevStylesLink',
+ setup() {
+ const routeIds = useRouterState({
+ select: (state) => state.matches.map((match) => match.routeId),
+ })
+
+ Vue.onMounted(() => {
+ // After hydration, remove the SSR-rendered dev styles link
+ document
+ .querySelectorAll('[data-tanstack-start-dev-styles]')
+ .forEach((el) => el.remove())
+ })
+
+ // Build the same href on both server and client for hydration match
+ const href = Vue.computed(
+ () =>
+ `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.value.join(','))}`,
+ )
+
+ return () =>
+ Vue.h('link', {
+ rel: 'stylesheet',
+ href: href.value,
+ 'data-tanstack-start-dev-styles': true,
+ })
+ },
+})
+
export const useTags = () => {
const router = useRouter()
@@ -152,12 +187,19 @@ export const HeadContent = Vue.defineComponent({
const tags = useTags()
return () => {
- return tags().map((tag) =>
+ const children = tags().map((tag) =>
Vue.h(Asset, {
...tag,
key: `tsr-meta-${JSON.stringify(tag)}`,
}),
)
+
+ // In dev mode, prepend the DevStylesLink
+ if (process.env.NODE_ENV !== 'production') {
+ return [Vue.h(DevStylesLink), ...children]
+ }
+
+ return children
}
},
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 18462b46d8f..57b57bc2d81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1516,6 +1516,52 @@ importers:
specifier: ^5.1.4
version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ e2e/react-start/css-modules:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ srvx:
+ specifier: ^0.10.0
+ version: 0.10.0
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/react-start/custom-basepath:
dependencies:
'@tanstack/react-router':
@@ -3352,6 +3398,43 @@ importers:
specifier: ^5.1.4
version: 5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ e2e/solid-start/css-modules:
+ dependencies:
+ '@tanstack/solid-router':
+ specifier: workspace:^
+ version: link:../../../packages/solid-router
+ '@tanstack/solid-start':
+ specifier: workspace:*
+ version: link:../../../packages/solid-start
+ solid-js:
+ specifier: 1.9.10
+ version: 1.9.10
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ srvx:
+ specifier: ^0.10.0
+ version: 0.10.0
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-plugin-solid:
+ specifier: ^2.11.10
+ version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/solid-start/custom-basepath:
dependencies:
'@tanstack/solid-router':
@@ -5103,6 +5186,43 @@ importers:
specifier: ^5.1.4
version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ e2e/vue-start/css-modules:
+ dependencies:
+ '@tanstack/vue-router':
+ specifier: workspace:*
+ version: link:../../../packages/vue-router
+ '@tanstack/vue-start':
+ specifier: workspace:*
+ version: link:../../../packages/vue-start
+ vue:
+ specifier: ^3.5.16
+ version: 3.5.25(typescript@5.9.2)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@vitejs/plugin-vue-jsx':
+ specifier: ^4.1.2
+ version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))
+ srvx:
+ specifier: ^0.10.0
+ version: 0.10.0
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/vue-start/custom-basepath:
dependencies:
'@tanstack/vue-router':
@@ -10380,7 +10500,7 @@ importers:
devDependencies:
'@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.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ 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))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
@@ -26708,13 +26828,13 @@ snapshots:
uuid: 11.1.0
write-file-atomic: 5.0.1
- '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+ '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/config': 23.2.0
'@netlify/dev-utils': 4.3.0
'@netlify/edge-functions-dev': 1.0.0
- '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+ '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
'@netlify/headers': 2.1.0
'@netlify/images': 1.3.0(@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.8.0)
'@netlify/redirects': 3.1.0
@@ -26782,12 +26902,12 @@ snapshots:
dependencies:
'@netlify/types': 2.1.0
- '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+ '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/dev-utils': 4.3.0
'@netlify/functions': 5.0.0
- '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+ '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -26877,9 +26997,9 @@ snapshots:
'@netlify/types@2.1.0': {}
- '@netlify/vite-plugin-tanstack-start@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.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin-tanstack-start@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))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/vite-plugin': 2.7.4(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.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ '@netlify/vite-plugin': 2.7.4(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))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
optionalDependencies:
'@tanstack/solid-start': link:packages/solid-start
@@ -26907,9 +27027,9 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/vite-plugin@2.7.4(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.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin@2.7.4(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))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+ '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
'@netlify/dev-utils': 4.3.0
dedent: 1.7.0(babel-plugin-macros@3.1.0)
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
@@ -26937,13 +27057,13 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+ '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.7.1
- '@vercel/nft': 0.29.4(rollup@4.52.5)
+ '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -30198,7 +30318,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.29.4(rollup@4.52.5)':
+ '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)