Skip to content

Aps 15770 cypress cli better ts support #993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/accessibility-automation/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ exports.createAccessibilityTestRun = async (user_config, framework) => {
logger.debug(`BrowserStack Accessibility Automation Test Run ID: ${response.data.data.id}`);

this.setAccessibilityCypressCapabilities(user_config, response.data);
helper.setBrowserstackCypressCliDependency(user_config);
if(user_config.run_settings.auto_import_dev_dependencies != true) helper.setBrowserstackCypressCliDependency(user_config);

} catch (error) {
if (error.response) {
Expand Down
3 changes: 3 additions & 0 deletions bin/commands/runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ module.exports = function run(args, rawArgs) {
// set the no-wrap
utils.setNoWrap(bsConfig, args);

// process auto-import dev dependencies
utils.processAutoImportDependencies(bsConfig.run_settings);

// add cypress dependency if missing
utils.setCypressNpmDependency(bsConfig);

Expand Down
2 changes: 2 additions & 0 deletions bin/helpers/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const createBuild = (bsConfig, zip) => {
if(error.response) {
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${error.response.data.message}`);
} else {
reject(error);
}
}
}).catch(function(err){
Expand Down
8 changes: 7 additions & 1 deletion bin/helpers/capabilityHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,17 @@ const getAccessibilityPlatforms = (bsConfig) => {
const addCypressZipStartLocation = (runSettings) => {
let resolvedHomeDirectoryPath = path.resolve(runSettings.home_directory);
let resolvedCypressConfigFilePath = path.resolve(runSettings.cypressConfigFilePath);
runSettings.cypressZipStartLocation = path.dirname(resolvedCypressConfigFilePath.split(resolvedHomeDirectoryPath)[1]);

// Convert to POSIX style paths for consistent behavior
let posixHomePath = resolvedHomeDirectoryPath.split(path.sep).join(path.posix.sep);
let posixConfigPath = resolvedCypressConfigFilePath.split(path.sep).join(path.posix.sep);

runSettings.cypressZipStartLocation = path.posix.dirname(posixConfigPath.split(posixHomePath)[1]);
runSettings.cypressZipStartLocation = runSettings.cypressZipStartLocation.substring(1);
logger.debug(`Setting cypress zip start location = ${runSettings.cypressZipStartLocation}`);
}


const validate = (bsConfig, args) => {
return new Promise(function (resolve, reject) {
logger.info(Constants.userMessages.VALIDATING_CONFIG);
Expand Down
22 changes: 22 additions & 0 deletions bin/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,28 @@ const validationMessages = {
"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",
NODE_VERSION_PARSING_ERROR:
"We weren't able to successfully parse the specified nodeVersion. We will be using the default nodeVersion to run your tests.",
AUTO_IMPORT_CONFLICT_ERROR:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, for example someone wants to add any dependency on top of auto import, that won't be allowed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.",
AUTO_IMPORT_INVALID_TYPE:
"'auto_import_dev_dependencies' must be a boolean value (true or false).",
PACKAGE_JSON_NOT_FOUND:
"package.json not found in project directory. Cannot auto-import devDependencies.",
PACKAGE_JSON_PERMISSION_DENIED:
"Cannot read package.json due to permission issues. Please check file permissions.",
PACKAGE_JSON_MALFORMED:
"package.json contains invalid JSON syntax. Please fix the JSON format.",
PACKAGE_JSON_NOT_OBJECT:
"package.json must contain a JSON object, not an array or other type.",
DEVDEPS_INVALID_FORMAT:
"devDependencies field in package.json must be an object, not an array or other type.",
EXCLUDE_DEPS_INVALID_TYPE:
"'exclude_dependencies' must be an array of strings.",
EXCLUDE_DEPS_INVALID_PATTERNS:
"'exclude_dependencies' must contain only string values representing regex patterns.",
INVALID_REGEX_PATTERN:
"Invalid regex pattern found in 'exclude_dependencies': {pattern}. Please provide valid regex patterns.",
DEPENDENCIES_PARAM_INVALID:
"Dependencies parameter must be an object.",
};

const cliMessages = {
Expand Down
5 changes: 3 additions & 2 deletions bin/helpers/packageInstaller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const setupPackageFolder = (runSettings, directoryPath) => {
}

// Combine win and mac specific dependencies if present
if (typeof runSettings.npm_dependencies === 'object') {
const combinedDependencies = combineMacWinNpmDependencies(runSettings);
if (combinedDependencies && Object.keys(combinedDependencies).length > 0) {
Object.assign(packageJSON, {
devDependencies: combineMacWinNpmDependencies(runSettings),
devDependencies: combinedDependencies,
});
}

Expand Down
144 changes: 133 additions & 11 deletions bin/helpers/readCypressConfigUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,120 @@
return constants.CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension) ? extension : 'js'
}

function resolveTsConfigPath(bsConfig, cypress_config_filepath) {
const working_dir = path.dirname(cypress_config_filepath);

// Priority order for finding tsconfig
const candidates = [
bsConfig.run_settings && bsConfig.run_settings.ts_config_file_path, // User specified
path.join(working_dir, 'tsconfig.json'), // Same directory as cypress config
path.join(working_dir, '..', 'tsconfig.json'), // Parent directory
path.join(process.cwd(), 'tsconfig.json') // Project root
].filter(Boolean).map(p => path.resolve(p));

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
logger.debug(`Found tsconfig at: ${candidate}`);
return candidate;
}
}

return null;
}

function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath) {
const working_dir = path.dirname(cypress_config_filepath);
const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc');
const tsc_alias_path = require.resolve('tsc-alias/dist/bin/index.js');

// Smart tsconfig detection and validation
const resolvedTsConfigPath = resolveTsConfigPath(bsConfig, cypress_config_filepath);
let hasValidTsConfig = false;

if (resolvedTsConfigPath) {
try {
// Validate the tsconfig is readable and valid JSON
const tsConfigContent = fs.readFileSync(resolvedTsConfigPath, 'utf8');
JSON.parse(tsConfigContent);
hasValidTsConfig = true;
logger.info(`Using existing tsconfig: ${resolvedTsConfigPath}`);
} catch (error) {
logger.warn(`Invalid tsconfig file: ${resolvedTsConfigPath}, falling back to default configuration. Error: ${error.message}`);
hasValidTsConfig = false;
}
} else {
logger.info('No tsconfig found, using default TypeScript configuration');
}

let tempTsConfig;

if (hasValidTsConfig) {
// Scenario 1: User has valid tsconfig - use extends approach
tempTsConfig = {
extends: resolvedTsConfigPath,
compilerOptions: {
// Force override critical parameters for BrowserStack compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
// Ensure these are always set regardless of base tsconfig
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
include: [cypress_config_filepath]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If cypress config imports other TS files, would those be compiled as well? Like one TS file, importing another and so on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes recursively it will compile, with current master beahviour this is same

};
} else {
// Scenario 2: No tsconfig or invalid tsconfig - create standalone with all basic parameters
tempTsConfig = {
compilerOptions: {
// Preserve old command-line parameters for backwards compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"declaration": false,

// Add essential missing parameters for robust compilation
"target": "es2017",
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": false, // Avoid breaking existing code
"noEmitOnError": false // Continue compilation even with errors
},
include: [cypress_config_filepath],
exclude: ["node_modules", "dist", "build"]
};
}

// Write the temporary tsconfig
const tempTsConfigPath = path.join(working_dir, 'tsconfig.singlefile.tmp.json');
fs.writeFileSync(tempTsConfigPath, JSON.stringify(tempTsConfig, null, 2));
logger.info(`Temporary tsconfig created at: ${tempTsConfigPath}`);

// Platform-specific command generation
const isWindows = /^win/.test(process.platform);

if (isWindows) {
// Windows: Use && to chain commands, no space after SET
const setNodePath = isWindows
? `set NODE_PATH=${bstack_node_modules_path}`
: `NODE_PATH="${bstack_node_modules_path}"`;

const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" && ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep --verbose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes helps in debugging

logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
} else {
// Unix/Linux/macOS: Use ; to separate commands or && to chain
const nodePathPrefix = `NODE_PATH=${bstack_node_modules_path}`;
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" && ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user can start --cli-debug mode and can see the converted files

logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
}
}

exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_modules_path) => {
const cypress_config_filename = bsConfig.run_settings.cypress_config_filename
const working_dir = path.dirname(cypress_config_filepath);
Expand All @@ -22,19 +136,12 @@
}
fs.mkdirSync(complied_js_dir, { recursive: true })

const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc')

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}"`
const { tscCommand, tempTsConfigPath } = generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath);

if (/^win/.test(process.platform)) {
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}"`
}


let tsc_output
try {
logger.debug(`Running: ${tsc_command}`)
tsc_output = cp.execSync(tsc_command, { cwd: working_dir })
logger.debug(`Running: ${tscCommand}`)
tsc_output = cp.execSync(tscCommand, { cwd: working_dir })
} catch (err) {
// error while compiling ts files
logger.debug(err.message);
Expand All @@ -44,6 +151,21 @@
logger.debug(`Saved compiled js output at: ${complied_js_dir}`);
logger.debug(`Finding compiled cypress config file in: ${complied_js_dir}`);

// Clean up the temporary tsconfig file
if (fs.existsSync(tempTsConfigPath)) {
fs.unlinkSync(tempTsConfigPath);
logger.debug(`Temporary tsconfig file removed: ${tempTsConfigPath}`);
}

if (tsc_output) {
logger.debug(tsc_output.toString());
}

if (!tsc_output) {
logger.error('No TypeScript compilation output available');
return null;
}

const lines = tsc_output.toString().split('\n');
let foundLine = null;
for (let i = 0; i < lines.length; i++) {
Expand All @@ -53,7 +175,7 @@
}
}
if (foundLine === null) {
logger.error(`No compiled cypress config found. There might some error running ${tsc_command} command`)
logger.error(`No compiled cypress config found. There might some error running ${tscCommand} command`)
return null
} else {
const compiled_cypress_config_filepath = foundLine.split('TSFILE: ').pop()
Expand Down
Loading
Loading