Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ad1266b
feat(integration): switch long-running test apps from dev to build+serve
jacekradko Feb 6, 2026
f15afe6
fix(e2e): fix template build scripts for build+serve mode
jacekradko Feb 6, 2026
00f15ca
fix(e2e): add node adapter to astro-hybrid template for build mode
jacekradko Feb 6, 2026
8ee0693
fix(e2e): assign serve result to outer-scoped variables in longRunnin…
jacekradko Feb 6, 2026
689f475
fix(e2e): use @astrojs/node@8 for astro-hybrid template (astro 4 compat)
jacekradko Feb 6, 2026
f18383e
fix(e2e): add debug logging to long-running app init, remove tsc from…
jacekradko Feb 7, 2026
0378aff
fix(e2e): add build timeout and cap serve polling attempts
jacekradko Feb 7, 2026
ba2ebe6
fix(e2e): add serve log file dump on polling failure for debugging
jacekradko Feb 7, 2026
d8d3001
fix(e2e): add lsof/ps debugging when serve polling fails
jacekradko Feb 7, 2026
7917eea
fix(e2e): check serve process liveness and port binding after 3s
jacekradko Feb 7, 2026
be51c2e
fix(e2e): better serve debug: lsof port check, pstree, 10s wait
jacekradko Feb 7, 2026
d963ae6
Merge remote-tracking branch 'origin/main' into jr/integration-build-…
jacekradko Feb 7, 2026
0c9111d
style: format application.ts
jacekradko Feb 7, 2026
33c0699
refactor(e2e): remove debug instrumentation from serve()
jacekradko Feb 7, 2026
a963ee2
fix(e2e): fix serverUrl port detection using URL constructor
jacekradko Feb 7, 2026
1eaf924
fix(e2e): fix production serve commands for tanstack-start and react-…
jacekradko Feb 8, 2026
5516aa4
fix(e2e): correct tanstack-start production entry to dist/server/serv…
jacekradko Feb 8, 2026
74ba697
fix(e2e): use srvx to serve tanstack-start production build
jacekradko Feb 8, 2026
0f3475d
style: format tanstack-start package.json
jacekradko Feb 8, 2026
7cc55ae
fix(integration): fix srvx static path resolution for tanstack-start
jacekradko Feb 8, 2026
978e231
fix(integration): pass .env vars to serve process
jacekradko Feb 8, 2026
dcec915
Merge branch 'main' into jr/integration-build-serve
jacekradko Feb 9, 2026
b9571bf
Merge branch 'main' into jr/integration-build-serve
jacekradko Feb 9, 2026
11395e2
Merge branch 'main' into jr/integration-build-serve
jacekradko Feb 10, 2026
2fb8426
Merge branch 'main' into jr/integration-build-serve
jacekradko Feb 10, 2026
0407556
Merge branch 'main' into jr/integration-build-serve
jacekradko Feb 11, 2026
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
51 changes: 51 additions & 0 deletions integration/models/__tests__/application.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
93 changes: 73 additions & 20 deletions integration/models/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ import type { EnvironmentConfig } from './environment.js';

export type Application = ReturnType<typeof application>;

/**
* 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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -144,25 +163,59 @@ 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 runtimeServerUrl = resolveServerUrl(opts.serverUrl, serverUrl, port);
log(`Will try to serve app at ${runtimeServerUrl}`);

if (opts.manualStart) {
state.serverUrl = runtimeServerUrl;
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<string, string> = {};
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() },
log: (msg: string) => {
serveOutput += `\n${msg}`;
log(msg);
},
env: { ...envFromFile, PORT: port.toString() },
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...');
Expand Down
37 changes: 30 additions & 7 deletions integration/models/longRunningApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,18 @@ 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');
const apiUrl = params.env.privateVariables.get('CLERK_API_URL');
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,
Expand All @@ -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;
Expand All @@ -94,16 +100,35 @@ 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 {
const { port, serverUrl, pid } = await app.dev({ detached: true });
stateFile.addLongRunningApp({ port, serverUrl, pid, id, appDir: app.appDir, env: params.env.toJson() });
log('Building app...');
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 dev:', 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);
throw error;
}
},
Expand All @@ -126,9 +151,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;
},
Expand Down
6 changes: 5 additions & 1 deletion integration/templates/astro-hybrid/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -15,6 +19,6 @@ export default defineConfig({
react(),
],
server: {
port: Number(process.env.PORT),
port: process.env.PORT ? Number(process.env.PORT) : undefined,
},
});
4 changes: 2 additions & 2 deletions integration/templates/astro-hybrid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"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"
},
"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",
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/astro-node/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export default defineConfig({
tailwind(),
],
server: {
port: Number(process.env.PORT),
port: process.env.PORT ? Number(process.env.PORT) : undefined,
},
});
2 changes: 1 addition & 1 deletion integration/templates/astro-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-router-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-router-node/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export default defineConfig({
}),
],
server: {
port: Number(process.env.PORT),
port: process.env.PORT ? Number(process.env.PORT) : undefined,
},
});
2 changes: 1 addition & 1 deletion integration/templates/react-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions integration/templates/tanstack-react-start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"build": "vite build && tsc --noEmit",
"build": "vite build",
"dev": "vite dev --port=$PORT",
"start": "vite start --port=$PORT"
"start": "srvx --static ../client dist/server/server.js"
},
"dependencies": {
"@tanstack/react-router": "1.157.16",
Expand All @@ -21,6 +21,7 @@
"@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",
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/vue-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion integration/tests/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading