UNPKG

nx

Version:

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

497 lines (496 loc) • 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPackageScope = getPackageScope; exports.nerfDart = nerfDart; exports.requestNerfDart = requestNerfDart; exports.normalizeNpmConfigKey = normalizeNpmConfigKey; exports.readNpmConfigEnv = readNpmConfigEnv; exports.mergeNpmConfigEnv = mergeNpmConfigEnv; exports.ignoresNpmConfigEnv = ignoresNpmConfigEnv; exports.setRegistry = setRegistry; exports.setScopedRegistry = setScopedRegistry; exports.setAuthToken = setAuthToken; exports.setAuthIdent = setAuthIdent; exports.setClientCertificate = setClientCertificate; exports.setCafile = setCafile; exports.setStrictSsl = setStrictSsl; exports.setProxies = setProxies; exports.ancestorDirectories = ancestorDirectories; exports.expandNpmEnvVars = expandNpmEnvVars; exports.escapeNpmEnvExpr = escapeNpmEnvExpr; exports.expandEnvVars = expandEnvVars; exports.expandYarnEnvVars = expandYarnEnvVars; exports.expandPnpmEnvVars = expandPnpmEnvVars; exports.bridgePnpmEnvVars = bridgePnpmEnvVars; exports.unresolvedPnpmEnvVars = unresolvedPnpmEnvVars; exports.pnpmEnvVarsResolve = pnpmEnvVarsResolve; exports.readEnvVar = readEnvVar; exports.readExpandedKey = readExpandedKey; exports.registryKeysFor = registryKeysFor; exports.hasCredentialFor = hasCredentialFor; exports.warnNativeCredential = warnNativeCredential; const path_1 = require("path"); const semver_1 = require("semver"); const logger_1 = require("../logger"); function getPackageScope(packageName) { if (packageName.startsWith('@')) { const slash = packageName.indexOf('/'); if (slash > 0) { return packageName.slice(0, slash); } } return null; } /** * Converts a registry URL into npm's nerf-dart key prefix (host + directory * path), e.g. `https://r.example.com/npm/` -> `//r.example.com/npm/`. * See https://github.com/npm/cli/blob/bb056c85059cfb39514614e31abba09f20ac1612/workspaces/config/lib/nerf-dart.js#L12-L17 */ function nerfDart(registryUrl) { try { const url = new URL(registryUrl); const dir = url.pathname.endsWith('/') ? url.pathname : url.pathname.slice(0, url.pathname.lastIndexOf('/') + 1); return `//${url.host}${dir}`; } catch { return null; } } /** * Where npm and pnpm both begin a lookup for `registry`, and what * registryKeysFor climbs from. Both append the trailing slash a registry path is * missing before darting (npm darts the request URI; pnpm does it in * getAuthHeaderByURI and pickSettingByUrl), so the walk starts at the request's * own directory and still reaches a setting pinned to `//h/api/npm/` for a * request to `https://h/api/npm`, which the plain dart begins above. * * A registry URL carrying a query or a fragment lands back on the plain dart: * npm builds its request URI by concatenation, so the package name joins the * query rather than the path, and its walk never reaches the deeper directory. */ function requestNerfDart(registry) { return nerfDart(registry.endsWith('/') ? registry : `${registry}/`); } /** * The setting name npm resolves an environment key to, null for a key npm does * not read. * See https://github.com/npm/cli/blob/bb056c85059cfb39514614e31abba09f20ac1612/workspaces/config/lib/index.js#L345-L356 */ function npmConfigSetting(envKey) { if (!/^npm_config_/i.test(envKey)) { return null; } const key = envKey.slice('npm_config_'.length); return key.startsWith('//') ? key : normalizeNpmConfigKey(key); } /** npm's key rewrite: non-leading `_` to `-`, then lowercased. */ function normalizeNpmConfigKey(key) { return key.replace(/(?!^)_/g, '-').toLowerCase(); } /** * The value npm resolves for `setting` out of an environment: the last non-empty * `npm_config_*` spelling wins (loadEnv). `setting` is the name npm looks the * value up under, so a scope npm rewrites (`@my_scope`) finds nothing. */ function readNpmConfigEnv(env, setting) { let value; for (const [key, candidate] of Object.entries(env)) { if (candidate && npmConfigSetting(key) === setting) { value = candidate; } } return value; } const BRIDGED_SETTINGS = new Set([ 'registry', 'ca', 'cafile', 'cert', 'key', 'strict-ssl', 'proxy', 'https-proxy', 'noproxy', ]); /** * Whether `setting` is one this module resolves on the package manager's behalf. * `userconfig` is deliberately absent: it selects npm's own config file rather * than a value the package manager resolves, and npm reading its own .npmrc is * outside what the overlay reproduces. */ function isBridgedSetting(setting) { return (setting.startsWith('//') || setting.endsWith(':registry') || BRIDGED_SETTINGS.has(setting)); } /** * Merges an npm_config_* overlay into the environment for a spawned npm, leaving * one non-empty spelling per setting: the overlay's where it carries the setting, * otherwise the ambient one npm itself would resolve. npm reads its env tier * last-write-wins over the received key order, and both macOS `/bin/sh` and npm's * own shell launcher rebuild that order, so a setting left spelled two ways * (`NPM_CONFIG_REGISTRY` beside `npm_config_registry`) goes to whichever one they * emit last instead of to the value resolved here. * * `managerIgnoresEnv` says which settings the package manager resolves without * reading `npm_config_*`. Bridged settings it answers true for are dropped even * where the overlay claims nothing: npm's env tier sits above every file, so * leaving one in place stops npm from reaching the .npmrc chain the package * manager itself resolved from. Settings outside the bridged set stay ambient * either way. */ function mergeNpmConfigEnv(baseEnv, overlay, managerIgnoresEnv = IGNORES_NONE) { const overlaid = new Set(Object.keys(overlay).map(npmConfigSetting).filter(Boolean)); const merged = {}; const keptSpelling = new Map(); for (const [key, value] of Object.entries(baseEnv)) { const setting = npmConfigSetting(key); if (setting === null) { merged[key] = value; continue; } // Even an empty ambient entry goes when the overlay carries the setting: a // Windows environment is case-insensitive, and only the first spelling in // it reaches the child, which would be this one rather than the overlay's. if (overlaid.has(setting)) { continue; } if (managerIgnoresEnv(setting) && isBridgedSetting(setting)) { continue; } // npm skips an empty value, so it neither overrides nor competes. if (!value) { merged[key] = value; continue; } const superseded = keptSpelling.get(setting); if (superseded !== undefined) { delete merged[superseded]; } keptSpelling.set(setting, key); merged[key] = value; } return { ...merged, ...overlay }; } const IGNORES_NONE = () => false; const IGNORES_ALL = () => true; // pnpm 11.6.0's readUrlScopedEnvConfig reads a `p?npm_config_//<dart>:<key>` // entry from the environment again, except `:tokenHelper`, which it refuses to // take from there. Named settings stay on pnpm's own `PNPM_CONFIG_*` prefix. const IGNORES_ALL_BUT_URL_SCOPED = (setting) => !setting.startsWith('//') || setting.endsWith(':tokenHelper'); /** * The settings the package manager resolves without reading `npm_config_*`, as * a predicate over setting names. A bridged setting it returns true for is one * the spawned npm never receives from the ambient environment; settings outside * the bridged set it has no say over. pnpm reads them all up to 10.x and stops at 11.0.0, * which switched to its own `PNPM_CONFIG_*` prefix, except that 11.6.0 restored * the URL-scoped credential keys; yarn berry has never read any; npm reads them by * definition, and bun reads them for the settings this module bridges. * * An undetermined or unparseable version answers false for every setting: * bridging is skipped or falls open there anyway, so the ambient environment * stays as it is. */ function ignoresNpmConfigEnv(packageManager, packageManagerVersion) { const version = packageManagerVersion ? (0, semver_1.parse)(packageManagerVersion) : null; if (!version) { return IGNORES_NONE; } switch (packageManager) { case 'npm': case 'bun': return IGNORES_NONE; case 'pnpm': if (version.major < 11) { return IGNORES_NONE; } return (0, semver_1.gte)(version, '11.6.0') ? IGNORES_ALL_BUT_URL_SCOPED : IGNORES_ALL; case 'yarn': return version.major >= 2 ? IGNORES_ALL : IGNORES_NONE; default: { // A new PackageManager member fails typecheck here until classified above; // callers outside a fall-open catch keep the ambient env instead of throwing. const _exhaustive = packageManager; return IGNORES_NONE; } } } function setRegistry(env, url) { env['npm_config_registry'] = url; } function setScopedRegistry(env, scope, url) { env[`npm_config_${scope}:registry`] = url; } /** * Keyed on the registry's own directory rather than the parent a path missing * its trailing slash darts to, so two registries under one parent keep separate * keys instead of handing each other's credential out. The same holds for the * two sinks below. */ function setAuthToken(env, registryUrl, token) { const dart = requestNerfDart(registryUrl); if (dart) { env[`npm_config_${dart}:_authToken`] = token; } } /** `_auth` carries base64(user:pass). */ function setAuthIdent(env, registryUrl, base64Ident) { const dart = requestNerfDart(registryUrl); if (dart) { env[`npm_config_${dart}:_auth`] = base64Ident; } } /** * npm presents a client certificate only when both halves are configured, so * they are set together; each is a path, not the material itself. * See https://github.com/npm/npm-registry-fetch/blob/v19.1.1/lib/auth.js#L170 */ function setClientCertificate(env, registryUrl, certfile, keyfile) { const dart = requestNerfDart(registryUrl); if (dart) { env[`npm_config_${dart}:certfile`] = certfile; env[`npm_config_${dart}:keyfile`] = keyfile; } } function setCafile(env, path) { env['npm_config_cafile'] = path; } function setStrictSsl(env, value) { env['npm_config_strict_ssl'] = String(value); } function setProxies(env, proxies) { if (proxies.httpProxy) { env['npm_config_proxy'] = proxies.httpProxy; } if (proxies.httpsProxy) { env['npm_config_https_proxy'] = proxies.httpsProxy; } if (proxies.noProxy) { env['npm_config_noproxy'] = proxies.noProxy; } } /** * Directories above `root` (exclusive), nearest first. yarn classic and berry * both read rc files from ancestor directories, which npm never sees because * its project-config walk stops at the first package.json. */ function ancestorDirectories(root) { const dirs = []; let current = (0, path_1.dirname)(root); while (true) { dirs.push(current); const parent = (0, path_1.dirname)(current); if (parent === current) { break; } current = parent; } return dirs; } const ENV_EXPR = /(?<!\\)(\\*)\$\{([^${}]+)\}/g; function replaceEnvExpr(value, resolve, { keepEscaped = false } = {}) { return value.replace(ENV_EXPR, (orig, esc, name) => { if (esc.length % 2) { return keepEscaped ? orig : orig.slice((esc.length + 1) / 2); } return esc.slice(esc.length / 2) + (resolve(name) ?? `$\{${name}}`); }); } const NPM_ENV_EXPR = /(?<!\\)(\\*)\$\{([^${}?]+)(\?)?\}/g; /** * Resolves `${VAR}` references to the value npm itself ends up with, escapes * consumed and the `${VAR?}` form falling back to an empty string. Use it to * predict what a value npm reads for itself becomes, not to produce one for it: * a bridged value goes through npm's own pass, which expandEnvVars accounts for. * The `${VAR?}` form only landed in npm 11.6.0, so against an older spawned npm * the prediction resolves a reference that npm itself would leave verbatim. * See https://github.com/npm/cli/blob/v11.16.0/workspaces/config/lib/env-replace.js */ function expandNpmEnvVars(value, env = process.env) { return value.replace(NPM_ENV_EXPR, (orig, esc, name, optional) => { if (esc.length % 2) { return orig.slice((esc.length + 1) / 2); } const fallback = optional ? '' : `$\{${name}}`; return esc.slice(esc.length / 2) + (env[name] ?? fallback); }); } /** * The value to bridge so that npm's own expansion produces `value`, for a * resolution that consumed the package manager's escapes rather than leaving * them for npm (a literal a reader never expanded, or an expander whose escape * rule is not npm's). Each reference gets an odd run of backslashes, which npm * halves back to what it started as. Only what npm's reader would act on is * escaped, so a `${` it leaves alone is not turned into a literal backslash. */ function escapeNpmEnvExpr(value) { return value.replace(NPM_ENV_EXPR, (_orig, esc, name, optional) => `${'\\'.repeat(esc.length * 2 + 1)}$\{${name}${optional ?? ''}}`); } /** * Expands `${VAR}` references from the environment the way npm/bun ini readers * do. Unknown variables are left verbatim. The result is bridged, so an escaped * reference keeps its escape for the spawned npm to consume. */ function expandEnvVars(value, env = process.env) { return replaceEnvExpr(value, (name) => env[name], { keepEscaped: true }); } const YARN_ENV_EXPR = /(\\*)\$\{([^}]+)\}/g; /** * Expands `${VAR}` the way yarn classic's own envReplace does, which parts from * npm's on both halves of its escape rule: an odd run of backslashes keeps * every one of them along with the reference, and an even one drops all of them * rather than half. A reference it resolves nothing for aborts yarn, so it * throws here into the caller's fall-open. * See https://github.com/yarnpkg/yarn/blob/v1.22.22/src/registries/npm-registry.js */ function expandYarnEnvVars(value, env = process.env) { return value.replace(YARN_ENV_EXPR, (orig, esc, name) => { if (esc.length % 2) { return orig; } const resolved = env[name]; if (resolved === undefined) { throw new Error(`Failed to replace env in config: ${orig}`); } return resolved; }); } const PNPM_ENV_DEFAULT = /([^:-]+)(:?)-(.+)/; /** * One `${...}` reference as pnpm's getEnvValue resolves it: the variable, or the * `${VAR-default}` fallback and its `${VAR:-default}` form, which falls back for * an empty value too and not just an unset one. Undefined for a reference pnpm * finds nothing for, which is what its two readers part ways over. */ function resolvePnpmEnvValue(name, env) { const matched = name.match(PNPM_ENV_DEFAULT); if (!matched) { return env[name]; } const [, variableName, colon, fallback] = matched; const resolved = env[variableName]; if (resolved === undefined) { return fallback; } return !resolved && colon ? fallback : resolved; } /** * Expands `${VAR}` the way pnpm's @pnpm/config.env-replace does. A reference * that resolves to nothing becomes an empty string, matching the envReplaceLossy * reader pnpm takes its config through from 11.0.0; keeping it verbatim would * put a literal `${VAR}` on the wire as if it were a credential. Below 11 the * reader throws instead and the whole file goes with it (readPnpmNpmrcMap), so * on that line nothing reaching this carries an unresolvable reference. * * This is what pnpm itself ends up with, escapes consumed. Use it for a key, * which nothing expands a second time, and for a value compared against pnpm's * own resolution; a value handed to the spawned npm goes through * bridgePnpmEnvVars instead. */ function expandPnpmEnvVars(value, env = process.env) { return replaceEnvExpr(value, (name) => resolvePnpmEnvValue(name, env) ?? ''); } /** * The same expansion in the form to hand the spawned npm: every `${VAR}` left in * what pnpm resolved is escaped, so npm reproduces it instead of expanding a * reference pnpm would have sent literally, whether that reference is one pnpm * kept escaped or one a variable's own value carries. * * The escaping runs over pnpm's whole result rather than per reference, because * a resolved value ending in a backslash joins the escape run of the reference * behind it. Escaping each piece on its own leaves npm reading the merged run, * whose parity says expand where pnpm's said keep. */ function bridgePnpmEnvVars(value, env = process.env) { return escapeNpmEnvExpr(expandPnpmEnvVars(value, env)); } /** The `${VAR}` references in `value` that pnpm's throwing reader dies on. */ function unresolvedPnpmEnvVars(value, env = process.env) { const unresolved = []; replaceEnvExpr(value, (name) => { const resolved = resolvePnpmEnvValue(name, env); if (resolved === undefined) { unresolved.push(`$\{${name}}`); } return resolved ?? ''; }); return unresolved; } /** Whether every `${VAR}` in `value` is one pnpm's throwing reader gets past. */ function pnpmEnvVarsResolve(value, env = process.env) { return unresolvedPnpmEnvVars(value, env).length === 0; } function readEnvVar(env, name) { return env[name] ?? env[name.toLowerCase()] ?? env[name.toUpperCase()]; } /** * Reads `map` under `setting`, matching how npm and pnpm both expand a `${VAR}` * in an .npmrc key before they look a value up under it; `setting` is already * the resolved form to match. Both readers assign in file order, so the last key * that `expand` turns into `setting` wins, a literal one included. */ function readExpandedKey(map, setting, expand) { let matched; for (const [rawKey, value] of map) { const expanded = rawKey.includes('${') ? expand(rawKey) : rawKey; if (expanded === setting) { matched = value; } } return matched; } /** * The registry keys npm looks a setting up under for `dart`, nearest first: it * strips one path segment at a time until only the host is left, which covers * the key spelled with and without its trailing slash. * See https://github.com/npm/npm-registry-fetch/blob/v18.0.2/lib/auth.js#L16-L26 */ function registryKeysFor(dart) { const keys = []; let regKey = dart; while (regKey.length > '//'.length) { keys.push(regKey); regKey = regKey.replace(/([^/]+|\/)$/, ''); } return keys; } /** * Whether npm would find a credential for `dart` among the values `read` * exposes, at the dart or at any parent of it. * See https://github.com/npm/npm-registry-fetch/blob/v18.0.2/lib/auth.js#L34-L49 */ function hasCredentialFor(dart, read) { return registryKeysFor(dart).some((regKey) => read(`${regKey}:_authToken`) || read(`${regKey}:_auth`) || (read(`${regKey}:username`) && read(`${regKey}:_password`)) || (read(`${regKey}:certfile`) && read(`${regKey}:keyfile`))); } const warnedNativeCredentials = new Set(); /** * npm reads the user's own .npmrc chain and the overlay cannot switch that off, * so npm can authenticate on a registry the package manager resolved but would * have queried anonymously. The fetch still succeeds, so nothing else reports it. * Warn only where the overlay is what sent npm to that registry: left to itself * npm would have used its own resolution and the same credentials, which is what * the user gets from npm anywhere else. * * `remediation` closes the message, because what the user can safely do about it * depends on whether the package manager reads .npmrc at all. */ function warnNativeCredential(env, dart, packageManager, remediation, npmVisible) { // Per registry: one migrate resolves several packages, and a scoped one can // send npm to a registry no earlier package reached. const warned = `${packageManager}\0${dart}`; if (warnedNativeCredentials.has(warned) || env['npm_config_registry'] === undefined || // A credential the overlay carries is the package manager's own and // outranks the file, so npm sending it reproduces rather than diverges. hasCredentialFor(dart, (key) => env[`npm_config_${key}`]) || !hasCredentialFor(dart, npmVisible)) { return; } warnedNativeCredentials.add(warned); logger_1.logger.warn(`npm will send the credential your .npmrc holds for ${dart} when fetching packages. ${packageManager} would not send it for this request. ${remediation}`); }