Skip to content

Commit c7e96f0

Browse files
Merge pull request #993 from browserstack/APS-15770-cypress-cli-better-ts-support
Aps 15770 cypress cli better ts support
2 parents 8801c5f + f26295e commit c7e96f0

26 files changed

+1881
-46
lines changed

bin/accessibility-automation/helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ exports.createAccessibilityTestRun = async (user_config, framework) => {
109109
logger.debug(`BrowserStack Accessibility Automation Test Run ID: ${response.data.data.id}`);
110110

111111
this.setAccessibilityCypressCapabilities(user_config, response.data);
112-
helper.setBrowserstackCypressCliDependency(user_config);
112+
if(user_config.run_settings.auto_import_dev_dependencies != true) helper.setBrowserstackCypressCliDependency(user_config);
113113

114114
} catch (error) {
115115
if (error.response) {

bin/commands/runs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ module.exports = function run(args, rawArgs) {
143143
// set the no-wrap
144144
utils.setNoWrap(bsConfig, args);
145145

146+
// process auto-import dev dependencies
147+
utils.processAutoImportDependencies(bsConfig.run_settings);
148+
146149
// add cypress dependency if missing
147150
utils.setCypressNpmDependency(bsConfig);
148151

bin/helpers/build.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const createBuild = (bsConfig, zip) => {
6565
if(error.response) {
6666
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
6767
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${error.response.data.message}`);
68+
} else {
69+
reject(error);
6870
}
6971
}
7072
}).catch(function(err){

bin/helpers/capabilityHelper.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,17 @@ const getAccessibilityPlatforms = (bsConfig) => {
183183
const addCypressZipStartLocation = (runSettings) => {
184184
let resolvedHomeDirectoryPath = path.resolve(runSettings.home_directory);
185185
let resolvedCypressConfigFilePath = path.resolve(runSettings.cypressConfigFilePath);
186-
runSettings.cypressZipStartLocation = path.dirname(resolvedCypressConfigFilePath.split(resolvedHomeDirectoryPath)[1]);
186+
187+
// Convert to POSIX style paths for consistent behavior
188+
let posixHomePath = resolvedHomeDirectoryPath.split(path.sep).join(path.posix.sep);
189+
let posixConfigPath = resolvedCypressConfigFilePath.split(path.sep).join(path.posix.sep);
190+
191+
runSettings.cypressZipStartLocation = path.posix.dirname(posixConfigPath.split(posixHomePath)[1]);
187192
runSettings.cypressZipStartLocation = runSettings.cypressZipStartLocation.substring(1);
188193
logger.debug(`Setting cypress zip start location = ${runSettings.cypressZipStartLocation}`);
189194
}
190195

196+
191197
const validate = (bsConfig, args) => {
192198
return new Promise(function (resolve, reject) {
193199
logger.info(Constants.userMessages.VALIDATING_CONFIG);

bin/helpers/constants.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ const validationMessages = {
196196
"You have specified '--record' flag but you've not provided the '--record-key' and we could not find any value in 'CYPRESS_RECORD_KEY' environment variable. Your record functionality on cypress.io dashboard might not work as it needs the key and projectId",
197197
NODE_VERSION_PARSING_ERROR:
198198
"We weren't able to successfully parse the specified nodeVersion. We will be using the default nodeVersion to run your tests.",
199+
AUTO_IMPORT_CONFLICT_ERROR:
200+
"Cannot use both 'auto_import_dev_dependencies' and manual npm dependency configuration. Please either set 'auto_import_dev_dependencies' to false or remove manual 'npm_dependencies', 'win_npm_dependencies', and 'mac_npm_dependencies' configurations.",
201+
AUTO_IMPORT_INVALID_TYPE:
202+
"'auto_import_dev_dependencies' must be a boolean value (true or false).",
203+
PACKAGE_JSON_NOT_FOUND:
204+
"package.json not found in project directory. Cannot auto-import devDependencies.",
205+
PACKAGE_JSON_PERMISSION_DENIED:
206+
"Cannot read package.json due to permission issues. Please check file permissions.",
207+
PACKAGE_JSON_MALFORMED:
208+
"package.json contains invalid JSON syntax. Please fix the JSON format.",
209+
PACKAGE_JSON_NOT_OBJECT:
210+
"package.json must contain a JSON object, not an array or other type.",
211+
DEVDEPS_INVALID_FORMAT:
212+
"devDependencies field in package.json must be an object, not an array or other type.",
213+
EXCLUDE_DEPS_INVALID_TYPE:
214+
"'exclude_dependencies' must be an array of strings.",
215+
EXCLUDE_DEPS_INVALID_PATTERNS:
216+
"'exclude_dependencies' must contain only string values representing regex patterns.",
217+
INVALID_REGEX_PATTERN:
218+
"Invalid regex pattern found in 'exclude_dependencies': {pattern}. Please provide valid regex patterns.",
219+
DEPENDENCIES_PARAM_INVALID:
220+
"Dependencies parameter must be an object.",
199221
};
200222

201223
const cliMessages = {

bin/helpers/packageInstaller.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ const setupPackageFolder = (runSettings, directoryPath) => {
3232
}
3333

3434
// Combine win and mac specific dependencies if present
35-
if (typeof runSettings.npm_dependencies === 'object') {
35+
const combinedDependencies = combineMacWinNpmDependencies(runSettings);
36+
if (combinedDependencies && Object.keys(combinedDependencies).length > 0) {
3637
Object.assign(packageJSON, {
37-
devDependencies: combineMacWinNpmDependencies(runSettings),
38+
devDependencies: combinedDependencies,
3839
});
3940
}
4041

bin/helpers/readCypressConfigUtil.js

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,120 @@ exports.detectLanguage = (cypress_config_filename) => {
1313
return constants.CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension) ? extension : 'js'
1414
}
1515

16+
function resolveTsConfigPath(bsConfig, cypress_config_filepath) {
17+
const working_dir = path.dirname(cypress_config_filepath);
18+
19+
// Priority order for finding tsconfig
20+
const candidates = [
21+
bsConfig.run_settings && bsConfig.run_settings.ts_config_file_path, // User specified
22+
path.join(working_dir, 'tsconfig.json'), // Same directory as cypress config
23+
path.join(working_dir, '..', 'tsconfig.json'), // Parent directory
24+
path.join(process.cwd(), 'tsconfig.json') // Project root
25+
].filter(Boolean).map(p => path.resolve(p));
26+
27+
for (const candidate of candidates) {
28+
if (fs.existsSync(candidate)) {
29+
logger.debug(`Found tsconfig at: ${candidate}`);
30+
return candidate;
31+
}
32+
}
33+
34+
return null;
35+
}
36+
37+
function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath) {
38+
const working_dir = path.dirname(cypress_config_filepath);
39+
const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc');
40+
const tsc_alias_path = require.resolve('tsc-alias/dist/bin/index.js');
41+
42+
// Smart tsconfig detection and validation
43+
const resolvedTsConfigPath = resolveTsConfigPath(bsConfig, cypress_config_filepath);
44+
let hasValidTsConfig = false;
45+
46+
if (resolvedTsConfigPath) {
47+
try {
48+
// Validate the tsconfig is readable and valid JSON
49+
const tsConfigContent = fs.readFileSync(resolvedTsConfigPath, 'utf8');
50+
JSON.parse(tsConfigContent);
51+
hasValidTsConfig = true;
52+
logger.info(`Using existing tsconfig: ${resolvedTsConfigPath}`);
53+
} catch (error) {
54+
logger.warn(`Invalid tsconfig file: ${resolvedTsConfigPath}, falling back to default configuration. Error: ${error.message}`);
55+
hasValidTsConfig = false;
56+
}
57+
} else {
58+
logger.info('No tsconfig found, using default TypeScript configuration');
59+
}
60+
61+
let tempTsConfig;
62+
63+
if (hasValidTsConfig) {
64+
// Scenario 1: User has valid tsconfig - use extends approach
65+
tempTsConfig = {
66+
extends: resolvedTsConfigPath,
67+
compilerOptions: {
68+
// Force override critical parameters for BrowserStack compatibility
69+
"outDir": path.basename(complied_js_dir),
70+
"listEmittedFiles": true,
71+
// Ensure these are always set regardless of base tsconfig
72+
"allowSyntheticDefaultImports": true,
73+
"esModuleInterop": true
74+
},
75+
include: [cypress_config_filepath]
76+
};
77+
} else {
78+
// Scenario 2: No tsconfig or invalid tsconfig - create standalone with all basic parameters
79+
tempTsConfig = {
80+
compilerOptions: {
81+
// Preserve old command-line parameters for backwards compatibility
82+
"outDir": path.basename(complied_js_dir),
83+
"listEmittedFiles": true,
84+
"allowSyntheticDefaultImports": true,
85+
"module": "commonjs",
86+
"declaration": false,
87+
88+
// Add essential missing parameters for robust compilation
89+
"target": "es2017",
90+
"moduleResolution": "node",
91+
"esModuleInterop": true,
92+
"allowJs": true,
93+
"skipLibCheck": true,
94+
"forceConsistentCasingInFileNames": true,
95+
"resolveJsonModule": true,
96+
"strict": false, // Avoid breaking existing code
97+
"noEmitOnError": false // Continue compilation even with errors
98+
},
99+
include: [cypress_config_filepath],
100+
exclude: ["node_modules", "dist", "build"]
101+
};
102+
}
103+
104+
// Write the temporary tsconfig
105+
const tempTsConfigPath = path.join(working_dir, 'tsconfig.singlefile.tmp.json');
106+
fs.writeFileSync(tempTsConfigPath, JSON.stringify(tempTsConfig, null, 2));
107+
logger.info(`Temporary tsconfig created at: ${tempTsConfigPath}`);
108+
109+
// Platform-specific command generation
110+
const isWindows = /^win/.test(process.platform);
111+
112+
if (isWindows) {
113+
// Windows: Use && to chain commands, no space after SET
114+
const setNodePath = isWindows
115+
? `set NODE_PATH=${bstack_node_modules_path}`
116+
: `NODE_PATH="${bstack_node_modules_path}"`;
117+
118+
const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" && ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
119+
logger.info(`TypeScript compilation command: ${tscCommand}`);
120+
return { tscCommand, tempTsConfigPath };
121+
} else {
122+
// Unix/Linux/macOS: Use ; to separate commands or && to chain
123+
const nodePathPrefix = `NODE_PATH=${bstack_node_modules_path}`;
124+
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" && ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
125+
logger.info(`TypeScript compilation command: ${tscCommand}`);
126+
return { tscCommand, tempTsConfigPath };
127+
}
128+
}
129+
16130
exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_modules_path) => {
17131
const cypress_config_filename = bsConfig.run_settings.cypress_config_filename
18132
const working_dir = path.dirname(cypress_config_filepath);
@@ -22,19 +136,12 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
22136
}
23137
fs.mkdirSync(complied_js_dir, { recursive: true })
24138

25-
const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc')
26-
27-
let tsc_command = `NODE_PATH=${bstack_node_modules_path} node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`
139+
const { tscCommand, tempTsConfigPath } = generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath);
28140

29-
if (/^win/.test(process.platform)) {
30-
tsc_command = `set NODE_PATH=${bstack_node_modules_path}&& node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`
31-
}
32-
33-
34141
let tsc_output
35142
try {
36-
logger.debug(`Running: ${tsc_command}`)
37-
tsc_output = cp.execSync(tsc_command, { cwd: working_dir })
143+
logger.debug(`Running: ${tscCommand}`)
144+
tsc_output = cp.execSync(tscCommand, { cwd: working_dir })
38145
} catch (err) {
39146
// error while compiling ts files
40147
logger.debug(err.message);
@@ -44,6 +151,21 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
44151
logger.debug(`Saved compiled js output at: ${complied_js_dir}`);
45152
logger.debug(`Finding compiled cypress config file in: ${complied_js_dir}`);
46153

154+
// Clean up the temporary tsconfig file
155+
if (fs.existsSync(tempTsConfigPath)) {
156+
fs.unlinkSync(tempTsConfigPath);
157+
logger.debug(`Temporary tsconfig file removed: ${tempTsConfigPath}`);
158+
}
159+
160+
if (tsc_output) {
161+
logger.debug(tsc_output.toString());
162+
}
163+
164+
if (!tsc_output) {
165+
logger.error('No TypeScript compilation output available');
166+
return null;
167+
}
168+
47169
const lines = tsc_output.toString().split('\n');
48170
let foundLine = null;
49171
for (let i = 0; i < lines.length; i++) {
@@ -53,7 +175,7 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
53175
}
54176
}
55177
if (foundLine === null) {
56-
logger.error(`No compiled cypress config found. There might some error running ${tsc_command} command`)
178+
logger.error(`No compiled cypress config found. There might some error running ${tscCommand} command`)
57179
return null
58180
} else {
59181
const compiled_cypress_config_filepath = foundLine.split('TSFILE: ').pop()

0 commit comments

Comments
 (0)