nx
Version:
258 lines (257 loc) • 11.1 kB
JavaScript
;
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`);
}
}
}