diff --git a/package.json b/package.json index 13463ce541..8b749fd3d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "yarn", "installationMethod": "unknown", - "version": "1.22.10", + "version": "1.22.22", + "packageManager": "yarn@1.22.17", "license": "BSD-2-Clause", "preferGlobal": true, "description": "📦🐈 Fast, reliable, and secure dependency management.", @@ -39,6 +40,7 @@ "object-path": "^0.11.2", "proper-lockfile": "^2.0.0", "puka": "^1.0.0", + "punycode": "1.4.1", "read": "^1.0.7", "request": "^2.87.0", "request-capture-har": "^1.2.2", diff --git a/scripts/build-webpack.js b/scripts/build-webpack.js index d0b166fa2d..a031df0cda 100755 --- a/scripts/build-webpack.js +++ b/scripts/build-webpack.js @@ -7,55 +7,22 @@ const resolve = require('resolve'); const util = require('util'); const fs = require('fs'); -const version = require('../package.json').version; -const basedir = path.join(__dirname, '../'); -const babelRc = JSON.parse(fs.readFileSync(path.join(basedir, '.babelrc'), 'utf8')); - -var PnpResolver = { - apply: function(resolver) { - resolver.plugin('resolve', function(request, callback) { - if (request.context.issuer === undefined) { - return callback(); - } - - let basedir; - let resolved; +// Otherwise Webpack 4 will be "helpful" and automatically mark the `punycode` package as external, +// despite us wanting to bundle it since it will be removed from future Node.js versions. +const binding = process.binding; +process.binding = name => { + let ret = binding(name); - if (!request.context.issuer) { - basedir = request.path; - } else if (request.context.issuer.startsWith('/')) { - basedir = path.dirname(request.context.issuer); - } else { - throw 42; - } - - try { - resolved = resolve.sync(request.request, {basedir}); - } catch (error) { - // TODO This is not good! But the `debug` package tries to require `supports-color` without declaring it in its - // package.json, and Webpack accepts this because it's in a try/catch, so we need to do it as well. - resolved = false; - } - - this.doResolve(['resolved'], Object.assign({}, request, { - path: resolved, - }), '', callback); - }); + if (name === `natives`) { + delete ret.punycode; } + + return ret; }; -const pnpOptions = fs.existsSync(`${__dirname}/../.pnp.js`) ? { - resolve: { - plugins: [ - PnpResolver, - ] - }, - resolveLoader: { - plugins: [ - PnpResolver, - ] - } -} : {}; +const version = require('../package.json').version; +const basedir = path.join(__dirname, '../'); +const babelRc = JSON.parse(fs.readFileSync(path.join(basedir, '.babelrc'), 'utf8')); // Use the real node __dirname and __filename in order to get Yarn's source // files on the user's system. See constants.js @@ -74,6 +41,11 @@ const compiler = webpack({ [`artifacts/yarn-${version}.js`]: path.join(basedir, 'src/cli/index.js'), 'packages/lockfile/index.js': path.join(basedir, 'src/lockfile/index.js'), }, + resolve: { + alias: { + punycode: require.resolve(`punycode/`), + }, + }, module: { rules: [ { @@ -110,13 +82,12 @@ const compiler = webpack({ }, target: 'node', node: nodeOptions, - ... pnpOptions, }); compiler.run((err, stats) => { const fileDependencies = stats.compilation.fileDependencies; const filenames = fileDependencies.map(x => x.replace(basedir, '')); - console.log(util.inspect(filenames, {maxArrayLength: null})); + //console.log(util.inspect(filenames, {maxArrayLength: null})); }); // @@ -126,6 +97,11 @@ compiler.run((err, stats) => { const compilerLegacy = webpack({ // devtool: 'inline-source-map', entry: path.join(basedir, 'src/cli/index.js'), + resolve: { + alias: { + punycode: require.resolve(`punycode/`), + }, + }, module: { rules: [ { @@ -157,7 +133,6 @@ const compilerLegacy = webpack({ }, target: 'node', node: nodeOptions, - ... pnpOptions, }); compilerLegacy.run((err, stats) => { diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index 0130617fb9..1ee1d82abc 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -39,7 +39,6 @@ export async function run(config: Config, reporter: Reporter, flags: Object, arg [ path.join(process.env.COREPACK_ROOT, 'dist/corepack.js'), `yarn@${flags.install || `stable`}`, - `yarn`, `init`, ...forwardedArgs, `--install=self`, diff --git a/src/cli/commands/policies.js b/src/cli/commands/policies.js index 7c64dda693..49e46f67dc 100644 --- a/src/cli/commands/policies.js +++ b/src/cli/commands/policies.js @@ -1,11 +1,21 @@ /* @flow */ +/* eslint-disable max-len */ import type {Reporter} from '../../reporters/index.js'; import type Config from '../../config.js'; +import {version} from '../../util/yarn-version.js'; +import * as child from '../../util/child.js'; import buildSubCommands from './_build-sub-commands.js'; import {getRcConfigForFolder} from '../../rc.js'; import * as fs from '../../util/fs.js'; import {stringify} from '../../lockfile'; +import {satisfiesWithPrereleases} from '../../util/semver.js'; +import {NODE_BIN_PATH} from '../../constants'; + +const V2_NAMES = ['berry', 'stable', 'canary', 'v2', '2']; + +const isLocalFile = (version: string) => version.match(/^\.{0,2}[\\/]/) || path.isAbsolute(version); +const isV2Version = (version: string) => satisfiesWithPrereleases(version, '>=2.0.0'); const chalk = require('chalk'); const invariant = require('invariant'); @@ -49,6 +59,7 @@ async function fetchReleases( ): Promise> { const token = process.env.GITHUB_TOKEN; const tokenUrlParameter = token ? `?access_token=${token}` : ''; + const request: Array = await config.requestManager.request({ url: `https://api.github.com/repos/yarnpkg/yarn/releases${tokenUrlParameter}`, json: true, @@ -98,32 +109,103 @@ export function hasWrapper(flags: Object, args: Array): boolean { const {run, setFlags, examples} = buildSubCommands('policies', { async setVersion(config: Config, reporter: Reporter, flags: Object, args: Array): Promise { - let range = args[0] || 'latest'; - let allowRc = flags.rc; + const initialRange = args[0] || 'latest'; + let range = initialRange; - reporter.log(`Resolving ${chalk.yellow(range)} to a url...`); + let allowRc = flags.rc; if (range === 'rc') { - range = 'latest'; + reporter.log( + `${chalk.yellow( + `Warning:`, + )} Your current Yarn binary is currently Yarn ${version}; to avoid potential breaking changes, 'set version rc' won't receive upgrades past the 1.22.x branch.\n To upgrade to the latest versions, run ${chalk.cyan( + `yarn set version`, + )} ${chalk.yellow.underline(`canary`)} instead. Sorry for the inconvenience.\n`, + ); + + range = '*'; allowRc = true; } if (range === 'latest') { + reporter.log( + `${chalk.yellow( + `Warning:`, + )} Your current Yarn binary is currently Yarn ${version}; to avoid potential breaking changes, 'set version latest' won't receive upgrades past the 1.22.x branch.\n To upgrade to the latest versions, run ${chalk.cyan( + `yarn set version`, + )} ${chalk.yellow.underline(`stable`)} instead. Sorry for the inconvenience.\n`, + ); + + range = '*'; + } + + if (range === 'classic') { range = '*'; } let bundleUrl; let bundleVersion; - let isV2 = false; + const isV2 = false; if (range === 'nightly' || range === 'nightlies') { + reporter.log( + `${chalk.yellow( + `Warning:`, + )} Nightlies only exist for Yarn 1.x; starting from 2.x onwards, you should use 'canary' instead`, + ); + bundleUrl = 'https://nightly.yarnpkg.com/latest.js'; bundleVersion = 'nightly'; - } else if (range === 'berry' || range === 'v2' || range === '2') { - bundleUrl = 'https://github.com/yarnpkg/berry/raw/master/packages/berry-cli/bin/berry.js'; - bundleVersion = 'berry'; - isV2 = true; + } else if (V2_NAMES.includes(range) || isLocalFile(range) || isV2Version(range)) { + const normalizedRange = isV2Version(range) ? range : range === `canary` ? `canary` : `stable`; + + if (process.env.COREPACK_ROOT) { + await child.spawn( + NODE_BIN_PATH, + [ + path.join(process.env.COREPACK_ROOT, 'dist/corepack.js'), + `yarn@${normalizedRange}`, + `set`, + `version`, + normalizedRange, + ], + { + stdio: 'inherit', + cwd: config.cwd, + }, + ); + + return; + } else { + const bundle = await fetchBundle( + config, + 'https://github.com/yarnpkg/berry/raw/master/packages/yarnpkg-cli/bin/yarn.js', + ); + + const yarnPath = path.resolve(config.lockfileFolder, `.yarn/releases/yarn-stable-temp.cjs`); + await fs.mkdirp(path.dirname(yarnPath)); + await fs.writeFile(yarnPath, bundle); + await fs.chmod(yarnPath, 0o755); + + try { + await child.spawn(NODE_BIN_PATH, [yarnPath, 'set', 'version', range], { + stdio: 'inherit', + cwd: config.lockfileFolder, + env: { + ...process.env, + YARN_IGNORE_PATH: `1`, + }, + }); + } catch (err) { + // eslint-disable-next-line no-process-exit + process.exit(1); + } + + return; + } } else { + reporter.log(`Resolving ${chalk.yellow(initialRange)} to a url...`); + let releases = []; try { diff --git a/src/cli/index.js b/src/cli/index.js index 4348adbb12..12f9f25974 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -26,6 +26,8 @@ import handleSignals from '../util/signal-handler.js'; import {boolify, boolifyWithDefault} from '../util/conversion.js'; import {ProcessTermError} from '../errors'; +const chalk = require('chalk'); + process.stdout.prependListener('error', err => { // swallow err only if downstream consumer process closed pipe early if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') { @@ -34,6 +36,29 @@ process.stdout.prependListener('error', err => { throw err; }); +function findPackageManager(base: string): string | null { + let prev = null; + let dir = base; + + do { + const p = path.join(dir, constants.NODE_PACKAGE_JSON); + + let data; + try { + data = JSON.parse(fs.readFileSync(p, `utf8`)); + } catch (err) {} + + if (data && typeof data.packageManager === `string`) { + return data.packageManager; + } + + prev = dir; + dir = path.dirname(dir); + } while (dir !== prev); + + return null; +} + function findProjectRoot(base: string): string { let prev = null; let dir = base; @@ -263,6 +288,38 @@ export async function main({ reporter.initPeakMemoryCounter(); const config = new Config(reporter); + + if (!process.env.COREPACK_ROOT && !process.env.SKIP_YARN_COREPACK_CHECK) { + const packageManager = findPackageManager(commander.cwd); + if (packageManager !== null) { + if (!packageManager.match(/^yarn@[01]\./)) { + reporter.error( + `This project's package.json defines ${chalk.gray('"packageManager": "yarn@')}${chalk.yellow( + `${packageManager.replace(/^yarn@/, ``).replace(/\+.*/, ``)}`, + )}${chalk.gray(`"`)}. However the current global version of Yarn is ${chalk.yellow(version)}.`, + ); + + process.stderr.write(`\n`); + process.stderr.write( + `Presence of the ${chalk.gray( + `"packageManager"`, + // eslint-disable-next-line max-len + )} field indicates that the project is meant to be used with Corepack, a tool included by default with all official Node.js distributions starting from 16.9 and 14.19.\n`, + ); + + process.stderr.write( + `Corepack must currently be enabled by running ${chalk.magenta( + `corepack enable`, + // $FlowIgnore + )} in your terminal. For more information, check out ${chalk.blueBright(`https://yarnpkg.com/corepack`)}.\n`, + ); + + exit(1); + return; + } + } + } + const outputWrapperEnabled = boolifyWithDefault(process.env.YARN_WRAP_OUTPUT, true); const shouldWrapOutput = outputWrapperEnabled && @@ -466,59 +523,6 @@ export async function main({ }); }; - 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']; @@ -609,7 +613,7 @@ export async function main({ if (err instanceof MessageError) { reporter.error(err.message); } else { - onUnexpectedError(err); + reporter.error(err.stack); } if (command.getDocsInfo) { diff --git a/src/errors.js b/src/errors.js index 5af95a7b44..152326b369 100644 --- a/src/errors.js +++ b/src/errors.js @@ -34,4 +34,11 @@ export class ResponseError extends Error { responseCode: number; } -export class OneTimePasswordError extends Error {} +export class OneTimePasswordError extends Error { + constructor(notice: string) { + super(); + this.notice = notice; + } + + notice: string; +} diff --git a/src/lockfile/index.js b/src/lockfile/index.js index 5926106dfb..03184df0a9 100644 --- a/src/lockfile/index.js +++ b/src/lockfile/index.js @@ -229,20 +229,19 @@ export default class Lockfile { invariant(remote, 'Package is missing a remote'); const remoteKey = keyForRemote(remote); - const seenPattern = remoteKey && seen.get(remoteKey); + const pkgName = getName(pattern); + + const seenKey = remoteKey ? `${remoteKey}#${pkgName}` : null; + const seenPattern = seenKey ? seen.get(seenKey) : null; + if (seenPattern) { // no point in duplicating it lockfile[pattern] = seenPattern; - - // if we're relying on our name being inferred and two of the patterns have - // different inferred names then we need to set it - if (!seenPattern.name && getName(pattern) !== pkg.name) { - seenPattern.name = pkg.name; - } continue; } + const obj = implodeEntry(pattern, { - name: pkg.name, + name: pkgName, version: pkg.version, uid: pkg._uid, resolved: remote.resolved, @@ -257,8 +256,8 @@ export default class Lockfile { lockfile[pattern] = obj; - if (remoteKey) { - seen.set(remoteKey, obj); + if (seenKey) { + seen.set(seenKey, obj); } } diff --git a/src/package-compatibility.js b/src/package-compatibility.js index d380b44a70..666a8424d6 100644 --- a/src/package-compatibility.js +++ b/src/package-compatibility.js @@ -116,9 +116,7 @@ export function checkOne(info: Manifest, config: Config, ignoreEngines: boolean) ref.ignore = true; ref.incompatible = true; - reporter.info(`${human}: ${msg}`); if (!didIgnore) { - reporter.info(reporter.lang('optionalCompatibilityExcluded', human)); didIgnore = true; } } else { diff --git a/src/rc.js b/src/rc.js index c8f48131c0..7da9708839 100644 --- a/src/rc.js +++ b/src/rc.js @@ -55,7 +55,7 @@ export function getRcConfigForFolder(cwd: string): {[key: string]: string} { function loadRcFile(fileText: string, filePath: string): {[key: string]: string} { let {object: values} = parse(fileText, filePath); - if (filePath.match(/\.yml$/) && typeof values.yarnPath === 'string') { + if (filePath.match(/\.yml$/) && values && typeof values.yarnPath === 'string') { values = {'yarn-path': values.yarnPath}; } diff --git a/src/registries/npm-registry.js b/src/registries/npm-registry.js index 083b8c1bab..de54a6ff74 100644 --- a/src/registries/npm-registry.js +++ b/src/registries/npm-registry.js @@ -116,7 +116,7 @@ export default class NpmRegistry extends Registry { let resolved = pathname; if (!REGEX_REGISTRY_PREFIX.test(pathname)) { - resolved = url.resolve(addSuffix(registry, '/'), pathname); + resolved = url.resolve(addSuffix(registry, '/'), `./${pathname}`); } if (REGEX_REGISTRY_ENFORCED_HTTPS.test(resolved)) { @@ -191,6 +191,10 @@ export default class NpmRegistry extends Registry { } this.reporter.info(this.reporter.lang('twoFactorAuthenticationEnabled')); + if (error.notice) { + this.reporter.info(error.notice); + } + this.otp = await getOneTimePassword(this.reporter); this.requestManager.clearCache(); diff --git a/src/util/child.js b/src/util/child.js index a0538e8974..3528cae463 100644 --- a/src/util/child.js +++ b/src/util/child.js @@ -6,7 +6,10 @@ import BlockingQueue from './blocking-queue.js'; import {ProcessSpawnError, ProcessTermError} from '../errors.js'; import {promisify} from './promise.js'; +const os = require('os'); const child = require('child_process'); +const fs = require('fs'); +const path = require('path'); export const queue = new BlockingQueue('child', constants.CHILD_CONCURRENCY); @@ -15,7 +18,26 @@ let uid = 0; export const exec = promisify(child.exec); +function validate(program: string, opts?: Object = {}) { + if (program.match(/[\\\/]/)) { + return; + } + + if (process.platform === 'win32' && process.env.PATHEXT) { + const cwd = opts.cwd || process.cwd(); + const pathext = process.env.PATHEXT; + + for (const ext of pathext.split(';')) { + const candidate = path.join(cwd, `${program}${ext}`); + if (fs.existsSync(candidate)) { + throw new Error(`Potentially dangerous call to "${program}" in ${cwd}`); + } + } + } +} + export function forkp(program: string, args: Array, opts?: Object): Promise { + validate(program, opts); const key = String(++uid); return new Promise((resolve, reject) => { const proc = child.fork(program, args, opts); @@ -25,13 +47,16 @@ export function forkp(program: string, args: Array, opts?: Object): Prom reject(error); }); - proc.on('close', exitCode => { - resolve(exitCode); + proc.on('close', (exitCode: number, signal: string) => { + const finalExitCode = + typeof exitCode !== `undefined` && exitCode !== null ? exitCode : 128 + os.constants.signals[signal]; + resolve(finalExitCode); }); }); } export function spawnp(program: string, args: Array, opts?: Object): Promise { + validate(program, opts); const key = String(++uid); return new Promise((resolve, reject) => { const proc = child.spawn(program, args, opts); @@ -41,8 +66,10 @@ export function spawnp(program: string, args: Array, opts?: Object): Pro reject(error); }); - proc.on('close', exitCode => { - resolve(exitCode); + proc.on('close', (exitCode: number, signal: string) => { + const finalExitCode = + typeof exitCode !== `undefined` && exitCode !== null ? exitCode : 128 + os.constants.signals[signal]; + resolve(finalExitCode); }); }); } @@ -73,6 +100,8 @@ export function spawn( key, (): Promise => new Promise((resolve, reject) => { + validate(program, opts); + const proc = child.spawn(program, args, opts); spawnedProcesses[key] = proc; diff --git a/src/util/request-manager.js b/src/util/request-manager.js index fd41bf2100..ff9cdf28bc 100644 --- a/src/util/request-manager.js +++ b/src/util/request-manager.js @@ -440,9 +440,8 @@ export default class RequestManager { if (res.statusCode === 401 && res.headers['www-authenticate']) { const authMethods = res.headers['www-authenticate'].split(/,\s*/).map(s => s.toLowerCase()); - if (authMethods.indexOf('otp') !== -1) { - reject(new OneTimePasswordError()); + reject(new OneTimePasswordError(res.headers['npm-notice'])); return; } } diff --git a/yarn.lock b/yarn.lock index fe1b1cd3f8..f432c813e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6140,7 +6140,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.4.1: +punycode@1.4.1, punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=