Skip to content

Angular CT: tmp tsconfig race when multiple libraries share path.basename(projectRoot) #33634

@edbzn

Description

@edbzn

Current behavior

When running Angular Component Testing for multiple libraries in parallel (e.g. in an Nx monorepo with nx run-many --target=component-test --parallel=5), specs can randomly fail with missing / mismatched specPattern, missing spec files, or "No specs found" style errors. Retries (retries: { runMode: 1 }) do not help — the same project often fails again on retry, and other runs at the same concurrency silently pick up the wrong config.

The cause is in @cypress/webpack-dev-server's Angular handler:

https://github.com/cypress-io/cypress/blob/develop/npm/webpack-dev-server/src/helpers/angularHandler.ts#L114-L168

const tsConfigPath = path.join(await getTempDir(path.basename(projectRoot)), 'tsconfig.json')
// ...
export async function getTempDir (projectName: string): Promise<string> {
  const cypressTempDir = path.join(tmpdir(), 'cypress-angular-ct', projectName)
  await fs.ensureDir(cypressTempDir)
  return cypressTempDir
}

The temp directory that holds Cypress' generated tsconfig.json is keyed only on path.basename(projectRoot). In a monorepo it is common to have multiple Cypress component-test projects whose project roots share the same basename but live at different paths, for example:

libs/feature-a/feat-shell/
libs/feature-b/feat-shell/
libs/feature-c/feat-shell/

All three resolve to the same temp path:

/tmp/cypress-angular-ct/feat-shell/tsconfig.json

When two such projects run component-test in parallel, generateTsConfig races on that single file. Whichever project writes last wins, so the other project's webpack build loads the wrong include / specPattern / supportFile paths and the spec fails. Because projectRoot / tmp dir resolution is deterministic, Cypress retries reproduce the same collision.

Desired behavior

The temp tsconfig path should be unique per projectRoot, not just per its basename, so that parallel component-test runs of libraries with colliding basenames never share a config file.

A simple, backwards-compatible fix is to suffix the directory name with a short hash of the absolute projectRoot, e.g.:

export async function getTempDir (projectName: string, projectRoot?: string): Promise<string> {
  const uniqueName = projectRoot
    ? `${projectName}-${createHash('sha1').update(projectRoot).digest('hex').slice(0, 8)}`
    : projectName
  const cypressTempDir = path.join(tmpdir(), 'cypress-angular-ct', uniqueName)
  await fs.ensureDir(cypressTempDir)
  return cypressTempDir
}

Keeping projectRoot optional preserves the current public signature for any external callers.

Test code to reproduce

Minimal Nx-style repro:

  1. Create two Angular component-testing projects whose directories share a basename, e.g. libs/feature-a/feat-shell and libs/feature-b/feat-shell, each with its own cypress.config.ts using framework: 'angular', bundler: 'webpack' and a single spec.
  2. Run them in parallel:
    nx run-many --target=component-test --projects=feature-a-feat-shell,feature-b-feat-shell --parallel=2
    
  3. Re-run the command several times. Some runs will fail for one of the two projects with spec-resolution errors while the other passes. Inspecting /tmp/cypress-angular-ct/feat-shell/tsconfig.json during the run shows the include paths alternate between the two projects.

In a real monorepo with ~20 Angular CT projects and --parallel=5, we observed failure rates up to ~16% on individual projects that share a basename (confirmed via Nx Cloud analytics over 2 weeks), while projects with unique basenames had 100% pass rates.

Cypress Version

15.x (@cypress/webpack-dev-server@5.2.1); the bug exists on develop as well.

Node version

20.x

Operating System

Linux (reproduced on CI runners) and macOS.

Debug Logs

# observable in /tmp/cypress-angular-ct/<basename>/tsconfig.json
# which flips between siblings during a parallel run

Other

Happy to send a PR with the hash-based fix + a unit test covering two distinct projectRoots that share a basename.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions