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
6 changes: 5 additions & 1 deletion e2e/solid-start/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"build:spa": "MODE=spa vite build && tsc --noEmit",
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"start:spa": "node server.js",
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
"test:e2e:ssrMode": "rm -rf port*.txt; playwright test --project=chromium",
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode"
"test:e2e:prerender": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode && pnpm run test:e2e:prerender"
},
"dependencies": {
"@tanstack/solid-router": "workspace:^",
Expand Down
17 changes: 13 additions & 4 deletions e2e/solid-start/basic/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
getDummyServerPort,
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }
import { isSpaMode } from './tests/utils/isSpaMode'
import { isPrerender } from './tests/utils/isPrerender'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(
`${packageJson.name}${isSpaMode ? '_spa' : ''}`,
Expand All @@ -16,15 +17,21 @@ const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
const spaModeCommand = `pnpm build:spa && pnpm start:spa`
const ssrModeCommand = `pnpm build && pnpm start`
const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`

const getCommand = () => {
if (isSpaMode) return spaModeCommand
if (isPrerender) return prerenderModeCommand
return ssrModeCommand
}
Comment on lines +20 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Review the dummy server lifecycle management for potential issues.

The prerenderModeCommand chains commands with &&, which could lead to:

  1. Resource leak: If build:prerender fails, stopDummyServer won't execute, leaving the dummy server running.
  2. Race condition: There's no synchronization between startDummyServer and build:prerender. If the dummy server takes time to initialize, the build might start before the server is ready.

The getCommand() helper refactoring is clean and improves readability.

Consider adding error handling to ensure cleanup:

-const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`
+const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && (pnpm build:prerender; pnpm run test:e2e:stopDummyServer) && pnpm start`

Or verify that the dummy server scripts include their own timeout/retry logic to handle startup delays.


🏁 Script executed:

#!/bin/bash
# View the playwright config file to see the current state
cat e2e/solid-start/basic/playwright.config.ts

Length of output: 1965


🏁 Script executed:

#!/bin/bash
# Search for dummy server script definitions in package.json or other config files
fd -t f "package.json" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && grep -A 2 -B 2 "startDummyServer\|stopDummyServer" {} 2>/dev/null || true'

Length of output: 254


🏁 Script executed:

#!/bin/bash
# Check for any scripts directory or test setup files that might define the dummy server
fd -t f "(startDummyServer|stopDummyServer|dummy)" e2e/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any error handling or cleanup logic in e2e test setup
rg -n "trap\|catch\|error\|cleanup" e2e/solid-start/basic/ --type ts --type js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for dummy server script definitions in root package.json
cat package.json | grep -A 100 '"scripts"' | head -50

Length of output: 2485


🏁 Script executed:

#!/bin/bash
# Search for these specific scripts across the entire codebase
rg -n "startDummyServer|stopDummyServer" --type json --type ts --type js -B 1 -A 1

Length of output: 1672


🏁 Script executed:

#!/bin/bash
# Check if there's a workspace configuration that might define these scripts
fd -t f "pnpm-workspace.yaml|turbo.json|package.json" -x grep -l "startDummyServer\|stopDummyServer" {} \;

Length of output: 73


🏁 Script executed:

#!/bin/bash
# View the global setup and teardown files to understand their robustness
cat e2e/solid-start/basic/tests/setup/global.setup.ts

Length of output: 220


🏁 Script executed:

#!/bin/bash
# View teardown file
cat e2e/solid-start/basic/tests/setup/global.teardown.ts

Length of output: 221


🏁 Script executed:

#!/bin/bash
# Check if there are any existing error handling patterns in the e2e config or setup
grep -r "trap\|error\|catch\|finally" e2e/solid-start/basic/tests/setup/ --include="*.ts" -n

Length of output: 0


Address dummy server lifecycle management issues in prerender command.

The resource leak and race condition concerns are confirmed valid:

  1. Resource leak: If build:prerender fails, stopDummyServer never executes due to && chaining, leaving the background dummy server process orphaned.
  2. Race condition: startDummyServer runs backgrounded (&), allowing build:prerender to start immediately without explicit synchronization. The server may still be initializing when the build begins.

The suggested fix using subshell syntax (pnpm build:prerender; pnpm run test:e2e:stopDummyServer) does ensure cleanup runs, but it changes error propagation—if cleanup fails, the subsequent pnpm start also fails.

Verify and apply one of these approaches:

  • Option A (Cleanup priority): Use the suggested subshell approach if cleanup failures should halt execution.
  • Option B (Explicit trap): Use shell trap to guarantee cleanup: trap 'pnpm run test:e2e:stopDummyServer' EXIT before the build.
  • Option C (Explicit wait): Add explicit wait logic after startDummyServer to ensure the dummy server is ready before proceeding.

Same issue exists in e2e/react-start/basic/playwright.config.ts line 20.

🤖 Prompt for AI Agents
In e2e/solid-start/basic/playwright.config.ts around lines 20 to 26, the
prerender command uses chained commands that can leak the background dummy
server and race with the build; replace the one-liner with a safe sequence that
starts the dummy server, waits for it to be ready, runs build:prerender, and
always stops the dummy server on exit—either (A) wrap build and cleanup in a
subshell so cleanup runs and failures propagate, (B) set a shell trap to run
pnpm run test:e2e:stopDummyServer on EXIT before starting the server, or (C) add
an explicit readiness wait after starting the server (e.g., polling a health
endpoint) then run build and finally stop the server; apply the same change to
e2e/react-start/basic/playwright.config.ts at line 20.

console.log('running in spa mode: ', isSpaMode.toString())
console.log('running in prerender mode: ', isPrerender.toString())
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
workers: 1,

reporter: [['line']],

globalSetup: './tests/setup/global.setup.ts',
Expand All @@ -36,7 +43,7 @@ export default defineConfig({
},

webServer: {
command: isSpaMode ? spaModeCommand : ssrModeCommand,
command: getCommand(),
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
Expand All @@ -53,7 +60,9 @@ export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
},
},
],
})
53 changes: 53 additions & 0 deletions e2e/solid-start/basic/tests/prerendering.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { isPrerender } from './utils/isPrerender'

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')

// These static routes should be automatically discovered and prerendered
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)

// Pathless layouts should NOT be prerendered (they start with _)
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout

// API routes should NOT be prerendered

expect(existsSync(join(distDir, 'api', 'users', 'index.html'))).toBe(
false,
) // /api/users
})
})

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

// "Select a post." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
expect(html).toContain('Select a post.')
})

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

// "Select a user." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'users/index.html'), 'utf-8')
expect(html).toContain('Select a user.')
})
})
})
9 changes: 6 additions & 3 deletions e2e/solid-start/basic/tests/search-params.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { isSpaMode } from '../tests/utils/isSpaMode'
import { isSpaMode } from 'tests/utils/isSpaMode'
import { isPrerender } from './utils/isPrerender'
Comment on lines +3 to +4
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent import paths for utilities.

Line 3 uses tests/utils/isSpaMode (absolute/tsconfig path) while line 4 uses ./utils/isPrerender (relative path). For consistency, both utilities should use the same import pattern.

Apply this diff to make imports consistent:

-import { isSpaMode } from 'tests/utils/isSpaMode'
-import { isPrerender } from './utils/isPrerender'
+import { isSpaMode } from './utils/isSpaMode'
+import { isPrerender } from './utils/isPrerender'

Or if tsconfig paths are preferred:

-import { isSpaMode } from 'tests/utils/isSpaMode'
-import { isPrerender } from './utils/isPrerender'
+import { isSpaMode } from 'tests/utils/isSpaMode'
+import { isPrerender } from 'tests/utils/isPrerender'
📝 Committable suggestion

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

Suggested change
import { isSpaMode } from 'tests/utils/isSpaMode'
import { isPrerender } from './utils/isPrerender'
import { isSpaMode } from 'tests/utils/isSpaMode'
import { isPrerender } from 'tests/utils/isPrerender'
🤖 Prompt for AI Agents
In e2e/solid-start/basic/tests/search-params.spec.ts around lines 3 to 4, the
two utility imports use inconsistent paths (one using tsconfig/absolute
'tests/utils/isSpaMode' and the other a relative './utils/isPrerender'); make
them consistent by changing the import so both use the same pattern—either
update line 3 to a relative import './utils/isSpaMode' or update line 4 to the
tsconfig path 'tests/utils/isPrerender' depending on the project convention
(prefer tsconfig paths if that is the established pattern), and run a quick
compile to ensure the chosen path resolves.

import type { Response } from '@playwright/test'

function expectRedirect(response: Response | null, endsWith: string) {
Expand All @@ -26,9 +27,11 @@ test.describe('/search-params/loader-throws-redirect', () => {
page,
}) => {
const response = await page.goto('/search-params/loader-throws-redirect')
if (!isSpaMode) {

if (!isSpaMode && !isPrerender) {
expectRedirect(response, '/search-params/loader-throws-redirect?step=a')
}

await expect(page.getByTestId('search-param')).toContainText('a')
expect(page.url().endsWith('/search-params/loader-throws-redirect?step=a'))
})
Expand All @@ -50,7 +53,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
const response = await page.goto('/search-params/default')
if (!isSpaMode) {
if (!isSpaMode && !isPrerender) {
expectRedirect(response, '/search-params/default?default=d1')
}
await expect(page.getByTestId('search-default')).toContainText('d1')
Expand Down
1 change: 1 addition & 0 deletions e2e/solid-start/basic/tests/utils/isPrerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isPrerender: boolean = process.env.MODE === 'prerender'
16 changes: 16 additions & 0 deletions e2e/solid-start/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import tsConfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
import viteSolid from 'vite-plugin-solid'
import { isSpaMode } from './tests/utils/isSpaMode'
import { isPrerender } from './tests/utils/isPrerender'

const spaModeConfiguration = {
enabled: true,
Expand All @@ -11,6 +12,20 @@ const spaModeConfiguration = {
},
}

const prerenderConfiguration = {
enabled: true,
filter: (page: { path: string }) =>
![
'/this-route-does-not-exist',
'/redirect',
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
'/search-params/default',
].some((p) => page.path.includes(p)),
maxRedirects: 100,
}

export default defineConfig({
server: {
port: 3000,
Expand All @@ -22,6 +37,7 @@ export default defineConfig({
// @ts-ignore we want to keep one test with verboseFileRoutes off even though the option is hidden
tanstackStart({
spa: isSpaMode ? spaModeConfiguration : undefined,
prerender: isPrerender ? prerenderConfiguration : undefined,
}),
viteSolid({ ssr: true }),
],
Expand Down
2 changes: 1 addition & 1 deletion packages/start-plugin-core/src/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,4 @@ export async function writeBundleToDisk({

await fsp.writeFile(fullPath, content)
}
}
}
Loading