|
"use strict"; |
|
|
|
const _ = require('lodash'); |
|
const os = require('os'); |
|
const path = require('path'); |
|
const chalk = require('chalk'); |
|
const debug = require('debug')('cypress:cli'); |
|
const { |
|
Listr |
|
} = require('listr2'); |
|
const Promise = require('bluebird'); |
|
const logSymbols = require('log-symbols'); |
|
const { |
|
stripIndent |
|
} = require('common-tags'); |
|
const fs = require('../fs'); |
|
const download = require('./download'); |
|
const util = require('../util'); |
|
const state = require('./state'); |
|
const unzip = require('./unzip'); |
|
const logger = require('../logger'); |
|
const { |
|
throwFormErrorText, |
|
errors |
|
} = require('../errors'); |
|
const verbose = require('../VerboseRenderer'); |
|
const { |
|
buildInfo, |
|
version |
|
} = require('../../package.json'); |
|
function _getBinaryUrlFromBuildInfo(arch, { |
|
commitSha, |
|
commitBranch |
|
}) { |
|
return `https://cdn.cypress.io/beta/binary/${version}/${os.platform()}-${arch}/${commitBranch}-${commitSha}/cypress.zip`; |
|
} |
|
const alreadyInstalledMsg = () => { |
|
if (!util.isPostInstall()) { |
|
logger.log(stripIndent` |
|
Skipping installation: |
|
|
|
Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway. |
|
`); |
|
} |
|
}; |
|
const displayCompletionMsg = () => { |
|
|
|
if (util.isInstalledGlobally()) { |
|
|
|
logger.log(); |
|
logger.warn(stripIndent` |
|
${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally. |
|
|
|
This will work, but it'\s not recommended. |
|
|
|
The recommended way to install Cypress is as a devDependency per project. |
|
|
|
You should probably run these commands: |
|
|
|
- ${chalk.cyan('npm uninstall -g cypress')} |
|
- ${chalk.cyan('npm install --save-dev cypress')} |
|
`); |
|
return; |
|
} |
|
logger.log(); |
|
logger.log('You can now open Cypress by running:', chalk.cyan(path.join('node_modules', '.bin', 'cypress'), 'open')); |
|
logger.log(); |
|
logger.log(chalk.grey('https://on.cypress.io/installing-cypress')); |
|
logger.log(); |
|
}; |
|
const downloadAndUnzip = ({ |
|
version, |
|
installDir, |
|
downloadDir |
|
}) => { |
|
const progress = { |
|
throttle: 100, |
|
onProgress: null |
|
}; |
|
const downloadDestination = path.join(downloadDir, `cypress-${process.pid}.zip`); |
|
const rendererOptions = getRendererOptions(); |
|
|
|
|
|
logger.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`); |
|
logger.log(); |
|
const tasks = new Listr([{ |
|
options: { |
|
title: util.titleize('Downloading Cypress') |
|
}, |
|
task: (ctx, task) => { |
|
|
|
progress.onProgress = progessify(task, 'Downloading Cypress'); |
|
return download.start({ |
|
version, |
|
downloadDestination, |
|
progress |
|
}).then(redirectVersion => { |
|
if (redirectVersion) version = redirectVersion; |
|
debug(`finished downloading file: ${downloadDestination}`); |
|
}).then(() => { |
|
|
|
util.setTaskTitle(task, util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer); |
|
}); |
|
} |
|
}, unzipTask({ |
|
progress, |
|
zipFilePath: downloadDestination, |
|
installDir, |
|
rendererOptions |
|
}), { |
|
options: { |
|
title: util.titleize('Finishing Installation') |
|
}, |
|
task: (ctx, task) => { |
|
const cleanup = () => { |
|
debug('removing zip file %s', downloadDestination); |
|
return fs.removeAsync(downloadDestination); |
|
}; |
|
return cleanup().then(() => { |
|
debug('finished installation in', installDir); |
|
util.setTaskTitle(task, util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer); |
|
}); |
|
} |
|
}], { |
|
rendererOptions |
|
}); |
|
|
|
|
|
return Promise.resolve(tasks.run()); |
|
}; |
|
const validateOS = () => { |
|
return util.getPlatformInfo().then(platformInfo => { |
|
return platformInfo.match(/(win32-x64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/); |
|
}); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
function getVersionOverride({ |
|
arch, |
|
envVarVersion, |
|
buildInfo |
|
}) { |
|
|
|
if (envVarVersion) { |
|
return envVarVersion; |
|
} |
|
if (buildInfo && !buildInfo.stable) { |
|
logger.log(chalk.yellow(stripIndent` |
|
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress. |
|
|
|
Bugs may be present which do not exist in production builds. |
|
|
|
This build was created from: |
|
* Commit SHA: ${buildInfo.commitSha} |
|
* Commit Branch: ${buildInfo.commitBranch} |
|
* Commit Timestamp: ${buildInfo.commitDate} |
|
`)); |
|
logger.log(); |
|
return _getBinaryUrlFromBuildInfo(arch, buildInfo); |
|
} |
|
} |
|
function getEnvVarVersion() { |
|
if (!util.getEnv('CYPRESS_INSTALL_BINARY')) return; |
|
|
|
|
|
|
|
const trimAndRemoveDoubleQuotes = true; |
|
const envVarVersion = util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes); |
|
debug('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion); |
|
return envVarVersion; |
|
} |
|
const start = async (options = {}) => { |
|
debug('installing with options %j', options); |
|
const envVarVersion = getEnvVarVersion(); |
|
if (envVarVersion === '0') { |
|
debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install'); |
|
logger.log(stripIndent` |
|
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`); |
|
logger.log(); |
|
return; |
|
} |
|
_.defaults(options, { |
|
force: false, |
|
buildInfo |
|
}); |
|
if (util.getEnv('CYPRESS_CACHE_FOLDER')) { |
|
const envCache = util.getEnv('CYPRESS_CACHE_FOLDER'); |
|
logger.log(stripIndent` |
|
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)} |
|
|
|
Previous installs of Cypress may not be found. |
|
`); |
|
logger.log(); |
|
} |
|
const pkgVersion = util.pkgVersion(); |
|
const arch = await util.getRealArch(); |
|
const versionOverride = getVersionOverride({ |
|
arch, |
|
envVarVersion, |
|
buildInfo: options.buildInfo |
|
}); |
|
const versionToInstall = versionOverride || pkgVersion; |
|
debug('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall); |
|
const installDir = state.getVersionDir(pkgVersion, options.buildInfo); |
|
const cacheDir = state.getCacheDir(); |
|
const binaryDir = state.getBinaryDir(pkgVersion); |
|
if (!(await validateOS())) { |
|
return throwFormErrorText(errors.invalidOS)(); |
|
} |
|
await fs.ensureDirAsync(cacheDir).catch({ |
|
code: 'EACCES' |
|
}, err => { |
|
return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent` |
|
Failed to access ${chalk.cyan(cacheDir)}: |
|
|
|
${err.message} |
|
`); |
|
}); |
|
const binaryPkg = await state.getBinaryPkgAsync(binaryDir); |
|
const binaryVersion = await state.getBinaryPkgVersion(binaryPkg); |
|
const shouldInstall = () => { |
|
if (!binaryVersion) { |
|
debug('no binary installed under cli version'); |
|
return true; |
|
} |
|
logger.log(); |
|
logger.log(stripIndent` |
|
Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)} |
|
`); |
|
logger.log(); |
|
if (options.force) { |
|
debug('performing force install over existing binary'); |
|
return true; |
|
} |
|
if (binaryVersion === versionToInstall || !util.isSemver(versionToInstall)) { |
|
|
|
alreadyInstalledMsg(); |
|
return false; |
|
} |
|
return true; |
|
}; |
|
|
|
|
|
if (!shouldInstall()) { |
|
return debug('Not downloading or installing binary'); |
|
} |
|
if (envVarVersion) { |
|
logger.log(chalk.yellow(stripIndent` |
|
${logSymbols.warning} Warning: Forcing a binary version different than the default. |
|
|
|
The CLI expected to install version: ${chalk.green(pkgVersion)} |
|
|
|
Instead we will install version: ${chalk.green(versionToInstall)} |
|
|
|
These versions may not work properly together. |
|
`)); |
|
logger.log(); |
|
} |
|
const getLocalFilePath = async () => { |
|
|
|
if (await fs.pathExistsAsync(versionToInstall)) { |
|
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false; |
|
} |
|
const possibleFile = util.formAbsolutePath(versionToInstall); |
|
debug('checking local file', possibleFile, 'cwd', process.cwd()); |
|
|
|
|
|
|
|
if ((await fs.pathExistsAsync(possibleFile)) && path.extname(possibleFile) === '.zip') { |
|
return possibleFile; |
|
} |
|
return false; |
|
}; |
|
const pathToLocalFile = await getLocalFilePath(); |
|
if (pathToLocalFile) { |
|
const absolutePath = path.resolve(versionToInstall); |
|
debug('found local file at', absolutePath); |
|
debug('skipping download'); |
|
const rendererOptions = getRendererOptions(); |
|
return new Listr([unzipTask({ |
|
progress: { |
|
throttle: 100, |
|
onProgress: null |
|
}, |
|
zipFilePath: absolutePath, |
|
installDir, |
|
rendererOptions |
|
})], { |
|
rendererOptions |
|
}).run(); |
|
} |
|
if (options.force) { |
|
debug('Cypress already installed at', installDir); |
|
debug('but the installation was forced'); |
|
} |
|
debug('preparing to download and unzip version ', versionToInstall, 'to path', installDir); |
|
const downloadDir = os.tmpdir(); |
|
await downloadAndUnzip({ |
|
version: versionToInstall, |
|
installDir, |
|
downloadDir |
|
}); |
|
|
|
|
|
await Promise.delay(1000); |
|
displayCompletionMsg(); |
|
}; |
|
module.exports = { |
|
start, |
|
_getBinaryUrlFromBuildInfo |
|
}; |
|
const unzipTask = ({ |
|
zipFilePath, |
|
installDir, |
|
progress, |
|
rendererOptions |
|
}) => { |
|
return { |
|
options: { |
|
title: util.titleize('Unzipping Cypress') |
|
}, |
|
task: (ctx, task) => { |
|
|
|
progress.onProgress = progessify(task, 'Unzipping Cypress'); |
|
return unzip.start({ |
|
zipFilePath, |
|
installDir, |
|
progress |
|
}).then(() => { |
|
util.setTaskTitle(task, util.titleize(chalk.green('Unzipped Cypress')), rendererOptions.renderer); |
|
}); |
|
} |
|
}; |
|
}; |
|
const progessify = (task, title) => { |
|
|
|
return (percentComplete, remaining) => { |
|
percentComplete = chalk.white(` ${percentComplete}%`); |
|
|
|
|
|
remaining = chalk.gray(`${remaining}s`); |
|
util.setTaskTitle(task, util.titleize(title, percentComplete, remaining), getRendererOptions().renderer); |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
const getRendererOptions = () => { |
|
let renderer = util.isCi() ? verbose : 'default'; |
|
if (logger.logLevel() === 'silent') { |
|
renderer = 'silent'; |
|
} |
|
return { |
|
renderer |
|
}; |
|
}; |