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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions e2e/react-start/basic-prerender/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/test-results/
/playwright-report/
/blob-report/
24 changes: 24 additions & 0 deletions e2e/react-start/basic-prerender/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "tanstack-react-start-e2e-basic-prerender",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"test:e2e:startDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.setup.ts\").then(m => m.default())' & node -e 'import(\"../basic-test-suite/src/setup/waitForDummyServer.ts\").then(m => m.default())'",
"test:e2e:stopDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"tanstack-react-start-e2e-basic": "workspace:*",
"tanstack-react-start-e2e-basic-test-suite": "workspace:*"
},
"nx": {
"targets": {
"test:e2e": {
"parallelism": false
}
}
}
}
48 changes: 48 additions & 0 deletions e2e/react-start/basic-prerender/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { defineConfig, devices } from '@playwright/test'
import {
getDummyServerPort,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const START_PORT = await getTestServerPort(`${packageJson.name}_start`)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Keep the prerender dummy-server lifecycle in this workspace.

Line 10 allocates EXTERNAL_PORT from tanstack-react-start-e2e-basic-prerender, but Line 27 starts/stops the dummy server via pnpm --dir ../basic .... Those scripts resolve their package/port-*.txt state from ../basic, so the dummy server can bind a different port than the one injected as VITE_EXTERNAL_PORT. The prerender build then talks to a port that no dummy server is serving.

🐛 Suggested fix
   webServer: {
     command:
-      'pnpm --dir ../basic run test:e2e:startDummyServer && pnpm --dir ../basic build:prerender && pnpm --dir ../basic run test:e2e:stopDummyServer && pnpm --dir ../basic start',
+      'node -e \'import("../basic-test-suite/src/setup/global.setup.ts").then((m) => m.default())\' & node -e \'import("../basic-test-suite/src/setup/waitForDummyServer.ts").then((m) => m.default())\' && pnpm --dir ../basic build:prerender && node -e \'import("../basic-test-suite/src/setup/global.teardown.ts").then((m) => m.default())\' && pnpm --dir ../basic start',

Also applies to: 25-37

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/basic-prerender/playwright.config.ts` at line 10,
EXTERNAL_PORT is being allocated in this workspace with getDummyServerPort but
the dummy server is started/stopped in ../basic, causing a port mismatch; update
the prerender dummy-server lifecycle so the same workspace manages both port
allocation and server scripts: keep using EXTERNAL_PORT from getDummyServerPort
and change any start/stop invocations that use "pnpm --dir ../basic ..." (or
equivalent commands referenced in the Playwright config) to run against this
workspace (e.g., remove or replace the ../basic dir flag so the scripts resolve
package/port-*.txt here), and ensure VITE_EXTERNAL_PORT is set to EXTERNAL_PORT
when launching the dummy server and when running the prerender build.

const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: '../basic-test-suite/src',
workers: 1,
reporter: [['line']],

globalTeardown: '../basic-test-suite/src/setup/global.teardown.ts',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check getPackageName implementation to verify it resolves correctly

# Find and display getPackageName implementation
fd -t f 'getPackageName.ts' e2e/react-start/basic-test-suite --exec cat {}

echo "---"
# Check if it reads from cwd or has hardcoded values
rg -n 'getPackageName|packageJson|process.cwd' e2e/react-start/basic-test-suite/src/utils/

Repository: TanStack/router

Length of output: 1276


🏁 Script executed:

# Check the full playwright.config.ts to see how packageJson is loaded
cat -n e2e/react-start/basic-prerender/playwright.config.ts | head -25

Repository: TanStack/router

Length of output: 912


🏁 Script executed:

# Check the package.json in basic-prerender to see its name
cat e2e/react-start/basic-prerender/package.json | jq '.name'

Repository: TanStack/router

Length of output: 101


🏁 Script executed:

# Check the package.json at the repo root to compare names
cat package.json | jq '.name'

Repository: TanStack/router

Length of output: 65


🏁 Script executed:

# Check global.teardown.ts to see exactly how it uses getPackageName
cat -n e2e/react-start/basic-test-suite/src/setup/global.teardown.ts

Repository: TanStack/router

Length of output: 352


Fix package name mismatch between server startup and teardown.

The dummy server port is allocated using packageJson.name (static value "tanstack-react-start-e2e-basic-prerender" from basic-prerender's package.json), but globalTeardown calls getPackageName() which reads process.cwd()/package.json at runtime. When tests run from the repository root, process.cwd() resolves to the repo root's package.json (name: "root"), causing the teardown to attempt stopping a server under the wrong package name. The dummy server will not be properly stopped.

Either pass the package name to the teardown function, or ensure getPackageName() is called with the correct context directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/basic-prerender/playwright.config.ts` at line 18, The
teardown is reading package name at runtime via getPackageName() which uses
process.cwd(), causing a mismatch with the static packageJson.name used when
starting the dummy server; change the config so the teardown receives the
correct package name from the config rather than reading process.cwd(): update
globalTeardown usage in playwright.config.ts (symbol: globalTeardown) to pass
the expected package name (packageJson.name from the basic-prerender package) or
modify getPackageName() calls in the teardown implementation (symbol:
getPackageName) to accept and use a baseDir argument and call it with the
basic-prerender directory so the teardown stops the server created with that
package name.


use: {
baseURL,
},

webServer: {
command:
'pnpm run test:e2e:startDummyServer && pnpm --dir ../basic build:prerender && pnpm --dir ../basic start',
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
MODE: 'prerender',
VITE_NODE_ENV: 'test',
VITE_EXTERNAL_PORT: String(EXTERNAL_PORT),
VITE_SERVER_PORT: String(PORT),
START_PORT: String(START_PORT),
PORT: String(PORT),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
5 changes: 5 additions & 0 deletions e2e/react-start/basic-preview/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/test-results/
/playwright-report/
/blob-report/
24 changes: 24 additions & 0 deletions e2e/react-start/basic-preview/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "tanstack-react-start-e2e-basic-preview",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"test:e2e:startDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.setup.ts\").then(m => m.default())' & node -e 'import(\"../basic-test-suite/src/setup/waitForDummyServer.ts\").then(m => m.default())'",
"test:e2e:stopDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e": "rm -rf port*.txt; MODE=preview playwright test --project=chromium"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"tanstack-react-start-e2e-basic": "workspace:*",
"tanstack-react-start-e2e-basic-test-suite": "workspace:*"
},
"nx": {
"targets": {
"test:e2e": {
"parallelism": false
}
}
}
}
47 changes: 47 additions & 0 deletions e2e/react-start/basic-preview/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineConfig, devices } from '@playwright/test'
import {
getDummyServerPort,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const START_PORT = await getTestServerPort(`${packageJson.name}_start`)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: '../basic-test-suite/src',
workers: 1,
reporter: [['line']],

globalTeardown: '../basic-test-suite/src/setup/global.teardown.ts',

use: {
baseURL,
},

webServer: {
command: `pnpm run test:e2e:startDummyServer && pnpm --dir ../basic build && pnpm --dir ../basic preview --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
MODE: 'preview',
VITE_NODE_ENV: 'test',
VITE_EXTERNAL_PORT: String(EXTERNAL_PORT),
VITE_SERVER_PORT: String(PORT),
START_PORT: String(START_PORT),
PORT: String(PORT),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
5 changes: 5 additions & 0 deletions e2e/react-start/basic-spa/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/test-results/
/playwright-report/
/blob-report/
24 changes: 24 additions & 0 deletions e2e/react-start/basic-spa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "tanstack-react-start-e2e-basic-spa",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"test:e2e:startDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.setup.ts\").then(m => m.default())' & node -e 'import(\"../basic-test-suite/src/setup/waitForDummyServer.ts\").then(m => m.default())'",
"test:e2e:stopDummyServer": "node -e 'import(\"../basic-test-suite/src/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e": "rm -rf port*.txt; MODE=spa playwright test --project=chromium"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"tanstack-react-start-e2e-basic": "workspace:*",
"tanstack-react-start-e2e-basic-test-suite": "workspace:*"
},
"nx": {
"targets": {
"test:e2e": {
"parallelism": false
}
}
}
}
48 changes: 48 additions & 0 deletions e2e/react-start/basic-spa/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { defineConfig, devices } from '@playwright/test'
import {
getDummyServerPort,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const START_PORT = await getTestServerPort(`${packageJson.name}_start`)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: '../basic-test-suite/src',
workers: 1,
reporter: [['line']],

globalTeardown: '../basic-test-suite/src/setup/global.teardown.ts',

use: {
baseURL,
},

webServer: {
command:
'pnpm run test:e2e:startDummyServer && pnpm --dir ../basic build:spa && pnpm --dir ../basic start',
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
env: {
MODE: 'spa',
VITE_NODE_ENV: 'test',
VITE_EXTERNAL_PORT: String(EXTERNAL_PORT),
VITE_SERVER_PORT: String(PORT),
START_PORT: String(START_PORT),
PORT: String(PORT),
},
},

projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
})
5 changes: 5 additions & 0 deletions e2e/react-start/basic-test-suite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/test-results/
/playwright-report/
/blob-report/
12 changes: 12 additions & 0 deletions e2e/react-start/basic-test-suite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "tanstack-react-start-e2e-basic-test-suite",
"private": true,
"sideEffects": false,
"type": "module",
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:*",
"@types/node": "^22.10.2",
"combinate": "^1.1.11"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import combinateImport from 'combinate'
import { test } from '@tanstack/router-e2e-utils'
import { isSpaMode } from '../tests/utils/isSpaMode'
import { isSpaMode } from './utils/isSpaMode'

// somehow playwright does not correctly import default exports
const combinate = (combinateImport as any).default as typeof combinateImport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { join } from 'node:path'
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { isPrerender } from './utils/isPrerender'
import { getBasicAppRoot } from './utils/getBasicAppRoot'

test.describe('Prerender Static Path Discovery', () => {
test.skip(!isPrerender, 'Skipping since not in prerender mode')
test.describe('Build Output Verification', () => {
test('should automatically discover and prerender static routes', () => {
// Check that static routes were automatically discovered and prerendered
const distDir = join(process.cwd(), 'dist', 'client')
const distDir = join(getBasicAppRoot(), 'dist', 'client')

// These static routes should be automatically discovered and prerendered
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
Expand All @@ -34,7 +35,7 @@ test.describe('Prerender Static Path Discovery', () => {

test.describe('Static Files Verification', () => {
test('should contain prerendered content in posts.html', () => {
const distDir = join(process.cwd(), 'dist', 'client')
const distDir = join(getBasicAppRoot(), 'dist', 'client')
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)

// "Select a post." should be in the prerendered HTML
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ import {
getTestServerPort,
test,
} from '@tanstack/router-e2e-utils'
import { isSpaMode } from '../tests/utils/isSpaMode'
import { isPreview } from '../tests/utils/isPreview'
import packageJson from '../package.json' with { type: 'json' }
import { getPackageName } from './utils/getPackageName.ts'

// somehow playwright does not correctly import default exports
const combinate = (combinateImport as any).default as typeof combinateImport
const packageName = getPackageName()

const PORT = await getTestServerPort(
`${packageJson.name}${isSpaMode ? '_spa' : ''}${isPreview ? '_preview' : ''}`,
)
const PORT = await getTestServerPort(packageName)

const EXTERNAL_HOST_PORT = await getDummyServerPort(packageJson.name)
const EXTERNAL_HOST_PORT = await getDummyServerPort(packageName)

test.describe('redirects', () => {
test.describe('internal', () => {
Expand Down
6 changes: 6 additions & 0 deletions e2e/react-start/basic-test-suite/src/setup/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
import { getPackageName } from '../utils/getPackageName.ts'

export default async function setup() {
await e2eStartDummyServer(getPackageName())
}
8 changes: 8 additions & 0 deletions e2e/react-start/basic-test-suite/src/setup/global.teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
import { getPackageName } from '../utils/getPackageName.ts'

export default async function teardown() {
try {
await e2eStopDummyServer(getPackageName())
} catch {}
}
24 changes: 24 additions & 0 deletions e2e/react-start/basic-test-suite/src/setup/waitForDummyServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getDummyServerPort } from '@tanstack/router-e2e-utils'
import { getPackageName } from '../utils/getPackageName.ts'

const timeoutMs = 10_000
const retryIntervalMs = 100

export default async function waitForDummyServer() {
const port = await getDummyServerPort(getPackageName())
const deadline = Date.now() + timeoutMs

while (Date.now() < deadline) {
try {
const response = await fetch(`http://localhost:${port}/`)

if (response.ok) {
return
}
} catch {}

await new Promise((resolve) => setTimeout(resolve, retryIntervalMs))
}

throw new Error(`Timed out waiting for dummy server on port ${port}`)
}
8 changes: 8 additions & 0 deletions e2e/react-start/basic-test-suite/src/utils/getBasicAppRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { join } from 'node:path'
import { getPackageName } from './getPackageName.ts'

export function getBasicAppRoot() {
return getPackageName() === 'tanstack-react-start-e2e-basic'
? process.cwd()
: join(process.cwd(), '../basic')
}
16 changes: 16 additions & 0 deletions e2e/react-start/basic-test-suite/src/utils/getPackageName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'

let packageName: string | undefined

export function getPackageName() {
if (!packageName) {
const packageJson = JSON.parse(
readFileSync(join(process.cwd(), 'package.json'), 'utf-8'),
) as { name: string }

packageName = packageJson.name
}

return packageName
}
1 change: 1 addition & 0 deletions e2e/react-start/basic-test-suite/src/utils/isPrerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isPrerender: boolean = process.env.MODE === 'prerender'
1 change: 1 addition & 0 deletions e2e/react-start/basic-test-suite/src/utils/isPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isPreview: boolean = process.env.MODE === 'preview'
1 change: 1 addition & 0 deletions e2e/react-start/basic-test-suite/src/utils/isSpaMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isSpaMode: boolean = process.env.MODE === 'spa'
Loading
Loading