Files
cypress/cli/lib/util.js
renovate[bot] d51db04755 chore(deps): update dependency hasha to v5.2.2 (#31880)
* chore(deps): update dependency hasha to v5.2.2

* use hashs instead of copied code

* chore(deps): update dependency hasha to v5.2.2

* move to dep instead of devDep

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jennifer Shehane <shehane.jennifer@gmail.com>
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
2025-06-16 16:58:37 -04:00

572 lines
14 KiB
JavaScript

const _ = require('lodash')
const arch = require('arch')
const os = require('os')
const ospath = require('ospath')
const hasha = require('hasha')
const la = require('lazy-ass')
const is = require('check-more-types')
const tty = require('tty')
const path = require('path')
const isCi = require('ci-info').isCI
const execa = require('execa')
const getos = require('getos')
const chalk = require('chalk')
const Promise = require('bluebird')
const cachedir = require('cachedir')
const logSymbols = require('log-symbols')
const executable = require('executable')
const { stripIndent } = require('common-tags')
const supportsColor = require('supports-color')
const isInstalledGlobally = require('is-installed-globally')
const logger = require('./logger')
const debug = require('debug')('cypress:cli')
const fs = require('./fs')
const pkg = require(path.join(__dirname, '..', 'package.json'))
const issuesUrl = 'https://github.com/cypress-io/cypress/issues'
const getosAsync = Promise.promisify(getos)
/**
* Returns SHA512 of a file
*/
const getFileChecksum = (filename) => {
la(is.unemptyString(filename), 'expected filename', filename)
return hasha.fromFile(filename, { algorithm: 'sha512' })
}
const getFileSize = (filename) => {
la(is.unemptyString(filename), 'expected filename', filename)
return fs.statAsync(filename).get('size')
}
const isBrokenGtkDisplayRe = /Gtk: cannot open display/
const stringify = (val) => {
return _.isObject(val) ? JSON.stringify(val) : val
}
function normalizeModuleOptions (options = {}) {
return _.mapValues(options, stringify)
}
/**
* Returns true if the platform is Linux. We do a lot of different
* stuff on Linux (like Xvfb) and it helps to has readable code
*/
const isLinux = () => {
return os.platform() === 'linux'
}
/**
* If the DISPLAY variable is set incorrectly, when trying to spawn
* Cypress executable we get an error like this:
```
[1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99
```
*/
const isBrokenGtkDisplay = (str) => {
return isBrokenGtkDisplayRe.test(str)
}
const isPossibleLinuxWithIncorrectDisplay = () => {
return isLinux() && process.env.DISPLAY
}
const logBrokenGtkDisplayWarning = () => {
debug('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting Xvfb')
// if we get this error, we are on Linux and DISPLAY is set
logger.warn(stripIndent`
${logSymbols.warning} Warning: Cypress failed to start.
This is likely due to a misconfigured DISPLAY environment variable.
DISPLAY was set to: "${process.env.DISPLAY}"
Cypress will attempt to fix the problem and rerun.
`)
logger.warn()
}
function stdoutLineMatches (expectedLine, stdout) {
const lines = stdout.split('\n').map((val) => val.trim())
return lines.some((line) => line === expectedLine)
}
/**
* Confirms if given value is a valid CYPRESS_INTERNAL_ENV value. Undefined values
* are valid, because the system can set the default one.
*
* @param {string} value
* @example util.isValidCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
*/
function isValidCypressInternalEnvValue (value) {
if (_.isUndefined(value)) {
// will get default value
return true
}
// names of config environments, see "packages/server/config/app.json"
const names = ['development', 'test', 'staging', 'production']
return _.includes(names, value)
}
/**
* Confirms if given value is a non-production CYPRESS_INTERNAL_ENV value.
* Undefined values are valid, because the system can set the default one.
*
* @param {string} value
* @example util.isNonProductionCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
*/
function isNonProductionCypressInternalEnvValue (value) {
return !_.isUndefined(value) && value !== 'production'
}
/**
* Prints NODE_OPTIONS using debug() module, but only
* if DEBUG=cypress... is set
*/
function printNodeOptions (log = debug) {
if (!log.enabled) {
return
}
if (process.env.NODE_OPTIONS) {
log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS)
} else {
log('NODE_OPTIONS is not set')
}
}
/**
* Removes double quote characters
* from the start and end of the given string IF they are both present
*
* @param {string} str Input string
* @returns {string} Trimmed string or the original string if there are no double quotes around it.
* @example
```
dequote('"foo"')
// returns string 'foo'
dequote('foo')
// returns string 'foo'
```
*/
const dequote = (str) => {
la(is.string(str), 'expected a string to remove double quotes', str)
if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') {
return str.substr(1, str.length - 2)
}
return str
}
const parseOpts = (opts) => {
opts = _.pick(opts,
'autoCancelAfterFailures',
'browser',
'cachePath',
'cacheList',
'cacheClear',
'cachePrune',
'ciBuildId',
'ct',
'component',
'config',
'configFile',
'cypressVersion',
'destination',
'detached',
'dev',
'e2e',
'exit',
'env',
'force',
'global',
'group',
'headed',
'headless',
'inspect',
'inspectBrk',
'key',
'path',
'parallel',
'port',
'project',
'quiet',
'reporter',
'reporterOptions',
'record',
'runnerUi',
'runProject',
'spec',
'tag')
if (opts.exit) {
opts = _.omit(opts, 'exit')
}
// some options might be quoted - which leads to unexpected results
// remove double quotes from certain options
const cleanOpts = { ...opts }
const toDequote = ['group', 'ciBuildId']
for (const prop of toDequote) {
if (_.has(opts, prop)) {
cleanOpts[prop] = dequote(opts[prop])
}
}
debug('parsed cli options %o', cleanOpts)
return cleanOpts
}
/**
* Copy of packages/server/lib/browsers/utils.ts
* because we need same functionality in CLI to show the path :(
*/
const getApplicationDataFolder = (...paths) => {
const { env } = process
// allow overriding the app_data folder
let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'
const PRODUCT_NAME = pkg.productName || pkg.name
const OS_DATA_PATH = ospath.data()
const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME)
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`
}
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths)
return p
}
const util = {
normalizeModuleOptions,
parseOpts,
isValidCypressInternalEnvValue,
isNonProductionCypressInternalEnvValue,
printNodeOptions,
isCi () {
return isCi
},
getEnvOverrides (options = {}) {
return _
.chain({})
.extend(util.getEnvColors())
.extend(util.getForceTty())
.omitBy(_.isUndefined) // remove undefined values
.mapValues((value) => { // stringify to 1 or 0
return value ? '1' : '0'
})
.extend(util.getOriginalNodeOptions())
.value()
},
getOriginalNodeOptions () {
const opts = {}
if (process.env.NODE_OPTIONS) {
opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS
}
return opts
},
getForceTty () {
return {
FORCE_STDIN_TTY: util.isTty(process.stdin.fd),
FORCE_STDOUT_TTY: util.isTty(process.stdout.fd),
FORCE_STDERR_TTY: util.isTty(process.stderr.fd),
}
},
getEnvColors () {
const sc = util.supportsColor()
return {
FORCE_COLOR: sc,
DEBUG_COLORS: sc,
MOCHA_COLORS: sc ? true : undefined,
}
},
isTty (fd) {
return tty.isatty(fd)
},
supportsColor () {
// if we've been explicitly told not to support
// color then turn this off
if (process.env.NO_COLOR) {
return false
}
// https://github.com/cypress-io/cypress/issues/1747
// always return true in CI providers
if (process.env.CI) {
return true
}
// ensure that both stdout and stderr support color
return Boolean(supportsColor.stdout) && Boolean(supportsColor.stderr)
},
cwd () {
return process.cwd()
},
pkgBuildInfo () {
return pkg.buildInfo
},
pkgVersion () {
return pkg.version
},
exit (code) {
process.exit(code)
},
logErrorExit1 (err) {
logger.error(err.message)
process.exit(1)
},
dequote,
titleize (...args) {
// prepend first arg with space
// and pad so that all messages line up
args[0] = _.padEnd(` ${args[0]}`, 24)
// get rid of any falsy values
args = _.compact(args)
return chalk.blue(...args)
},
calculateEta (percent, elapsed) {
// returns the number of seconds remaining
// if we're at 100% already just return 0
if (percent === 100) {
return 0
}
// take the percentage and divide by one
// and multiple that against elapsed
// subtracting what's already elapsed
return elapsed * (1 / (percent / 100)) - elapsed
},
convertPercentToPercentage (num) {
// convert a percent with values between 0 and 1
// with decimals, so that it is between 0 and 100
// and has no decimal places
return Math.round(_.isFinite(num) ? (num * 100) : 0)
},
secsRemaining (eta) {
// calculate the seconds reminaing with no decimal places
return (_.isFinite(eta) ? (eta / 1000) : 0).toFixed(0)
},
setTaskTitle (task, title, renderer) {
// only update the renderer title when not running in CI
if (renderer === 'default' && task.title !== title) {
task.title = title
}
},
isInstalledGlobally () {
return isInstalledGlobally
},
isSemver (str) {
return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str)
},
isExecutableAsync (filePath) {
return Promise.resolve(executable(filePath))
},
isLinux,
getOsVersionAsync () {
return Promise.try(() => {
if (isLinux()) {
return getosAsync()
.then((osInfo) => {
return [osInfo.dist, osInfo.release].join(' - ')
})
.catch(() => {
return os.release()
})
}
return os.release()
})
},
async getPlatformInfo () {
const [version, osArch] = await Promise.all([
util.getOsVersionAsync(),
this.getRealArch(),
])
return stripIndent`
Platform: ${os.platform()}-${osArch} (${version})
Cypress Version: ${util.pkgVersion()}
`
},
_cachedArch: undefined,
/**
* Attempt to return the real system arch (not process.arch, which is only the Node binary's arch)
*/
async getRealArch () {
if (this._cachedArch) return this._cachedArch
async function _getRealArch () {
const osPlatform = os.platform()
// eslint-disable-next-line no-restricted-syntax
const osArch = os.arch()
debug('detecting arch %o', { osPlatform, osArch })
if (osArch === 'arm64') return 'arm64'
if (osPlatform === 'darwin') {
// could possibly be x64 node on arm64 darwin, check if we are being translated by Rosetta
// https://stackoverflow.com/a/65347893/3474615
const { stdout } = await execa('sysctl', ['-n', 'sysctl.proc_translated']).catch(() => '')
debug('rosetta check result: %o', { stdout })
if (stdout === '1') return 'arm64'
}
if (osPlatform === 'linux') {
// could possibly be x64 node on arm64 linux, check the "machine hardware name"
// list of names for reference: https://stackoverflow.com/a/45125525/3474615
const { stdout } = await execa('uname', ['-m']).catch(() => '')
debug('arm uname -m result: %o ', { stdout })
if (['aarch64_be', 'aarch64', 'armv8b', 'armv8l'].includes(stdout)) return 'arm64'
}
// eslint-disable-next-line no-restricted-syntax
const pkgArch = arch()
if (pkgArch === 'x86') return 'ia32'
return pkgArch
}
return (this._cachedArch = await _getRealArch())
},
// attention:
// when passing relative path to NPM post install hook, the current working
// directory is set to the `node_modules/cypress` folder
// the user is probably passing relative path with respect to root package folder
formAbsolutePath (filename) {
if (path.isAbsolute(filename)) {
return filename
}
return path.join(process.cwd(), '..', '..', filename)
},
getEnv (varName, trim) {
la(is.unemptyString(varName), 'expected environment variable name, not', varName)
const configVarName = `npm_config_${varName}`
const configVarNameLower = configVarName.toLowerCase()
const packageConfigVarName = `npm_package_config_${varName}`
let result
if (process.env.hasOwnProperty(varName)) {
debug(`Using ${varName} from environment variable`)
result = process.env[varName]
} else if (process.env.hasOwnProperty(configVarName)) {
debug(`Using ${varName} from npm config`)
result = process.env[configVarName]
} else if (process.env.hasOwnProperty(configVarNameLower)) {
debug(`Using ${varName.toLowerCase()} from npm config`)
result = process.env[configVarNameLower]
} else if (process.env.hasOwnProperty(packageConfigVarName)) {
debug(`Using ${varName} from package.json config`)
result = process.env[packageConfigVarName]
}
// environment variables are often set double quotes to escape characters
// and on Windows it can lead to weird things: for example
// set FOO="C:\foo.txt" && node -e "console.log('>>>%s<<<', process.env.FOO)"
// will print
// >>>"C:\foo.txt" <<<
// see https://github.com/cypress-io/cypress/issues/4506#issuecomment-506029942
// so for sanity sake we should first trim whitespace characters and remove
// double quotes around environment strings if the caller is expected to
// use this environment string as a file path
return trim ? dequote(_.trim(result)) : result
},
getCacheDir () {
return cachedir('Cypress')
},
isPostInstall () {
return process.env.npm_lifecycle_event === 'postinstall'
},
exec: execa,
stdoutLineMatches,
issuesUrl,
isBrokenGtkDisplay,
logBrokenGtkDisplayWarning,
isPossibleLinuxWithIncorrectDisplay,
getGitHubIssueUrl (number) {
la(is.positive(number), 'github issue should be a positive number', number)
la(_.isInteger(number), 'github issue should be an integer', number)
return `${issuesUrl}/${number}`
},
getFileChecksum,
getFileSize,
getApplicationDataFolder,
}
module.exports = util