Skip to content

Commit 1314ebb

Browse files
Bhargavi-BSclaude
andcommitted
fix(config): robust TS cypress-config compile in NX monorepos (SDK-6463)
When the cypress config is TypeScript and lives in an NX/monorepo, the extends temp tsconfig inherited base options (noEmit / emitDeclarationOnly / composite / noEmitOnError) that suppress or redirect the compiled JS, and any tsc type-error short-circuited the '&&'-chained tsc-alias so path aliases were left un-rewritten. The compiled config then could not be found/required, the error was silently swallowed, and getNumberOfSpecFiles fell back to a default glob that found 0 specs -> setParallels collapsed parallels to 1. Force emit-friendly overrides in the extends temp tsconfig and run tsc-alias unconditionally (& / ; instead of &&). Adds regression tests covering the emit-override and unconditional-alias behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4048be3 commit 1314ebb

2 files changed

Lines changed: 78 additions & 5 deletions

File tree

bin/helpers/readCypressConfigUtil.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,20 @@ function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, c
9090
"listEmittedFiles": true,
9191
// Ensure these are always set regardless of base tsconfig
9292
"allowSyntheticDefaultImports": true,
93-
"esModuleInterop": true
93+
"esModuleInterop": true,
94+
// Force a clean, self-contained JS emit even when the extended tsconfig
95+
// (common in NX / monorepo setups) sets options that suppress or redirect
96+
// the JS output. Without these overrides, base options such as
97+
// noEmit / emitDeclarationOnly / composite / noEmitOnError leave the
98+
// compiled cypress config missing, surfacing as
99+
// "Cypress config file not found at: ...tmpBstackCompiledJs/..." (SDK-6463).
100+
"noEmit": false,
101+
"emitDeclarationOnly": false,
102+
"composite": false,
103+
"declaration": false,
104+
"declarationMap": false,
105+
"noEmitOnError": false,
106+
"incremental": false
94107
},
95108
include: [cypress_config_filepath]
96109
};
@@ -135,13 +148,25 @@ function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, c
135148
? `set NODE_PATH=${bstack_node_modules_path}`
136149
: `NODE_PATH="${bstack_node_modules_path}"`;
137150

138-
const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" && ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
151+
// Use '&' (unconditional) instead of '&&' between tsc and tsc-alias so the alias
152+
// rewrite ALWAYS runs even when tsc exits non-zero. tsc returns a non-zero exit
153+
// code on any type error (very common when a single config file is compiled out of
154+
// its normal monorepo project context), which with '&&' would skip tsc-alias and
155+
// leave path aliases (e.g. @org/lib) un-rewritten -> the compiled config fails to
156+
// require -> "Cypress config file not found" (SDK-6463). convertTsConfig already
157+
// tolerates tsc errors by parsing the emitted-files output.
158+
const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" & ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
139159
logger.info(`TypeScript compilation command: ${tscCommand}`);
140160
return { tscCommand, tempTsConfigPath };
141161
} else {
142-
// Unix/Linux/macOS: Use ; to separate commands or && to chain
162+
// Unix/Linux/macOS: Use ';' (unconditional) between tsc and tsc-alias so the alias
163+
// rewrite ALWAYS runs even when tsc exits non-zero (type errors are common when a
164+
// single config file is compiled out of its monorepo context). With '&&', a tsc
165+
// error would skip tsc-alias and leave path aliases (e.g. @org/lib) un-rewritten,
166+
// making the compiled config impossible to require (SDK-6463). convertTsConfig
167+
// already tolerates tsc errors by parsing the emitted-files output.
143168
const nodePathPrefix = `NODE_PATH=${bstack_node_modules_path}`;
144-
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" && ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
169+
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" ; ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
145170
logger.info(`TypeScript compilation command: ${tscCommand}`);
146171
return { tscCommand, tempTsConfigPath };
147172
}

test/unit/bin/helpers/readCypressConfigUtil.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,58 @@ describe("readCypressConfigUtil", () => {
304304
const writeFileSyncStub = sandbox.stub(fs, 'writeFileSync');
305305

306306
const result = generateTscCommandAndTempTsConfig(bsConfig, 'path/to/tmpBstackPackages', 'path/to/tmpBstackCompiledJs', 'path/to/cypress.config.ts');
307-
307+
308308
expect(result.tscCommand).to.include('NODE_PATH=path/to/tmpBstackPackages');
309309
expect(result.tscCommand).to.include('tsc-alias');
310310
});
311+
312+
// SDK-6463: NX/monorepo base tsconfigs can set noEmit/emitDeclarationOnly/composite/
313+
// noEmitOnError, which suppress or redirect the compiled cypress config JS and break
314+
// the read. The extends temp tsconfig must force a clean self-contained JS emit.
315+
it('should force emit-friendly compilerOptions overrides in extends approach (SDK-6463)', () => {
316+
const bsConfig = { run_settings: { ts_config_file_path: 'existing/tsconfig.json' } };
317+
const existsSyncStub = sandbox.stub(fs, 'existsSync');
318+
existsSyncStub.withArgs(path.resolve('existing/tsconfig.json')).returns(true);
319+
sandbox.stub(fs, 'readFileSync').returns('{}');
320+
const writeFileSyncStub = sandbox.stub(fs, 'writeFileSync');
321+
322+
generateTscCommandAndTempTsConfig(bsConfig, 'path/to/tmpBstackPackages', 'path/to/tmpBstackCompiledJs', 'path/to/cypress.config.ts');
323+
324+
const tempConfig = JSON.parse(writeFileSyncStub.getCall(0).args[1]);
325+
expect(tempConfig.extends).to.eql(path.resolve('existing/tsconfig.json'));
326+
expect(tempConfig.compilerOptions.noEmit).to.be.false;
327+
expect(tempConfig.compilerOptions.emitDeclarationOnly).to.be.false;
328+
expect(tempConfig.compilerOptions.composite).to.be.false;
329+
expect(tempConfig.compilerOptions.noEmitOnError).to.be.false;
330+
expect(tempConfig.compilerOptions.declaration).to.be.false;
331+
});
332+
333+
// SDK-6463: tsc returns a non-zero exit code on any type error (common when a single
334+
// config file is compiled out of its monorepo context). With '&&', tsc-alias would be
335+
// skipped and path aliases left un-rewritten. tsc-alias must run unconditionally.
336+
it('should run tsc-alias unconditionally on Unix (";" not "&&") (SDK-6463)', () => {
337+
sinon.stub(process, 'platform').value('linux');
338+
const bsConfig = { run_settings: {} };
339+
sandbox.stub(fs, 'existsSync').returns(false);
340+
sandbox.stub(fs, 'writeFileSync');
341+
342+
const result = generateTscCommandAndTempTsConfig(bsConfig, 'path/to/tmpBstackPackages', 'path/to/tmpBstackCompiledJs', 'path/to/cypress.config.ts');
343+
344+
expect(result.tscCommand).to.not.include('&&');
345+
expect(result.tscCommand).to.match(/--project "[^"]*" ; NODE_PATH=/);
346+
});
347+
348+
it('should run tsc-alias unconditionally on Windows ("&" between tsc and tsc-alias) (SDK-6463)', () => {
349+
sinon.stub(process, 'platform').value('win32');
350+
const bsConfig = { run_settings: {} };
351+
sandbox.stub(fs, 'existsSync').returns(false);
352+
sandbox.stub(fs, 'writeFileSync');
353+
354+
const result = generateTscCommandAndTempTsConfig(bsConfig, 'path/to/tmpBstackPackages', 'path/to/tmpBstackCompiledJs', 'path/to/cypress.config.ts');
355+
356+
// unconditional '&' connects the tsc invocation to the tsc-alias invocation
357+
expect(result.tscCommand).to.match(/--project "[^"]*" & set NODE_PATH=/);
358+
});
311359
});
312360

313361
describe('convertTsConfig', () => {

0 commit comments

Comments
 (0)