/
githubmirror
/
yarn
Обзор
Документация
Войти
/
githubmirror
/
yarn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/cli/index.js
674 строки
23 KB
Bram Gotink
Spawn yarn .cjs files also using process.execPath (#8144)
25 май 2020, 15:36
Не верифицирован
25 май 2020, 15:36
4df89bc
Код
Авторство
О чём код?
/* @flow */ import http from 'http'; import net from 'net'; import path from 'path'; import commander from 'commander'; import fs from 'fs'; import invariant from 'invariant'; import lockfile from 'proper-lockfile'; import loudRejection from 'loud-rejection'; import onDeath from 'death'; import semver from 'semver'; import {ConsoleReporter, JSONReporter} from '../reporters/index.js'; import {registries, registryNames} from '../registries/index.js'; import commands from './commands/index.js'; import * as constants from '../constants.js'; import * as network from '../util/network.js'; import {MessageError} from '../errors.js'; import Config from '../config.js'; import {getRcConfigForCwd, getRcArgs} from '../rc.js'; import {spawnp, forkp} from '../util/child.js'; import {version} from '../util/yarn-version.js'; import handleSignals from '../util/signal-handler.js'; import {boolify, boolifyWithDefault} from '../util/conversion.js'; import {ProcessTermError} from '../errors'; process.stdout.prependListener('error', err => { // swallow err only if downstream consumer process closed pipe early if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') { return; } throw err; }); function findProjectRoot(base: string): string { let prev = null; let dir = base; do { if (fs.existsSync(path.join(dir, constants.NODE_PACKAGE_JSON))) { return dir; } prev = dir; dir = path.dirname(dir); } while (dir !== prev); return base; } export async function main({ startArgs, args, endArgs, }: { startArgs: Array<string>, args: Array<string>, endArgs: Array<string>, }): Promise<void> { const collect = (val, acc) => { acc.push(val); return acc; }; loudRejection(); handleSignals(); // set global options commander.version(version, '-v, --version'); commander.usage('[command] [flags]'); commander.option('--no-default-rc', 'prevent Yarn from automatically detecting yarnrc and npmrc files'); commander.option( '--use-yarnrc <path>', 'specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc)', collect, [], ); commander.option('--verbose', 'output verbose messages on internal operations'); commander.option('--offline', 'trigger an error if any required dependencies are not available in local cache'); commander.option('--prefer-offline', 'use network only if dependencies are not available in local cache'); commander.option('--enable-pnp, --pnp', "enable the Plug'n'Play installation"); commander.option('--disable-pnp', "disable the Plug'n'Play installation"); commander.option('--strict-semver'); commander.option('--json', 'format Yarn log messages as lines of JSON (see jsonlines.org)'); commander.option('--ignore-scripts', "don't run lifecycle scripts"); commander.option('--har', 'save HAR output of network traffic'); commander.option('--ignore-platform', 'ignore platform checks'); commander.option('--ignore-engines', 'ignore engines check'); commander.option('--ignore-optional', 'ignore optional dependencies'); commander.option('--force', 'install and build packages even if they were built before, overwrite lockfile'); commander.option('--skip-integrity-check', 'run install without checking if node_modules is installed'); commander.option('--check-files', 'install will verify file tree of packages for consistency'); commander.option('--no-bin-links', "don't generate bin links when setting up packages"); commander.option('--flat', 'only allow one version of a package'); commander.option('--prod, --production [prod]', '', boolify); commander.option('--no-lockfile', "don't read or generate a lockfile"); commander.option('--pure-lockfile', "don't generate a lockfile"); commander.option('--frozen-lockfile', "don't generate a lockfile and fail if an update is needed"); commander.option('--update-checksums', 'update package checksums from current repository'); commander.option('--link-duplicates', 'create hardlinks to the repeated modules in node_modules'); commander.option('--link-folder <path>', 'specify a custom folder to store global links'); commander.option('--global-folder <path>', 'specify a custom folder to store global packages'); commander.option( '--modules-folder <path>', 'rather than installing modules into the node_modules folder relative to the cwd, output them here', ); commander.option('--preferred-cache-folder <path>', 'specify a custom folder to store the yarn cache if possible'); commander.option('--cache-folder <path>', 'specify a custom folder that must be used to store the yarn cache'); commander.option('--mutex <type>[:specifier]', 'use a mutex to ensure only one yarn instance is executing'); commander.option( '--emoji [bool]', 'enable emoji in output', boolify, process.platform === 'darwin' || process.env.TERM_PROGRAM === 'Hyper' || process.env.TERM_PROGRAM === 'HyperTerm' || process.env.TERM_PROGRAM === 'Terminus', ); commander.option('-s, --silent', 'skip Yarn console logs, other types of logs (script output) will be printed'); commander.option('--cwd <cwd>', 'working directory to use', process.cwd()); commander.option('--proxy <host>', ''); commander.option('--https-proxy <host>', ''); commander.option('--registry <url>', 'override configuration registry'); commander.option('--no-progress', 'disable progress bar'); commander.option('--network-concurrency <number>', 'maximum number of concurrent network requests', parseInt); commander.option('--network-timeout <milliseconds>', 'TCP timeout for network requests', parseInt); commander.option('--non-interactive', 'do not show interactive prompts'); commander.option( '--scripts-prepend-node-path [bool]', 'prepend the node executable dir to the PATH in scripts', boolify, ); commander.option('--no-node-version-check', 'do not warn when using a potentially unsupported Node version'); commander.option('--focus', 'Focus on a single workspace by installing remote copies of its sibling workspaces.'); commander.option('--otp <otpcode>', 'one-time password for two factor authentication'); // if -v is the first command, then always exit after returning the version if (args[0] === '-v') { console.log(version.trim()); process.exitCode = 0; return; } // get command name const firstNonFlagIndex = args.findIndex((arg, idx, arr) => { const isOption = arg.startsWith('-'); const prev = idx > 0 && arr[idx - 1]; const prevOption = prev && prev.startsWith('-') && commander.optionFor(prev); const boundToPrevOption = prevOption && (prevOption.optional || prevOption.required); return !isOption && !boundToPrevOption; }); let preCommandArgs; let commandName = ''; if (firstNonFlagIndex > -1) { preCommandArgs = args.slice(0, firstNonFlagIndex); commandName = args[firstNonFlagIndex]; args = args.slice(firstNonFlagIndex + 1); } else { preCommandArgs = args; args = []; } let isKnownCommand = Object.prototype.hasOwnProperty.call(commands, commandName); const isHelp = arg => arg === '--help' || arg === '-h'; const helpInPre = preCommandArgs.findIndex(isHelp); const helpInArgs = args.findIndex(isHelp); const setHelpMode = () => { if (isKnownCommand) { args.unshift(commandName); } commandName = 'help'; isKnownCommand = true; }; if (helpInPre > -1) { preCommandArgs.splice(helpInPre); setHelpMode(); } else if (isKnownCommand && helpInArgs === 0) { args.splice(helpInArgs); setHelpMode(); } if (!commandName) { commandName = 'install'; isKnownCommand = true; } if (commandName === ('set': string) && args[0] === 'version') { commandName = ('policies': string); args.splice(0, 1, 'set-version'); isKnownCommand = true; } if (!isKnownCommand) { // if command is not recognized, then set default to `run` args.unshift(commandName); commandName = 'run'; } const command = commands[commandName]; let warnAboutRunDashDash = false; // we are using "yarn <script> -abc", "yarn run <script> -abc", or "yarn node -abc", we want -abc // to be script options, not yarn options // PROXY_COMMANDS is a map of command name to the number of preservedArgs const PROXY_COMMANDS = { run: 1, // yarn run {command} create: 1, // yarn create {project} node: 0, // yarn node workspaces: 1, // yarn workspaces {command} workspace: 2, // yarn workspace {package} {command} }; if (PROXY_COMMANDS.hasOwnProperty(commandName)) { if (endArgs.length === 0) { // $FlowFixMe doesn't like that PROXY_COMMANDS doesn't have keys for all commands. let preservedArgs = PROXY_COMMANDS[commandName]; // If the --into option immediately follows the command (or the script name in the "run/create" // case), we parse them as regular options so that we can cd into them if (args[preservedArgs] === `--into`) { preservedArgs += 2; } endArgs = ['--', ...args.splice(preservedArgs)]; } else { warnAboutRunDashDash = true; } } args = [...preCommandArgs, ...args]; command.setFlags(commander); commander.parse([ ...startArgs, // we use this for https://github.com/tj/commander.js/issues/346, otherwise // it will strip some args that match with any options 'this-arg-will-get-stripped-later', ...getRcArgs(commandName, args), ...args, ]); commander.args = commander.args.concat(endArgs.slice(1)); // we strip cmd console.assert(commander.args.length >= 1); console.assert(commander.args[0] === 'this-arg-will-get-stripped-later'); commander.args.shift(); // const Reporter = commander.json ? JSONReporter : ConsoleReporter; const reporter = new Reporter({ emoji: process.stdout.isTTY && commander.emoji, verbose: commander.verbose, noProgress: !commander.progress, isSilent: boolifyWithDefault(process.env.YARN_SILENT, false) || commander.silent, nonInteractive: commander.nonInteractive, }); const exit = exitCode => { process.exitCode = exitCode || 0; reporter.close(); }; reporter.initPeakMemoryCounter(); const config = new Config(reporter); const outputWrapperEnabled = boolifyWithDefault(process.env.YARN_WRAP_OUTPUT, true); const shouldWrapOutput = outputWrapperEnabled && !commander.json && command.hasWrapper(commander, commander.args); if (shouldWrapOutput) { reporter.header(commandName, {name: 'yarn', version}); } if (commander.nodeVersionCheck && !semver.satisfies(process.versions.node, constants.SUPPORTED_NODE_VERSIONS)) { reporter.warn(reporter.lang('unsupportedNodeVersion', process.versions.node, constants.SUPPORTED_NODE_VERSIONS)); } if (command.noArguments && commander.args.length) { reporter.error(reporter.lang('noArguments')); reporter.info(command.getDocsInfo); exit(1); return; } // if (commander.yes) { reporter.warn(reporter.lang('yesWarning')); } // if (!commander.offline && network.isOffline()) { reporter.warn(reporter.lang('networkWarning')); } // const run = (): Promise<void> => { invariant(command, 'missing command'); if (warnAboutRunDashDash) { reporter.warn(reporter.lang('dashDashDeprecation')); } return command.run(config, reporter, commander, commander.args).then(exitCode => { if (shouldWrapOutput) { reporter.footer(false); } return exitCode; }); }; // const runEventuallyWithFile = (mutexFilename: ?string, isFirstTime?: boolean): Promise<void> => { return new Promise(resolve => { const lockFilename = mutexFilename || path.join(config.cwd, constants.SINGLE_INSTANCE_FILENAME); lockfile.lock(lockFilename, {realpath: false}, (err: mixed, release: (() => void) => void) => { if (err) { if (isFirstTime) { reporter.warn(reporter.lang('waitingInstance')); } setTimeout(() => { resolve(runEventuallyWithFile(mutexFilename, false)); }, 200); // do not starve the CPU } else { onDeath(() => { process.exitCode = 1; }); resolve(run().then(() => new Promise(resolve => release(resolve)))); } }); }); }; const runEventuallyWithNetwork = (mutexPort: ?string): Promise<void> => { return new Promise((resolve, reject) => { const connectionOptions = { port: +mutexPort || constants.SINGLE_INSTANCE_PORT, host: 'localhost', }; function startServer() { const clients = new Set(); const server = http.createServer(manager); // The server must not prevent us from exiting server.unref(); // No socket must timeout, so that they aren't closed before we exit server.timeout = 0; // If we fail to setup the server, we ask the existing one for its name server.on('error', () => { reportServerName(); }); // If we succeed, keep track of all the connected sockets to close them later server.on('connection', socket => { clients.add(socket); socket.on('close', () => { clients.delete(socket); }); }); server.listen(connectionOptions, () => { // Don't forget to kill the sockets if we're being killed via signals onDeath(killSockets); // Also kill the sockets if we finish, whether it's a success or a failure run().then( res => { killSockets(); resolve(res); }, err => { killSockets(); reject(err); }, ); }); function manager(request, response) { response.writeHead(200); response.end(JSON.stringify({cwd: config.cwd, pid: process.pid})); } function killSockets() { try { server.close(); } catch (err) { // best effort } for (const socket of clients) { try { socket.destroy(); } catch (err) { // best effort } } // If the process hasn't exited in the next 5s, it has stalled and we abort const timeout = setTimeout(() => { console.error('Process stalled'); if (process._getActiveHandles) { console.error('Active handles:'); // $FlowFixMe: getActiveHandles is undocumented, but it exists for (const handle of process._getActiveHandles()) { console.error(` - ${handle.constructor.name}`); } } // eslint-disable-next-line no-process-exit process.exit(1); }, 5000); // This timeout must not prevent us from exiting // $FlowFixMe: Node's setTimeout returns a Timeout, not a Number timeout.unref(); } } function reportServerName() { const request = http.get(connectionOptions, response => { const buffers = []; response.on('data', buffer => { buffers.push(buffer); }); response.on('end', () => { try { const {cwd, pid} = JSON.parse(Buffer.concat(buffers).toString()); reporter.warn(reporter.lang('waitingNamedInstance', pid, cwd)); } catch (error) { reporter.verbose(error); reject(new Error(reporter.lang('mutexPortBusy', connectionOptions.port))); return; } waitForTheNetwork(); }); response.on('error', () => { startServer(); }); }); request.on('error', () => { startServer(); }); } function waitForTheNetwork() { const socket = net.createConnection(connectionOptions); socket.on('error', () => { // catch & ignore, the retry is handled in 'close' }); socket.on('close', () => { startServer(); }); } startServer(); }); }; function onUnexpectedError(err: Error) { function indent(str: string): string { return '\n ' + str.trim().split('\n').join('\n '); } const log = []; log.push(`Arguments: ${indent(process.argv.join(' '))}`); log.push(`PATH: ${indent(process.env.PATH || 'undefined')}`); log.push(`Yarn version: ${indent(version)}`); log.push(`Node version: ${indent(process.versions.node)}`); log.push(`Platform: ${indent(process.platform + ' ' + process.arch)}`); log.push(`Trace: ${indent(err.stack)}`); // add manifests for (const registryName of registryNames) { const possibleLoc = path.join(config.cwd, registries[registryName].filename); const manifest = fs.existsSync(possibleLoc) ? fs.readFileSync(possibleLoc, 'utf8') : 'No manifest'; log.push(`${registryName} manifest: ${indent(manifest)}`); } // lockfile const lockLoc = path.join( config.lockfileFolder || config.cwd, // lockfileFolder might not be set at this point constants.LOCKFILE_FILENAME, ); const lockfile = fs.existsSync(lockLoc) ? fs.readFileSync(lockLoc, 'utf8') : 'No lockfile'; log.push(`Lockfile: ${indent(lockfile)}`); const errorReportLoc = writeErrorReport(log); reporter.error(reporter.lang('unexpectedError', err.message)); if (errorReportLoc) { reporter.info(reporter.lang('bugReport', errorReportLoc)); } } function writeErrorReport(log): ?string { const errorReportLoc = config.enableMetaFolder ? path.join(config.cwd, constants.META_FOLDER, 'yarn-error.log') : path.join(config.cwd, 'yarn-error.log'); try { fs.writeFileSync(errorReportLoc, log.join('\n\n') + '\n'); } catch (err) { reporter.error(reporter.lang('fileWriteError', errorReportLoc, err.message)); return undefined; } return errorReportLoc; } const cwd = command.shouldRunInCurrentCwd ? commander.cwd : findProjectRoot(commander.cwd); const folderOptionKeys = ['linkFolder', 'globalFolder', 'preferredCacheFolder', 'cacheFolder', 'modulesFolder']; // Resolve all folder options relative to cwd const resolvedFolderOptions = {}; folderOptionKeys.forEach(folderOptionKey => { const folderOption = commander[folderOptionKey]; const resolvedFolderOption = folderOption ? path.resolve(commander.cwd, folderOption) : folderOption; resolvedFolderOptions[folderOptionKey] = resolvedFolderOption; }); await config .init({ cwd, commandName, ...resolvedFolderOptions, enablePnp: commander.pnp, disablePnp: commander.disablePnp, enableDefaultRc: commander.defaultRc, extraneousYarnrcFiles: commander.useYarnrc, binLinks: commander.binLinks, preferOffline: commander.preferOffline, captureHar: commander.har, ignorePlatform: commander.ignorePlatform, ignoreEngines: commander.ignoreEngines, ignoreScripts: commander.ignoreScripts, offline: commander.preferOffline || commander.offline, looseSemver: !commander.strictSemver, production: commander.production, httpProxy: commander.proxy, httpsProxy: commander.httpsProxy, registry: commander.registry, networkConcurrency: commander.networkConcurrency, networkTimeout: commander.networkTimeout, nonInteractive: commander.nonInteractive, updateChecksums: commander.updateChecksums, focus: commander.focus, otp: commander.otp, }) .then(() => { // lockfile check must happen after config.init sets lockfileFolder if (command.requireLockfile && !fs.existsSync(path.join(config.lockfileFolder, constants.LOCKFILE_FILENAME))) { throw new MessageError(reporter.lang('noRequiredLockfile')); } // option "no-progress" stored in yarn config const noProgressConfig = config.registries.yarn.getOption('no-progress'); if (noProgressConfig) { reporter.disableProgress(); } // verbose logs outputs process.uptime() with this line we can sync uptime to absolute time on the computer reporter.verbose(`current time: ${new Date().toISOString()}`); const mutex: mixed = commander.mutex; if (mutex && typeof mutex === 'string') { const separatorLoc = mutex.indexOf(':'); let mutexType; let mutexSpecifier; if (separatorLoc === -1) { mutexType = mutex; mutexSpecifier = undefined; } else { mutexType = mutex.substring(0, separatorLoc); mutexSpecifier = mutex.substring(separatorLoc + 1); } if (mutexType === 'file') { return runEventuallyWithFile(mutexSpecifier, true).then(exit); } else if (mutexType === 'network') { return runEventuallyWithNetwork(mutexSpecifier).then(exit); } else { throw new MessageError(`Unknown single instance type ${mutexType}`); } } else { return run().then(exit); } }) .catch((err: Error) => { reporter.verbose(err.stack); if (err instanceof ProcessTermError && reporter.isSilent) { return exit(err.EXIT_CODE || 1); } if (err instanceof MessageError) { reporter.error(err.message); } else { onUnexpectedError(err); } if (command.getDocsInfo) { reporter.info(command.getDocsInfo); } if (err instanceof ProcessTermError) { return exit(err.EXIT_CODE || 1); } return exit(1); }); } async function start(): Promise<void> { const rc = getRcConfigForCwd(process.cwd(), process.argv.slice(2)); const yarnPath = rc['yarn-path'] || rc['yarnPath']; if (yarnPath && !boolifyWithDefault(process.env.YARN_IGNORE_PATH, false)) { const argv = process.argv.slice(2); const opts = {stdio: 'inherit', env: Object.assign({}, process.env, {YARN_IGNORE_PATH: 1})}; let exitCode = 0; process.on(`SIGINT`, () => { // We don't want SIGINT to kill our process; we want it to kill the // innermost process, whose end will cause our own to exit. }); try { if (/\.[cm]?js$/.test(yarnPath)) { exitCode = await spawnp(process.execPath, [yarnPath, ...argv], opts); } else { exitCode = await spawnp(yarnPath, argv, opts); } } catch (firstError) { try { exitCode = await forkp(yarnPath, argv, opts); } catch (error) { throw firstError; } } process.exitCode = exitCode; } else { // ignore all arguments after a -- const doubleDashIndex = process.argv.findIndex(element => element === '--'); const startArgs = process.argv.slice(0, 2); const args = process.argv.slice(2, doubleDashIndex === -1 ? process.argv.length : doubleDashIndex); const endArgs = doubleDashIndex === -1 ? [] : process.argv.slice(doubleDashIndex); await main({startArgs, args, endArgs}); } } // When this module is compiled via Webpack, its child // count will be 0 since it is a single-file bundle. export const autoRun = module.children.length === 0; if (require.main === module) { start().catch(error => { console.error(error.stack || error.message || error); process.exitCode = 1; }); } export default start;