UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

613 lines (612 loc) • 27.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getYarnBerrySpawnRegistryEnv = getYarnBerrySpawnRegistryEnv; const fs_1 = require("fs"); const minimatch_1 = require("minimatch"); const os_1 = require("os"); const path_1 = require("path"); const semver_1 = require("semver"); const fileutils_1 = require("../fileutils"); const logger_1 = require("../logger"); const npmrc_1 = require("../package-manager-config/npmrc"); const utils_1 = require("./utils"); /* * yarn berry (2/3/4) registry resolution (verified on 2.4.2, 3.8.7, 4.16.0): * * - Sources, highest first: YARN_* env vars > project .yarnrc.yml > ancestor * .yarnrc.yml files (closer dir wins) > ~/.yarnrc.yml > defaults * (first-writer-wins for scalars; npmScopes/npmRegistries merge per entry). * See https://github.com/yarnpkg/berry/blob/a26895a80d2784a5be92c54d5e7622bc9b0864a5/packages/yarnpkg-core/sources/Configuration.ts#L1424 * - Fetch registry for @scope/pkg: npmScopes.<scope-without-@>.npmRegistryServer, * else npmRegistryServer (default https://registry.yarnpkg.com). * - .npmrc files are completely ignored. * * Because npm-visible surfaces are irrelevant to berry, the resolved values * are always injected (default and, for scoped targets, the scoped key), so a * stray .npmrc cannot steer the spawned npm away from what berry would do. * * Auth (npmAuthToken/npmAuthIdent at scope > npmRegistries[registry] > global) * lives in .yarnrc.yml, which npm cannot read; it is translated to nerf-darted * npm keys. TLS: caFilePath (v2/3) / httpsCaFilePath (v4), the * httpsCertFilePath/httpsKeyFilePath client-certificate pair (3.2.0 on), * per-host networkSettings (hostname globs, longest key first), enableStrictSsl * (global only). A matching networkSettings entry is read before the global * value of the same key, so for those keys alone it outranks the env vars too. * enableNetwork is resolved the same way but has no npm counterpart, so a host * berry refuses to reach is reported rather than reproduced. */ const BERRY_DEFAULT_REGISTRY = 'https://registry.yarnpkg.com'; // Berry rewrote its env-var expander in 4.13.0, tightening the variable-name // class from [\d\w_]+ (a leading _ or digit is valid) to [a-zA-Z]\w*. const BERRY_ENV_PARSER_REWRITE = '4.13.0'; // httpsCertFilePath/httpsKeyFilePath landed in 3.2.0, as an rc setting and // inside networkSettings at once. Below it berry aborts on the setting, so // there is no resolution left to reproduce. const BERRY_CLIENT_CERT_SETTINGS = '3.2.0'; const JSR_REGISTRY = 'https://npm.jsr.io'; function getYarnBerrySpawnRegistryEnv(packageName, root, yarnVersion) { const env = {}; const scope = (0, utils_1.getPackageScope)(packageName); const rcFiles = collectRcFiles(root); // berry v4 deep-merges map settings (npmScopes/npmRegistries/networkSettings) // per sub-key across rc files; v2/v3 take the whole entry from the highest- // priority file that defines the key, omitted sub-keys included as their // defaults. const deepMerge = (0, semver_1.major)(yarnVersion) >= 4; const resolveMapEntry = (pick) => { if (!deepMerge) { return firstDefinedIn(rcFiles, pick); } const merged = {}; let any = false; for (const key of [ 'npmRegistryServer', 'npmAuthToken', 'npmAuthIdent', 'npmAlwaysAuth', ]) { const value = firstDefinedIn(rcFiles, (c) => pick(c)?.[key]); if (value !== undefined) { merged[key] = value; any = true; } } return any ? merged : undefined; }; // Berry expands env vars in every string setting at parse time, so expand // before the URL parse and nerf-dart. An unexpanded // `npmRegistryServer: "${MY_REGISTRY}"` has no parseable host, so its auth // and TLS keys are never emitted. const legacy = (0, semver_1.lt)(yarnVersion, BERRY_ENV_PARSER_REWRITE); const defaultRegistry = expandBerryEnvVars(process.env['YARN_NPM_REGISTRY_SERVER'] ?? firstDefinedIn(rcFiles, (c) => c.npmRegistryServer) ?? BERRY_DEFAULT_REGISTRY, legacy); // npmScopes keys are scope names without the leading @. const scopeName = scope?.slice(1); const scopeEntry = scopeName ? resolveMapEntry((c) => c.npmScopes?.[scopeName]) : undefined; // A scope configured in npmScopes (even auth-only or empty) with no // npmRegistryServer routes to berry's shape default (registry.yarnpkg.com), // not the top-level npmRegistryServer. Berry seeds the scope entry with that // default and returns it directly. const scopeConfigured = scopeName !== undefined && rcFiles.some((f) => f.config.npmScopes !== undefined && // Own keys only: the parsed-YAML object carries Object.prototype, and a // scope named after one of its members (@constructor) is not configured. Object.hasOwn(f.config.npmScopes, scopeName)); const jsrDefault = scopeName === 'jsr' && !scopeConfigured && (0, semver_1.gte)(yarnVersion, '4.9.0'); const effectiveRegistry = scopeConfigured ? expandBerryEnvVars(scopeEntry?.npmRegistryServer ?? BERRY_DEFAULT_REGISTRY, legacy) : jsrDefault ? JSR_REGISTRY : defaultRegistry; (0, utils_1.setRegistry)(env, defaultRegistry); if (scope) { (0, utils_1.setScopedRegistry)(env, scope, effectiveRegistry); } // berry's getAuthConfiguration picks ONE config object by specificity and // reads token-then-ident from that object alone, so the tiers never mix. A // present-but-credential-less npmRegistries entry therefore emits no auth // rather than falling back to the global or env credentials. const registryKey = selectNpmRegistriesKey(rcFiles, effectiveRegistry); let authToken; let authIdent; let alwaysAuth = false; if (scopeEntry?.npmAuthToken !== undefined || scopeEntry?.npmAuthIdent !== undefined) { authToken = scopeEntry.npmAuthToken; authIdent = scopeEntry.npmAuthIdent; alwaysAuth = isBerryTrueBoolean(scopeEntry.npmAlwaysAuth); } else if (registryKey !== undefined) { const registryEntry = resolveMapEntry((c) => c.npmRegistries?.[registryKey]); authToken = registryEntry?.npmAuthToken; authIdent = registryEntry?.npmAuthIdent; alwaysAuth = isBerryTrueBoolean(registryEntry?.npmAlwaysAuth); } else { // berry's getAuthConfiguration ends on the merged configuration with no // source-tier tracking, so a global npmAuthToken/npmAuthIdent (any file // tier, the home rc included, or env) authenticates whichever registry the // other tiers selected. Reproduced deliberately for parity. authToken = process.env['YARN_NPM_AUTH_TOKEN'] ?? firstDefinedIn(rcFiles, (c) => c.npmAuthToken); authIdent = process.env['YARN_NPM_AUTH_IDENT'] ?? firstDefinedIn(rcFiles, (c) => c.npmAuthIdent); alwaysAuth = isBerryTrueBoolean(process.env['YARN_NPM_ALWAYS_AUTH'] ?? firstDefinedIn(rcFiles, (c) => c.npmAlwaysAuth)); } // Expand ${VAR} before use so the npmAuthIdent base64 decision and the // bridged value are computed on the real credentials, matching berry. // Env-sourced values are literal (no-op). authToken = expandBerryValue(authToken, legacy); authIdent = expandBerryValue(authIdent, legacy); // A scoped fetch authenticates (berry forces BEST_EFFORT); an unscoped fetch // only when npmAlwaysAuth is set on the selected config (npm view/pack leaves // berry's authType at CONFIGURATION). if (scope || alwaysAuth) { if (authToken) { (0, utils_1.setAuthToken)(env, effectiveRegistry, authToken); } else if (authIdent) { (0, utils_1.setAuthIdent)(env, effectiveRegistry, encodeIdent(authIdent, yarnVersion)); } } applyTls(env, rcFiles, effectiveRegistry, yarnVersion); // Unlike yarn classic there is no gate to consult here. Everything berry // would send is already in the overlay, which the warning checks against, so // it runs once the last of it (the client certificate) is in. The dart is the // one npm resolves for a request to this registry, since the overlay writes // there and the check climbs from it. const dart = (0, utils_1.requestNerfDart)(effectiveRegistry); if (dart) { (0, utils_1.warnNativeCredential)(env, dart, 'yarn', 'Remove that credential from .npmrc if npm should not authenticate there; yarn never reads that file.', npmrcReader(root)); } // Berry's expander consumes the backslash that escapes a reference, so what // it resolves can hold a `${VAR}` npm would go on to resolve for itself, // sending a credential berry keeps literal. Escaping every value it produced // hands npm the same text back. Keys are left as they are: a reference can // only reach one through a registry host, which is not a host berry resolves // either. for (const [key, value] of Object.entries(env)) { env[key] = (0, utils_1.escapeNpmEnvExpr)(value); } return env; } /** * Reads a key the way the spawned npm would from the .npmrc files berry ignores. * The env tier is left out on purpose: berry never reads npm_config_*, so the * spawn strips every bridged ambient one (mergeNpmConfigEnv), the nerf-darted * keys this reader is probed with are all bridged, and what berry does send * is in the overlay the warning checks separately. npm also reads a * <globalPrefix>/etc npmrc and its own builtin one, which are not enumerated * here: missing one only means the warning stays silent. The maps are read once * because the caller probes dozens of keys walking npm's credential ladder. */ function npmrcReader(root) { const maps = [(0, path_1.join)(root, '.npmrc'), (0, path_1.join)((0, os_1.homedir)(), '.npmrc')].map((path) => { const map = (0, npmrc_1.readNpmrcMap)(path); // npm silently treats an .npmrc it cannot read as absent; this reader only // mirrors npm's own view, so an unreadable file keeps the warning silent. return map === 'unreadable' ? null : map; }); return (key) => { for (const map of maps) { const value = map && (0, utils_1.readExpandedKey)(map, key, utils_1.expandNpmEnvVars); if (value !== undefined) { return value; } } return undefined; }; } function collectRcFiles(root) { // YARN_RC_FILENAME renames the files berry finds by walking up, but not the // home one: findFolderRcFile reads the constant instead of the setting. const rcName = process.env['YARN_RC_FILENAME'] ?? '.yarnrc.yml'; // berry looks the project and ancestor files up before reading them but opens // the home one outright, so a symlink loop is absent in the first group and // aborts yarn in the last (measured on 4.10.3). const paths = [ { path: (0, path_1.join)(root, rcName), lookedUp: true }, ...(0, utils_1.ancestorDirectories)(root).map((dir) => ({ path: (0, path_1.join)(dir, rcName), lookedUp: true, })), { path: (0, path_1.join)((0, os_1.homedir)(), '.yarnrc.yml'), lookedUp: false }, ]; const files = []; for (const { path, lookedUp } of paths) { if (lookedUp && !(0, fs_1.existsSync)(path)) { continue; } // An unreadable, corrupt or non-mapping rc file aborts yarn itself, so there // is no resolution left to reproduce. Every shape propagates to the caller's // fall-open; dropping the file instead would resolve to berry's default // registry while still bridging the credentials the other files declare. let config; try { // berry loads its rc files through @yarnpkg/parsers, which calls js-yaml // with the failsafe schema and json: true. That combination is what makes // `npmAuthToken: 12345` a string and a repeated key last-wins rather than // an error, so read them the same way instead of re-typing the scalars. config = (0, fileutils_1.readYamlFile)(path, { json: true, failsafe: true }); } catch (e) { // Nothing looked this one up, so an absent file surfaces as the read's // own ENOENT rather than as a missed lookup. if (!lookedUp && e?.code === 'ENOENT') { continue; } // The parse error quotes the lines around the fault, which in an rc file // is credential material, and the caller logs whatever reaches it. throw new Error(`The yarn rc file at ${path} could not be read.`); } // An empty or comment-only file is a valid rc that declares nothing. if (config === undefined || config === null) { continue; } if (typeof config !== 'object' || Array.isArray(config)) { throw new Error(`The yarn rc file at ${path} is not a settings mapping.`); } normalizeMapSettings(config, path); files.push({ path, config }); } return files; } /** * Berry types npmScopes/npmRegistries/networkSettings and their entries, and * rejects the wrong shape before it resolves anything: a null is tolerated (that * level keeps its defaults) but any other non-object aborts yarn with "must be * an object". Reproduce both, so a config berry refuses to run on never reads as * if berry had accepted it. Verified on 3.8.7 and 4.15.0. */ function normalizeMapSettings(config, path) { config.npmScopes = normalizeMapSetting(config.npmScopes, 'npmScopes', path); // npmRegistries is declared with normalizeKeys, so berry strips the trailing // slash off each key as it loads the map, and lets a colliding key win last // the way berry's Map does. const registries = normalizeMapSetting(config.npmRegistries, 'npmRegistries', path); config.npmRegistries = registries ? Object.fromEntries(Object.entries(registries).map(([key, entry]) => [ normalizeRegistryKey(key), entry, ])) : undefined; config.networkSettings = normalizeMapSetting(config.networkSettings, 'networkSettings', path); } function normalizeMapSetting(map, setting, path) { const entries = asBerryObject(map, setting, path); if (!entries) { return undefined; } for (const key of Object.keys(entries)) { // A null entry is still a configured key, with every sub-setting defaulted. entries[key] = asBerryObject(entries[key], `${setting}["${key}"]`, path) ?? {}; } return entries; } function asBerryObject(value, setting, path) { if (value === undefined || value === null) { return undefined; } if (typeof value !== 'object' || Array.isArray(value)) { throw new Error(`The yarn setting "${setting}" in ${path} is not an object.`); } return value; } /** * Berry looks the effective registry up in its merged npmRegistries map by exact * key first and only then with the scheme stripped, so the key has to be picked * across every rc file before any entry is read: an exact key in the home rc * still beats a scheme-less one in the project rc. */ function selectNpmRegistriesKey(rcFiles, registry) { const exact = normalizeRegistryKey(registry); // berry strips any scheme (not just http/https) for the second lookup. for (const key of [exact, exact.replace(/^[a-z]+:/, '')]) { if (rcFiles.some((f) => f.config.npmRegistries?.[key] !== undefined)) { return key; } } return undefined; } /** Berry's normalizeRegistry, which it also applies to every npmRegistries key. */ function normalizeRegistryKey(registry) { return registry.replace(/\/$/, ''); } /** * npm's `_auth` is base64(user:pass). Berry v2 stores npmAuthIdent already * base64-encoded; v3/v4 accept both and base64-encode at request time only * when the value still contains a `:`. */ function encodeIdent(ident, yarnVersion) { if ((0, semver_1.major)(yarnVersion) >= 3 && ident.includes(':')) { return Buffer.from(ident).toString('base64'); } return ident; } function applyTls(env, rcFiles, effectiveRegistry, yarnVersion) { const legacy = (0, semver_1.lt)(yarnVersion, BERRY_ENV_PARSER_REWRITE); const v4 = (0, semver_1.major)(yarnVersion) >= 4; // The other major's CA-setting name aborts berry, as an rc setting and as a // YARN_* env var alike, so only the name the running major accepts has // anything left to reproduce. const caKey = v4 ? 'httpsCaFilePath' : 'caFilePath'; const caEnvKey = v4 ? 'YARN_HTTPS_CA_FILE_PATH' : 'YARN_CA_FILE_PATH'; let host; try { host = new URL(effectiveRegistry).hostname; } catch { } // Berry's getNetworkSettings falls back to a global setting only after the // matching per-host entries, so a per-host value outranks the YARN_* env vars // while those still outrank the rc files. const network = resolveNetworkSettings(rcFiles, host, v4); // Berry expands env vars in path settings, then resolves an rc-sourced path // relative to that rc file's directory and an env-sourced one relative to the // directory yarn was invoked from. const resolvePath = (key, envKey) => { const path = network[key] ?? envPath(process.env[envKey]) ?? firstPathIn(rcFiles, key); return path ? (0, path_1.resolve)(path.baseDir, expandBerryEnvVars(path.value, legacy)) : undefined; }; const cafile = resolvePath(caKey, caEnvKey); if (cafile) { (0, utils_1.setCafile)(env, cafile); } if ((0, semver_1.gte)(yarnVersion, BERRY_CLIENT_CERT_SETTINGS)) { // Berry sets cert and key independently (httpUtils has no joint gate); a // lone half only fails there because Node cannot present a certificate // without its key, so it stays out of the overlay. const certfile = resolvePath('httpsCertFilePath', 'YARN_HTTPS_CERT_FILE_PATH'); const keyfile = resolvePath('httpsKeyFilePath', 'YARN_HTTPS_KEY_FILE_PATH'); if (certfile && keyfile) { (0, utils_1.setClientCertificate)(env, effectiveRegistry, certfile, keyfile); } } const enableNetwork = network.enableNetwork ?? process.env['YARN_ENABLE_NETWORK'] ?? firstDefinedIn(rcFiles, (c) => c.enableNetwork); if (host && enableNetwork !== undefined && isBerryFalseBoolean(enableNetwork)) { warnDisabledNetwork(host); } const strictSsl = process.env['YARN_ENABLE_STRICT_SSL'] ?? firstDefinedIn(rcFiles, (c) => c.enableStrictSsl); if (strictSsl !== undefined) { (0, utils_1.setStrictSsl)(env, !isBerryFalseBoolean(strictSsl)); } (0, utils_1.setProxies)(env, { httpProxy: expandBerryValue(network.httpProxy ?? process.env['YARN_HTTP_PROXY'] ?? firstDefinedIn(rcFiles, (c) => c.httpProxy), legacy), httpsProxy: expandBerryValue(network.httpsProxy ?? process.env['YARN_HTTPS_PROXY'] ?? firstDefinedIn(rcFiles, (c) => c.httpsProxy), legacy), }); } let warnedDisabledNetwork = false; /** * Berry refuses to reach a host it has enableNetwork off for and exits (verified * on 4.15.0: the registry is never contacted). The spawned npm has no such * setting and will make the request, so say so once rather than reproducing a * refusal that would leave the workspace un-migrated. */ function warnDisabledNetwork(host) { if (warnedDisabledNetwork) { return; } warnedDisabledNetwork = true; logger_1.logger.warn(`yarn is configured not to use the network for ${host} (enableNetwork is false), so yarn itself would refuse to fetch from it. Packages will still be fetched from that registry.`); } function envPath(value) { return value ? { value, baseDir: process.cwd() } : undefined; } function firstPathIn(rcFiles, key) { for (const file of rcFiles) { const value = file.config[key]; if (value) { return { value, baseDir: (0, path_1.dirname)(file.path) }; } } return undefined; } const BERRY_PATH_KEYS = [ 'caFilePath', 'httpsCaFilePath', 'httpsCertFilePath', 'httpsKeyFilePath', ]; // Reproduces berry getNetworkSettings. Berry builds the merged map lowest // priority first and its sort is stable, so two globs of equal length are // consulted in that same order. function resolveNetworkSettings(rcFiles, host, deepMerge) { const result = {}; if (!host) { return result; } const merged = new Map(); for (const file of [...rcFiles].reverse()) { const settings = file.config.networkSettings; if (!settings) { continue; } const baseDir = (0, path_1.dirname)(file.path); for (const [hostKey, entry] of Object.entries(settings)) { // Walking lowest priority first, a later file is the higher-priority one: // under v4 each sub-key it defines wins, and under v2/v3 it owns the whole // entry, so a lower file never contributes a missing sub-key there. const m = (deepMerge && merged.get(hostKey)) || {}; for (const key of BERRY_PATH_KEYS) { if (entry[key] != null) { m[key] = { value: entry[key], baseDir }; } } if (entry.enableNetwork != null) { m.enableNetwork = entry.enableNetwork; } if (entry.httpProxy != null) { m.httpProxy = entry.httpProxy; } if (entry.httpsProxy != null) { m.httpsProxy = entry.httpsProxy; } merged.set(hostKey, m); } } const matching = [...merged.entries()] .filter(([key]) => matchesHostGlob(host, key)) .sort((a, b) => b[0].length - a[0].length) .map(([, m]) => m); for (const m of matching) { for (const key of BERRY_PATH_KEYS) { result[key] ??= m[key]; } result.enableNetwork ??= m.enableNetwork; result.httpProxy ??= m.httpProxy; result.httpsProxy ??= m.httpsProxy; } return result; } /** * Berry matches a networkSettings key against the hostname with micromatch, * which parses two forms differently from minimatch: a bare `(a|b)` is an * extglob alternation rather than literal parentheses, and a leading `!` negates * the pattern only when it is not the start of a `!(...)` extglob. Left to * minimatch a `!(...)` key matches nearly every host, which would hand one * host's CA and proxy to the registry. */ function matchesHostGlob(host, key) { return (0, minimatch_1.minimatch)(host, key.replace(/(^|[^\\@!+*?])\(/g, '$1@('), { nonegate: key.startsWith('!('), }); } // Berry's miscUtils.replaceEnvVariables, applied to every string setting: // ${VAR}, ${VAR-default} (default when unset) and ${VAR:-default} (default when // unset or empty). Berry throws on an undefined bare ${VAR}, which aborts berry // itself, so a working workspace never has one; we leave the reference literal // rather than failing the migrate. // see https://github.com/yarnpkg/berry/blob/c5857bdee5737425b879492db5e2732a5e6e14f2/packages/yarnpkg-core/sources/miscUtils.ts#L473 function expandBerryEnvVars(value, legacy, env = process.env) { return legacy ? expandBerryEnvVarsLegacy(value, env) : scanBerryEnv(value, 0, env, false).text; } // Berry's pre-4.13 miscUtils.replaceEnvVariables. // See https://github.com/yarnpkg/berry/blob/%40yarnpkg/cli/4.12.0/packages/yarnpkg-core/sources/miscUtils.ts function expandBerryEnvVarsLegacy(value, env) { return value.replace(/\\?\$\{(?<variableName>[\d\w_]+)(?<colon>:)?(?:-(?<fallback>[^}]*))?\}/g, (match, ...args) => { if (match.startsWith('\\')) { return match.slice(1); } const { variableName, colon, fallback } = args[args.length - 1]; const resolved = env[variableName]; if (resolved || (Object.hasOwn(env, variableName) && !colon)) { return resolved; } return fallback ?? match; }); } // Scans from `start`, expanding env-var references. When `nested` is set the // scan also stops just after the unescaped `}` that closes an enclosing default. function scanBerryEnv(input, start, env, nested) { let text = ''; let i = start; while (i < input.length) { const c = input[i]; if (c === '\\' && i + 1 < input.length && '\\$}'.includes(input[i + 1])) { text += input[i + 1]; i += 2; } else if (nested && c === '}') { return { text, end: i + 1 }; } else if (c === '$' && input[i + 1] === '{') { const ref = parseBerryRef(input, i, env); if (ref) { text += ref.text; i = ref.end; } else { // Malformed `${...` (berry would throw); emit it literally and continue. text += '${'; i += 2; } } else { text += c; i += 1; } } return { text, end: i }; } // Parses one ${NAME} / ${NAME-default} / ${NAME:-default} at `start`, which // must be the `${`. The default is scanned brace-balanced so a nested ${...} is // captured whole. function parseBerryRef(input, start, env) { const nameMatch = input.slice(start + 2).match(/^[a-zA-Z]\w*/); if (!nameMatch) { return null; } const name = nameMatch[0]; let i = start + 2 + name.length; const value = env[name]; if (input[i] === '}') { // ${NAME}: an undefined value aborts berry; leave the reference literal. return { text: value !== undefined ? value : input.slice(start, i + 1), end: i + 1, }; } let emptyIsUnset = false; if (input[i] === ':' && input[i + 1] === '-') { emptyIsUnset = true; i += 2; } else if (input[i] === '-') { i += 1; } else { return null; } // Parse (and expand) the default region even when the value wins, so `end` // skips past the matching close brace. const fallback = scanBerryEnv(input, i, env, true); const useValue = emptyIsUnset ? value !== undefined && value !== '' : value !== undefined; return { text: useValue ? value : fallback.text, end: fallback.end, }; } // Berry's miscUtils.parseBoolean false set for SettingsType.BOOLEAN. function isBerryFalseBoolean(value) { return value === false || value === 0 || value === 'false' || value === '0'; } // Its true set. Berry throws on a value in neither, which aborts berry itself, // so each side keeps the tolerance that is safe for its setting: an unparseable // enableStrictSsl keeps TLS verification on, an unparseable npmAlwaysAuth leaves // an unscoped fetch unauthenticated. function isBerryTrueBoolean(value) { return value === true || value === 1 || value === 'true' || value === '1'; } function expandBerryValue(value, legacy) { return value === undefined ? undefined : expandBerryEnvVars(value, legacy); } function firstDefinedIn(rcFiles, read) { for (const file of rcFiles) { const value = read(file.config); if (value !== undefined) { return value; } } return undefined; }