import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; /** * Nx refuses to run a task with `parallelism: false` that depends on a * continuous task. `@nx/playwright/plugin` sets `parallelism: false` whenever * a config has a webServer no inferred Nx task covers (our mocks and the web * backend launch as plain `node` processes), and it infers continuous serve * dependencies only from servers with `reuseExistingServer`, which the * configs enable only when `CI` is unset. A target graph can therefore pass * in CI and still fail locally, so every Playwright target is validated with * Nx's own task-graph checks under both environments. */ const RESULT_PREFIX = 'e2e-task-graphs-result:'; export const MODES = { local: { CI: undefined }, ci: { CI: 'true' }, }; export function playwrightTargets(projectGraph) { return Object.values(projectGraph.nodes) .flatMap((node) => Object.entries(node.data.targets ?? {}) .filter(([, target]) => target.metadata?.technologies?.includes('playwright') ) .map(([target]) => ({ project: node.name, target })) ) .sort((a, b) => `${a.project}:${a.target}`.localeCompare(`${b.project}:${b.target}`) ); } export async function findInvalidTaskGraphs(projectGraph) { const { createTaskGraph } = await import('nx/src/tasks-runner/create-task-graph.js'); const { assertTaskGraphDoesNotContainInvalidTargets } = await import('nx/src/tasks-runner/task-graph-utils.js'); return playwrightTargets(projectGraph).flatMap(({ project, target }) => { try { assertTaskGraphDoesNotContainInvalidTargets( createTaskGraph( projectGraph, {}, [project], [target], undefined, {} ) ); return []; } catch (error) { return [{ task: `${project}:${target}`, message: error.message }]; } }); } function childEnv(mode) { const env = { ...process.env, NX_DAEMON: 'false' }; for (const [name, value] of Object.entries(MODES[mode])) { if (value === undefined) delete env[name]; else env[name] = value; } return env; } async function checkCurrentEnvironment() { const { createProjectGraphAsync } = await import('@nx/devkit'); const projectGraph = await createProjectGraphAsync({ exitOnError: true }); const failures = await findInvalidTaskGraphs(projectGraph); const checked = playwrightTargets(projectGraph).length; console.log(`${RESULT_PREFIX}${JSON.stringify({ checked, failures })}`); } function main() { let failed = false; for (const mode of Object.keys(MODES)) { const output = execFileSync( process.execPath, [fileURLToPath(import.meta.url), '--current-environment'], { env: childEnv(mode), encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], } ); const result = output .split('\n') .find((line) => line.startsWith(RESULT_PREFIX)); const { checked, failures } = JSON.parse( result.slice(RESULT_PREFIX.length) ); if (checked === 0) { failed = true; console.error(`[${mode}] no Playwright targets were inferred`); } for (const { task, message } of failures) { failed = true; console.error( `[${mode}] ${task}\n ${message.replace(/\n/g, '\n ')}` ); } if (failures.length === 0 && checked > 0) { console.log( `[${mode}] ${checked} Playwright task graphs are valid` ); } } process.exitCode = failed ? 1 : 0; } if (process.argv[1] === fileURLToPath(import.meta.url)) { if (process.argv.includes('--current-environment')) { await checkCurrentEnvironment(); } else { main(); } }