Skip to content

esbuild executor runs unnecessary type-check in TS Solution Setup, writes poisoned tsbuildinfo #34492

@wewelll

Description

@wewelll

Current Behavior

When @nx/esbuild:esbuild runs in a TS Solution Setup monorepo with skipTypeCheck: true and declaration: false, the esbuild executor still calls runTypeCheck due to this condition in esbuild.impl.ts:

if (!options.skipTypeCheck || options.isTsSolutionSetup) {

The || options.isTsSolutionSetup clause forces type checking to run for ALL TS Solution Setup projects, regardless of skipTypeCheck. When declaration: false, the type check runs in noEmit mode with ignoreDiagnostics: true (lines 257-259 of esbuild.impl.ts), making it completely pointless — no declarations emitted, no diagnostics reported.

However, this pointless type check has a harmful side effect: TypeScript's program.emit() still writes a minimal 19-byte tsbuildinfo file ({"version":"5.9.3"}) when the project's tsconfig inherits composite: true (which is always true in TS Solution Setup, since isUsingTsSolutionSetup() requires composite: true in tsconfig.base.json).

This causes a race condition when typecheck and build targets run in parallel:

  1. build (esbuild) finishes first → calls runTypeCheck in noEmit mode → writes poisoned 19-byte tsbuildinfo
  2. typecheck (tsc --build) sees the recent tsbuildinfo timestamp → considers project "up-to-date" → skips .d.ts emission
  3. Downstream projects (e.g., tsconfig.spec.json) fail with TS6305 because .d.ts files were never emitted

This is non-deterministic — different projects fail on each run depending on timing. CI may pass because typecheck and build typically run in separate jobs (clean environments), but local development with nx run-many --targets=typecheck,build --parallel fails consistently.

Expected Behavior

When skipTypeCheck: true and declaration: false, the esbuild executor should not run type checking at all in TS Solution Setup. The isTsSolutionSetup override should only force type checking when declarations actually need to be generated.

Root Cause Analysis

The code flow that creates the problem:

  1. normalize.ts (lines 59-77): When user sets declaration: false explicitly, normalization correctly keeps skipTypeCheck: true
  2. esbuild.impl.ts (line 195): || options.isTsSolutionSetup overrides skipTypeCheck, forcing runTypeCheck
  3. esbuild.impl.ts (lines 238-244): Since declaration is false, mode is noEmit
  4. esbuild.impl.ts (lines 257-259): Since isTsSolutionSetup && skipTypeCheck, sets ignoreDiagnostics: true
  5. run-type-check.ts (line 143): Sets noEmit: true but inherits composite: true from tsconfig
  6. run-type-check.ts (line 106): program.emit() writes a 19-byte tsbuildinfo despite noEmit: true (TypeScript behavior with composite: true)

The type check in step 2-5 does nothing useful (no declarations, no diagnostics) — it only produces the harmful tsbuildinfo file.

Proposed Fix (Two-Part)

Primary fix: Skip the unnecessary type check (esbuild.impl.ts)

  // Line 195 (non-watch mode)
- if (!options.skipTypeCheck || options.isTsSolutionSetup) {
+ if (!options.skipTypeCheck || (options.isTsSolutionSetup && options.declaration)) {

  // Lines 139-140 (watch mode)
  if (
    !options.skipTypeCheck ||
-   options.isTsSolutionSetup
+   (options.isTsSolutionSetup && options.declaration)
  ) {

This ensures type checking is only forced in TS Solution Setup when declarations actually need to be generated. When skipTypeCheck: true AND declaration: false, the type check is skipped entirely — which is exactly what the user requested and has no downsides.

Defense-in-depth: Prevent tsbuildinfo writes in noEmit mode (run-type-check.ts)

- : { noEmit: true };
+ : { noEmit: true, composite: false };

Setting composite: false alongside noEmit: true prevents TypeScript from writing tsbuildinfo files even if runTypeCheck is called in noEmit mode from other code paths.

Steps to Reproduce

  1. Create an NX workspace with TS Solution Setup
  2. Set composite: true in root tsconfig.base.json
  3. Configure at least one project with @nx/esbuild:esbuild executor, skipTypeCheck: true, declaration: false
  4. Run nx run-many --targets=typecheck,build --all --parallel=16
  5. Observe non-deterministic TS6305 errors on various projects

Nx Report

Node           : 24.11.1
OS             : darwin-arm64
pnpm           : 10.26.2

nx                     : 22.5.0
@nx/js                 : 22.5.0
@nx/esbuild            : 22.5.0
typescript             : 5.9.3

Failure Logs

error TS6305: Output file '/path/to/project/out-tsc/app/src/domain/SomeEntity.d.ts' has not been built from source file '/path/to/project/src/domain/SomeEntity.ts'.

Multiple TS6305 errors appear across different projects on each run (non-deterministic).

Additional Information

  • We are using a pnpm patch as a workaround with both fixes applied
  • The root cause is that isTsSolutionSetup forces a type check that has no useful output when declaration: false and skipTypeCheck: true

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions