diff --git a/e2e/e2e-utils/src/fixture.ts b/e2e/e2e-utils/src/fixture.ts index abb7b1d564d..aab4a943b19 100644 --- a/e2e/e2e-utils/src/fixture.ts +++ b/e2e/e2e-utils/src/fixture.ts @@ -1,16 +1,36 @@ import { test as base, expect } from '@playwright/test' export interface TestFixtureOptions { + /** + * List of error message patterns to ignore in console output. + * Supports both strings (substring match) and RegExp patterns. + * + * @example + * test.use({ + * whitelistErrors: [ + * 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED', + * /Failed to load resource/, + * ], + * }) + */ whitelistErrors: Array } export const test = base.extend({ - whitelistErrors: [[], { option: true }], + whitelistErrors: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + await use([]) + }, + { option: true }, + ], page: async ({ page, whitelistErrors }, use) => { const errorMessages: Array = [] + // Ensure whitelistErrors is always an array (defensive fallback) + const errors = Array.isArray(whitelistErrors) ? whitelistErrors : [] page.on('console', (m) => { if (m.type() === 'error') { const text = m.text() - for (const whitelistError of whitelistErrors) { + for (const whitelistError of errors) { if ( (typeof whitelistError === 'string' && text.includes(whitelistError)) || diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts index ea5bc50a072..36ed021c44c 100644 --- a/e2e/react-start/basic/tests/navigation.spec.ts +++ b/e2e/react-start/basic/tests/navigation.spec.ts @@ -4,7 +4,7 @@ import { test } from '@tanstack/router-e2e-utils' test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test('Navigating to post', async ({ page }) => { diff --git a/e2e/react-start/basic/tests/not-found.spec.ts b/e2e/react-start/basic/tests/not-found.spec.ts index 3e3bffd0944..6a98b568b31 100644 --- a/e2e/react-start/basic/tests/not-found.spec.ts +++ b/e2e/react-start/basic/tests/not-found.spec.ts @@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', 'NotFound error during hydration for routeId', ], }) diff --git a/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts b/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts index de0059f9683..4267de8b645 100644 --- a/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts +++ b/e2e/react-start/basic/tests/open-redirect-prevention.spec.ts @@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils' test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) diff --git a/e2e/react-start/basic/tests/params.spec.ts b/e2e/react-start/basic/tests/params.spec.ts index 737c82f35d2..505e63ef433 100644 --- a/e2e/react-start/basic/tests/params.spec.ts +++ b/e2e/react-start/basic/tests/params.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test.describe('Unicode route rendering', () => { diff --git a/e2e/react-start/css-modules/package.json b/e2e/react-start/css-modules/package.json index a52c3872eda..79e306c9e9e 100644 --- a/e2e/react-start/css-modules/package.json +++ b/e2e/react-start/css-modules/package.json @@ -6,29 +6,44 @@ "scripts": { "dev": "vite dev --port 3000", "dev:e2e": "vite dev --port $PORT", + "dev:nitro": "VITE_USE_NITRO=true vite dev --port 3000", + "dev:e2e:nitro": "VITE_USE_NITRO=true vite dev --port $PORT", + "dev:basepath": "VITE_BASE_PATH=/my-app vite dev --port 3000", + "dev:e2e:basepath": "VITE_BASE_PATH=/my-app vite dev --port $PORT", + "dev:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1", + "dev:e2e:cloudflare": "echo 'Cloudflare dev mode has React duplication issues - use build+preview instead' && exit 1", "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:dev:nitro": "MODE=dev VITE_CONFIG=nitro playwright test --project=chromium", + "test:e2e:dev:basepath": "MODE=dev VITE_CONFIG=basepath playwright test --project=chromium", + "_test:e2e:dev:cloudflare": "MODE=dev VITE_CONFIG=cloudflare playwright test --project=chromium", "test:e2e:prod": "playwright test --project=chromium", - "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev" + "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev", + "test:e2e:nitro": "rm -rf port*.txt; pnpm run test:e2e:dev:nitro", + "test:e2e:basepath": "rm -rf port*.txt; pnpm run test:e2e:dev:basepath", + "_test:e2e:cloudflare": "echo 'Cloudflare dev mode disabled - React duplication issues' && exit 0" }, "dependencies": { - "@tanstack/react-router": "workspace:^", - "@tanstack/react-start": "workspace:^", + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.15.1", "@playwright/test": "^1.50.1", - "@tanstack/router-e2e-utils": "workspace:^", + "@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", + "nitro": "npm:nitro-nightly@latest", "srvx": "^0.10.0", "typescript": "^5.7.2", "vite": "^7.1.7", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "wrangler": "^4.22.0" } } diff --git a/e2e/react-start/css-modules/playwright.config.ts b/e2e/react-start/css-modules/playwright.config.ts index 7582bbbb531..644798ca99d 100644 --- a/e2e/react-start/css-modules/playwright.config.ts +++ b/e2e/react-start/css-modules/playwright.config.ts @@ -2,9 +2,19 @@ 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}` +const mode = process.env.MODE ?? 'prod' +const isDev = mode === 'dev' +const viteConfig = process.env.VITE_CONFIG // 'nitro' | 'basepath' | 'cloudflare' | undefined +const PORT = await getTestServerPort( + viteConfig ? `${packageJson.name}-${viteConfig}` : packageJson.name, +) + +// When using basepath config, the app is served at /my-app +const basePath = viteConfig === 'basepath' ? '/my-app' : '' +const baseURL = `http://localhost:${PORT}${basePath}` + +// Select the appropriate dev command based on VITE_CONFIG +const devCommand = viteConfig ? `pnpm dev:e2e:${viteConfig}` : 'pnpm dev:e2e' export default defineConfig({ testDir: './tests', @@ -19,7 +29,7 @@ export default defineConfig({ }, webServer: { - command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`, + command: isDev ? devCommand : `pnpm build && PORT=${PORT} pnpm start`, url: baseURL, reuseExistingServer: !process.env.CI, stdout: 'pipe', diff --git a/e2e/react-start/css-modules/tests/css.spec.ts b/e2e/react-start/css-modules/tests/css.spec.ts index 9fb18bbd3d6..bdfd2308a4b 100644 --- a/e2e/react-start/css-modules/tests/css.spec.ts +++ b/e2e/react-start/css-modules/tests/css.spec.ts @@ -1,31 +1,54 @@ -import { expect, request } from '@playwright/test' +import { expect } from '@playwright/test' import { test } from '@tanstack/router-e2e-utils' +// Whitelist errors that can occur in CI: +// - net::ERR_NAME_NOT_RESOLVED: transient network issues +// - 504 (Outdated Optimize Dep): Vite dependency optimization reload +const whitelistErrors = [ + 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED', + 'Failed to load resource: the server responded with a status of 504', +] + test.describe('CSS styles in SSR (dev mode)', () => { + test.use({ whitelistErrors }) + // Warmup: trigger Vite's dependency optimization before running tests - // This prevents "504 (Outdated Optimize Dep)" errors during actual tests - test.beforeAll(async ({ baseURL }) => { - const context = await request.newContext() + // This prevents "optimized dependencies changed. reloading" during actual tests + // We use a real browser context since dep optimization happens on JS load, not HTTP requests + test.beforeAll(async ({ browser, baseURL }) => { + const context = await browser.newContext() + const page = await context.newPage() try { - // Hit both pages to trigger any dependency optimization - await context.get(baseURL!) - await context.get(`${baseURL}/modules`) - // Give Vite time to complete optimization - await new Promise((resolve) => setTimeout(resolve, 1000)) - // Hit again after optimization - await context.get(baseURL!) + // Load both pages to trigger dependency optimization + await page.goto(baseURL!) + await page.waitForTimeout(2000) // Wait for deps to optimize + await page.goto(`${baseURL}/modules`) + await page.waitForTimeout(2000) + // Load again after optimization completes + await page.goto(baseURL!) + await page.waitForTimeout(1000) } catch { // Ignore errors during warmup } finally { - await context.dispose() + await context.close() } }) + // Helper to build full URL from baseURL and path + // Playwright's goto with absolute paths (like '/modules') ignores baseURL's path portion + // So we need to manually construct the full URL + const buildUrl = (baseURL: string, path: string) => { + return baseURL.replace(/\/$/, '') + path + } + test.describe('with JavaScript disabled', () => { - test.use({ javaScriptEnabled: false }) + test.use({ javaScriptEnabled: false, whitelistErrors }) - test('global CSS is applied on initial page load', async ({ page }) => { - await page.goto('/') + test('global CSS is applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -48,8 +71,11 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('12px') }) - test('CSS modules are applied on initial page load', async ({ page }) => { - await page.goto('/modules') + test('CSS modules are applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) const card = page.getByTestId('module-card') await expect(card).toBeVisible() @@ -77,8 +103,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('8px') }) - test('global CSS class names are NOT scoped', async ({ page }) => { - await page.goto('/') + test('global CSS class names are NOT scoped', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -89,37 +115,47 @@ test.describe('CSS styles in SSR (dev mode)', () => { }) }) - test('styles persist after hydration', async ({ page }) => { - await page.goto('/') - - // Wait for hydration - await page.waitForTimeout(1000) + test('styles persist after hydration', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) + // Wait for hydration and styles to be applied const element = page.getByTestId('global-styled') - const backgroundColor = await element.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(59, 130, 246)') - }) + await expect(element).toBeVisible() - test('CSS modules styles persist after hydration', async ({ page }) => { - await page.goto('/modules') + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await element.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(59, 130, 246)') + }).toPass({ timeout: 5000 }) + }) - // Wait for hydration - await page.waitForTimeout(1000) + test('CSS modules styles persist after hydration', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) + // Wait for hydration and styles to be applied const card = page.getByTestId('module-card') - const backgroundColor = await card.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(240, 253, 244)') + await expect(card).toBeVisible() + + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await card.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(240, 253, 244)') + }).toPass({ timeout: 5000 }) }) test('styles work correctly after client-side navigation', async ({ page, + baseURL, }) => { // Start from home - await page.goto('/') + await page.goto(buildUrl(baseURL!, '/')) await page.waitForTimeout(1000) // Verify initial styles @@ -132,7 +168,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate to modules page await page.getByTestId('nav-modules').click() - await page.waitForURL('/modules') + // Use glob pattern to match with or without basepath + await page.waitForURL('**/modules') // Verify CSS modules styles const card = page.getByTestId('module-card') @@ -144,7 +181,9 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate back to home await page.getByTestId('nav-home').click() - await page.waitForURL('/') + // Match home URL with or without trailing slash and optional query string + // Matches: /, /?, /my-app, /my-app/, /my-app?foo=bar + await page.waitForURL(/\/([^/]*)(\/)?($|\?)/) // Verify global styles still work await expect(globalElement).toBeVisible() diff --git a/e2e/react-start/css-modules/vite.config.cloudflare.ts b/e2e/react-start/css-modules/vite.config.cloudflare.ts new file mode 100644 index 00000000000..7015976b974 --- /dev/null +++ b/e2e/react-start/css-modules/vite.config.cloudflare.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { cloudflare } from '@cloudflare/vite-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import viteReact from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + cloudflare({ viteEnvironment: { name: 'ssr' }, inspectorPort: false }), + tanstackStart(), + viteReact(), + ], +}) diff --git a/e2e/react-start/css-modules/vite.config.ts b/e2e/react-start/css-modules/vite.config.ts index c2c28ae93b7..6027af4bfa0 100644 --- a/e2e/react-start/css-modules/vite.config.ts +++ b/e2e/react-start/css-modules/vite.config.ts @@ -3,15 +3,28 @@ 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(), - ], +// Environment variables for different test configurations: +// - VITE_BASE_PATH: Set to '/my-app' for basepath testing +// - VITE_USE_NITRO: Set to 'true' to enable Nitro server +const basePath = process.env.VITE_BASE_PATH +const useNitro = process.env.VITE_USE_NITRO === 'true' + +export default defineConfig(async () => { + // Dynamically import nitro only when needed to avoid loading it when not used + const nitroPlugin = useNitro ? [(await import('nitro/vite')).nitro()] : [] + + return { + base: basePath, + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart(), + viteReact(), + ...nitroPlugin, + ], + } }) diff --git a/e2e/react-start/css-modules/wrangler.jsonc b/e2e/react-start/css-modules/wrangler.jsonc new file mode 100644 index 00000000000..5762162d794 --- /dev/null +++ b/e2e/react-start/css-modules/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "tanstack-start-css-modules", + "compatibility_date": "2025-09-24", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/react-start/server-entry", +} diff --git a/e2e/react-start/serialization-adapters/tests/app.spec.ts b/e2e/react-start/serialization-adapters/tests/app.spec.ts index 292212a4d1a..a74ed1af4fe 100644 --- a/e2e/react-start/serialization-adapters/tests/app.spec.ts +++ b/e2e/react-start/serialization-adapters/tests/app.spec.ts @@ -41,7 +41,7 @@ async function checkNestedData(page: Page) { } test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 499/, + 'Failed to load resource: the server responded with a status of 499', ], }) test.describe('SSR serialization adapters', () => { diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index 920cf602f34..b63b399a847 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -370,7 +370,7 @@ test.describe('server function sets cookies', () => { test.describe('server functions with async validation', () => { test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 500/, + 'Failed to load resource: the server responded with a status of 500', ], }) @@ -739,7 +739,7 @@ test('redirect via server function with middleware does not cause serialization test.describe('unhandled exception in middleware (issue #5266)', () => { // Whitelist the expected 500 error since this test verifies error handling - test.use({ whitelistErrors: [/500/] }) + test.use({ whitelistErrors: ['500'] }) test('does not crash server and shows error component', async ({ page }) => { // This test verifies that when a middleware throws an unhandled exception, diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts index 0a64f58417f..ed0b0d3a4d0 100644 --- a/e2e/solid-start/basic/tests/navigation.spec.ts +++ b/e2e/solid-start/basic/tests/navigation.spec.ts @@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils' test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test('Navigating to post', async ({ page }) => { diff --git a/e2e/solid-start/basic/tests/not-found.spec.ts b/e2e/solid-start/basic/tests/not-found.spec.ts index 2962f2bafa4..9c6092c8096 100644 --- a/e2e/solid-start/basic/tests/not-found.spec.ts +++ b/e2e/solid-start/basic/tests/not-found.spec.ts @@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', 'NotFound error during hydration for routeId', ], }) diff --git a/e2e/solid-start/basic/tests/params.spec.ts b/e2e/solid-start/basic/tests/params.spec.ts index 737c82f35d2..505e63ef433 100644 --- a/e2e/solid-start/basic/tests/params.spec.ts +++ b/e2e/solid-start/basic/tests/params.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test.describe('Unicode route rendering', () => { diff --git a/e2e/solid-start/css-modules/package.json b/e2e/solid-start/css-modules/package.json index 27417100bf6..4acefb88ae7 100644 --- a/e2e/solid-start/css-modules/package.json +++ b/e2e/solid-start/css-modules/package.json @@ -14,13 +14,13 @@ "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev" }, "dependencies": { - "@tanstack/solid-router": "workspace:^", - "@tanstack/solid-start": "workspace:^", + "@tanstack/solid-router": "workspace:*", + "@tanstack/solid-start": "workspace:*", "solid-js": "^1.9.10" }, "devDependencies": { "@playwright/test": "^1.50.1", - "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/router-e2e-utils": "workspace:*", "@types/node": "^22.10.2", "srvx": "^0.10.0", "typescript": "^5.7.2", diff --git a/e2e/solid-start/css-modules/playwright.config.ts b/e2e/solid-start/css-modules/playwright.config.ts index 7582bbbb531..0b810d5372c 100644 --- a/e2e/solid-start/css-modules/playwright.config.ts +++ b/e2e/solid-start/css-modules/playwright.config.ts @@ -2,7 +2,8 @@ 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 mode = process.env.MODE ?? 'prod' +const isDev = mode === 'dev' const PORT = await getTestServerPort(packageJson.name) const baseURL = `http://localhost:${PORT}` diff --git a/e2e/solid-start/css-modules/tests/css.spec.ts b/e2e/solid-start/css-modules/tests/css.spec.ts index 9fb18bbd3d6..bdfd2308a4b 100644 --- a/e2e/solid-start/css-modules/tests/css.spec.ts +++ b/e2e/solid-start/css-modules/tests/css.spec.ts @@ -1,31 +1,54 @@ -import { expect, request } from '@playwright/test' +import { expect } from '@playwright/test' import { test } from '@tanstack/router-e2e-utils' +// Whitelist errors that can occur in CI: +// - net::ERR_NAME_NOT_RESOLVED: transient network issues +// - 504 (Outdated Optimize Dep): Vite dependency optimization reload +const whitelistErrors = [ + 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED', + 'Failed to load resource: the server responded with a status of 504', +] + test.describe('CSS styles in SSR (dev mode)', () => { + test.use({ whitelistErrors }) + // Warmup: trigger Vite's dependency optimization before running tests - // This prevents "504 (Outdated Optimize Dep)" errors during actual tests - test.beforeAll(async ({ baseURL }) => { - const context = await request.newContext() + // This prevents "optimized dependencies changed. reloading" during actual tests + // We use a real browser context since dep optimization happens on JS load, not HTTP requests + test.beforeAll(async ({ browser, baseURL }) => { + const context = await browser.newContext() + const page = await context.newPage() try { - // Hit both pages to trigger any dependency optimization - await context.get(baseURL!) - await context.get(`${baseURL}/modules`) - // Give Vite time to complete optimization - await new Promise((resolve) => setTimeout(resolve, 1000)) - // Hit again after optimization - await context.get(baseURL!) + // Load both pages to trigger dependency optimization + await page.goto(baseURL!) + await page.waitForTimeout(2000) // Wait for deps to optimize + await page.goto(`${baseURL}/modules`) + await page.waitForTimeout(2000) + // Load again after optimization completes + await page.goto(baseURL!) + await page.waitForTimeout(1000) } catch { // Ignore errors during warmup } finally { - await context.dispose() + await context.close() } }) + // Helper to build full URL from baseURL and path + // Playwright's goto with absolute paths (like '/modules') ignores baseURL's path portion + // So we need to manually construct the full URL + const buildUrl = (baseURL: string, path: string) => { + return baseURL.replace(/\/$/, '') + path + } + test.describe('with JavaScript disabled', () => { - test.use({ javaScriptEnabled: false }) + test.use({ javaScriptEnabled: false, whitelistErrors }) - test('global CSS is applied on initial page load', async ({ page }) => { - await page.goto('/') + test('global CSS is applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -48,8 +71,11 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('12px') }) - test('CSS modules are applied on initial page load', async ({ page }) => { - await page.goto('/modules') + test('CSS modules are applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) const card = page.getByTestId('module-card') await expect(card).toBeVisible() @@ -77,8 +103,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('8px') }) - test('global CSS class names are NOT scoped', async ({ page }) => { - await page.goto('/') + test('global CSS class names are NOT scoped', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -89,37 +115,47 @@ test.describe('CSS styles in SSR (dev mode)', () => { }) }) - test('styles persist after hydration', async ({ page }) => { - await page.goto('/') - - // Wait for hydration - await page.waitForTimeout(1000) + test('styles persist after hydration', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) + // Wait for hydration and styles to be applied const element = page.getByTestId('global-styled') - const backgroundColor = await element.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(59, 130, 246)') - }) + await expect(element).toBeVisible() - test('CSS modules styles persist after hydration', async ({ page }) => { - await page.goto('/modules') + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await element.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(59, 130, 246)') + }).toPass({ timeout: 5000 }) + }) - // Wait for hydration - await page.waitForTimeout(1000) + test('CSS modules styles persist after hydration', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) + // Wait for hydration and styles to be applied const card = page.getByTestId('module-card') - const backgroundColor = await card.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(240, 253, 244)') + await expect(card).toBeVisible() + + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await card.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(240, 253, 244)') + }).toPass({ timeout: 5000 }) }) test('styles work correctly after client-side navigation', async ({ page, + baseURL, }) => { // Start from home - await page.goto('/') + await page.goto(buildUrl(baseURL!, '/')) await page.waitForTimeout(1000) // Verify initial styles @@ -132,7 +168,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate to modules page await page.getByTestId('nav-modules').click() - await page.waitForURL('/modules') + // Use glob pattern to match with or without basepath + await page.waitForURL('**/modules') // Verify CSS modules styles const card = page.getByTestId('module-card') @@ -144,7 +181,9 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate back to home await page.getByTestId('nav-home').click() - await page.waitForURL('/') + // Match home URL with or without trailing slash and optional query string + // Matches: /, /?, /my-app, /my-app/, /my-app?foo=bar + await page.waitForURL(/\/([^/]*)(\/)?($|\?)/) // Verify global styles still work await expect(globalElement).toBeVisible() diff --git a/e2e/solid-start/serialization-adapters/tests/app.spec.ts b/e2e/solid-start/serialization-adapters/tests/app.spec.ts index 292212a4d1a..a74ed1af4fe 100644 --- a/e2e/solid-start/serialization-adapters/tests/app.spec.ts +++ b/e2e/solid-start/serialization-adapters/tests/app.spec.ts @@ -41,7 +41,7 @@ async function checkNestedData(page: Page) { } test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 499/, + 'Failed to load resource: the server responded with a status of 499', ], }) test.describe('SSR serialization adapters', () => { diff --git a/e2e/vue-start/basic/tests/navigation.spec.ts b/e2e/vue-start/basic/tests/navigation.spec.ts index 0a64f58417f..ed0b0d3a4d0 100644 --- a/e2e/vue-start/basic/tests/navigation.spec.ts +++ b/e2e/vue-start/basic/tests/navigation.spec.ts @@ -3,7 +3,7 @@ import { test } from '@tanstack/router-e2e-utils' test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test('Navigating to post', async ({ page }) => { diff --git a/e2e/vue-start/basic/tests/not-found.spec.ts b/e2e/vue-start/basic/tests/not-found.spec.ts index 2962f2bafa4..9c6092c8096 100644 --- a/e2e/vue-start/basic/tests/not-found.spec.ts +++ b/e2e/vue-start/basic/tests/not-found.spec.ts @@ -8,7 +8,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', 'NotFound error during hydration for routeId', ], }) diff --git a/e2e/vue-start/basic/tests/params.spec.ts b/e2e/vue-start/basic/tests/params.spec.ts index de8423406ba..46ed630994c 100644 --- a/e2e/vue-start/basic/tests/params.spec.ts +++ b/e2e/vue-start/basic/tests/params.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 404/, + 'Failed to load resource: the server responded with a status of 404', ], }) test.describe('Unicode route rendering', () => { diff --git a/e2e/vue-start/css-modules/package.json b/e2e/vue-start/css-modules/package.json index b2e267f9b56..353301906e9 100644 --- a/e2e/vue-start/css-modules/package.json +++ b/e2e/vue-start/css-modules/package.json @@ -14,13 +14,13 @@ "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev" }, "dependencies": { - "@tanstack/vue-router": "workspace:^", - "@tanstack/vue-start": "workspace:^", + "@tanstack/vue-router": "workspace:*", + "@tanstack/vue-start": "workspace:*", "vue": "^3.5.16" }, "devDependencies": { "@playwright/test": "^1.50.1", - "@tanstack/router-e2e-utils": "workspace:^", + "@tanstack/router-e2e-utils": "workspace:*", "@types/node": "^22.10.2", "@vitejs/plugin-vue-jsx": "^4.1.2", "srvx": "^0.10.0", diff --git a/e2e/vue-start/css-modules/playwright.config.ts b/e2e/vue-start/css-modules/playwright.config.ts index 7582bbbb531..0b810d5372c 100644 --- a/e2e/vue-start/css-modules/playwright.config.ts +++ b/e2e/vue-start/css-modules/playwright.config.ts @@ -2,7 +2,8 @@ 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 mode = process.env.MODE ?? 'prod' +const isDev = mode === 'dev' const PORT = await getTestServerPort(packageJson.name) const baseURL = `http://localhost:${PORT}` diff --git a/e2e/vue-start/css-modules/tests/css.spec.ts b/e2e/vue-start/css-modules/tests/css.spec.ts index 9fb18bbd3d6..1ed0a4ee3a1 100644 --- a/e2e/vue-start/css-modules/tests/css.spec.ts +++ b/e2e/vue-start/css-modules/tests/css.spec.ts @@ -1,31 +1,56 @@ -import { expect, request } from '@playwright/test' +import { expect } from '@playwright/test' import { test } from '@tanstack/router-e2e-utils' +// Whitelist errors that can occur in CI: +// - net::ERR_NAME_NOT_RESOLVED: transient network issues +// - 504 (Outdated Optimize Dep): Vite dependency optimization reload +// - net::ERR_INTERNET_DISCONNECTED: transient network issues +const whitelistErrors = [ + 'Failed to load resource: net::ERR_NAME_NOT_RESOLVED', + 'Failed to load resource: net::ERR_INTERNET_DISCONNECTED', + 'Failed to load resource: the server responded with a status of 504', +] + test.describe('CSS styles in SSR (dev mode)', () => { + test.use({ whitelistErrors }) + // Warmup: trigger Vite's dependency optimization before running tests - // This prevents "504 (Outdated Optimize Dep)" errors during actual tests - test.beforeAll(async ({ baseURL }) => { - const context = await request.newContext() + // This prevents "optimized dependencies changed. reloading" during actual tests + // We use a real browser context since dep optimization happens on JS load, not HTTP requests + test.beforeAll(async ({ browser, baseURL }) => { + const context = await browser.newContext() + const page = await context.newPage() try { - // Hit both pages to trigger any dependency optimization - await context.get(baseURL!) - await context.get(`${baseURL}/modules`) - // Give Vite time to complete optimization - await new Promise((resolve) => setTimeout(resolve, 1000)) - // Hit again after optimization - await context.get(baseURL!) + // Load both pages to trigger dependency optimization + await page.goto(baseURL!) + await page.waitForTimeout(2000) // Wait for deps to optimize + await page.goto(`${baseURL}/modules`) + await page.waitForTimeout(2000) + // Load again after optimization completes + await page.goto(baseURL!) + await page.waitForTimeout(1000) } catch { // Ignore errors during warmup } finally { - await context.dispose() + await context.close() } }) + // Helper to build full URL from baseURL and path + // Playwright's goto with absolute paths (like '/modules') ignores baseURL's path portion + // So we need to manually construct the full URL + const buildUrl = (baseURL: string, path: string) => { + return baseURL.replace(/\/$/, '') + path + } + test.describe('with JavaScript disabled', () => { - test.use({ javaScriptEnabled: false }) + test.use({ javaScriptEnabled: false, whitelistErrors }) - test('global CSS is applied on initial page load', async ({ page }) => { - await page.goto('/') + test('global CSS is applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -48,8 +73,11 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('12px') }) - test('CSS modules are applied on initial page load', async ({ page }) => { - await page.goto('/modules') + test('CSS modules are applied on initial page load', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) const card = page.getByTestId('module-card') await expect(card).toBeVisible() @@ -77,8 +105,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { expect(borderRadius).toBe('8px') }) - test('global CSS class names are NOT scoped', async ({ page }) => { - await page.goto('/') + test('global CSS class names are NOT scoped', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) const element = page.getByTestId('global-styled') await expect(element).toBeVisible() @@ -89,37 +117,47 @@ test.describe('CSS styles in SSR (dev mode)', () => { }) }) - test('styles persist after hydration', async ({ page }) => { - await page.goto('/') - - // Wait for hydration - await page.waitForTimeout(1000) + test('styles persist after hydration', async ({ page, baseURL }) => { + await page.goto(buildUrl(baseURL!, '/')) + // Wait for hydration and styles to be applied const element = page.getByTestId('global-styled') - const backgroundColor = await element.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(59, 130, 246)') - }) + await expect(element).toBeVisible() - test('CSS modules styles persist after hydration', async ({ page }) => { - await page.goto('/modules') + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await element.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(59, 130, 246)') + }).toPass({ timeout: 5000 }) + }) - // Wait for hydration - await page.waitForTimeout(1000) + test('CSS modules styles persist after hydration', async ({ + page, + baseURL, + }) => { + await page.goto(buildUrl(baseURL!, '/modules')) + // Wait for hydration and styles to be applied const card = page.getByTestId('module-card') - const backgroundColor = await card.evaluate( - (el) => getComputedStyle(el).backgroundColor, - ) - expect(backgroundColor).toBe('rgb(240, 253, 244)') + await expect(card).toBeVisible() + + // Wait for CSS to be applied (background color should not be transparent) + await expect(async () => { + const backgroundColor = await card.evaluate( + (el) => getComputedStyle(el).backgroundColor, + ) + expect(backgroundColor).toBe('rgb(240, 253, 244)') + }).toPass({ timeout: 5000 }) }) test('styles work correctly after client-side navigation', async ({ page, + baseURL, }) => { // Start from home - await page.goto('/') + await page.goto(buildUrl(baseURL!, '/')) await page.waitForTimeout(1000) // Verify initial styles @@ -132,7 +170,8 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate to modules page await page.getByTestId('nav-modules').click() - await page.waitForURL('/modules') + // Use glob pattern to match with or without basepath + await page.waitForURL('**/modules') // Verify CSS modules styles const card = page.getByTestId('module-card') @@ -144,7 +183,9 @@ test.describe('CSS styles in SSR (dev mode)', () => { // Navigate back to home await page.getByTestId('nav-home').click() - await page.waitForURL('/') + // Match home URL with or without trailing slash and optional query string + // Matches: /, /?, /my-app, /my-app/, /my-app?foo=bar + await page.waitForURL(/\/([^/]*)(\/)?($|\?)/) // Verify global styles still work await expect(globalElement).toBeVisible() diff --git a/e2e/vue-start/serialization-adapters/tests/app.spec.ts b/e2e/vue-start/serialization-adapters/tests/app.spec.ts index 292212a4d1a..a74ed1af4fe 100644 --- a/e2e/vue-start/serialization-adapters/tests/app.spec.ts +++ b/e2e/vue-start/serialization-adapters/tests/app.spec.ts @@ -41,7 +41,7 @@ async function checkNestedData(page: Page) { } test.use({ whitelistErrors: [ - /Failed to load resource: the server responded with a status of 499/, + 'Failed to load resource: the server responded with a status of 499', ], }) test.describe('SSR serialization adapters', () => { diff --git a/nx.json b/nx.json index 4f8c811d34d..3c404cd55bc 100644 --- a/nx.json +++ b/nx.json @@ -45,6 +45,16 @@ "dependsOn": ["^build"], "inputs": ["default", "^production"] }, + "test:e2e:nitro": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production"] + }, + "test:e2e:basepath": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production"] + }, "test:types": { "cache": true, "dependsOn": ["^build"], diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx index 8da3b5b279b..026136c60d7 100644 --- a/packages/react-router/src/HeadContent.tsx +++ b/packages/react-router/src/HeadContent.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { escapeHtml } from '@tanstack/router-core' +import { buildDevStylesUrl, escapeHtml } from '@tanstack/router-core' import { Asset } from './Asset' import { useRouter } from './useRouter' import { useRouterState } from './useRouterState' @@ -211,7 +211,6 @@ export const useTags = () => { */ function DevStylesLink() { const router = useRouter() - const routeIds = useRouterState({ select: (state) => state.matches.map((match) => match.routeId), }) @@ -223,8 +222,7 @@ function DevStylesLink() { .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(','))}` + const href = buildDevStylesUrl(router.basepath, routeIds) return ( ): string { return result } + +/** + * Builds the dev-mode CSS styles URL for route-scoped CSS collection. + * Used by HeadContent components in all framework implementations to construct + * the URL for the `/@tanstack-start/styles.css` endpoint. + * + * @param basepath - The router's basepath (may or may not have leading slash) + * @param routeIds - Array of matched route IDs to include in the CSS collection + * @returns The full URL path for the dev styles CSS endpoint + */ +export function buildDevStylesUrl( + basepath: string, + routeIds: Array, +): string { + // Trim all leading and trailing slashes from basepath + const trimmedBasepath = basepath.replace(/^\/+|\/+$/g, '') + // Build normalized basepath: empty string for root, or '/path' for non-root + const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}` + return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}` +} diff --git a/packages/solid-router/src/HeadContent.tsx b/packages/solid-router/src/HeadContent.tsx index 8a1d638a38b..e6cf3e44f86 100644 --- a/packages/solid-router/src/HeadContent.tsx +++ b/packages/solid-router/src/HeadContent.tsx @@ -1,7 +1,7 @@ import * as Solid from 'solid-js' import { MetaProvider } from '@solidjs/meta' import { For, Show, onMount } from 'solid-js' -import { escapeHtml } from '@tanstack/router-core' +import { buildDevStylesUrl, escapeHtml } from '@tanstack/router-core' import { Asset } from './Asset' import { useRouter } from './useRouter' import { useRouterState } from './useRouterState' @@ -204,6 +204,7 @@ export const useTags = () => { * 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), }) @@ -215,9 +216,7 @@ function DevStylesLink() { .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(','))}` + const href = () => buildDevStylesUrl(router.basepath, routeIds()) return } 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 index 4c226ae425c..39bf7e6608e 100644 --- a/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts +++ b/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts @@ -44,7 +44,7 @@ export async function collectDevStyles( for (const entry of entries) { const normalizedPath = entry.replace(/\\/g, '/') - let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath) + let node = viteDevServer.moduleGraph.getModuleById(normalizedPath) // If module isn't in the graph yet, request it to trigger transform if (!node) { @@ -53,7 +53,7 @@ export async function collectDevStyles( } catch (err) { // Ignore - the module might not exist yet } - node = await viteDevServer.moduleGraph.getModuleById(normalizedPath) + node = viteDevServer.moduleGraph.getModuleById(normalizedPath) } if (node) { 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 1fe8856310c..39caed21259 100644 --- a/packages/start-plugin-core/src/dev-server-plugin/plugin.ts +++ b/packages/start-plugin-core/src/dev-server-plugin/plugin.ts @@ -49,6 +49,61 @@ export function devServerPlugin({ `Server environment ${VITE_ENVIRONMENT_NAMES.server} not found`, ) } + + // CSS middleware is always installed - it doesn't depend on the server environment type + // This ensures dev styles work with nitro, cloudflare, and other environments + viteDevServer.middlewares.use(async (req, res, next) => { + const url = req.url ?? '' + if (!url.startsWith('/@tanstack-start/styles.css')) { + return next() + } + + try { + // 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 ?? '') + } catch (e) { + // Log error but still return valid CSS response to avoid MIME type issues + console.error('[tanstack-start] Error collecting dev styles:', e) + res.setHeader('Content-Type', 'text/css') + res.setHeader('Cache-Control', 'no-store') + res.end( + `/* Error collecting styles: ${e instanceof Error ? e.message : String(e)} */`, + ) + } + }) + const { startConfig } = getConfig() const installMiddleware = startConfig.vite?.installDevServerMiddleware if (installMiddleware === false) { @@ -76,52 +131,6 @@ 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 a7666d20a7d..a022b77b376 100644 --- a/packages/vue-router/src/HeadContent.tsx +++ b/packages/vue-router/src/HeadContent.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' -import { escapeHtml } from '@tanstack/router-core' +import { buildDevStylesUrl, escapeHtml } from '@tanstack/router-core' import { Asset } from './Asset' import { useRouter } from './useRouter' import { useRouterState } from './useRouterState' @@ -15,6 +15,7 @@ import type { RouterManagedTag } from '@tanstack/router-core' const DevStylesLink = Vue.defineComponent({ name: 'DevStylesLink', setup() { + const router = useRouter() const routeIds = useRouterState({ select: (state) => state.matches.map((match) => match.routeId), }) @@ -26,10 +27,8 @@ const DevStylesLink = Vue.defineComponent({ .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(','))}`, + const href = Vue.computed(() => + buildDevStylesUrl(router.basepath, routeIds.value), ) return () => diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78feae441bc..e0af67d54fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1531,11 +1531,14 @@ importers: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.15.1 + version: 1.15.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))(workerd@1.20251118.0)(wrangler@4.49.1) '@playwright/test': specifier: ^1.56.1 version: 1.56.1 '@tanstack/router-e2e-utils': - specifier: workspace:^ + specifier: workspace:* version: link:../../e2e-utils '@types/node': specifier: 22.10.2 @@ -1549,6 +1552,9 @@ importers: '@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)) + nitro: + specifier: npm:nitro-nightly@latest + version: nitro-nightly@3.0.1-20260109-100347-99691fcb(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(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)) srvx: specifier: ^0.10.0 version: 0.10.0 @@ -1561,6 +1567,9 @@ importers: 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)) + wrangler: + specifier: ^4.22.0 + version: 4.49.1 e2e/react-start/custom-basepath: dependencies: @@ -3401,7 +3410,7 @@ importers: e2e/solid-start/css-modules: dependencies: '@tanstack/solid-router': - specifier: workspace:^ + specifier: workspace:* version: link:../../../packages/solid-router '@tanstack/solid-start': specifier: workspace:* @@ -3414,7 +3423,7 @@ importers: specifier: ^1.56.1 version: 1.56.1 '@tanstack/router-e2e-utils': - specifier: workspace:^ + specifier: workspace:* version: link:../../e2e-utils '@types/node': specifier: 22.10.2 @@ -5202,7 +5211,7 @@ importers: specifier: ^1.56.1 version: 1.56.1 '@tanstack/router-e2e-utils': - specifier: workspace:^ + specifier: workspace:* version: link:../../e2e-utils '@types/node': specifier: 22.10.2 @@ -10500,7 +10509,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)) @@ -21739,6 +21748,25 @@ packages: xml2js: optional: true + nitro-nightly@3.0.1-20260109-100347-99691fcb: + resolution: {integrity: sha512-qokcOf8TrWIYP05HrhdnV5k+Yt4oelxhRsaMfP99X3HmtAsS5dv/NGQXpwFMbMhUwtY9Z8nuhy2oOWnqK7lE/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + rolldown: '*' + rollup: ^4.55.1 + vite: ^7.1.7 + xml2js: ^0.6.2 + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + vite: + optional: true + xml2js: + optional: true + nitropack@2.12.6: resolution: {integrity: sha512-DEq31s0SP4/Z5DIoVBRo9DbWFPWwIoYD4cQMEz7eE+iJMiAP+1k9A3B9kcc6Ihc0jDJmfUcHYyh6h2XlynCx6g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -26257,12 +26285,12 @@ snapshots: '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -26738,8 +26766,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -26828,13 +26856,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 @@ -26902,12 +26930,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 @@ -26997,9 +27025,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 @@ -27027,9 +27055,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) @@ -27057,13 +27085,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 @@ -30318,7 +30346,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) @@ -34751,6 +34779,54 @@ snapshots: - sqlite3 - uploadthing + nitro-nightly@3.0.1-20260109-100347-99691fcb(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(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: + consola: 3.4.2 + crossws: 0.4.1(srvx@0.10.0) + db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + h3: 2.0.1-rc.7(crossws@0.4.1(srvx@0.10.0)) + jiti: 2.6.1 + nf3: 0.3.4 + ofetch: 2.0.0-alpha.3 + ohash: 2.0.11 + oxc-minify: 0.107.0 + oxc-transform: 0.107.0 + srvx: 0.10.0 + undici: 7.18.2 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) + optionalDependencies: + 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) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - chokidar + - drizzle-orm + - idb-keyval + - ioredis + - lru-cache + - mongodb + - mysql2 + - sqlite3 + - uploadthing + nitropack@2.12.6(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(encoding@0.1.13)(mysql2@3.15.3): dependencies: '@cloudflare/kv-asset-handler': 0.4.0