Files
bun-src/test/cli/install/config-precedence.test.ts
2026-08-27 21:09:14 +00:00

805 lines
36 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { existsSync, readFileSync, readdirSync } from "fs";
import { VerdaccioRegistry, bunEnv, bunExe, readdirSorted, tempDir } from "harness";
import { join } from "path";
// Own config dir: other install files' VerdaccioRegistry start()/stop() delete the shared htpasswd, invalidating our token.
const sharedRegistryDir = join(import.meta.dir, "registry");
const sharedVerdaccioConfig = readFileSync(join(sharedRegistryDir, "verdaccio.yaml"), "utf8");
if (!sharedVerdaccioConfig.includes("storage: ./packages")) {
throw new Error("registry/verdaccio.yaml no longer has a 'storage: ./packages' line to redirect");
}
const registryDir = tempDir("config-precedence-registry", {
"verdaccio.yaml": sharedVerdaccioConfig.replace(
"storage: ./packages",
`storage: ${JSON.stringify(join(sharedRegistryDir, "packages"))}`,
),
});
const registry = new VerdaccioRegistry({ configPath: join(String(registryDir), "verdaccio.yaml") });
let authToken: string;
beforeAll(async () => {
await registry.start();
authToken = await registry.generateUser("config-precedence", "verysecure");
});
afterAll(() => {
registry.stop();
registryDir[Symbol.dispose]();
});
const authLine = () => `//localhost:${registry.port}/:_authToken=${authToken}\n`;
const registryUrlWithoutSlash = () => `http://localhost:${registry.port}`;
const bunfig = (install: Record<string, unknown>) => Bun.TOML.stringify({ install });
const packageJson = (dependencies: Record<string, string>, extra: Record<string, unknown> = {}) =>
JSON.stringify({ name: "config-precedence", version: "1.0.0", dependencies, ...extra });
/** A registry that must never be contacted. */
function deadRegistry() {
let hits = 0;
const server = Bun.serve({
port: 0,
fetch() {
hits++;
return new Response("wrong registry", { status: 500 });
},
});
return {
server,
url: `http://localhost:${server.port}/`,
get hits() {
return hits;
},
[Symbol.dispose]() {
server.stop(true);
},
};
}
/** Forwards to verdaccio while recording every Authorization header it receives. */
function capturingRegistry() {
const authorizations: (string | null)[] = [];
const upstream = registry.registryUrl();
const server = Bun.serve({
port: 0,
async fetch(req) {
authorizations.push(req.headers.get("authorization"));
const { pathname, search } = new URL(req.url);
const res = await fetch(new URL(pathname.slice(1) + search, upstream), {
headers: { authorization: req.headers.get("authorization") ?? "" },
});
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
// Tarball URLs in the manifest must point back here or bun (correctly) withholds credentials from the other origin.
const body = contentType.includes("json")
? (await res.text()).replaceAll(upstream, url)
: await res.arrayBuffer();
return new Response(body, { status: res.status, headers: { "content-type": contentType } });
},
});
const url = `http://localhost:${server.port}/`;
return {
port: server.port,
url,
authorizations,
[Symbol.dispose]() {
server.stop(true);
},
};
}
async function run(root: string, cmd: string[], env: Record<string, string | undefined> = {}) {
const home = join(root, "home");
const fullEnv: Record<string, string | undefined> = {
...bunEnv,
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: home,
BUN_INSTALL_CACHE_DIR: join(root, "cache"),
...env,
};
for (const key in fullEnv) if (fullEnv[key] === undefined) delete fullEnv[key];
await using proc = Bun.spawn({
cmd: [bunExe(), ...cmd],
cwd: join(root, "project"),
env: fullEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}
const install = (root: string, args: string[] = [], env?: Record<string, string | undefined>) =>
run(root, ["install", ...args], env);
const isIsolated = (root: string) => existsSync(join(root, "project", "node_modules", ".bun"));
const installed = (root: string, ...pkg: string[]) =>
existsSync(join(root, "project", "node_modules", ...pkg, "package.json"));
const projectPackageJson = (root: string) => JSON.parse(readFileSync(join(root, "project", "package.json"), "utf8"));
const postinstallRan = (root: string) =>
existsSync(join(root, "project", "node_modules", "lifecycle-postinstall", "postinstall.txt"));
const hiddenHoistDir = (root: string) => join(root, "project", "node_modules", ".bun", "node_modules");
describe.concurrent("bun install config precedence", () => {
test("project bunfig linker beats ~/.npmrc install-strategy", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "install-strategy=hoisted\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), linker: "isolated" }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(isIsolated(String(dir))).toBe(true);
expect(exitCode).toBe(0);
});
test("project bunfig registry beats project .npmrc registry", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${dead.url}\n`,
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "no-deps", "package.json"))).toBe(true);
expect(exitCode).toBe(0);
});
test("project bunfig registry beats ~/.npmrc registry", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `registry=${dead.url}\n`,
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "no-deps", "package.json"))).toBe(true);
expect(exitCode).toBe(0);
});
test("project .npmrc registry beats ~/.npmrc registry", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `registry=${dead.url}\n`,
"project/.npmrc": `registry=${registry.registryUrl()}\n`,
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "no-deps", "package.json"))).toBe(true);
expect(exitCode).toBe(0);
});
test("~/.npmrc _authToken applies to the registry set in project bunfig", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": authLine(),
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(existsSync(join(String(dir), "project", "node_modules", "@needs-auth", "test-pkg", "package.json"))).toBe(
true,
);
expect(exitCode).toBe(0);
});
test("project .npmrc scoped registry and token apply alongside a bunfig registry", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `@needs-auth:registry=${registry.registryUrl()}\n${authLine()}`,
"project/bunfig.toml": bunfig({ registry: dead.url }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "@needs-auth", "test-pkg", "package.json"))).toBe(
true,
);
expect(exitCode).toBe(0);
});
test("bunfig scoped registry keeps credentials from ~/.npmrc", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": authLine(),
"project/bunfig.toml": bunfig({ registry: dead.url, scopes: { "needs-auth": registry.registryUrl() } }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "@needs-auth", "test-pkg", "package.json"))).toBe(
true,
);
expect(exitCode).toBe(0);
});
test("~/.npmrc registry= plus its _authToken still authenticate the registry set in project bunfig", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `registry=${dead.url}\n${authLine()}`,
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("project .npmrc _authToken applies when bunfig spells the same registry with a trailing slash", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${registryUrlWithoutSlash()}\n${authLine()}`,
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("project .npmrc scoped _authToken applies when bunfig spells the scope registry with a trailing slash", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `@needs-auth:registry=${registryUrlWithoutSlash()}\n${authLine()}`,
"project/bunfig.toml": bunfig({ registry: dead.url, scopes: { "needs-auth": registry.registryUrl() } }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig registry token beats the _authToken for the same registry in ~/.npmrc", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `//localhost:${capture.port}/:_authToken=token-from-npmrc\n`,
"project/bunfig.toml": bunfig({ registry: { url: capture.url, token: authToken } }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig scoped registry beats the same scope in .npmrc", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `@types:registry=${dead.url}\n`,
"project/bunfig.toml": bunfig({ registry: dead.url, scopes: { types: registry.registryUrl() } }),
"project/package.json": packageJson({ "@types/no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "@types", "no-deps", "package.json"))).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig registry credentials survive a same-URL registry= line in project .npmrc", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${registry.registryUrl()}\n`,
"project/bunfig.toml": bunfig({ registry: { url: registry.registryUrl(), token: authToken } }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(existsSync(join(String(dir), "project", "node_modules", "@needs-auth", "test-pkg", "package.json"))).toBe(
true,
);
expect(exitCode).toBe(0);
});
test("bunfig scoped registry credentials survive the same scope URL in project .npmrc", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `@needs-auth:registry=${registry.registryUrl()}\n`,
"project/bunfig.toml": bunfig({
registry: dead.url,
scopes: { "needs-auth": { url: registry.registryUrl(), token: authToken } },
}),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(existsSync(join(String(dir), "project", "node_modules", "@needs-auth", "test-pkg", "package.json"))).toBe(
true,
);
expect(exitCode).toBe(0);
});
// `registry = "<url>"` and `registry = { url = "<url>" }` must read credentials
// written into the URL the same way.
const registryForms: [form: string, registry: (url: string) => unknown][] = [
["string", url => url],
["object", url => ({ url })],
];
const userBasicAuth = `Basic ${Buffer.from("config-precedence:verysecure").toString("base64")}`;
test.each(registryForms)("bunfig registry %s sends the user:password written into its URL", async (_, registry) => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/bunfig.toml": bunfig({
registry: registry(`http://config-precedence:verysecure@localhost:${capture.port}/`),
}),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([userBasicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test.each(registryForms)("bunfig registry %s sends the :token written into its URL", async (_, registry) => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/bunfig.toml": bunfig({ registry: registry(`http://:token-from-url@localhost:${capture.port}/`) }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set(["Bearer token-from-url"]));
expect(installed(String(dir), "no-deps")).toBe(true);
expect(exitCode).toBe(0);
});
test.each(registryForms)(
"bunfig scoped registry %s sends the user:password written into its URL",
async (_, registry) => {
using dead = deadRegistry();
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/bunfig.toml": bunfig({
registry: dead.url,
scopes: { "needs-auth": registry(`http://config-precedence:verysecure@localhost:${capture.port}/`) },
}),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(new Set(capture.authorizations)).toStrictEqual(new Set([userBasicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
},
);
// Credential keys replace the credentials written into the URL as a set.
// `authToken` is only known after beforeAll, hence the thunks.
const keyedRegistries: [
name: string,
registry: (port: number) => Record<string, string>,
expectedAuthorization: () => string,
][] = [
[
"username/password keys beat the user:password written into its URL",
port => ({
url: `http://user-from-url:password-from-url@localhost:${port}/`,
username: "config-precedence",
password: "verysecure",
}),
() => userBasicAuth,
],
[
"username/password keys beat the :token written into its URL",
port => ({
url: `http://:token-from-url@localhost:${port}/`,
username: "config-precedence",
password: "verysecure",
}),
() => userBasicAuth,
],
[
"token key beats the user:password written into its URL",
port => ({ url: `http://user-from-url:password-from-url@localhost:${port}/`, token: authToken }),
() => `Bearer ${authToken}`,
],
];
test.each(keyedRegistries)("bunfig registry object %s", async (_, registry, expectedAuthorization) => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/bunfig.toml": bunfig({ registry: registry(capture.port) }),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([expectedAuthorization()]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("credentials written into a bunfig registry object URL beat the _authToken for the same registry in ~/.npmrc", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `//localhost:${capture.port}/:_authToken=token-from-npmrc\n`,
"project/bunfig.toml": bunfig({
registry: { url: `http://config-precedence:verysecure@localhost:${capture.port}/` },
}),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([userBasicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("--linker beats ~/.npmrc, project .npmrc and bunfig", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "install-strategy=hoisted\n",
"project/.npmrc": "install-strategy=hoisted\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), linker: "hoisted" }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), ["--linker", "isolated"]);
expect(stderr).not.toContain("error:");
expect(isIsolated(String(dir))).toBe(true);
expect(exitCode).toBe(0);
});
test("~/.npmrc install-strategy applies when bunfig does not set a linker", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "install-strategy=linked\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl() }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(isIsolated(String(dir))).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig exact = true beats project .npmrc save-exact=false", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": "save-exact=false\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), exact: true }),
"project/package.json": packageJson({}),
});
const { stderr, exitCode } = await run(String(dir), ["add", "no-deps"]);
expect(stderr).not.toContain("error:");
expect(projectPackageJson(String(dir)).dependencies).toStrictEqual({ "no-deps": "2.0.0" });
expect(exitCode).toBe(0);
});
test("bunfig exact = false beats ~/.npmrc save-exact=true", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "save-exact=true\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), exact: false }),
"project/package.json": packageJson({}),
});
const { stderr, exitCode } = await run(String(dir), ["add", "no-deps"]);
expect(stderr).not.toContain("error:");
expect(projectPackageJson(String(dir)).dependencies).toStrictEqual({ "no-deps": "^2.0.0" });
expect(exitCode).toBe(0);
});
test("bunfig ignoreScripts = true beats project .npmrc ignore-scripts=false", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": "ignore-scripts=false\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), ignoreScripts: true }),
"project/package.json": packageJson(
{ "lifecycle-postinstall": "1.0.0" },
{ trustedDependencies: ["lifecycle-postinstall"] },
),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(installed(String(dir), "lifecycle-postinstall")).toBe(true);
expect(postinstallRan(String(dir))).toBe(false);
expect(exitCode).toBe(0);
});
test("bunfig ignoreScripts = false beats ~/.npmrc ignore-scripts=true", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "ignore-scripts=true\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), ignoreScripts: false }),
"project/package.json": packageJson(
{ "lifecycle-postinstall": "1.0.0" },
{ trustedDependencies: ["lifecycle-postinstall"] },
),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(postinstallRan(String(dir))).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig publicHoistPattern beats project .npmrc public-hoist-pattern", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": "public-hoist-pattern=no-deps\n",
"project/bunfig.toml": bunfig({
registry: registry.registryUrl(),
linker: "isolated",
publicHoistPattern: "*types*",
}),
"project/package.json": packageJson({ "two-range-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(await readdirSorted(join(String(dir), "project", "node_modules"))).toStrictEqual([
".bun",
"@types",
"two-range-deps",
]);
expect(exitCode).toBe(0);
});
test("bunfig hoistPattern beats ~/.npmrc hoist-pattern", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "hoist-pattern=@types/*\n",
"project/bunfig.toml": bunfig({
registry: registry.registryUrl(),
linker: "isolated",
hoistPattern: "no-deps",
}),
"project/package.json": packageJson({ "two-range-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(await readdirSorted(hiddenHoistDir(String(dir)))).toStrictEqual(["no-deps"]);
expect(exitCode).toBe(0);
});
test("bunfig hoist = true beats project .npmrc hoist=false", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": "hoist=false\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), linker: "isolated", hoist: true }),
"project/package.json": packageJson({ "two-range-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(await readdirSorted(hiddenHoistDir(String(dir)))).toStrictEqual(["@types", "no-deps", "two-range-deps"]);
expect(exitCode).toBe(0);
});
test("bunfig hoist = false beats ~/.npmrc hoist=true", async () => {
using dir = tempDir("config-precedence", {
"home/.npmrc": "hoist=true\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), linker: "isolated", hoist: false }),
"project/package.json": packageJson({ "two-range-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(installed(String(dir), "two-range-deps")).toBe(true);
expect(existsSync(hiddenHoistDir(String(dir)))).toBe(false);
expect(exitCode).toBe(0);
});
test("scopes from .npmrc and bunfig are merged", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `@types:registry=${registry.registryUrl()}\n`,
"project/bunfig.toml": bunfig({
registry: dead.url,
scopes: { "needs-auth": { url: registry.registryUrl(), token: authToken } },
}),
"project/package.json": packageJson({ "@types/no-deps": "1.0.0", "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(installed(String(dir), "@types", "no-deps")).toBe(true);
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig install.cache.dir beats project .npmrc cache=", async () => {
using dir = tempDir("config-precedence", {
"project/.npmrc": "cache=../npmrc-cache\n",
"project/bunfig.toml": bunfig({ registry: registry.registryUrl(), cache: { dir: "../bunfig-cache" } }),
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], { BUN_INSTALL_CACHE_DIR: undefined });
expect(stderr).not.toContain("error:");
expect(readdirSync(join(String(dir), "bunfig-cache")).filter(name => name.startsWith("no-deps@"))).toStrictEqual([
"[email protected]@@localhost@@@1",
]);
expect(existsSync(join(String(dir), "npmrc-cache"))).toBe(false);
expect(installed(String(dir), "no-deps")).toBe(true);
expect(exitCode).toBe(0);
});
test("bunfig registry username/password survive a same-URL registry= line in project .npmrc", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${capture.url}\n`,
"project/bunfig.toml": bunfig({
registry: { url: capture.url, username: "config-precedence", password: "verysecure" },
}),
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir));
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(
new Set([`Basic ${Buffer.from("config-precedence:verysecure").toString("base64")}`]),
);
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
// `http://user:pass@host/` in a registry URL is sent as Basic auth and `http://:token@host/` as a Bearer
// token, however the URL reaches bun. The .npmrc / bunfig string forms already did this; these cover
// --registry and the registry env vars, which used to drop the credentials.
const basicAuth = `Basic ${Buffer.from("config-precedence:verysecure").toString("base64")}`;
const withUserinfo = (capture: { port: number }, userinfo: string) => `http://${userinfo}@localhost:${capture.port}/`;
test("--registry with user:pass@ in the URL sends Basic auth", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [
"--registry",
withUserinfo(capture, "config-precedence:verysecure"),
]);
expect(stderr).not.toContain("error:");
expect(stderr).not.toContain("verysecure");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([basicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("--registry with :token@ in the URL sends it as a Bearer token", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), ["--registry", withUserinfo(capture, `:${authToken}`)]);
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("BUN_CONFIG_TOKEN beats credentials embedded in the --registry URL", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), ["--registry", withUserinfo(capture, ":wrong-token")], {
BUN_CONFIG_TOKEN: authToken,
});
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test.each(["BUN_CONFIG_REGISTRY", "NPM_CONFIG_REGISTRY", "npm_config_registry"])(
"%s with user:pass@ in the URL sends Basic auth",
async key => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], {
[key]: withUserinfo(capture, "config-precedence:verysecure"),
});
expect(stderr).not.toContain("error:");
expect(stderr).not.toContain("verysecure");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([basicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
},
);
test("BUN_CONFIG_REGISTRY with :token@ in the URL sends it as a Bearer token", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], {
BUN_CONFIG_REGISTRY: withUserinfo(capture, `:${authToken}`),
});
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("user:pass@ in --registry replaces the .npmrc _authToken for the same host", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${capture.url}\n//localhost:${capture.port}/:_authToken=token-from-npmrc\n`,
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [
"--registry",
withUserinfo(capture, "config-precedence:verysecure"),
]);
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([basicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("user:pass@ in BUN_CONFIG_REGISTRY replaces the .npmrc _authToken for the same host", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${capture.url}\n//localhost:${capture.port}/:_authToken=token-from-npmrc\n`,
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], {
BUN_CONFIG_REGISTRY: withUserinfo(capture, "config-precedence:verysecure"),
});
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([basicAuth]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("BUN_CONFIG_TOKEN beats the _authToken in ~/.npmrc", async () => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"home/.npmrc": `registry=${capture.url}\n//localhost:${capture.port}/:_authToken=token-from-npmrc\n`,
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], { BUN_CONFIG_TOKEN: authToken });
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test.each(["BUN_CONFIG_TOKEN", "NPM_CONFIG_TOKEN"])(
"%s applies to the registry passed with --registry",
async key => {
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), ["--registry", capture.url], { [key]: authToken });
expect(stderr).not.toContain("error:");
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
},
);
test("--registry drops the _authToken of the .npmrc registry but keeps BUN_CONFIG_TOKEN", async () => {
using dead = deadRegistry();
using capture = capturingRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${dead.url}\n//localhost:${dead.server.port}/:_authToken=token-for-dead\n`,
"project/package.json": packageJson({ "@needs-auth/test-pkg": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), ["--registry", capture.url], {
BUN_CONFIG_TOKEN: authToken,
});
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(new Set(capture.authorizations)).toStrictEqual(new Set([`Bearer ${authToken}`]));
expect(installed(String(dir), "@needs-auth", "test-pkg")).toBe(true);
expect(exitCode).toBe(0);
});
test("NPM_CONFIG_REGISTRY beats registry= in project .npmrc", async () => {
using dead = deadRegistry();
using dir = tempDir("config-precedence", {
"project/.npmrc": `registry=${dead.url}\n`,
"project/package.json": packageJson({ "no-deps": "1.0.0" }),
});
const { stderr, exitCode } = await install(String(dir), [], { NPM_CONFIG_REGISTRY: registry.registryUrl() });
expect(stderr).not.toContain("error:");
expect(dead.hits).toBe(0);
expect(installed(String(dir), "no-deps")).toBe(true);
expect(exitCode).toBe(0);
});
});