nx
Version:
1,002 lines (1,001 loc) • 44.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getYarnClassicSpawnRegistryEnv = getYarnClassicSpawnRegistryEnv;
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const fileutils_1 = require("../fileutils");
const npmrc_1 = require("../package-manager-config/npmrc");
const utils_1 = require("./utils");
/*
* yarn classic (1.x) registry resolution (verified on 1.22.22):
*
* Config files, highest precedence first (yarn merges them earlier-wins):
* project .{npmrc,yarnrc} > home .{npmrc,yarnrc} > <globalPrefix>/etc/{npmrc,
* yarnrc} > ancestor .{npmrc,yarnrc} walking up to the filesystem root.
* `home` is os.homedir(), except as root (uid 0, no FAKEROOTKEY) where yarn
* reads /usr/local/share first and the real home second. <globalPrefix> is
* $PREFIX, else dirname(dirname(process.execPath)) (dirname on Windows).
*
* Unscoped registry: a `--registry`/`--install.registry` line in a CLI-rc file
* (yarn injects it as a default CLI arg) > npm_config_registry env >
* YARN_REGISTRY env > .npmrc registry (the npm-config chain is exhausted first)
* > .yarnrc registry > https://registry.yarnpkg.com.
*
* Scoped: @scope:registry in npm_config_ env > yarn_ env (both merged into the
* npm registry's config above its files, any casing, a non-leading `_` read as
* `-`; a dotted key never resolves from env because mergeEnv nests it) >
* .npmrc > .yarnrc > the unscoped chain. An empty env value masks the file
* chain of its tier. npm still reads a spelling yarn ignored or masked (a
* dotted env key, or the .npmrc entry behind an empty env value npm skips), so
* the overlay then pins yarn's effective registry on the scope.
*
* Option keys (cafile, strict-ssl, proxy) resolve the other way around, and off
* the env first: `yarn_<key>` > .yarnrc > `npm_config_<key>` > .npmrc > yarn's
* DEFAULTS. A key DEFAULTS carries never reaches the npm tier at all, which is
* why no npm-config source can turn TLS verification off for yarn. `always-auth`
* is not one of these: it is read off the npm registry's own config for the
* registry being queried, not off .yarnrc.
*
* npm natively reads the project, home, and <globalPrefix>/etc .npmrc plus env
* vars identically, so bridging is only needed when a yarn-only surface wins
* (YARN_REGISTRY, any .yarnrc, ancestor .npmrc, the root /usr/local/share home,
* or a CLI `--registry` line).
*
* See https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/registries/npm-registry.js#L345-L436
*/
const YARN_CLASSIC_DEFAULT_REGISTRY = 'https://registry.yarnpkg.com';
function getYarnClassicSpawnRegistryEnv(packageName, root) {
const env = {};
const scope = (0, utils_1.getPackageScope)(packageName);
const realHome = (0, os_1.homedir)();
const { primary, secondary } = yarnHomeTiers(realHome);
const ancestors = (0, utils_1.ancestorDirectories)(root);
const etcDir = globalEtcDir();
// yarn ranks <prefix>/etc above the real home it adds under root, so the
// secondary home tier follows etc.
const sources = [
dotfiles(root, true),
dotfiles(primary.dir, primary.npmNative),
{
npmrcPath: (0, path_1.join)(etcDir, 'npmrc'),
yarnrcPath: (0, path_1.join)(etcDir, 'yarnrc'),
npmNative: true,
},
...(secondary ? [dotfiles(secondary.dir, secondary.npmNative)] : []),
...ancestors.map((dir) => dotfiles(dir, false)),
];
const npmrcChain = sources.map((s) => ({
npmNative: s.npmNative,
map: toYarnValueMap(readChainNpmrcMap(s.npmrcPath)),
}));
const cliRcPaths = yarnCliRcPaths(root, ancestors, primary.dir);
// The two readers overlap on the tiers above without covering each other, and
// what a file tolerates depends on which of them reach it, so each read is
// derived from the two path sets rather than from the tier it sits in.
const lookedUpPaths = new Set(sources.map((s) => s.yarnrcPath));
const ungatedPaths = new Set(cliRcPaths);
const yarnrcChain = sources.map((s) => ({
// npm never reads .yarnrc.
npmNative: false,
map: readYarnrcMap(s.yarnrcPath, ungatedPaths.has(s.yarnrcPath) ? 'both' : 'looked-up'),
}));
const cliRegistryChain = cliRcPaths.map((rcPath) => ({
npmNative: false,
map: readYarnrcMap(rcPath, lookedUpPaths.has(rcPath) ? 'both' : 'cli-rc'),
}));
const authRegistry = resolveRegistry(env, npmrcChain, yarnrcChain, cliRegistryChain, scope);
// yarn tilde-expands paths against userHomeDir.default (the primary home).
resolveOptions(env, npmrcChain, yarnrcChain, root, primary.dir);
resolveAuth(env, npmrcChain, scope, authRegistry);
// Everything above is what yarn itself ends up with: an .npmrc value with its
// own escape rule applied, and a .yarnrc one it never env-replaces at all.
// Both can still hold a `${VAR}` the spawned npm would resolve, so escape
// them back into the text npm hands on unchanged.
for (const [key, value] of Object.entries(env)) {
env[key] = (0, utils_1.escapeNpmEnvExpr)(value);
}
return env;
}
// yarn reads registry auth only from the .npmrc chain, never .yarnrc. Bridge a
// yarn-only winner only where yarn would send it: bridging unconditionally makes
// npm authenticate where yarn stays anonymous, and 401 on a registry that serves
// the package without credentials.
function resolveAuth(env, npmrcChain, scope, authRegistry) {
const prefixes = registryOptionPrefixes(authRegistry);
const requestDart = (0, utils_1.requestNerfDart)(authRegistry);
// always-auth is read for the registry yarn is about to query, not for the
// dart a credential came from.
const authenticates = scope !== null || alwaysAuthFor(prefixes, npmrcChain);
if (!authenticates) {
if (requestDart) {
(0, utils_1.warnNativeCredential)(env, requestDart, 'yarn', 'yarn does send it for scoped packages, and for any registry with always-auth set, so removing it from .npmrc would stop those from authenticating too.', (key) => {
const match = firstString(npmrcChain, key);
return ((0, utils_1.readNpmConfigEnv)(process.env, key) ??
(match?.npmNative ? match.value : undefined));
});
}
return;
}
if (!requestDart) {
return;
}
// Only what the ladder itself resolved is carried over. yarn picks a
// credential per registry, never per URL: whatever request it is about to
// make, the header is getAuthByRegistry's answer for the registry it resolved,
// and a key matching the request URL only decides whether to send one at all.
// So any other nerf-darted key in the chain is one yarn would not have read,
// and copying it over verbatim would only put it on npm's own walk.
//
// getAuthByRegistry runs the whole ladder for one form before it tries the
// next, so a bare token outranks a registry-scoped basic pair, and the pair
// only counts when both halves resolve. npm honors auth in the nerf-darted
// form alone, so whichever form wins is re-keyed onto the dart. Re-keying a
// bare key out of a file npm reads for itself is not wasted either: npm
// refuses to run on a bare auth key in its own config (ERR_INVALID_AUTH,
// --force included) before any overlay matters. The _auth/_password base64
// carries over as-is.
let winner;
for (let rank = 0; rank < CREDENTIAL_FORMS.length && !winner; rank++) {
const form = CREDENTIAL_FORMS[rank];
const values = [];
for (const key of form) {
const match = resolveCredential(prefixes, npmrcChain, key);
if (!match) {
break;
}
values.push(match);
}
if (values.length === form.length) {
winner = { form, rank, values };
}
}
if (!winner) {
return;
}
// npm already holds this at the dart it starts its own walk from, so writing
// it there would only restate it.
if (!winner.values.every((match) => match.prefix === requestDart && match.npmNative)) {
winner.form.forEach((key, index) => {
env[`npm_config_${requestDart}:${key}`] = winner.values[index].value;
});
}
// yarn sends this same credential to any other host it already holds one for.
// requestNeedsAuth only asks whether some key covers the URL about to be
// fetched, a tarball served off the registry's path or host among them, and
// the header stays getAuthByRegistry's answer for the resolved registry. npm
// reads that host's own key instead, so the winner is written there too.
//
// Every declared dart under a gated one gets it, not just the gating dart:
// npm settles on the deepest declared dart covering the URL, so leaving a
// deeper one alone would let it answer in the gated one's place. Within a
// dart, only a form npm ranks above the winner has to be cancelled.
const outranking = CREDENTIAL_FORMS.slice(0, winner.rank).flat();
for (const dart of gatedAuthDarts(npmrcChain, requestDart)) {
winner.form.forEach((key, index) => {
env[`npm_config_${dart}:${key}`] = winner.values[index].value;
});
for (const key of outranking) {
// Read across npm's own tiers rather than yarn's: a yarn-only file
// declaring this key first hides an npm-native one that npm still reads.
const native = (0, utils_1.readNpmConfigEnv)(process.env, `${dart}:${key}`) ??
npmNativeValue(npmrcChain, `${dart}:${key}`);
if (native !== undefined) {
// npm reads this one before the form yarn picked and would answer with
// the host's own credential. An empty value leaves npm reading the
// file's; the `null` literal is what cancels it.
env[`npm_config_${dart}:${key}`] = 'null';
}
}
}
}
/** The value the spawned npm reads for `key` from a file it opens itself. */
function npmNativeValue(npmrcChain, key) {
for (const file of npmrcChain) {
const value = file.npmNative ? file.map?.get(key) : undefined;
if (typeof value === 'string') {
return value;
}
}
return undefined;
}
function dartParts(dart) {
try {
// The dart is protocol-relative; lending it one lets the URL parser
// lower-case the host, which is what makes `//CDN.example.com/` answer for
// a request npm keys as `//cdn.example.com/`.
const url = new URL(`http:${dart}`);
// yarn matches on a path that still carries the query, so a query-bearing
// key only covers a request whose own sorted query extends it, which a
// tarball URL's never does; dropping the dart reproduces that. A fragment
// is different: normalize-url deletes it, so the dart matches on the path
// in front of it, which url.pathname already is.
if (url.search) {
return null;
}
// The parser also rewrites a host yarn's own normalization leaves alone, a
// legacy IPv4 spelling (`127.1`) or an IDN among them. Matching on the
// rewritten one would open a host yarn never opened, so anything the parse
// did not leave as it found it is dropped instead.
if (dart.slice(2).split(/[/#]/)[0].toLowerCase() !== url.hostname) {
return null;
}
// normalize-url's order: collapse, decode, then the trailing slash. A
// malformed escape throws out of decodeURI into the catch below, which is
// where yarn's own normalize call would have thrown too.
const foldPath = decodeURI(url.pathname.replace(/\/{2,}/g, '/'));
return {
host: url.host,
matchHost: url.hostname.replace(/\.$/, '').replace(/^www\./, ''),
path: url.pathname,
foldPath,
matchPath: foldPath.replace(/(?!^)\/$/, ''),
};
}
catch {
return null;
}
}
/**
* Every dart the overlay has to answer for once yarn's requestNeedsAuth opens a
* host up: the gating darts themselves, and any dart declaring a credential
* underneath one, which npm would otherwise settle on first.
*
* Both sides are compared the way yarn compares them, host against host and
* then path against path. Run as one string the relation would read
* `//h.example` as covering `//h.example.evil` and hand that origin the
* registry's credential. The path stays a plain prefix test, which is yarn's
* own. The registry's dart is left out, since the winner already goes there.
*/
function gatedAuthDarts(npmrcChain, requestDart) {
const gates = [];
const declared = [];
for (const file of npmrcChain) {
if (!file.map) {
continue;
}
for (const key of file.map.keys()) {
const parts = key.split(':');
if (!parts[0].startsWith('//')) {
continue;
}
const dart = dartParts(parts[0]);
if (!dart) {
continue;
}
// yarn's own condition, precedence included: the length test binds to
// `_authToken` alone. So a host carrying a port, which puts that port
// where the key name has to be, gates through neither.
if ((parts.length === 2 && parts[1] === '_authToken') ||
parts[1] === '_password') {
gates.push(dart);
}
if (parts.length === 2 && AUTH_KEYS.has(parts[1])) {
declared.push(dart);
}
}
}
const darts = new Set();
for (const gate of gates) {
for (const dart of [gate, ...declared]) {
if (dart.matchHost !== gate.matchHost ||
!dart.matchPath.startsWith(gate.matchPath)) {
continue;
}
// The folding means one dart can answer for two spellings on each side,
// host and path, and which ones the fetch uses is not known here, so
// every pairing goes in: npm walks the tarball URL's own spelling, so a
// key folded away from it is only found where the walk lands.
for (const host of new Set([dart.host, dart.matchHost])) {
for (const path of new Set([dart.path, dart.foldPath])) {
const key = `//${host}${path}`;
if (key !== requestDart) {
darts.add(key);
}
}
}
}
}
return darts;
}
/**
* The config-key prefixes yarn's NpmRegistry.getRegistryOption tries, in its
* order: the registry itself, then the same without the protocol, then without
* a trailing `registry` segment, recursing into each. It never climbs a path
* the way npm does, so the directory above the registry is reachable only
* through that suffix rewrite, which carries no segment boundary of its own: a
* `.../myregistry/` registry strips to `.../my/`.
*/
function registryOptionPrefixes(registry) {
const prefixes = [];
const visit = (candidate) => {
const prefix = candidate.endsWith('/') ? candidate : `${candidate}/`;
if (prefixes.includes(prefix)) {
return;
}
prefixes.push(prefix);
if (/^https?:/i.test(prefix)) {
visit(prefix.replace(/^https?:/i, ''));
}
if (/registry\/?$/.test(prefix)) {
visit(prefix.replace(/registry\/?$/, ''));
}
};
visit(registry);
return prefixes;
}
// getRegistryOrGlobalOption chains the ladder and then the bare key with `||`,
// so an empty value falls through rather than settling the lookup.
function resolveCredential(prefixes, npmrcChain, key) {
for (const prefix of prefixes) {
const match = firstString(npmrcChain, `${prefix}:${key}`);
if (match?.value) {
return { ...match, prefix };
}
}
const bare = firstString(npmrcChain, key);
return bare?.value ? { ...bare, prefix: null } : undefined;
}
// .yarnrc never feeds always-auth: yarn reads it from NpmRegistry's own config,
// walking its registry ladder before the bare key. Unlike resolveOption this
// ignores npmNative, since it drives the auth gate rather than a bridge, and it
// chains with `||`, so a falsy rung falls through to the next.
function alwaysAuthFor(prefixes, npmrcChain) {
for (const prefix of prefixes) {
const key = `${prefix}:always-auth`;
if (yarnConfigValue(key, envReachable(key) ? key : null, npmrcChain)) {
return true;
}
}
// The bare key's env spelling carries the underscore mergeEnv rewrites into
// the dash an rc file spells out.
return Boolean(yarnConfigValue('always-auth', 'always_auth', npmrcChain));
}
// mergeEnv lowercases the whole variable name and stores it through objectPath,
// which splits on `.` into nested objects while every read is flat, so a
// registry-scoped env key only reaches yarn dot-free and already lowercase (a
// key for //localhost:PORT/ authenticates an unscoped fetch, the same key for
// //127.0.0.1:PORT/ does not).
function envReachable(key) {
return !key.includes('.') && key === key.toLowerCase();
}
// yarn merges the `yarn_` env prefix before any rc file is read and then
// `npm_config_` over it into the same flat map, so an env value replaces the
// file's at that key and npm's spelling replaces yarn's.
function yarnConfigValue(key, envName, npmrcChain) {
if (envName !== null) {
const fromEnv = (0, utils_1.readEnvVar)(process.env, `npm_config_${envName}`) ??
(0, utils_1.readEnvVar)(process.env, `yarn_${envName}`);
if (fromEnv !== undefined) {
return normalizeYarnConfigValue(fromEnv);
}
}
return firstDefined(npmrcChain, key)?.value;
}
/** Mirrors yarn's BaseRegistry.normalizeConfigOption. */
function normalizeYarnConfigValue(value) {
return value === 'true' ? true : value === 'false' ? false : value;
}
// getAuthByRegistry's order: a bearer token, else a basic one, else the
// username/password pair, which it only sends when both halves resolve.
const CREDENTIAL_FORMS = [
['_authToken'],
['_auth'],
['username', '_password'],
];
// Every key npm's own hasAuth reads a dart as authenticated on. The client
// certificate pair is in because npm settles on the deepest dart carrying any
// of these, even though it ranks the pair below all three forms above.
const AUTH_KEYS = new Set([...CREDENTIAL_FORMS.flat(), 'certfile', 'keyfile']);
function resolveRegistry(env, npmrcChain, yarnrcChain, cliYarnrcChain, scope) {
const scopedRegistry = scope
? resolveScopedRegistry(env, npmrcChain, yarnrcChain, scope)
: undefined;
const unscopedRegistry = resolveUnscopedRegistry(env, npmrcChain, yarnrcChain, cliYarnrcChain);
if (scope) {
pinScopedRegistry(env, npmrcChain, scope, scopedRegistry ?? unscopedRegistry);
}
// yarn's default is npmjs' CNAME and npm stays on registry.npmjs.org, so the
// dart lands where npm queries.
return scopedRegistry ?? unscopedRegistry ?? 'https://registry.npmjs.org/';
}
// npm reads scoped spellings yarn cannot: a dotted env key mergeEnv nests out
// of yarn's flat lookups, and the .npmrc entry behind an empty env value, which
// masks the file for yarn while npm skips the empty. Wherever the resolution
// above left the scoped key unclaimed and npm's own read (env, else its native
// .npmrc chain) lands elsewhere, yarn's effective registry is written over it.
function pinScopedRegistry(env, npmrcChain, scope, effectiveRegistry) {
const scopedKey = `${scope}:registry`;
if (env[`npm_config_${scopedKey}`] !== undefined) {
return;
}
const npmReads = (0, utils_1.readNpmConfigEnv)(process.env, scopedKey) ??
npmNativeValue(npmrcChain, scopedKey);
const effective = effectiveRegistry ?? 'https://registry.npmjs.org/';
if (npmReads !== undefined && npmReads !== effective) {
(0, utils_1.setScopedRegistry)(env, scope, effective);
}
}
function resolveUnscopedRegistry(env, npmrcChain, yarnrcChain, cliYarnrcChain) {
// 1. A `--registry`/`--install.registry` line in a CLI-rc .yarnrc lands at
// yarn's CLI tier, above npm_config_registry env, so it always needs bridging.
const cliRegistry = firstString(cliYarnrcChain, '--install.registry') ??
firstString(cliYarnrcChain, '--registry');
if (cliRegistry) {
(0, utils_1.setRegistry)(env, cliRegistry.value);
return cliRegistry.value;
}
// 2. npm_config_registry env: npm resolves it natively.
const npmConfigRegistry = (0, utils_1.readEnvVar)(process.env, 'npm_config_registry');
if (npmConfigRegistry !== undefined) {
return npmConfigRegistry;
}
// 3. YARN_REGISTRY env (yarn-only).
const yarnRegistryEnv = (0, utils_1.readEnvVar)(process.env, 'YARN_REGISTRY');
if (yarnRegistryEnv !== undefined) {
(0, utils_1.setRegistry)(env, yarnRegistryEnv);
return yarnRegistryEnv;
}
// 4. The .npmrc chain is exhausted before .yarnrc is consulted.
const npmrcRegistry = firstString(npmrcChain, 'registry');
if (npmrcRegistry) {
if (!npmrcRegistry.npmNative) {
(0, utils_1.setRegistry)(env, npmrcRegistry.value);
}
return npmrcRegistry.value;
}
// 5. .yarnrc registry (every entry yarn-only). The yarn default is npmjs'
// CNAME; leaving npm on registry.npmjs.org keeps nerf-darted auth working.
const yarnrcRegistry = firstString(yarnrcChain, 'registry');
if (yarnrcRegistry &&
yarnrcRegistry.value.replace(/\/$/, '') !== YARN_CLASSIC_DEFAULT_REGISTRY) {
(0, utils_1.setRegistry)(env, yarnrcRegistry.value);
return yarnrcRegistry.value;
}
return undefined;
}
// The value yarn's mergeEnv resolves for `key` under `prefix`: the whole env
// key is lowercased, the prefix stripped, `__` becomes `.` and a non-leading
// `_` becomes `-`, later entries overwriting earlier ones, an empty value
// included. mergeEnv writes through objectPath.set, which nests a dotted key
// while yarn's lookups stay flat, so a dotted key resolves from no spelling.
function readYarnMergedEnv(prefix, key) {
if (key.includes('.')) {
return undefined;
}
let value;
for (const [envKey, candidate] of Object.entries(process.env)) {
if (candidate === undefined) {
continue;
}
const lowerKey = envKey.toLowerCase();
if (!lowerKey.startsWith(prefix)) {
continue;
}
const normalized = lowerKey
.slice(prefix.length)
.replace(/__/g, '.')
.replace(/([^_])_/g, '$1-');
if (normalized === key) {
value = candidate;
}
}
return value;
}
// Returns the scoped registry yarn resolves even when npm reads it natively, so
// auth can dart onto it.
function resolveScopedRegistry(env, npmrcChain, yarnrcChain, scope) {
const scopedKey = `${scope}:registry`;
// yarn merges yarn_ env into the npm registry config at init and npm_config_
// env over it, with the .npmrc chain under both; the yarn registry tier then
// holds yarn_ env over the .yarnrc chain. An empty env value still occupies
// its key, so it masks the file chain of its tier and falls through as unset.
const npmConfigEnvRegistry = readYarnMergedEnv('npm_config_', scopedKey);
const yarnEnvRegistry = readYarnMergedEnv('yarn_', scopedKey);
const npmTierEnvRegistry = npmConfigEnvRegistry ?? yarnEnvRegistry;
if (npmTierEnvRegistry) {
if (npmConfigEnvRegistry) {
// npm reads its own env tier natively unless its key rewrite resolves
// the spelling to another setting, which is when the bridge takes over.
if ((0, utils_1.readNpmConfigEnv)(process.env, scopedKey) !== npmConfigEnvRegistry) {
(0, utils_1.setScopedRegistry)(env, scope, npmConfigEnvRegistry);
}
}
else {
(0, utils_1.setScopedRegistry)(env, scope, npmTierEnvRegistry);
}
return npmTierEnvRegistry;
}
if (npmTierEnvRegistry === undefined) {
const npmScoped = firstString(npmrcChain, scopedKey);
if (npmScoped) {
if (!npmScoped.npmNative) {
(0, utils_1.setScopedRegistry)(env, scope, npmScoped.value);
}
return npmScoped.value;
}
}
// The yarn registry tier: an npm-tier empty still reaches it, and a truthy
// yarn_ value only gets here masked behind an empty npm_config_ spelling.
if (yarnEnvRegistry) {
(0, utils_1.setScopedRegistry)(env, scope, yarnEnvRegistry);
return yarnEnvRegistry;
}
if (yarnEnvRegistry === undefined) {
const yarnScoped = firstString(yarnrcChain, scopedKey);
if (yarnScoped) {
(0, utils_1.setScopedRegistry)(env, scope, yarnScoped.value);
return yarnScoped.value;
}
}
return undefined;
}
function resolveOptions(env, npmrcChain, yarnrcChain, root, home) {
const cafile = yarnEnvOption('cafile') ??
resolveOption(firstString, npmrcChain, yarnrcChain, 'cafile');
if (cafile) {
(0, utils_1.setCafile)(env, resolveYarnPath(cafile, root, home));
}
// DEFAULTS carries `strict-ssl`, so no npm-config source reaches yarn's value
// (an .npmrc `strict-ssl=false` and `npm_config_strict_ssl=false` both leave
// verification on). That cuts both ways: npm also has to be told to keep
// verifying where its own config would stop, so a declared value is bridged in
// either direction. An env value arrives as a string and yarn coerces the bare
// booleans out of it first, so `YARN_STRICT_SSL=false` does turn verification
// off where a quoted `"false"` in a file does not.
const strictSslEnv = yarnEnvOption('strict-ssl');
const strictSsl = strictSslEnv !== undefined
? normalizeYarnConfigValue(strictSslEnv)
: firstDefined(yarnrcChain, 'strict-ssl')?.value;
if (strictSsl !== undefined &&
truthyStrictSsl(strictSsl) !== npmVerifiesTls(npmrcChain)) {
(0, utils_1.setStrictSsl)(env, truthyStrictSsl(strictSsl));
}
(0, utils_1.setProxies)(env, {
httpProxy: yarnEnvOption('proxy') ??
resolveOption(firstString, npmrcChain, yarnrcChain, 'proxy'),
httpsProxy: yarnEnvOption('https-proxy') ??
resolveOption(firstString, npmrcChain, yarnrcChain, 'https-proxy'),
});
}
/**
* The `yarn_` env tier for an option key (YARN_CAFILE, YARN_STRICT_SSL, ...).
* yarn merges it before any rc file is read and getOption consults yarn's own
* config before npm's, so it outranks every file and a `npm_config_` value of
* the same key (with both YARN_CAFILE and npm_config_cafile set, `yarn config
* get cafile` returns the YARN_ one). npm cannot see it under that name.
*/
function yarnEnvOption(key) {
return (0, utils_1.readEnvVar)(process.env, `yarn_${key.replace(/-/g, '_')}`);
}
// Yarn's option resolution below its own env tier: .yarnrc, then npm's config.
// Returns only a value that needs bridging, so a native .npmrc winner comes back
// undefined.
function resolveOption(lookup, npmrcChain, yarnrcChain, key) {
const yarnrc = lookup(yarnrcChain, key);
if (yarnrc) {
return yarnrc.value;
}
// The `npm_config_` env tier outranks every .npmrc and npm reads it itself, so
// a value there needs no bridge and shadows the file chain below it.
if ((0, utils_1.readNpmConfigEnv)(process.env, key) !== undefined) {
return undefined;
}
const npmrc = lookup(npmrcChain, key);
return npmrc && !npmrc.npmNative ? npmrc.value : undefined;
}
function dotfiles(dir, npmNative) {
return {
npmrcPath: (0, path_1.join)(dir, '.npmrc'),
yarnrcPath: (0, path_1.join)(dir, '.yarnrc'),
npmNative,
};
}
// yarn's userHomeDir.default is /usr/local/share when running as root (uid 0, no
// FAKEROOTKEY) and the real home otherwise; under root the real home stays on as
// a second tier. The primary is the tilde-expansion base, and npm reads only the
// real home natively.
function yarnHomeTiers(home) {
const isRoot = process.platform !== 'win32' &&
typeof process.getuid === 'function' &&
process.getuid() === 0 &&
!process.env.FAKEROOTKEY;
if (isRoot) {
return {
primary: { dir: '/usr/local/share', npmNative: false },
secondary: { dir: home, npmNative: true },
};
}
return { primary: { dir: home, npmNative: true } };
}
/**
* The files yarn injects `--`-prefixed lines from as default CLI args. It
* collects them through getRcPaths, a wider set than the tiers the registry
* client reads, and merges them last-wins, so this list runs the other way
* round: highest precedence first, to be read first-wins.
* See https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/util/rc.js#L11-L62
*/
function yarnCliRcPaths(root, ancestors, primaryHome) {
const paths = [];
if (process.env.YARN_CONFIG) {
paths.push(process.env.YARN_CONFIG);
}
// Read straight off the env here rather than through os.homedir(), and every
// home tier dropped when it is unset, both as yarn does it.
const home = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
if (home) {
paths.push((0, path_1.join)(home, '.yarnrc.yml'), (0, path_1.join)(home, '.yarnrc'), (0, path_1.join)(home, '.yarn', 'config'), (0, path_1.join)(home, '.config', 'yarn'), (0, path_1.join)(home, '.config', 'yarn', 'config'), yarnConfigDir(primaryHome));
}
if (process.platform !== 'win32') {
// The literal /etc, not the <prefix>/etc tier the registry client reads.
paths.push((0, path_1.join)('/etc', 'yarnrc'), (0, path_1.join)('/etc', 'yarn', 'config'));
}
// A .yarnrc.yml sibling rides along with every .yarnrc yarn names, and lands
// above it.
for (const dir of [root, ...ancestors]) {
paths.push((0, path_1.join)(dir, '.yarnrc.yml'), (0, path_1.join)(dir, '.yarnrc'));
}
return paths;
}
// Mirrors yarn's getConfigDir, which resolves against userHomeDir: the same
// root-aware home yarnHomeTiers picks as the primary tier.
function yarnConfigDir(primaryHome) {
if (process.platform === 'win32') {
return process.env.LOCALAPPDATA
? (0, path_1.join)(process.env.LOCALAPPDATA, 'Yarn', 'Config')
: (0, path_1.join)(primaryHome, '.config', 'yarn');
}
return process.env.XDG_CONFIG_HOME
? (0, path_1.join)(process.env.XDG_CONFIG_HOME, 'yarn')
: (0, path_1.join)(primaryHome, '.config', 'yarn');
}
// Mirrors yarn's getGlobalPrefix.
function globalEtcDir() {
// Falsy rather than absent, so an exported but empty PREFIX falls through to
// the executable's own prefix instead of resolving `etc` against the cwd.
if (process.env.PREFIX) {
return (0, path_1.join)(process.env.PREFIX, 'etc');
}
if (process.platform === 'win32') {
return (0, path_1.join)((0, path_1.dirname)(process.execPath), 'etc');
}
const prefix = (0, path_1.dirname)((0, path_1.dirname)(process.execPath));
// DESTDIR reroots the prefix on Unix only, and only once PREFIX has passed.
return (0, path_1.join)(process.env.DESTDIR ? (0, path_1.join)(process.env.DESTDIR, prefix) : prefix, 'etc');
}
function firstDefined(chain, key) {
for (const file of chain) {
const value = file.map?.get(key);
if (value !== undefined) {
return { value, npmNative: file.npmNative };
}
}
return undefined;
}
// yarn stores a bare `false`/number as a non-string, never a valid
// registry/path/proxy value, so skip those entries rather than coercing.
function firstString(chain, key) {
for (const file of chain) {
const value = file.map?.get(key);
if (typeof value === 'string') {
return { value, npmNative: file.npmNative };
}
}
return undefined;
}
// yarn computes strictSSL as Boolean(getOption('strict-ssl')), so a quoted
// `"false"` (a string) keeps verification on.
function truthyStrictSsl(value) {
return Boolean(value);
}
// Only a literal `false` turns npm's own strict-ssl off, and toYarnValueMap has
// already typed that as a boolean.
function npmVerifiesTls(npmrcChain) {
const fromEnv = (0, utils_1.readNpmConfigEnv)(process.env, 'strict-ssl');
if (fromEnv !== undefined) {
return fromEnv !== 'false';
}
const native = firstDefined(npmrcChain.filter((file) => file.npmNative), 'strict-ssl');
return native?.value !== false;
}
// yarn expands a leading `~/` to the home dir, then path.resolve()s against the
// cwd (the workspace root). Resolve to an absolute path since the spawned npm
// may run from a temp dir.
function resolveYarnPath(value, root, home) {
if (value === '~') {
return home;
}
if (value.startsWith('~/') || value.startsWith('~\\')) {
return (0, path_1.resolve)(home, value.slice(2));
}
return (0, path_1.resolve)(root, value);
}
// yarn looks each .npmrc in its chain up before opening it, and unlike .yarnrc
// nothing reads it a second time ungated, so whatever the lookup misses counts
// as absent and only a file it finds and cannot open aborts yarn.
function readChainNpmrcMap(path) {
if (!(0, fs_1.existsSync)(path)) {
return null;
}
const map = (0, npmrc_1.readNpmrcMap)(path);
if (map === 'unreadable') {
// No resolution left to reproduce. Reading on without the file would
// resolve the registry from the remaining ones, silently landing on an
// ancestor's or the default.
throw new Error(`The .npmrc at ${path} could not be read.`);
}
return map;
}
function toYarnValueMap(map) {
if (!map) {
return null;
}
// yarn env-replaces `${VAR}` in .npmrc values itself, and with a grammar of
// its own, so expand here rather than leaving the spawned npm to apply its.
// The ini reader yields strings; coercing the bare booleans lines these values
// up with the .yarnrc side.
const result = new Map();
for (const [key, value] of map) {
result.set(key, normalizeYarnConfigValue((0, utils_1.expandYarnEnvVars)(value)));
}
return result;
}
/**
* Parses one of yarn classic's rc files into a last-write-wins map. Yarn reads
* it with its lockfile parser first, so one rejected line costs the whole file
* rather than just that line, then retries the whole file with js-yaml and
* honors what the retry accepts, which is how `registry: https://host/` works
* despite the lockfile grammar throwing on it. A `.yml` skips straight to the
* retry's parser.
* See https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/lockfile/parse.js#L384-L409
*
* @yarnpkg/lockfile on npm is a 2018 snapshot of that parser and has since
* diverged: its name token excludes `.`, so it rejects the `cafile ./ca.pem`
* that yarn 1.22 accepts. Reading with it would drop whole files yarn honors.
*/
function readYarnrcMap(path, readers) {
// Whatever the lookup misses is absent to yarn as well, but only where no
// second reader opens the file behind its back.
if (readers === 'looked-up' && !(0, fs_1.existsSync)(path)) {
return null;
}
// Unconditional even for a `.yml`, which parses from the path below: this
// open is what classifies the fault, and neither parser carries a tolerance.
let raw;
try {
raw = (0, fs_1.readFileSync)(path, 'utf-8');
}
catch (e) {
// The CLI-arg pass spares ENOENT and EISDIR. A file the registry client
// also reads keeps only ENOENT, since a directory passes its lookup and
// then dies on the open. Anything left leaves no resolution to reproduce.
// See https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/util/rc.js#L64-L79
if (e?.code === 'ENOENT' ||
(e?.code === 'EISDIR' && readers === 'cli-rc')) {
return null;
}
throw new Error(`The yarn config at ${path} could not be read.`);
}
// A .yml goes to the failsafe YAML schema alone, with no lockfile grammar in
// front and no retry behind, which is why only a mapping declares anything.
if (path.endsWith('.yml')) {
const map = parseYarnrcAsYaml(path);
// yarn keeps `yarnPath` alone from a .yml that names one, dropping the CLI
// args read here.
// See https://github.com/yarnpkg/yarn/blob/740c38c3a962c30ddb344a919bbfb7065620714b/src/rc.js#L55-L70
return typeof map.get('yarnPath') === 'string' ? new Map() : map;
}
try {
return parseYarnrc(raw);
}
catch {
return parseYarnrcAsYaml(path);
}
}
/**
* Yarn's own fallback for an rc file its lockfile parser rejects, and its only
* parser for a `.yml`. The failsafe schema makes every scalar a string, and
* classic passes the schema alone where berry also passes `json: true`, so a
* duplicate key throws here rather than resolving last-wins.
*/
function parseYarnrcAsYaml(path) {
let loaded;
try {
loaded = (0, fileutils_1.readYamlFile)(path, { failsafe: true });
}
catch {
// Rejected by both parsers, where yarn rethrows the first error and dies
// rather than reading on without the file. Keep the parse error out of the
// message: it quotes the lines around the fault, which here can be
// credential material.
throw new Error(`The yarn config at ${path} could not be read.`);
}
const map = new Map();
// Yarn ignores a document that is not a mapping rather than failing, which is
// how an unquoted `registry https://host/` (one bare scalar to YAML) ends up
// declaring nothing.
if (!loaded || typeof loaded !== 'object' || Array.isArray(loaded)) {
return map;
}
for (const [key, value] of Object.entries(loaded)) {
// Anything the failsafe schema did not make a string is a nested block, and
// nothing read here is one. A `false` arriving as the truthy string 'false'
// is yarn's own behavior, not a loss.
if (typeof value === 'string') {
map.set(key, value);
}
}
return map;
}
/**
* Yarn's lockfile tokenizer. A bare word stops at `:`, which is why an unquoted
* URL value breaks the file: its `://` splits into three tokens.
*/
function* tokenizeYarnrc(input) {
let lastNewline = false;
while (input.length) {
let chop = 0;
if (input[0] === '\n' || input[0] === '\r') {
chop = input[0] === '\r' && input[1] === '\n' ? 2 : 1;
yield { type: 'newline' };
}
else if (input[0] === '#') {
const end = input.indexOf('\n');
chop = end === -1 ? input.length : end;
}
else if (input[0] === ' ') {
if (lastNewline) {
let size = 1;
while (input[size] === ' ') {
size++;
}
if (size % 2) {
throw new Error('Invalid number of spaces');
}
chop = size;
yield { type: 'indent', value: size / 2 };
}
else {
chop = 1;
}
}
else if (input[0] === '"') {
let i = 1;
for (; i < input.length; i++) {
if (input[i] === '"' &&
!(input[i - 1] === '\\' && input[i - 2] !== '\\')) {
i++;
break;
}
}
chop = i;
try {
yield { type: 'value', value: JSON.parse(input.slice(0, i)) };
}
catch {
yield { type: 'invalid' };
}
}
else if (/^[0-9]/.test(input)) {
const digits = /^[0-9]+/.exec(input)[0];
chop = digits.length;
yield { type: 'value', value: +digits };
}
else if (input.startsWith('true')) {
chop = 4;
yield { type: 'value', value: true };
}
else if (input.startsWith('false')) {
chop = 5;
yield { type: 'value', value: false };
}
else if (input[0] === ':') {
chop = 1;
yield { type: 'colon' };
}
else if (input[0] === ',') {
chop = 1;
yield { type: 'comma' };
}
else if (/^[a-zA-Z/.-]/.test(input)) {
let i = 0;
while (i < input.length && !': \n\r,'.includes(input[i])) {
i++;
}
chop = i;
yield { type: 'value', value: input.slice(0, i) };
}
else {
yield { type: 'invalid' };
}
if (!chop) {
throw new Error('Made no progress');
}
lastNewline = input[0] === '\n' || (input[0] === '\r' && input[1] === '\n');
input = input.slice(chop);
}
yield { type: 'eof' };
}
/**
* Yarn's lockfile parser, flattened: only top-level scalar settings are kept,
* since nothing read here is an object, but a nested block is still parsed so
* its presence does not discard the settings around it. Throws on anything
* yarn's parser rejects.
*/
function parseYarnrc(raw) {
// Yarn strips a UTF-8 BOM before tokenising (parse.js stripBOM); left in
// place it breaks the first name token, dropping the file to the YAML retry,
// which declares nothing for the yarn-grammar shape.
if (raw.charCodeAt(0) === 0xfeff) {
raw = raw.slice(1);
}
const tokens = tokenizeYarnrc(raw);
const map = new Map();
const next = () => tokens.next().value;
let token = next();
const parseLevel = (indent, keep) => {
while (true) {
if (token.type === 'newline') {
token = next();
if (!indent) {
continue;
}
if (token.type !== 'indent') {
return;
}
if (token.value !== indent) {
return;
}
token = next();
}
else if (token.type === 'indent') {
if (token.value !== indent) {
return;
}
token = next();
}
else if (token.type === 'eof') {
return;
}
else if (token.type === 'value') {
const keys = [String(token.value)];
token = next();
while (token.type === 'comma') {
token = next();
if (token.type !== 'value') {
throw new Error('Expected string');
}
keys.push(String(token.value));
token = next();
}
const wasColon = token.type === 'colon';
if (wasColon) {
token = next();
}
if (token.type === 'value') {
const value = token.value;
if (keep) {
for (const key of keys) {
map.set(key, value);
}
}
token = next();
}
else if (wasColon) {
parseLevel(indent + 1, false);
if (indent && token.type !== 'indent') {
return;
}
}
else {
throw new Error('Invalid value type');
}
}
else {
throw new Error(`Unknown token: ${token.type}`);
}
}
};
parseLevel(0, true);
return map;
}