Files
bun.sh/test/cli/install/bun-publish.test.ts
robobun c5005a37d7 Fix --tolerate-republish flag in bun publish (continues PR #22107) (#22381)
## Summary
This PR continues the work from #22107 to fix the `--tolerate-republish`
flag implementation in `bun publish`.

### Changes:
- **Pre-check version existence**: Before attempting to publish with
`--tolerate-republish`, check if the version already exists on the
registry
- **Improved version checking**: Use GET request to package endpoint
instead of HEAD, then parse JSON response to check if specific version
exists
- **Correct output stream**: Output warning to stderr instead of stdout
for consistency with test expectations
- **Better error handling**: Update test to accept both 403 and 409 HTTP
error codes for duplicate publish attempts

### Test fixes:
The tests were failing because:
1. The mock registry returns 409 Conflict (not 403) for duplicate
packages
2. The warning message wasn't appearing in stderr as expected
3. The version check was using HEAD request which doesn't reliably
return version info

## Test plan
- [x] Fixed failing tests for `--tolerate-republish` functionality
- [x] Tests now properly handle both 403 and 409 error responses
- [x] Warning messages appear correctly in stderr

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-09-30 13:25:50 -07:00

933 lines
32 KiB
TypeScript

import { file, spawn, write } from "bun";
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { exists, rm } from "fs/promises";
import {
VerdaccioRegistry,
bunExe,
bunEnv as env,
isWindows,
pack,
runBunInstall,
stderrForInstall,
tmpdirSync,
} from "harness";
import { join } from "path";
const registry = new VerdaccioRegistry();
beforeAll(async () => {
await registry.start();
});
afterAll(() => {
registry.stop();
});
export async function publish(
env: any,
cwd: string,
...args: string[]
): Promise<{ out: string; err: string; exitCode: number }> {
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "publish", ...args],
cwd,
stdout: "pipe",
stderr: "pipe",
env,
});
const out = await stdout.text();
const err = stderrForInstall(await stderr.text());
const exitCode = await exited;
return { out, err, exitCode };
}
describe("otp", async () => {
const mockRegistryFetch = function (opts: {
token: string;
setAuthHeader?: boolean;
otpFail?: boolean;
npmNotice?: boolean;
xLocalCache?: boolean;
expectedCI?: string;
}) {
return async function (req: Request) {
const { token, setAuthHeader = true, otpFail = false, npmNotice = false, xLocalCache = false } = opts;
if (req.url.includes("otp-pkg")) {
if (opts.expectedCI) {
expect(req.headers.get("user-agent")).toContain("ci/" + opts.expectedCI);
}
if (req.headers.get("npm-otp") === token) {
if (otpFail) {
return new Response(
JSON.stringify({
error: "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.",
}),
{ status: 401 },
);
} else {
return new Response("OK", { status: 200 });
}
} else {
const headers = new Headers();
if (setAuthHeader) headers.set("www-authenticate", "OTP");
// `bun publish` won't request a url from a message in the npm-notice header, but we
// can test that it's displayed
if (npmNotice) headers.set("npm-notice", `visit http://localhost:${this.port}/auth to login`);
// npm-notice will be ignored
if (xLocalCache) headers.set("x-local-cache", "true");
return new Response(
JSON.stringify({
// this isn't accurate, but we just want to check that finding this string works
mock: setAuthHeader ? "" : "one-time password",
authUrl: `http://localhost:${this.port}/auth`,
doneUrl: `http://localhost:${this.port}/done`,
}),
{
status: 401,
headers,
},
);
}
} else if (req.url.endsWith("auth")) {
expect.unreachable("url given to user, bun publish should not request");
} else if (req.url.endsWith("done")) {
// send a fake response saying the user has authenticated successfully with the auth url
return new Response(JSON.stringify({ token: token }), { status: 200 });
}
expect.unreachable("unexpected url");
};
};
for (const setAuthHeader of [true, false]) {
test("mock web login" + (setAuthHeader ? "" : " (without auth header)"), async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const token = await registry.generateUser("otp" + (setAuthHeader ? "" : "noheader"), "otp");
using mockRegistry = Bun.serve({
port: 0,
fetch: mockRegistryFetch({ token }),
});
const bunfig = `
[install]
cache = false
registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`;
await Promise.all([
rm(join(registry.packagesPath, "otp-pkg-1"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "otp-pkg-1",
version: "2.2.2",
dependencies: {
"otp-pkg-1": "2.2.2",
},
}),
),
]);
const { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
});
}
test("otp failure", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const token = await registry.generateUser("otp-fail", "otp");
using mockRegistry = Bun.serve({
port: 0,
fetch: mockRegistryFetch({ token, otpFail: true }),
});
const bunfig = `
[install]
cache = false
registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`;
await Promise.all([
rm(join(registry.packagesPath, "otp-pkg-2"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "otp-pkg-2",
version: "1.1.1",
dependencies: {
"otp-pkg-2": "1.1.1",
},
}),
),
]);
const { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(1);
expect(err).toContain(" - Received invalid OTP");
});
for (const shouldIgnoreNotice of [false, true]) {
test(`npm-notice with login url${shouldIgnoreNotice ? " (ignored)" : ""}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
// Situation: user has 2FA enabled account with faceid sign-in.
// They run `bun publish` with --auth-type=legacy, prompting them
// to enter their OTP. Because they have faceid sign-in, they don't
// have a code to enter, so npm sends a message in the npm-notice
// header with a url for logging in.
const token = await registry.generateUser(`otp-notice${shouldIgnoreNotice ? "-ignore" : ""}`, "otp");
using mockRegistry = Bun.serve({
port: 0,
fetch: mockRegistryFetch({ token, npmNotice: true, xLocalCache: shouldIgnoreNotice }),
});
const bunfig = `
[install]
cache = false
registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`;
await Promise.all([
rm(join(registry.packagesPath, "otp-pkg-3"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "otp-pkg-3",
version: "3.3.3",
dependencies: {
"otp-pkg-3": "3.3.3",
},
}),
),
]);
const { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
if (shouldIgnoreNotice) {
expect(err).not.toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`);
} else {
expect(err).toContain(`note: visit http://localhost:${mockRegistry.port}/auth to login`);
}
});
}
const fakeCIEnvs = [
{ ci: "expo-application-services", envs: { EAS_BUILD: "hi" } },
{ ci: "codemagic", envs: { CM_BUILD_ID: "hi" } },
{ ci: "vercel", envs: { "NOW_BUILDER": "hi" } },
];
for (const envInfo of fakeCIEnvs) {
test(`CI user agent name: ${envInfo.ci}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const token = await registry.generateUser(`otp-${envInfo.ci}`, "otp");
using mockRegistry = Bun.serve({
port: 0,
fetch: mockRegistryFetch({ token, expectedCI: envInfo.ci }),
});
const bunfig = `
[install]
cache = false
registry = { url = "http://localhost:${mockRegistry.port}", token = "${token}" }`;
await Promise.all([
rm(join(registry.packagesPath, "otp-pkg-4"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "otp-pkg-4",
version: "4.4.4",
dependencies: {
"otp-pkg-4": "4.4.4",
},
}),
),
]);
const { out, err, exitCode } = await publish(
{ ...env, ...envInfo.envs, ...{ BUILDKITE: undefined, GITHUB_ACTIONS: undefined } },
packageDir,
);
expect(exitCode).toBe(0);
});
}
});
test("can publish a package then install it", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("basic");
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-1"), { recursive: true, force: true }),
write(
packageJson,
JSON.stringify({
name: "publish-pkg-1",
version: "1.1.1",
dependencies: {
"publish-pkg-1": "1.1.1",
},
}),
),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
const { out, err, exitCode } = await publish(env, packageDir);
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
expect(await exists(join(packageDir, "node_modules", "publish-pkg-1", "package.json"))).toBeTrue();
});
test("can publish from a tarball", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("tarball");
const json = {
name: "publish-pkg-2",
version: "2.2.2",
dependencies: {
"publish-pkg-2": "2.2.2",
},
};
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-2"), { recursive: true, force: true }),
write(packageJson, JSON.stringify(json)),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
await pack(packageDir, env);
let { out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-2-2.2.2.tgz");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
expect(await exists(join(packageDir, "node_modules", "publish-pkg-2", "package.json"))).toBeTrue();
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-2"), { recursive: true, force: true }),
rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }),
rm(join(packageDir, "node_modules"), { recursive: true, force: true }),
]);
// now with an absoute path
({ out, err, exitCode } = await publish(env, packageDir, join(packageDir, "publish-pkg-2-2.2.2.tgz")));
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir, { savesLockfile: false });
expect(await file(join(packageDir, "node_modules", "publish-pkg-2", "package.json")).json()).toEqual(json);
});
test("can publish scoped packages", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("scoped-pkg");
const json = {
name: "@scoped/pkg-1",
version: "1.1.1",
dependencies: {
"@scoped/pkg-1": "1.1.1",
},
};
await Promise.all([
rm(join(registry.packagesPath, "@scoped", "pkg-1"), { recursive: true, force: true }),
write(packageJson, JSON.stringify(json)),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
const { out, err, exitCode } = await publish(env, packageDir);
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
expect(await file(join(packageDir, "node_modules", "@scoped", "pkg-1", "package.json")).json()).toEqual(json);
});
for (const info of [
{ user: "bin1", bin: "bin1.js" },
{ user: "bin2", bin: { bin1: "bin1.js", bin2: "bin2.js" } },
{ user: "bin3", directories: { bin: "bins" } },
]) {
test(`can publish and install binaries with ${JSON.stringify(info)}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false } });
const publishDir = tmpdirSync();
const bunfig = await registry.authBunfig("binaries-" + info.user);
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-" + info.user), { recursive: true, force: true }),
write(
join(publishDir, "package.json"),
JSON.stringify({
name: "publish-pkg-" + info.user,
version: "1.1.1",
...info,
}),
),
write(join(publishDir, "bunfig.toml"), bunfig),
write(join(publishDir, "bin1.js"), `#!/usr/bin/env bun\nconsole.log("bin1!")`),
write(join(publishDir, "bin2.js"), `#!/usr/bin/env bun\nconsole.log("bin2!")`),
write(join(publishDir, "bins", "bin3.js"), `#!/usr/bin/env bun\nconsole.log("bin3!")`),
write(join(publishDir, "bins", "moredir", "bin4.js"), `#!/usr/bin/env bun\nconsole.log("bin4!")`),
write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
["publish-pkg-" + info.user]: "1.1.1",
},
}),
),
]);
const { out, err, exitCode } = await publish(env, publishDir);
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(out).toContain(`+ publish-pkg-${info.user}@1.1.1`);
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
const results = await Promise.all([
exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin1.bunx" : "bin1")),
exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin2.bunx" : "bin2")),
exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin3.js.bunx" : "bin3.js")),
exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin4.js.bunx" : "bin4.js")),
exists(join(packageDir, "node_modules", ".bin", isWindows ? "moredir" : "moredir/bin4.js")),
exists(
join(
packageDir,
"node_modules",
".bin",
isWindows ? `publish-pkg-${info.user}.bunx` : "publish-pkg-" + info.user,
),
),
]);
switch (info.user) {
case "bin1": {
expect(results).toEqual([false, false, false, false, false, true]);
break;
}
case "bin2": {
expect(results).toEqual([true, true, false, false, false, false]);
break;
}
case "bin3": {
expect(results).toEqual([false, false, true, true, !isWindows, false]);
break;
}
}
});
}
test("dependencies are installed", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const publishDir = tmpdirSync();
const bunfig = await registry.authBunfig("manydeps");
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-deps"), { recursive: true, force: true }),
write(
join(publishDir, "package.json"),
JSON.stringify(
{
name: "publish-pkg-deps",
version: "1.1.1",
dependencies: {
"no-deps": "1.0.0",
},
peerDependencies: {
"a-dep": "1.0.1",
},
optionalDependencies: {
"basic-1": "1.0.0",
},
},
null,
2,
),
),
write(join(publishDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"publish-pkg-deps": "1.1.1",
},
}),
),
]);
let { out, err, exitCode } = await publish(env, publishDir);
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(out).toContain("+ publish-pkg-deps@1.1.1");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
const results = await Promise.all([
exists(join(packageDir, "node_modules", "no-deps", "package.json")),
exists(join(packageDir, "node_modules", "a-dep", "package.json")),
exists(join(packageDir, "node_modules", "basic-1", "package.json")),
]);
expect(results).toEqual([true, true, true]);
});
test("can publish workspace package", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("workspace");
const pkgJson = {
name: "publish-pkg-3",
version: "3.3.3",
dependencies: {
"publish-pkg-3": "3.3.3",
},
};
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-3"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "root",
workspaces: ["packages/*"],
}),
),
write(join(packageDir, "packages", "publish-pkg-3", "package.json"), JSON.stringify(pkgJson)),
]);
await publish(env, join(packageDir, "packages", "publish-pkg-3"));
await write(packageJson, JSON.stringify({ name: "root", "dependencies": { "publish-pkg-3": "3.3.3" } }));
await runBunInstall(env, packageDir);
expect(await file(join(packageDir, "node_modules", "publish-pkg-3", "package.json")).json()).toEqual(pkgJson);
});
describe("--dry-run", async () => {
test("does not publish", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("dryrun");
await Promise.all([
rm(join(registry.packagesPath, "dry-run-1"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "dry-run-1",
version: "1.1.1",
dependencies: {
"dry-run-1": "1.1.1",
},
}),
),
]);
const { out, err, exitCode } = await publish(env, packageDir, "--dry-run");
expect(exitCode).toBe(0);
expect(await exists(join(registry.packagesPath, "dry-run-1"))).toBeFalse();
});
test("does not publish from tarball path", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("dryruntarball");
await Promise.all([
rm(join(registry.packagesPath, "dry-run-2"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "dry-run-2",
version: "2.2.2",
dependencies: {
"dry-run-2": "2.2.2",
},
}),
),
]);
await pack(packageDir, env);
const { out, err, exitCode } = await publish(env, packageDir, "./dry-run-2-2.2.2.tgz", "--dry-run");
expect(exitCode).toBe(0);
expect(await exists(join(registry.packagesPath, "dry-run-2"))).toBeFalse();
});
});
describe("lifecycle scripts", async () => {
const script = `const fs = require("fs");
fs.writeFileSync(process.argv[2] + ".txt", \`
prepublishOnly: \${fs.existsSync("prepublishOnly.txt")}
publish: \${fs.existsSync("publish.txt")}
postpublish: \${fs.existsSync("postpublish.txt")}
prepack: \${fs.existsSync("prepack.txt")}
prepare: \${fs.existsSync("prepare.txt")}
postpack: \${fs.existsSync("postpack.txt")}\`)`;
const json = {
name: "publish-pkg-4",
version: "4.4.4",
scripts: {
// should happen in this order
"prepublishOnly": `${bunExe()} script.js prepublishOnly`,
"prepack": `${bunExe()} script.js prepack`,
"prepare": `${bunExe()} script.js prepare`,
"postpack": `${bunExe()} script.js postpack`,
"publish": `${bunExe()} script.js publish`,
"postpublish": `${bunExe()} script.js postpublish`,
},
dependencies: {
"publish-pkg-4": "4.4.4",
},
};
for (const arg of [[], ["--dry-run"]]) {
test(`should run in order${arg.length > 0 ? " (--dry-run)" : ""}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("lifecycle" + (arg.length > 0 ? "dry" : ""));
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-4"), { recursive: true, force: true }),
write(packageJson, JSON.stringify(json)),
write(join(packageDir, "script.js"), script),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
const { out, err, exitCode } = await publish(env, packageDir, ...arg);
expect(exitCode).toBe(0);
const results = await Promise.all([
file(join(packageDir, "prepublishOnly.txt")).text(),
file(join(packageDir, "prepack.txt")).text(),
file(join(packageDir, "prepare.txt")).text(),
file(join(packageDir, "postpack.txt")).text(),
file(join(packageDir, "publish.txt")).text(),
file(join(packageDir, "postpublish.txt")).text(),
]);
expect(results).toEqual([
"\nprepublishOnly: false\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false",
"\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: false\nprepare: false\npostpack: false",
"\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: false\npostpack: false",
"\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: false",
"\nprepublishOnly: true\npublish: false\npostpublish: false\nprepack: true\nprepare: true\npostpack: true",
"\nprepublishOnly: true\npublish: true\npostpublish: false\nprepack: true\nprepare: true\npostpack: true",
]);
});
}
test("--ignore-scripts", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("ignorescripts");
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-5"), { recursive: true, force: true }),
write(packageJson, JSON.stringify(json)),
write(join(packageDir, "script.js"), script),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
const { out, err, exitCode } = await publish(env, packageDir, "--ignore-scripts");
expect(exitCode).toBe(0);
const results = await Promise.all([
exists(join(packageDir, "prepublishOnly.txt")),
exists(join(packageDir, "prepack.txt")),
exists(join(packageDir, "prepare.txt")),
exists(join(packageDir, "postpack.txt")),
exists(join(packageDir, "publish.txt")),
exists(join(packageDir, "postpublish.txt")),
]);
expect(results).toEqual([false, false, false, false, false, false]);
});
});
test("attempting to publish a private package should fail", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("privatepackage");
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-6"), { recursive: true, force: true }),
write(
packageJson,
JSON.stringify({
name: "publish-pkg-6",
version: "6.6.6",
private: true,
dependencies: {
"publish-pkg-6": "6.6.6",
},
}),
),
write(join(packageDir, "bunfig.toml"), bunfig),
]);
// should fail
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(1);
expect(err).toContain("error: attempted to publish a private package");
expect(await exists(join(registry.packagesPath, "publish-pkg-6-6.6.6.tgz"))).toBeFalse();
// try tarball
await pack(packageDir, env);
({ out, err, exitCode } = await publish(env, packageDir, "./publish-pkg-6-6.6.6.tgz"));
expect(exitCode).toBe(1);
expect(err).toContain("error: attempted to publish a private package");
expect(await exists(join(packageDir, "publish-pkg-6-6.6.6.tgz"))).toBeTrue();
});
describe("access", async () => {
test("--access", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("accessflag");
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-7"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(
packageJson,
JSON.stringify({
name: "publish-pkg-7",
version: "7.7.7",
}),
),
]);
// should fail
let { out, err, exitCode } = await publish(env, packageDir, "--access", "restricted");
expect(exitCode).toBe(1);
expect(err).toContain("error: unable to restrict access to unscoped package");
({ out, err, exitCode } = await publish(env, packageDir, "--access", "public"));
expect(exitCode).toBe(0);
expect(await exists(join(registry.packagesPath, "publish-pkg-7"))).toBeTrue();
});
for (const access of ["restricted", "public"]) {
test(`access ${access}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("access" + access);
const pkgJson = {
name: "@secret/publish-pkg-8",
version: "8.8.8",
dependencies: {
"@secret/publish-pkg-8": "8.8.8",
},
publishConfig: {
access,
},
};
await Promise.all([
rm(join(registry.packagesPath, "@secret", "publish-pkg-8"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
expect(await file(join(packageDir, "node_modules", "@secret", "publish-pkg-8", "package.json")).json()).toEqual(
pkgJson,
);
});
}
});
describe("tag", async () => {
test("can publish with a tag", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("simpletag");
const pkgJson = {
name: "publish-pkg-9",
version: "9.9.9",
dependencies: {
"publish-pkg-9": "simpletag",
},
};
await Promise.all([
rm(join(registry.packagesPath, "publish-pkg-9"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag");
expect(exitCode).toBe(0);
await runBunInstall(env, packageDir);
expect(await file(join(packageDir, "node_modules", "publish-pkg-9", "package.json")).json()).toEqual(pkgJson);
});
});
it("$npm_command is accurate during publish", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
JSON.stringify({
name: "publish-pkg-10",
version: "1.0.0",
scripts: {
publish: "echo $npm_command",
},
}),
);
await write(join(packageDir, "bunfig.toml"), await registry.authBunfig("npm_command"));
await rm(join(registry.packagesPath, "publish-pkg-10"), { recursive: true, force: true });
let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag");
expect(err).toBe(`$ echo $npm_command\n`);
expect(out.split("\n")).toEqual([
`bun publish ${Bun.version_with_sha}`,
``,
`packed 95B package.json`,
``,
`Total files: 1`,
expect.stringContaining(`Shasum: `),
expect.stringContaining(`Integrity: sha512-`),
`Unpacked size: 95B`,
expect.stringContaining(`Packed size: `),
`Tag: simpletag`,
`Access: default`,
`Registry: http://localhost:${registry.port}/`,
``,
` + publish-pkg-10@1.0.0`,
`publish`,
``,
]);
expect(exitCode).toBe(0);
});
it("$npm_lifecycle_event is accurate during publish", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await write(
packageJson,
`{
"name": "publish-pkg-11",
"version": "1.0.0",
"scripts": {
"prepublish": "echo 1 $npm_lifecycle_event",
"publish": "echo 2 $npm_lifecycle_event",
"postpublish": "echo 3 $npm_lifecycle_event",
},
}
`,
);
await write(join(packageDir, "bunfig.toml"), await registry.authBunfig("npm_lifecycle_event"));
await rm(join(registry.packagesPath, "publish-pkg-11"), { recursive: true, force: true });
let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag");
expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`);
expect(out.split("\n")).toEqual([
`bun publish ${Bun.version_with_sha}`,
``,
`packed 256B package.json`,
``,
`Total files: 1`,
expect.stringContaining(`Shasum: `),
expect.stringContaining(`Integrity: sha512-`),
`Unpacked size: 256B`,
expect.stringContaining(`Packed size: `),
`Tag: simpletag`,
`Access: default`,
`Registry: http://localhost:${registry.port}/`,
``,
` + publish-pkg-11@1.0.0`,
`2 publish`,
`3 postpublish`,
``,
]);
expect(exitCode).toBe(0);
});
describe("--tolerate-republish", async () => {
test("republishing normally fails", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-fail");
const pkgJson = {
name: "republish-test-1",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-1"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-1@1.0.0");
// Second publish should fail
({ out, err, exitCode } = await publish(env, packageDir));
expect(exitCode).toBe(1);
expect(err).toMatch(/403|409|already exists|already present|cannot publish/);
});
test("republishing with --tolerate-republish skips when version exists", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-tolerate");
const pkgJson = {
name: "republish-test-2",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-2"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir);
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-2@1.0.0");
// Second publish with --tolerate-republish should skip
({ out, err, exitCode } = await publish(env, packageDir, "--tolerate-republish"));
expect(exitCode).toBe(0);
expect(err).toBe("warn: Registry already knows about version 1.0.0; skipping.\n");
expect(err).not.toContain("error:");
});
test("republishing tarball with --tolerate-republish skips when version exists", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("republish-tarball");
const pkgJson = {
name: "republish-test-3",
version: "1.0.0",
};
await Promise.all([
rm(join(registry.packagesPath, "republish-test-3"), { recursive: true, force: true }),
write(join(packageDir, "bunfig.toml"), bunfig),
write(packageJson, JSON.stringify(pkgJson)),
]);
// Create tarball
await pack(packageDir, env);
// First publish should succeed
let { out, err, exitCode } = await publish(env, packageDir, "./republish-test-3-1.0.0.tgz");
expect(exitCode).toBe(0);
expect(out).toContain("+ republish-test-3@1.0.0");
// Second publish with --tolerate-republish should skip
({ out, err, exitCode } = await publish(env, packageDir, "./republish-test-3-1.0.0.tgz", "--tolerate-republish"));
expect(exitCode).toBe(0);
expect(err).toBe("warn: Registry already knows about version 1.0.0; skipping.\n");
expect(err).not.toContain("error:");
});
});