From ad1266bd5dd8f767de9eda3cf062e47b4bf7ec1c Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 15:52:15 -0600 Subject: [PATCH 01/20] feat(integration): switch long-running test apps from dev to build+serve --- integration/models/application.ts | 47 +++++++++++++++----- integration/models/longRunningApplication.ts | 14 +++--- integration/tests/global.setup.ts | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index fd4a4d00fa6..dc08034d903 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -144,25 +144,48 @@ export const application = ( get serveOutput() { return serveOutput; }, - serve: async (opts: { port?: number; manualStart?: boolean } = {}) => { + serve: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => { const log = logger.child({ prefix: 'serve' }).info; const port = opts.port || (await getPort()); - // TODO: get serverUrl as in dev() - const serverUrl = `http://localhost:${port}`; - // If this is ever used as a background process, we need to make sure - // it's not using the log function. See the dev() method above + const getServerUrl = () => { + if (opts.serverUrl) { + return opts.serverUrl.includes(':') ? opts.serverUrl : `${opts.serverUrl}:${port}`; + } + return serverUrl || `http://localhost:${port}`; + }; + const runtimeServerUrl = getServerUrl(); + log(`Will try to serve app at ${runtimeServerUrl}`); + + if (opts.manualStart) { + state.serverUrl = runtimeServerUrl; + return { port, serverUrl: runtimeServerUrl }; + } + const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() }, - log: (msg: string) => { - serveOutput += `\n${msg}`; - log(msg); - }, + detached: opts.detached, + stdout: opts.detached ? fs.openSync(stdoutFilePath, 'a') : undefined, + stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined, + log: opts.detached + ? undefined + : (msg: string) => { + serveOutput += `\n${msg}`; + log(msg); + }, }); + + if (opts.detached) { + const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; + await waitForServer(runtimeServerUrl, { log, maxAttempts: Infinity, shouldExit }); + } else { + await waitForIdleProcess(proc); + } + + log(`Server started at ${runtimeServerUrl}, pid: ${proc.pid}`); cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL')); - await waitForIdleProcess(proc); - state.serverUrl = serverUrl; - return { port, serverUrl, pid: proc }; + state.serverUrl = runtimeServerUrl; + return { port, serverUrl: runtimeServerUrl, pid: proc.pid }; }, stop: async () => { logger.info('Stopping...'); diff --git a/integration/models/longRunningApplication.ts b/integration/models/longRunningApplication.ts index b7e17ced2df..04a7870d75b 100644 --- a/integration/models/longRunningApplication.ts +++ b/integration/models/longRunningApplication.ts @@ -100,10 +100,16 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => throw error; } try { - const { port, serverUrl, pid } = await app.dev({ detached: true }); + await app.build(); + } catch (error) { + console.error('Error during app build:', error); + throw error; + } + try { + const { port, serverUrl, pid } = await app.serve({ detached: true }); stateFile.addLongRunningApp({ port, serverUrl, pid, id, appDir: app.appDir, env: params.env.toJson() }); } catch (error) { - console.error('Error during app dev:', error); + console.error('Error during app serve:', error); throw error; } }, @@ -126,9 +132,7 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => setup: () => Promise.resolve(), withEnv: () => Promise.resolve(), teardown: () => Promise.resolve(), - build: () => { - throw new Error('build for long running apps is not supported yet'); - }, + build: () => Promise.resolve(), get name() { return name; }, diff --git a/integration/tests/global.setup.ts b/integration/tests/global.setup.ts index 377477b4979..9125fab770d 100644 --- a/integration/tests/global.setup.ts +++ b/integration/tests/global.setup.ts @@ -5,7 +5,7 @@ import { appConfigs } from '../presets'; import { fs, parseEnvOptions, startClerkJsHttpServer, startClerkUiHttpServer } from '../scripts'; setup('start long running apps', async () => { - setup.setTimeout(90_000); + setup.setTimeout(300_000); await fs.ensureDir(constants.TMP_DIR); From f15afe603cb1e6e44650356a7a7f26cdb76efddd Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 16:01:44 -0600 Subject: [PATCH 02/20] fix(e2e): fix template build scripts for build+serve mode --- integration/templates/astro-hybrid/astro.config.mjs | 2 +- integration/templates/astro-hybrid/package.json | 2 +- integration/templates/astro-node/astro.config.mjs | 2 +- integration/templates/astro-node/package.json | 2 +- integration/templates/react-router-node/vite.config.ts | 2 +- integration/templates/react-vite/package.json | 2 +- integration/templates/vue-vite/package.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integration/templates/astro-hybrid/astro.config.mjs b/integration/templates/astro-hybrid/astro.config.mjs index 8568979754d..d64bc0e1f0e 100644 --- a/integration/templates/astro-hybrid/astro.config.mjs +++ b/integration/templates/astro-hybrid/astro.config.mjs @@ -15,6 +15,6 @@ export default defineConfig({ react(), ], server: { - port: Number(process.env.PORT), + port: process.env.PORT ? Number(process.env.PORT) : undefined, }, }); diff --git a/integration/templates/astro-hybrid/package.json b/integration/templates/astro-hybrid/package.json index 0577b94f676..5e4ea50478f 100644 --- a/integration/templates/astro-hybrid/package.json +++ b/integration/templates/astro-hybrid/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "astro": "astro", - "build": "astro check && astro build", + "build": "astro build", "dev": "astro dev", "preview": "astro preview --port $PORT", "start": "astro dev --port $PORT" diff --git a/integration/templates/astro-node/astro.config.mjs b/integration/templates/astro-node/astro.config.mjs index 41e54f926f8..54bd79e7f1c 100644 --- a/integration/templates/astro-node/astro.config.mjs +++ b/integration/templates/astro-node/astro.config.mjs @@ -22,6 +22,6 @@ export default defineConfig({ tailwind(), ], server: { - port: Number(process.env.PORT), + port: process.env.PORT ? Number(process.env.PORT) : undefined, }, }); diff --git a/integration/templates/astro-node/package.json b/integration/templates/astro-node/package.json index d459a625002..9642a60ceac 100644 --- a/integration/templates/astro-node/package.json +++ b/integration/templates/astro-node/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "astro": "astro", - "build": "astro check && astro build", + "build": "astro build", "dev": "astro dev", "preview": "astro preview --port $PORT", "start": "astro dev --port $PORT" diff --git a/integration/templates/react-router-node/vite.config.ts b/integration/templates/react-router-node/vite.config.ts index fb860e8215f..df191826314 100644 --- a/integration/templates/react-router-node/vite.config.ts +++ b/integration/templates/react-router-node/vite.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ }), ], server: { - port: Number(process.env.PORT), + port: process.env.PORT ? Number(process.env.PORT) : undefined, }, }); diff --git a/integration/templates/react-vite/package.json b/integration/templates/react-vite/package.json index 97ace6085d8..9c68b22de69 100644 --- a/integration/templates/react-vite/package.json +++ b/integration/templates/react-vite/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "tsc && vite build", + "build": "vite build", "dev": "vite --port $PORT --no-open", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview --port $PORT --no-open" diff --git a/integration/templates/vue-vite/package.json b/integration/templates/vue-vite/package.json index 98cf8d6d186..c15b18cdcef 100644 --- a/integration/templates/vue-vite/package.json +++ b/integration/templates/vue-vite/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "vue-tsc -b && vite build", + "build": "vite build", "dev": "vite --port $PORT", "preview": "vite preview --port $PORT" }, From 00f15ca2d4cd31cd98ee857f64300fd23dcfbc9e Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 16:07:35 -0600 Subject: [PATCH 03/20] fix(e2e): add node adapter to astro-hybrid template for build mode --- integration/templates/astro-hybrid/astro.config.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration/templates/astro-hybrid/astro.config.mjs b/integration/templates/astro-hybrid/astro.config.mjs index d64bc0e1f0e..9e5347065c2 100644 --- a/integration/templates/astro-hybrid/astro.config.mjs +++ b/integration/templates/astro-hybrid/astro.config.mjs @@ -1,9 +1,13 @@ import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; import clerk from '@clerk/astro'; import react from '@astrojs/react'; export default defineConfig({ output: 'hybrid', + adapter: node({ + mode: 'standalone', + }), integrations: [ clerk({ appearance: { From 8ee06931ac6114d4537157f946ef680b8175a0b9 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 16:12:03 -0600 Subject: [PATCH 04/20] fix(e2e): assign serve result to outer-scoped variables in longRunningApplication --- integration/models/longRunningApplication.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration/models/longRunningApplication.ts b/integration/models/longRunningApplication.ts index 04a7870d75b..62d28bba7ba 100644 --- a/integration/models/longRunningApplication.ts +++ b/integration/models/longRunningApplication.ts @@ -106,8 +106,12 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => throw error; } try { - const { port, serverUrl, pid } = await app.serve({ detached: true }); - stateFile.addLongRunningApp({ port, serverUrl, pid, id, appDir: app.appDir, env: params.env.toJson() }); + const serveResult = await app.serve({ detached: true }); + port = serveResult.port; + serverUrl = serveResult.serverUrl; + pid = serveResult.pid; + appDir = app.appDir; + stateFile.addLongRunningApp({ port, serverUrl, pid, id, appDir, env: params.env.toJson() }); } catch (error) { console.error('Error during app serve:', error); throw error; From 689f475eb6e96072282add271b15cebe32ca0a71 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 16:27:56 -0600 Subject: [PATCH 05/20] fix(e2e): use @astrojs/node@8 for astro-hybrid template (astro 4 compat) --- integration/templates/astro-hybrid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/astro-hybrid/package.json b/integration/templates/astro-hybrid/package.json index 5e4ea50478f..deba74ac54c 100644 --- a/integration/templates/astro-hybrid/package.json +++ b/integration/templates/astro-hybrid/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@astrojs/check": "^0.9.4", - "@astrojs/node": "^9.4.2", + "@astrojs/node": "^8.0.0", "@astrojs/react": "^3.6.2", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", From f18383e6df573bad93f6a8aabb7f90069aa2ee83 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 20:31:12 -0600 Subject: [PATCH 06/20] fix(e2e): add debug logging to long-running app init, remove tsc from tanstack build --- integration/models/longRunningApplication.ts | 14 +++++++++++++- .../templates/tanstack-react-start/package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/integration/models/longRunningApplication.ts b/integration/models/longRunningApplication.ts index 62d28bba7ba..6aa5c2d6320 100644 --- a/integration/models/longRunningApplication.ts +++ b/integration/models/longRunningApplication.ts @@ -59,6 +59,8 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => // will be called by global.setup.ts and by the test runner // the first time this is called, the app starts and the state is persisted in the state file init: async () => { + const log = (msg: string) => console.log(`[${name}] ${msg}`); + log('Starting init...'); try { const publishableKey = params.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); const secretKey = params.env.privateVariables.get('CLERK_SECRET_KEY'); @@ -66,8 +68,9 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => const { instanceType, frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); if (instanceType !== 'development') { - console.log('Clerk: skipping setup of testing tokens for non-development instance'); + log('Skipping setup of testing tokens for non-development instance'); } else { + log('Setting up testing tokens...'); await clerkSetup({ publishableKey, frontendApiUrl, @@ -76,13 +79,16 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => apiUrl, dotenv: false, }); + log('Testing tokens setup complete'); } } catch (error) { console.error('Error setting up testing tokens:', error); throw error; } try { + log('Committing config...'); app = await config.commit(); + log(`Config committed, appDir: ${app.appDir}`); } catch (error) { console.error('Error committing config:', error); throw error; @@ -94,23 +100,29 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => throw error; } try { + log('Running setup (pnpm install)...'); await app.setup(); + log('Setup complete'); } catch (error) { console.error('Error during app setup:', error); throw error; } try { + log('Building app...'); await app.build(); + log('Build complete'); } catch (error) { console.error('Error during app build:', error); throw error; } try { + log('Starting serve (detached)...'); const serveResult = await app.serve({ detached: true }); port = serveResult.port; serverUrl = serveResult.serverUrl; pid = serveResult.pid; appDir = app.appDir; + log(`Serve complete: port=${port}, serverUrl=${serverUrl}, pid=${pid}`); stateFile.addLongRunningApp({ port, serverUrl, pid, id, appDir, env: params.env.toJson() }); } catch (error) { console.error('Error during app serve:', error); diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index 7121e87e555..728207f3c97 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "build": "vite build && tsc --noEmit", + "build": "vite build", "dev": "vite dev --port=$PORT", "start": "vite start --port=$PORT" }, From 0378aff88b0d714b12fde1370552fd621f273534 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 20:58:54 -0600 Subject: [PATCH 07/20] fix(e2e): add build timeout and cap serve polling attempts --- integration/models/application.ts | 2 +- integration/models/longRunningApplication.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index dc08034d903..7ace2eb3ea1 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -177,7 +177,7 @@ export const application = ( if (opts.detached) { const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; - await waitForServer(runtimeServerUrl, { log, maxAttempts: Infinity, shouldExit }); + await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); } else { await waitForIdleProcess(proc); } diff --git a/integration/models/longRunningApplication.ts b/integration/models/longRunningApplication.ts index 6aa5c2d6320..18be6c14204 100644 --- a/integration/models/longRunningApplication.ts +++ b/integration/models/longRunningApplication.ts @@ -109,7 +109,10 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => } try { log('Building app...'); - await app.build(); + const buildTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Build timed out after 120s for ${name}`)), 120_000), + ); + await Promise.race([app.build(), buildTimeout]); log('Build complete'); } catch (error) { console.error('Error during app build:', error); From ba2ebe6a81eadbcec968101ff60a830a5bebd24c Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 21:16:24 -0600 Subject: [PATCH 08/20] fix(e2e): add serve log file dump on polling failure for debugging --- integration/models/application.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index 7ace2eb3ea1..9c20f49b1a0 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -176,8 +176,25 @@ export const application = ( }); if (opts.detached) { + proc.on('exit', (code, signal) => { + log(`Serve process exited: code=${code}, signal=${signal}`); + }); const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; - await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); + try { + await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); + } catch (e) { + // Dump log files for debugging + try { + const stdout = await fs.readFile(stdoutFilePath, 'utf-8'); + const stderr = await fs.readFile(stderrFilePath, 'utf-8'); + log(`Serve stdout log:\n${stdout}`); + log(`Serve stderr log:\n${stderr}`); + } catch { + log('Could not read serve log files'); + } + log(`Serve process exitCode=${proc.exitCode}, killed=${proc.killed}, pid=${proc.pid}`); + throw e; + } } else { await waitForIdleProcess(proc); } From d8d30019b345864d12fcb6603b8462f1066cb25d Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 21:38:10 -0600 Subject: [PATCH 09/20] fix(e2e): add lsof/ps debugging when serve polling fails --- integration/models/application.ts | 39 ++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index 9c20f49b1a0..d3d38920db4 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process'; import * as path from 'node:path'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -161,6 +162,7 @@ export const application = ( return { port, serverUrl: runtimeServerUrl }; } + log(`Running serve command: "${scripts.serve}" with PORT=${port}, detached=${opts.detached}`); const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() }, @@ -174,21 +176,46 @@ export const application = ( log(msg); }, }); + log(`Serve process spawned: pid=${proc.pid}`); if (opts.detached) { proc.on('exit', (code, signal) => { log(`Serve process exited: code=${code}, signal=${signal}`); }); - const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; + const shouldExit = () => { + if (proc.exitCode != null) { + log(`Serve process has exitCode=${proc.exitCode}`); + return true; + } + return false; + }; try { await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); } catch (e) { - // Dump log files for debugging + // Check what the serve process is actually doing + try { + const lsofOut = execSync(`lsof -i -P -n -p ${proc.pid} 2>&1 || true`, { encoding: 'utf-8' }); + log(`lsof for serve pid ${proc.pid}:\n${lsofOut || '(no output)'}`); + } catch { + log(`Could not run lsof for pid ${proc.pid}`); + } + try { + const psOut = execSync('ps aux 2>&1 || true', { encoding: 'utf-8' }); + const serveLines = psOut + .split('\n') + .filter( + l => + l.includes(String(proc.pid)) || l.includes('react-router') || l.includes('vite') || l.includes(String(port)), + ); + log(`Related processes:\n${serveLines.join('\n') || '(none found)'}`); + } catch { + log('Could not run ps'); + } try { - const stdout = await fs.readFile(stdoutFilePath, 'utf-8'); - const stderr = await fs.readFile(stderrFilePath, 'utf-8'); - log(`Serve stdout log:\n${stdout}`); - log(`Serve stderr log:\n${stderr}`); + const stdoutContent = await fs.readFile(stdoutFilePath, 'utf-8'); + const stderrContent = await fs.readFile(stderrFilePath, 'utf-8'); + log(`Serve stdout:\n${stdoutContent || '(empty)'}`); + log(`Serve stderr:\n${stderrContent || '(empty)'}`); } catch { log('Could not read serve log files'); } From 7917eeae7dba0f4e656b5f187141621642112e7d Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 21:56:04 -0600 Subject: [PATCH 10/20] fix(e2e): check serve process liveness and port binding after 3s --- integration/models/application.ts | 35 ++++++++++++------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index d3d38920db4..a674b54ea24 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -179,9 +179,19 @@ export const application = ( log(`Serve process spawned: pid=${proc.pid}`); if (opts.detached) { - proc.on('exit', (code, signal) => { - log(`Serve process exited: code=${code}, signal=${signal}`); - }); + // Give the process a moment to start, then check its state + await new Promise(res => setTimeout(res, 3000)); + try { + const procExists = execSync(`kill -0 ${proc.pid} 2>&1 && echo "alive" || echo "dead"`, { + encoding: 'utf-8', + }).trim(); + log(`Serve process ${proc.pid} status after 3s: ${procExists}`); + const ssOut = execSync(`ss -tlnp 2>&1 | head -20 || true`, { encoding: 'utf-8' }); + log(`Listening ports:\n${ssOut}`); + } catch { + log('Could not check serve process status'); + } + const shouldExit = () => { if (proc.exitCode != null) { log(`Serve process has exitCode=${proc.exitCode}`); @@ -192,25 +202,6 @@ export const application = ( try { await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); } catch (e) { - // Check what the serve process is actually doing - try { - const lsofOut = execSync(`lsof -i -P -n -p ${proc.pid} 2>&1 || true`, { encoding: 'utf-8' }); - log(`lsof for serve pid ${proc.pid}:\n${lsofOut || '(no output)'}`); - } catch { - log(`Could not run lsof for pid ${proc.pid}`); - } - try { - const psOut = execSync('ps aux 2>&1 || true', { encoding: 'utf-8' }); - const serveLines = psOut - .split('\n') - .filter( - l => - l.includes(String(proc.pid)) || l.includes('react-router') || l.includes('vite') || l.includes(String(port)), - ); - log(`Related processes:\n${serveLines.join('\n') || '(none found)'}`); - } catch { - log('Could not run ps'); - } try { const stdoutContent = await fs.readFile(stdoutFilePath, 'utf-8'); const stderrContent = await fs.readFile(stderrFilePath, 'utf-8'); From be51c2ed50efad3bed4916ea05918228e95a884f Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 6 Feb 2026 22:15:09 -0600 Subject: [PATCH 11/20] fix(e2e): better serve debug: lsof port check, pstree, 10s wait --- integration/models/application.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index a674b54ea24..2c4c811a6c0 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -180,16 +180,23 @@ export const application = ( if (opts.detached) { // Give the process a moment to start, then check its state - await new Promise(res => setTimeout(res, 3000)); + await new Promise(res => setTimeout(res, 10000)); try { - const procExists = execSync(`kill -0 ${proc.pid} 2>&1 && echo "alive" || echo "dead"`, { + const procAlive = execSync(`kill -0 ${proc.pid} 2>&1 && echo "alive" || echo "dead"`, { encoding: 'utf-8', }).trim(); - log(`Serve process ${proc.pid} status after 3s: ${procExists}`); - const ssOut = execSync(`ss -tlnp 2>&1 | head -20 || true`, { encoding: 'utf-8' }); - log(`Listening ports:\n${ssOut}`); - } catch { - log('Could not check serve process status'); + log(`Serve process ${proc.pid} status after 10s: ${procAlive}`); + // Check port binding + const portCheck = execSync( + `lsof -i :${port} -P -n 2>&1 || echo "(lsof: no results)"`, + { encoding: 'utf-8' }, + ); + log(`Port ${port} check:\n${portCheck}`); + // Check process tree + const pstree = execSync(`pstree -p ${proc.pid} 2>&1 || echo "(pstree failed)"`, { encoding: 'utf-8' }); + log(`Process tree:\n${pstree}`); + } catch (debugErr) { + log(`Debug check error: ${debugErr}`); } const shouldExit = () => { From 0c9111de6f61005f3e5345d9635ba79f171fa59f Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 14:31:55 -0600 Subject: [PATCH 12/20] style: format application.ts --- integration/models/application.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index 2c4c811a6c0..107bf83c6ac 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -187,10 +187,7 @@ export const application = ( }).trim(); log(`Serve process ${proc.pid} status after 10s: ${procAlive}`); // Check port binding - const portCheck = execSync( - `lsof -i :${port} -P -n 2>&1 || echo "(lsof: no results)"`, - { encoding: 'utf-8' }, - ); + const portCheck = execSync(`lsof -i :${port} -P -n 2>&1 || echo "(lsof: no results)"`, { encoding: 'utf-8' }); log(`Port ${port} check:\n${portCheck}`); // Check process tree const pstree = execSync(`pstree -p ${proc.pid} 2>&1 || echo "(pstree failed)"`, { encoding: 'utf-8' }); From 33c069937f8b3410a359f6ec9a68250a08b3c1a3 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 14:51:08 -0600 Subject: [PATCH 13/20] refactor(e2e): remove debug instrumentation from serve() --- integration/models/application.ts | 43 ++----------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index 107bf83c6ac..dc08034d903 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -1,4 +1,3 @@ -import { execSync } from 'node:child_process'; import * as path from 'node:path'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -162,7 +161,6 @@ export const application = ( return { port, serverUrl: runtimeServerUrl }; } - log(`Running serve command: "${scripts.serve}" with PORT=${port}, detached=${opts.detached}`); const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() }, @@ -176,47 +174,10 @@ export const application = ( log(msg); }, }); - log(`Serve process spawned: pid=${proc.pid}`); if (opts.detached) { - // Give the process a moment to start, then check its state - await new Promise(res => setTimeout(res, 10000)); - try { - const procAlive = execSync(`kill -0 ${proc.pid} 2>&1 && echo "alive" || echo "dead"`, { - encoding: 'utf-8', - }).trim(); - log(`Serve process ${proc.pid} status after 10s: ${procAlive}`); - // Check port binding - const portCheck = execSync(`lsof -i :${port} -P -n 2>&1 || echo "(lsof: no results)"`, { encoding: 'utf-8' }); - log(`Port ${port} check:\n${portCheck}`); - // Check process tree - const pstree = execSync(`pstree -p ${proc.pid} 2>&1 || echo "(pstree failed)"`, { encoding: 'utf-8' }); - log(`Process tree:\n${pstree}`); - } catch (debugErr) { - log(`Debug check error: ${debugErr}`); - } - - const shouldExit = () => { - if (proc.exitCode != null) { - log(`Serve process has exitCode=${proc.exitCode}`); - return true; - } - return false; - }; - try { - await waitForServer(runtimeServerUrl, { log, maxAttempts: 120, shouldExit }); - } catch (e) { - try { - const stdoutContent = await fs.readFile(stdoutFilePath, 'utf-8'); - const stderrContent = await fs.readFile(stderrFilePath, 'utf-8'); - log(`Serve stdout:\n${stdoutContent || '(empty)'}`); - log(`Serve stderr:\n${stderrContent || '(empty)'}`); - } catch { - log('Could not read serve log files'); - } - log(`Serve process exitCode=${proc.exitCode}, killed=${proc.killed}, pid=${proc.pid}`); - throw e; - } + const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0; + await waitForServer(runtimeServerUrl, { log, maxAttempts: Infinity, shouldExit }); } else { await waitForIdleProcess(proc); } From a963ee21ff12f47982d4386e0c134bfb8a131fbc Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 15:12:08 -0600 Subject: [PATCH 14/20] fix(e2e): fix serverUrl port detection using URL constructor The getServerUrl helper used .includes(':') to detect an explicit port, which false-positived on the scheme colon (e.g. http://localhost). Extract resolveServerUrl using the URL constructor and apply to both dev() and serve(). --- .../models/__tests__/application.test.ts | 51 +++++++++++++++++++ integration/models/application.ts | 41 ++++++++++----- 2 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 integration/models/__tests__/application.test.ts diff --git a/integration/models/__tests__/application.test.ts b/integration/models/__tests__/application.test.ts new file mode 100644 index 00000000000..6e2d52d0e2e --- /dev/null +++ b/integration/models/__tests__/application.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveServerUrl } from '../application'; + +describe('resolveServerUrl', () => { + describe('with opts.serverUrl', () => { + it('appends port to a URL without an explicit port', () => { + expect(resolveServerUrl('http://localhost', undefined, 3000)).toBe('http://localhost:3000'); + }); + + it('appends port to an https URL without an explicit port', () => { + expect(resolveServerUrl('https://example.com', undefined, 4000)).toBe('https://example.com:4000'); + }); + + it('preserves an explicit port in the URL', () => { + expect(resolveServerUrl('http://localhost:8080', undefined, 3000)).toBe('http://localhost:8080'); + }); + + it('handles a URL with a path (returns origin only)', () => { + expect(resolveServerUrl('http://localhost/some/path', undefined, 3000)).toBe('http://localhost:3000'); + }); + + it('handles a bare hostname by appending port', () => { + expect(resolveServerUrl('myhost', undefined, 5000)).toBe('myhost:5000'); + }); + + it('handles a bare IP address by appending port', () => { + expect(resolveServerUrl('127.0.0.1', undefined, 5000)).toBe('127.0.0.1:5000'); + }); + }); + + describe('with fallback serverUrl', () => { + it('uses fallback when opts.serverUrl is undefined', () => { + expect(resolveServerUrl(undefined, 'http://fallback:9000', 3000)).toBe('http://fallback:9000'); + }); + + it('prefers opts.serverUrl over fallback', () => { + expect(resolveServerUrl('http://localhost', 'http://fallback:9000', 3000)).toBe('http://localhost:3000'); + }); + }); + + describe('with no serverUrl at all', () => { + it('defaults to http://localhost with the given port', () => { + expect(resolveServerUrl(undefined, undefined, 4567)).toBe('http://localhost:4567'); + }); + + it('defaults when fallback is empty string', () => { + expect(resolveServerUrl(undefined, '', 4567)).toBe('http://localhost:4567'); + }); + }); +}); diff --git a/integration/models/application.ts b/integration/models/application.ts index dc08034d903..c22b492a84b 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -9,6 +9,31 @@ import type { EnvironmentConfig } from './environment.js'; export type Application = ReturnType; +/** + * Resolves the server URL for a dev/serve process, ensuring the runtime port + * is always reflected in the URL. Uses the URL constructor to detect whether + * an explicit port is present (avoiding false positives from the scheme colon). + */ +export const resolveServerUrl = ( + optsServerUrl: string | undefined, + fallbackServerUrl: string | undefined, + port: number, +): string => { + if (optsServerUrl) { + try { + const parsed = new URL(optsServerUrl); + if (!parsed.port) { + parsed.port = String(port); + } + return parsed.origin; + } catch { + // Bare host (e.g. "localhost"), not a full URL + return `${optsServerUrl}:${port}`; + } + } + return fallbackServerUrl || `http://localhost:${port}`; +}; + export const application = ( config: ApplicationConfig, appDirPath: string, @@ -58,13 +83,7 @@ export const application = ( dev: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => { const log = logger.child({ prefix: 'dev' }).info; const port = opts.port || (await getPort()); - const getServerUrl = () => { - if (opts.serverUrl) { - return opts.serverUrl.includes(':') ? opts.serverUrl : `${opts.serverUrl}:${port}`; - } - return serverUrl || `http://localhost:${port}`; - }; - const runtimeServerUrl = getServerUrl(); + const runtimeServerUrl = resolveServerUrl(opts.serverUrl, serverUrl, port); log(`Will try to serve app at ${runtimeServerUrl}`); if (opts.manualStart) { // for debugging, you can start the dev server manually by cd'ing into the temp dir @@ -147,13 +166,7 @@ export const application = ( serve: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => { const log = logger.child({ prefix: 'serve' }).info; const port = opts.port || (await getPort()); - const getServerUrl = () => { - if (opts.serverUrl) { - return opts.serverUrl.includes(':') ? opts.serverUrl : `${opts.serverUrl}:${port}`; - } - return serverUrl || `http://localhost:${port}`; - }; - const runtimeServerUrl = getServerUrl(); + const runtimeServerUrl = resolveServerUrl(opts.serverUrl, serverUrl, port); log(`Will try to serve app at ${runtimeServerUrl}`); if (opts.manualStart) { From 1eaf924a20e72892bc0e1d0024bb601ff5b96168 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 20:22:53 -0600 Subject: [PATCH 15/20] fix(e2e): fix production serve commands for tanstack-start and react-router --- integration/templates/react-router-node/package.json | 2 +- integration/templates/tanstack-react-start/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/templates/react-router-node/package.json b/integration/templates/react-router-node/package.json index 3bcf6de6ba8..5c1d1d77262 100644 --- a/integration/templates/react-router-node/package.json +++ b/integration/templates/react-router-node/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "react-router build", "dev": "react-router dev --port $PORT", - "start": "react-router-serve ./build/server/index.js", + "start": "NODE_ENV=production react-router-serve ./build/server/index.js", "typecheck": "react-router typegen && tsc --build --noEmit" }, "dependencies": { diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index 728207f3c97..e83fc1cea0c 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "vite build", "dev": "vite dev --port=$PORT", - "start": "vite start --port=$PORT" + "start": "node .output/server/index.mjs" }, "dependencies": { "@tanstack/react-router": "1.157.16", From 5516aa4bebc2c67e78902694df75f36103b73ff6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 20:30:52 -0600 Subject: [PATCH 16/20] fix(e2e): correct tanstack-start production entry to dist/server/server.js --- integration/templates/tanstack-react-start/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index e83fc1cea0c..2170b29004e 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "vite build", "dev": "vite dev --port=$PORT", - "start": "node .output/server/index.mjs" + "start": "node dist/server/server.js" }, "dependencies": { "@tanstack/react-router": "1.157.16", From 74ba697a3f17eda826fbb46e82b2b7002e4d2814 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 21:36:08 -0600 Subject: [PATCH 17/20] fix(e2e): use srvx to serve tanstack-start production build --- integration/templates/tanstack-react-start/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index 2170b29004e..8410e566b78 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "vite build", "dev": "vite dev --port=$PORT", - "start": "node dist/server/server.js" + "start": "srvx --static dist/client dist/server/server.js" }, "dependencies": { "@tanstack/react-router": "1.157.16", @@ -24,6 +24,7 @@ "tailwindcss": "^4.0.8", "typescript": "^5.7.2", "vite": "^7.1.7", + "srvx": "^0.11.2", "vite-tsconfig-paths": "^5.1.4" } } From 0f3475d04b66e143d99164a1af7802f6509b2a86 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 21:52:36 -0600 Subject: [PATCH 18/20] style: format tanstack-start package.json --- integration/templates/tanstack-react-start/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index 8410e566b78..ee024d381ef 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -21,10 +21,10 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", + "srvx": "^0.11.2", "tailwindcss": "^4.0.8", "typescript": "^5.7.2", "vite": "^7.1.7", - "srvx": "^0.11.2", "vite-tsconfig-paths": "^5.1.4" } } From 7cc55ae3f1879f656f15665eb3d747d8a69d6239 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 22:24:05 -0600 Subject: [PATCH 19/20] fix(integration): fix srvx static path resolution for tanstack-start srvx resolves --static path relative to the entry file directory (dist/server/), not CWD. Use ../client to correctly resolve to dist/client/ where built assets live. --- integration/templates/tanstack-react-start/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/tanstack-react-start/package.json b/integration/templates/tanstack-react-start/package.json index ee024d381ef..e830bac5bdf 100644 --- a/integration/templates/tanstack-react-start/package.json +++ b/integration/templates/tanstack-react-start/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "vite build", "dev": "vite dev --port=$PORT", - "start": "srvx --static dist/client dist/server/server.js" + "start": "srvx --static ../client dist/server/server.js" }, "dependencies": { "@tanstack/react-router": "1.157.16", From 978e2314e88535110bcf32da599685b086f0e657 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sat, 7 Feb 2026 22:44:30 -0600 Subject: [PATCH 20/20] fix(integration): pass .env vars to serve process Production servers like react-router-serve don't auto-load .env files (unlike Vite dev or srvx CLI). Read the .env file and pass all vars as process env to the serve command so Clerk keys are available at runtime. --- integration/models/application.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index c22b492a84b..d0662225eb7 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -174,9 +174,26 @@ export const application = ( return { port, serverUrl: runtimeServerUrl }; } + // Read .env file and pass as process env vars since production servers + // may not auto-load .env files (e.g., react-router-serve) + const envFromFile: Record = {}; + const envFilePath = path.resolve(appDirPath, '.env'); + if (fs.existsSync(envFilePath)) { + const envContent = fs.readFileSync(envFilePath, 'utf-8'); + for (const line of envContent.split('\n')) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const eqIdx = trimmed.indexOf('='); + if (eqIdx > 0) { + envFromFile[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + } + } + } + const proc = run(scripts.serve, { cwd: appDirPath, - env: { PORT: port.toString() }, + env: { ...envFromFile, PORT: port.toString() }, detached: opts.detached, stdout: opts.detached ? fs.openSync(stdoutFilePath, 'a') : undefined, stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined,