UNPKG

nx

Version:

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

258 lines (257 loc) • 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getBunSpawnRegistryEnv = getBunSpawnRegistryEnv; const path_1 = require("path"); const semver_1 = require("semver"); const bunfig_1 = require("../package-manager-config/bunfig"); const npmrc_1 = require("../package-manager-config/npmrc"); const utils_1 = require("./utils"); /* * bun registry resolution (verified on 1.2.23 and 1.3.14, gates bisected): * * CLI --registry > BUN_CONFIG_REGISTRY > NPM_CONFIG_REGISTRY > * npm_config_registry > project .npmrc > global ($XDG_CONFIG_HOME else * $HOME)/.npmrc > project bunfig.toml > global bunfig > registry.npmjs.org. * .npmrc beats bunfig at every level, including scoped keys. Env registry * values must start with http(s):// or they are ignored. .npmrc support * exists from bun 1.1.18; [install].ca/cafile from 1.1.31. When * XDG_CONFIG_HOME is set, bun reads $XDG_CONFIG_HOME/.npmrc INSTEAD of * ~/.npmrc, which npm always reads. * See https://github.com/oven-sh/bun/blob/bun-v1.2.23/src/install/PackageManager.zig#L791 * * The final default/scoped registry is always injected: bun does not read * npm-only surfaces (e.g. $PREFIX/etc/npmrc), so npm must not fall back to * them. */ const BUN_DEFAULT_REGISTRY = 'https://registry.npmjs.org/'; function getBunSpawnRegistryEnv(packageName, root, bunVersion) { const env = {}; const scope = (0, utils_1.getPackageScope)(packageName); const npmrcSupported = !bunVersion || (0, semver_1.gte)(bunVersion, '1.1.18'); const tlsSupported = !bunVersion || (0, semver_1.gte)(bunVersion, '1.1.31'); const globalConfigBase = (0, bunfig_1.getBunGlobalConfigBase)(process.env); const projectNpmrc = npmrcSupported ? readBunNpmrcMap((0, path_1.join)(root, '.npmrc')) : null; const globalNpmrc = npmrcSupported && globalConfigBase ? readBunNpmrcMap((0, path_1.join)(globalConfigBase, '.npmrc')) : null; const projectBunfig = readBunfigInstall((0, path_1.join)(root, 'bunfig.toml')); const globalBunfig = globalConfigBase ? readBunfigInstall((0, path_1.join)(globalConfigBase, '.bunfig.toml')) : null; // Swap npm's userconfig so the auth/TLS keys npm reads natively come from // the same file bun would use. if (npmrcSupported && process.env.XDG_CONFIG_HOME) { env['npm_config_userconfig'] = (0, path_1.join)(process.env.XDG_CONFIG_HOME, '.npmrc'); } const envRegistry = [ process.env.BUN_CONFIG_REGISTRY, process.env.NPM_CONFIG_REGISTRY, process.env.npm_config_registry, ].find((value) => value && /^https?:\/\//.test(value)); // An .npmrc registry goes through both of bun's expansions: its ini reader // resolves `${VAR}` anywhere in the value, and the scope it then builds // resolves a whole-value `$VAR`. Without the second, `registry=$MY_REGISTRY` // stays a literal npm rejects as an invalid URL. const npmrcRegistryValue = (key) => { const raw = projectNpmrc?.get(key) ?? globalNpmrc?.get(key); return raw === undefined ? undefined : expandBunRegistryUrl((0, utils_1.expandEnvVars)(raw)); }; const bunfigValue = (read, fallbackUrl) => { for (const install of [projectBunfig, globalBunfig]) { if (!install) { continue; } const value = read(install); if (value !== undefined) { return normalizeBunRegistryValue(value, fallbackUrl); } } return undefined; }; const npmrcRegistry = npmrcRegistryValue('registry'); const defaultPick = envRegistry ? { url: envRegistry } : npmrcRegistry ? { url: npmrcRegistry } : (bunfigValue((install) => install.registry, BUN_DEFAULT_REGISTRY) ?? { url: BUN_DEFAULT_REGISTRY, }); (0, utils_1.setRegistry)(env, defaultPick.url); applyBunAuth(env, defaultPick); if (scope) { const npmrcScoped = npmrcRegistryValue(`${scope}:registry`); // [install.scopes] keys are accepted with or without the leading @. const scopedPick = npmrcScoped ? { url: npmrcScoped } : (bunfigValue((install) => install.scopes?.[scope] ?? install.scopes?.[scope.slice(1)], // A scope entry that declares only credentials keeps the default // registry as its url, so its token still reaches the right host. defaultPick.url) ?? defaultPick); (0, utils_1.setScopedRegistry)(env, scope, scopedPick.url); if (scopedPick !== defaultPick) { applyBunAuth(env, scopedPick); } } if (tlsSupported) { // .npmrc TLS keys beat bunfig's; npm reads the npmrc family natively. const npmrcHasTls = projectNpmrc?.has('cafile') || projectNpmrc?.has('ca') || globalNpmrc?.has('cafile') || globalNpmrc?.has('ca'); if (!npmrcHasTls) { const cafile = projectBunfig?.cafile ?? globalBunfig?.cafile; if (cafile) { // bun resolves cafile relative to the project dir. (0, utils_1.setCafile)(env, (0, path_1.resolve)(root, cafile)); } else { const ca = projectBunfig?.ca ?? globalBunfig?.ca; if (ca) { // npm reconstructs a config array from the env by splitting the // value on a blank line. env['npm_config_ca'] = Array.isArray(ca) ? ca.join('\n\n') : ca; } } } } return env; } // bun silently resolves as though an .npmrc it cannot read were absent // (verified on 1.3.13, EACCES and EISDIR both), so collapse that state the // same way rather than warning or aborting. function readBunNpmrcMap(path) { const map = (0, npmrc_1.readNpmrcMap)(path); return map === 'unreadable' ? null : map; } function normalizeBunRegistryValue(value, fallbackUrl) { if (typeof value !== 'string') { const result = { url: value.url ? expandBunRegistryUrl(value.url) : fallbackUrl, }; for (const field of ['token', 'username', 'password']) { const expanded = expandBunAuthValue(value[field]); if (expanded !== undefined) { result[field] = expanded; } } return result; } const expanded = expandBunRegistryUrl(value); try { const url = new URL(expanded); if (url.username || url.password) { // bun treats a username with no password as no credentials at all. const credentials = url.username && url.password ? { username: url.username, password: url.password } : url.password ? // `https://:token@host/` carries a token as the bare password. { token: url.password } : {}; url.username = ''; url.password = ''; return { url: url.toString(), ...credentials }; } } catch { } return { url: expanded }; } // bun expands a whole-value `$VARNAME` (no braces) in bunfig credential fields // via env.getAuto; an unset var keeps the literal, and `${VAR}` is expanded // only in its .npmrc. function expandBunAuthValue(value) { if (value === undefined || value.length < 2 || value[0] !== '$') { return value; } return process.env[value.slice(1)] ?? value; } // Bun expands a `$`-prefixed bunfig registry URL (Scope.fromAPI): it looks up // the name with surrounding slashes trimmed and uses it only when the result is // longer than one character, else keeps the literal. function expandBunRegistryUrl(value) { if (!value || value[0] !== '$') { return value; } const name = value.slice(1).replace(/^\/+|\/+$/g, ''); const replaced = process.env[name]; return replaced && replaced.length > 1 ? replaced : value; } function applyBunAuth(env, value) { if (value.token) { (0, utils_1.setAuthToken)(env, value.url, value.token); } else if (value.username && value.password) { const dart = (0, utils_1.requestNerfDart)(value.url); if (dart) { env[`npm_config_${dart}:username`] = value.username; // npm expects _password base64-encoded. env[`npm_config_${dart}:_password`] = Buffer.from(value.password).toString('base64'); } } } function readBunfigInstall(path) { const parsed = (0, bunfig_1.readBunfigRaw)(path); if (parsed === null || parsed === 'unreadable') { return null; } if (parsed === 'invalid') { // The throw reaches the caller's fall-open. Skipping the file instead would // pin npm to the default registry as though the workspace configured none, // overriding a registry npm resolves from a file of its own. throw new Error(`The bunfig at ${path} could not be parsed.`); } const install = parsed.install; if (!install || typeof install !== 'object' || Array.isArray(install)) { return null; } validateBunfigInstall(install, path); return install; } /** * bun type-checks the [install] table before it resolves anything and aborts on * a value of the wrong shape. Reproduce that rather than carrying a number into * a URL parse or a path join, where it would throw somewhere unrelated or * bridge a value bun never accepted. */ function validateBunfigInstall(install, path) { const fail = (what) => { throw new Error(`The bunfig at ${path} declares ${what}.`); }; if (install.registry !== undefined) { validateBunRegistryValue(install.registry, 'install.registry', fail); } if (install.scopes !== undefined) { if (typeof install.scopes !== 'object' || Array.isArray(install.scopes)) { fail('an install.scopes that is not a table'); } for (const [key, value] of Object.entries(install.scopes)) { validateBunRegistryValue(value, `install.scopes["${key}"]`, fail); } } if (install.cafile !== undefined && typeof install.cafile !== 'string') { fail('an install.cafile that is not a string'); } if (install.ca !== undefined && typeof install.ca !== 'string' && !(Array.isArray(install.ca) && install.ca.every((entry) => typeof entry === 'string'))) { fail('an install.ca that is neither a string nor an array of strings'); } } function validateBunRegistryValue(value, setting, fail) { if (typeof value === 'string') { return; } if (typeof value !== 'object' || value === null || Array.isArray(value)) { fail(`a ${setting} that is neither a URL string nor a table`); } for (const field of ['url', 'token', 'username', 'password']) { const field_ = value[field]; if (field_ !== undefined && typeof field_ !== 'string') { fail(`a ${setting}.${field} that is not a string`); } } }