/
githubmirror
/
yarn
Обзор
Документация
Войти
/
githubmirror
/
yarn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/util/execute-lifecycle-script.js
372 строки
11 KB
Eric Amodio
Adds vscode keys - since they can be large (#5420) (#7419)
23 июл 2019, 20:34
23 июл 2019, 20:34
a3b1294
Код
Авторство
О чём код?
/* @flow */ import type Config from '../config.js'; import {MessageError, ProcessTermError} from '../errors.js'; import * as constants from '../constants.js'; import * as child from './child.js'; import * as fs from './fs.js'; import {dynamicRequire} from './dynamic-require.js'; import {makePortableProxyScript} from './portable-script.js'; import {fixCmdWinSlashes} from './fix-cmd-win-slashes.js'; import {getBinFolder as getGlobalBinFolder, run as globalRun} from '../cli/commands/global.js'; const path = require('path'); export type LifecycleReturn = Promise<{ cwd: string, command: string, stdout: string, }>; export const IGNORE_MANIFEST_KEYS: Set<string> = new Set([ 'readme', 'notice', 'licenseText', 'activationEvents', 'contributes', ]); // We treat these configs as internal, thus not expose them to process.env. // This helps us avoid some gyp issues when building native modules. // See https://github.com/yarnpkg/yarn/issues/2286. const IGNORE_CONFIG_KEYS = ['lastUpdateCheck']; let wrappersFolder = null; export async function getWrappersFolder(config: Config): Promise<string> { if (wrappersFolder) { return wrappersFolder; } wrappersFolder = await fs.makeTempDir(); await makePortableProxyScript(process.execPath, wrappersFolder, { proxyBasename: 'node', }); await makePortableProxyScript(process.execPath, wrappersFolder, { proxyBasename: 'yarn', prependArguments: [process.argv[1]], }); return wrappersFolder; } const INVALID_CHAR_REGEX = /\W/g; export async function makeEnv( stage: string, cwd: string, config: Config, ): { [key: string]: string, } { const env = { NODE: process.execPath, INIT_CWD: process.cwd(), // This lets `process.env.NODE` to override our `process.execPath`. // This is a bit confusing but it is how `npm` was designed so we // try to be compatible with that. ...process.env, }; // Merge in the `env` object specified in .yarnrc const customEnv = config.getOption('env'); if (customEnv && typeof customEnv === 'object') { Object.assign(env, customEnv); } env.npm_lifecycle_event = stage; env.npm_node_execpath = env.NODE; env.npm_execpath = env.npm_execpath || (process.mainModule && process.mainModule.filename); // Set the env to production for npm compat if production mode. // https://github.com/npm/npm/blob/30d75e738b9cb7a6a3f9b50e971adcbe63458ed3/lib/utils/lifecycle.js#L336 if (config.production) { env.NODE_ENV = 'production'; } // Note: npm_config_argv environment variable contains output of nopt - command-line // parser used by npm. Since we use other parser, we just roughly emulate it's output. (See: #684) env.npm_config_argv = JSON.stringify({ remain: [], cooked: config.commandName === 'run' ? [config.commandName, stage] : [config.commandName], original: process.argv.slice(2), }); const manifest = await config.maybeReadManifest(cwd); if (manifest) { if (manifest.scripts && Object.prototype.hasOwnProperty.call(manifest.scripts, stage)) { env.npm_lifecycle_script = manifest.scripts[stage]; } // add npm_package_* const queue = [['', manifest]]; while (queue.length) { const [key, val] = queue.pop(); if (typeof val === 'object') { for (const subKey in val) { const fullKey = [key, subKey].filter(Boolean).join('_'); if (fullKey && fullKey[0] !== '_' && !IGNORE_MANIFEST_KEYS.has(fullKey)) { queue.push([fullKey, val[subKey]]); } } } else { let cleanVal = String(val); if (cleanVal.indexOf('\n') >= 0) { cleanVal = JSON.stringify(cleanVal); } //replacing invalid chars with underscore const cleanKey = key.replace(INVALID_CHAR_REGEX, '_'); env[`npm_package_${cleanKey}`] = cleanVal; } } } // add npm_config_* and npm_package_config_* from yarn config const keys: Set<string> = new Set([ ...Object.keys(config.registries.yarn.config), ...Object.keys(config.registries.npm.config), ]); const cleaned = Array.from(keys) .filter(key => !key.match(/:_/) && IGNORE_CONFIG_KEYS.indexOf(key) === -1) .map(key => { let val = config.getOption(key); if (!val) { val = ''; } else if (typeof val === 'number') { val = '' + val; } else if (typeof val !== 'string') { val = JSON.stringify(val); } if (val.indexOf('\n') >= 0) { val = JSON.stringify(val); } return [key, val]; }); // add npm_config_* for (const [key, val] of cleaned) { const cleanKey = key.replace(/^_+/, ''); const envKey = `npm_config_${cleanKey}`.replace(INVALID_CHAR_REGEX, '_'); env[envKey] = val; } // add npm_package_config_* if (manifest && manifest.name) { const packageConfigPrefix = `${manifest.name}:`; for (const [key, val] of cleaned) { if (key.indexOf(packageConfigPrefix) !== 0) { continue; } const cleanKey = key.replace(/^_+/, '').replace(packageConfigPrefix, ''); const envKey = `npm_package_config_${cleanKey}`.replace(INVALID_CHAR_REGEX, '_'); env[envKey] = val; } } // split up the path const envPath = env[constants.ENV_PATH_KEY]; const pathParts = envPath ? envPath.split(path.delimiter) : []; // Include node-gyp version that was bundled with the current Node.js version, // if available. pathParts.unshift(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'node-gyp-bin')); pathParts.unshift( path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'node-gyp-bin'), ); // Include node-gyp version from homebrew managed npm, if available. pathParts.unshift( path.join(path.dirname(process.execPath), '..', 'libexec', 'lib', 'node_modules', 'npm', 'bin', 'node-gyp-bin'), ); // Add global bin folder if it is not present already, as some packages depend // on a globally-installed version of node-gyp. const globalBin = await getGlobalBinFolder(config, {}); if (pathParts.indexOf(globalBin) === -1) { pathParts.unshift(globalBin); } // Add node_modules .bin folders to the PATH for (const registryFolder of config.registryFolders) { const binFolder = path.join(registryFolder, '.bin'); if (config.workspacesEnabled && config.workspaceRootFolder) { pathParts.unshift(path.join(config.workspaceRootFolder, binFolder)); } pathParts.unshift(path.join(config.linkFolder, binFolder)); pathParts.unshift(path.join(cwd, binFolder)); } let pnpFile; if (process.versions.pnp) { pnpFile = dynamicRequire.resolve('pnpapi'); } else { const candidate = `${config.lockfileFolder}/${constants.PNP_FILENAME}`; if (await fs.exists(candidate)) { pnpFile = candidate; } } if (pnpFile) { const pnpApi = dynamicRequire(pnpFile); const packageLocator = pnpApi.findPackageLocator(`${cwd}/`); const packageInformation = pnpApi.getPackageInformation(packageLocator); for (const [name, reference] of packageInformation.packageDependencies.entries()) { const dependencyInformation = pnpApi.getPackageInformation({name, reference}); if (!dependencyInformation || !dependencyInformation.packageLocation) { continue; } const binFolder = `${dependencyInformation.packageLocation}/.bin`; if (await fs.exists(binFolder)) { pathParts.unshift(binFolder); } } // Note that NODE_OPTIONS doesn't support any style of quoting its arguments at the moment // For this reason, it won't work if the user has a space inside its $PATH env.NODE_OPTIONS = env.NODE_OPTIONS || ''; env.NODE_OPTIONS = `--require ${pnpFile} ${env.NODE_OPTIONS}`; } pathParts.unshift(await getWrappersFolder(config)); // join path back together env[constants.ENV_PATH_KEY] = pathParts.join(path.delimiter); return env; } export async function executeLifecycleScript({ stage, config, cwd, cmd, isInteractive, onProgress, customShell, }: { stage: string, config: Config, cwd: string, cmd: string, isInteractive?: boolean, onProgress?: (chunk: Buffer | string) => void, customShell?: string, }): LifecycleReturn { const env = await makeEnv(stage, cwd, config); await checkForGypIfNeeded(config, cmd, env[constants.ENV_PATH_KEY].split(path.delimiter)); if (process.platform === 'win32' && (!customShell || customShell === 'cmd')) { // handle windows run scripts starting with a relative path cmd = fixCmdWinSlashes(cmd); } // By default (non-interactive), pipe everything to the terminal and run child process detached // as long as it's not Windows (since windows does not have /dev/tty) let stdio = ['ignore', 'pipe', 'pipe']; let detached = process.platform !== 'win32'; if (isInteractive) { stdio = 'inherit'; detached = false; } const shell = customShell || true; const stdout = await child.spawn(cmd, [], {cwd, env, stdio, detached, shell}, onProgress); return {cwd, command: cmd, stdout}; } export default executeLifecycleScript; let checkGypPromise: ?Promise<void> = null; /** * Special case: Some packages depend on node-gyp, but don't specify this in * their package.json dependencies. They assume that node-gyp is available * globally. We need to detect this case and show an error message. */ function checkForGypIfNeeded(config: Config, cmd: string, paths: Array<string>): Promise<void> { if (cmd.substr(0, cmd.indexOf(' ')) !== 'node-gyp') { return Promise.resolve(); } // Ensure this only runs once, rather than multiple times in parallel. if (!checkGypPromise) { checkGypPromise = _checkForGyp(config, paths); } return checkGypPromise; } async function _checkForGyp(config: Config, paths: Array<string>): Promise<void> { const {reporter} = config; // Check every directory in the PATH const allChecks = await Promise.all(paths.map(dir => fs.exists(path.join(dir, 'node-gyp')))); if (allChecks.some(Boolean)) { // node-gyp is available somewhere return; } reporter.info(reporter.lang('packageRequiresNodeGyp')); try { await globalRun(config, reporter, {}, ['add', 'node-gyp']); } catch (e) { throw new MessageError(reporter.lang('nodeGypAutoInstallFailed', e.message)); } } export async function execFromManifest(config: Config, commandName: string, cwd: string): Promise<void> { const pkg = await config.maybeReadManifest(cwd); if (!pkg || !pkg.scripts) { return; } const cmd: ?string = pkg.scripts[commandName]; if (cmd) { await execCommand({stage: commandName, config, cmd, cwd, isInteractive: true}); } } export async function execCommand({ stage, config, cmd, cwd, isInteractive, customShell, }: { stage: string, config: Config, cmd: string, cwd: string, isInteractive: boolean, customShell?: string, }): Promise<void> { const {reporter} = config; try { reporter.command(cmd); await executeLifecycleScript({stage, config, cwd, cmd, isInteractive, customShell}); return Promise.resolve(); } catch (err) { if (err instanceof ProcessTermError) { const formattedError = new ProcessTermError( err.EXIT_SIGNAL ? reporter.lang('commandFailedWithSignal', err.EXIT_SIGNAL) : reporter.lang('commandFailedWithCode', err.EXIT_CODE), ); formattedError.EXIT_CODE = err.EXIT_CODE; formattedError.EXIT_SIGNAL = err.EXIT_SIGNAL; throw formattedError; } else { throw err; } } }