diff --git a/e2e/e2e-utils/package.json b/e2e/e2e-utils/package.json index 3dfbb0b2f..4d3a71a17 100644 --- a/e2e/e2e-utils/package.json +++ b/e2e/e2e-utils/package.json @@ -11,13 +11,15 @@ "./widget": "./src/widget.ts" }, "scripts": { - "typecheck": "tsc -p tsconfig.json --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit", + "deploy": "tsx src/deploy-cli.ts" }, "dependencies": { "@playwright/test": "^1.61.1" }, "devDependencies": { "@types/node": "^26.1.0", + "tsx": "^4.22.4", "typescript": "^6.0.3" } } diff --git a/e2e/e2e-utils/src/config.ts b/e2e/e2e-utils/src/config.ts index 24dff332d..79671c517 100644 --- a/e2e/e2e-utils/src/config.ts +++ b/e2e/e2e-utils/src/config.ts @@ -1,25 +1,33 @@ +import {tmpdir} from 'node:os' +import {join} from 'node:path' import {defineConfig, devices, type ReporterDescription} from '@playwright/test' import {E2E_PORTS, HARNESS_E2E_PORTS, type E2EApp, type HarnessApp} from './ports.js' +const DEFAULT_WEB_SERVER_TIMEOUT = 180_000 + function reporters(): ReporterDescription[] { if (!process.env.GITHUB_ACTIONS) return [['line']] return [['line'], ['json', {outputFile: 'test-results.json'}]] } -function serverEntry(command: string, port: number) { +function serverEntry(command: string, port: number, webServerTimeout: number) { return { command, url: `http://localhost:${port}`, reuseExistingServer: !process.env.CI, stdout: 'pipe' as const, - timeout: 180_000, + timeout: webServerTimeout, env: {CONCIV_E2E: '1'}, } } +export function deployDir(app: E2EApp): string { + return join(tmpdir(), 'conciv-e2e-deploy', app) +} + export function e2eConfig( app: E2EApp, - opts: {command: (port: number) => string; timeout?: number}, + opts: {command: (port: number) => string; timeout?: number; webServerTimeout?: number}, ): ReturnType { const port = E2E_PORTS[app] const timeout = opts.timeout ?? 90_000 @@ -30,7 +38,11 @@ export function e2eConfig( expect: {timeout}, reporter: reporters(), use: {baseURL: `http://localhost:${port}`}, - webServer: serverEntry(`rm -rf .conciv && ${opts.command(port)}`, port), + webServer: serverEntry( + `rm -rf .conciv && ${opts.command(port)}`, + port, + opts.webServerTimeout ?? DEFAULT_WEB_SERVER_TIMEOUT, + ), projects: [{name: 'chromium', use: {...devices['Desktop Chrome']}}], }) } @@ -61,7 +73,7 @@ export function harnessMatrixConfig(opts: { timeout: 150_000, reporter: reporters(), webServer: entries.map(([harness, port]) => - serverEntry(`rm -rf .conciv-${harness} && ${opts.command(harness, port)}`, port), + serverEntry(`rm -rf .conciv-${harness} && ${opts.command(harness, port)}`, port, DEFAULT_WEB_SERVER_TIMEOUT), ), projects: entries.map(([harness, port]) => ({ name: harness, diff --git a/e2e/e2e-utils/src/deploy-cli.ts b/e2e/e2e-utils/src/deploy-cli.ts new file mode 100644 index 000000000..4bb32a5d5 --- /dev/null +++ b/e2e/e2e-utils/src/deploy-cli.ts @@ -0,0 +1,19 @@ +import {deployPackedApp} from './deploy.js' +import {isE2EApp} from './ports.js' + +async function main(): Promise { + const [, , app, pnpmFilter] = process.argv + if (app === undefined || pnpmFilter === undefined) { + throw new Error('usage: deploy-cli.ts ') + } + if (!isE2EApp(app)) { + throw new Error(`"${app}" is not a known e2e app`) + } + const target = await deployPackedApp(app, pnpmFilter) + console.log(`deployed ${pnpmFilter} to ${target}`) +} + +main().catch((error: unknown) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/e2e/e2e-utils/src/deploy.ts b/e2e/e2e-utils/src/deploy.ts new file mode 100644 index 000000000..519d201e1 --- /dev/null +++ b/e2e/e2e-utils/src/deploy.ts @@ -0,0 +1,36 @@ +import {execFile} from 'node:child_process' +import {existsSync, mkdirSync, rmSync} from 'node:fs' +import {dirname, join} from 'node:path' +import {promisify} from 'node:util' +import {deployDir} from './config.js' +import type {E2EApp} from './ports.js' + +const execFileAsync = promisify(execFile) + +function findWorkspaceRoot(startDir: string): string { + let dir = startDir + for (let depth = 0; depth < 12; depth++) { + if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + throw new Error(`workspace root (pnpm-workspace.yaml) not found above ${startDir}`) +} + +function resolveFresh(): boolean { + return process.env.CONCIV_DEPLOY_FRESH === '1' +} + +export async function deployPackedApp(app: E2EApp, pnpmFilter: string): Promise { + const target = deployDir(app) + rmSync(target, {recursive: true, force: true}) + mkdirSync(dirname(target), {recursive: true}) + const resolveFlags = resolveFresh() ? [] : ['--prefer-offline'] + const workspaceRoot = findWorkspaceRoot(process.cwd()) + await execFileAsync('pnpm', ['--filter', pnpmFilter, 'deploy', '--legacy', '--prod=false', ...resolveFlags, target], { + cwd: workspaceRoot, + maxBuffer: 64 * 1024 * 1024, + }) + return target +} diff --git a/e2e/e2e-utils/src/ports.ts b/e2e/e2e-utils/src/ports.ts index 06e9622c9..d5012133c 100644 --- a/e2e/e2e-utils/src/ports.ts +++ b/e2e/e2e-utils/src/ports.ts @@ -16,6 +16,10 @@ export const E2E_PORTS = { export type E2EApp = keyof typeof E2E_PORTS +export function isE2EApp(name: string): name is E2EApp { + return Object.hasOwn(E2E_PORTS, name) +} + export const HARNESS_E2E_PORTS = { claude: 5271, codex: 5272, diff --git a/e2e/nextjs-component/package.json b/e2e/nextjs-component/package.json index fa3af59bb..8d4975847 100644 --- a/e2e/nextjs-component/package.json +++ b/e2e/nextjs-component/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "test:e2e": "playwright test" + "test:e2e": "pnpm --filter @conciv/e2e-utils run deploy nextjs-component conciv-e2e-nextjs-component && playwright test" }, "dependencies": { "@conciv/extension-terminal": "workspace:*", diff --git a/e2e/nextjs-component/playwright.config.ts b/e2e/nextjs-component/playwright.config.ts index 312d39264..07b9fcd97 100644 --- a/e2e/nextjs-component/playwright.config.ts +++ b/e2e/nextjs-component/playwright.config.ts @@ -1,11 +1,6 @@ -import {e2eConfig} from '@conciv/e2e-utils/config' +import {deployDir, e2eConfig} from '@conciv/e2e-utils/config' export default e2eConfig('nextjs-component', { - command: (port) => - [ - 'DEPLOY_DIR="$(mktemp -d)/app"', - `pnpm --filter conciv-e2e-nextjs-component deploy --legacy --prod=false "$DEPLOY_DIR"`, - `cd "$DEPLOY_DIR"`, - `pnpm exec next dev --port ${port}`, - ].join(' && '), + command: (port) => `cd "${deployDir('nextjs-component')}" && pnpm exec next dev --port ${port}`, + webServerTimeout: 60_000, }) diff --git a/e2e/nextjs/package.json b/e2e/nextjs/package.json index c72c8e153..bb585b74e 100644 --- a/e2e/nextjs/package.json +++ b/e2e/nextjs/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "test:e2e": "playwright test", + "test:e2e": "pnpm --filter @conciv/e2e-utils run deploy nextjs conciv-e2e-nextjs && playwright test", "update-packed-lockfile": "tsx packed/update-lockfile.ts" }, "dependencies": { diff --git a/e2e/nextjs/playwright.config.ts b/e2e/nextjs/playwright.config.ts index f9039e67d..5bbc146a5 100644 --- a/e2e/nextjs/playwright.config.ts +++ b/e2e/nextjs/playwright.config.ts @@ -1,11 +1,6 @@ -import {e2eConfig} from '@conciv/e2e-utils/config' +import {deployDir, e2eConfig} from '@conciv/e2e-utils/config' export default e2eConfig('nextjs', { - command: (port) => - [ - 'DEPLOY_DIR="$(mktemp -d)/app"', - `pnpm --filter conciv-e2e-nextjs deploy --legacy --prod=false "$DEPLOY_DIR"`, - `cd "$DEPLOY_DIR"`, - `pnpm exec next dev --port ${port}`, - ].join(' && '), + command: (port) => `cd "${deployDir('nextjs')}" && pnpm exec next dev --port ${port}`, + webServerTimeout: 60_000, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d76ef670b..8770d30e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -755,6 +755,9 @@ importers: '@types/node': specifier: ^26.1.0 version: 26.1.0 + tsx: + specifier: ^4.22.4 + version: 4.22.4 typescript: specifier: ^6.0.3 version: 6.0.3 diff --git a/turbo.json b/turbo.json index 6ee92555a..761ca010c 100644 --- a/turbo.json +++ b/turbo.json @@ -59,7 +59,7 @@ "test:e2e": { "dependsOn": ["^build"], "cache": false, - "env": ["CI", "GITHUB_ACTIONS", "CONCIV_E2E", "VITEST_MAX_FORKS", "VITEST_MAX_WORKERS"] + "env": ["CI", "GITHUB_ACTIONS", "CONCIV_E2E", "CONCIV_DEPLOY_FRESH", "VITEST_MAX_FORKS", "VITEST_MAX_WORKERS"] } } }