fix(publish): "tarball" and "_attachment" path fix (#16630)

This commit is contained in:
Dylan Conway
2025-01-24 18:48:42 -08:00
committed by GitHub
parent fb0f54840e
commit 8e2cf8665a
5 changed files with 986 additions and 851 deletions

View File

@@ -1402,7 +1402,7 @@ pub const PackCommand = struct {
if (comptime !for_publish) {
if (manager.options.pack_destination.len == 0) {
Output.pretty("\n{}\n", .{fmtTarballFilename(package_name, package_version)});
Output.pretty("\n{}\n", .{fmtTarballFilename(package_name, package_version, .normalize)});
} else {
var dest_buf: PathBuffer = undefined;
const abs_tarball_dest, _ = absTarballDestination(
@@ -1726,7 +1726,6 @@ pub const PackCommand = struct {
json.source,
shasum,
integrity,
abs_tarball_dest,
);
printArchivedFilesAndPackages(
@@ -1739,7 +1738,7 @@ pub const PackCommand = struct {
if (comptime !for_publish) {
if (manager.options.pack_destination.len == 0) {
Output.pretty("\n{}\n", .{fmtTarballFilename(package_name, package_version)});
Output.pretty("\n{}\n", .{fmtTarballFilename(package_name, package_version, .normalize)});
} else {
Output.pretty("\n{s}\n", .{abs_tarball_dest});
}
@@ -1809,11 +1808,11 @@ pub const PackCommand = struct {
);
const tarball_name = std.fmt.bufPrint(dest_buf[strings.withoutTrailingSlash(tarball_destination_dir).len..], "/{}\x00", .{
fmtTarballFilename(package_name, package_version),
fmtTarballFilename(package_name, package_version, .normalize),
}) catch {
Output.errGeneric("archive destination name too long: \"{s}/{}\"", .{
strings.withoutTrailingSlash(tarball_destination_dir),
fmtTarballFilename(package_name, package_version),
fmtTarballFilename(package_name, package_version, .normalize),
});
Global.crash();
};
@@ -1824,18 +1823,29 @@ pub const PackCommand = struct {
};
}
fn fmtTarballFilename(package_name: string, package_version: string) TarballNameFormatter {
pub fn fmtTarballFilename(package_name: string, package_version: string, style: TarballNameFormatter.Style) TarballNameFormatter {
return .{
.package_name = package_name,
.package_version = package_version,
.style = style,
};
}
const TarballNameFormatter = struct {
package_name: string,
package_version: string,
style: Style,
pub const Style = enum {
normalize,
raw,
};
pub fn format(this: TarballNameFormatter, comptime _: string, _: std.fmt.FormatOptions, writer: anytype) !void {
if (this.style == .raw) {
return writer.print("{s}-{s}.tgz", .{ this.package_name, this.package_version });
}
if (this.package_name[0] == '@') {
if (this.package_name.len > 1) {
if (strings.indexOfChar(this.package_name, '/')) |slash| {

View File

@@ -242,7 +242,6 @@ pub const PublishCommand = struct {
json_source,
shasum,
integrity,
abs_tarball_path,
);
Pack.Context.printSummary(
@@ -876,7 +875,6 @@ pub const PublishCommand = struct {
json_source: logger.Source,
shasum: sha.SHA1.Digest,
integrity: sha.SHA512.Digest,
abs_tarball_path: stringZ,
) OOM!string {
bun.assertWithLocation(json.isObject(), @src());
@@ -928,10 +926,12 @@ pub const PublishCommand = struct {
.value = Expr.init(
E.String,
.{
.data = try bun.fmt.allocPrint(allocator, "http://{s}/{s}/-/{s}", .{
strings.withoutTrailingSlash(registry.url.href),
.data = try bun.fmt.allocPrint(allocator, "http://{s}/{s}/-/{}", .{
// always use replace https with http
// https://github.com/npm/cli/blob/9281ebf8e428d40450ad75ba61bc6f040b3bf896/workspaces/libnpmpublish/lib/publish.js#L120
strings.withoutTrailingSlash(strings.withoutPrefixComptime(registry.url.href, "https://")),
package_name,
std.fs.path.basename(abs_tarball_path),
Pack.fmtTarballFilename(package_name, package_version, .raw),
}),
},
logger.Loc.Empty,
@@ -1362,8 +1362,8 @@ pub const PublishCommand = struct {
// "_attachments"
{
try writer.print(",\"_attachments\":{{\"{s}\":{{\"content_type\":\"{s}\",\"data\":\"", .{
std.fs.path.basename(ctx.abs_tarball_path),
try writer.print(",\"_attachments\":{{\"{}\":{{\"content_type\":\"{s}\",\"data\":\"", .{
Pack.fmtTarballFilename(ctx.package_name, ctx.package_version, .raw),
"application/octet-stream",
});

View File

@@ -742,38 +742,9 @@ ljelkjwelkgjw;lekj;lkejflkj
});
});
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 Bun.readableStreamToText(stdout);
const err = stderrForInstall(await Bun.readableStreamToText(stderr));
const exitCode = await exited;
return { out, err, exitCode };
}
async function authBunfig(user: string) {
const authToken = await generateRegistryUser(user, user);
return `
[install]
cache = false
registry = { url = "http://localhost:${port}/", token = "${authToken}" }
saveTextLockfile = false
`;
}
describe("whoami", async () => {
test("can get username", async () => {
const bunfig = await authBunfig("whoami");
const bunfig = await verdaccio.authBunfig("whoami");
await Promise.all([
write(
packageJson,
@@ -919,690 +890,6 @@ describe("whoami", async () => {
});
});
describe("publish", async () => {
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 token = await generateRegistryUser("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(verdaccio.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 token = await generateRegistryUser("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(verdaccio.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 () => {
// 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 generateRegistryUser(`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(verdaccio.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 token = await generateRegistryUser(`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(verdaccio.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 bunfig = await authBunfig("basic");
await Promise.all([
rm(join(verdaccio.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 bunfig = await authBunfig("tarball");
const json = {
name: "publish-pkg-2",
version: "2.2.2",
dependencies: {
"publish-pkg-2": "2.2.2",
},
};
await Promise.all([
rm(join(verdaccio.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(verdaccio.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);
expect(await file(join(packageDir, "node_modules", "publish-pkg-2", "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 publishDir = tmpdirSync();
const bunfig = await authBunfig("binaries-" + info.user);
console.log({ packageDir, publishDir });
await Promise.all([
rm(join(verdaccio.packagesPath, "publish-pkg-bins"), { recursive: true, force: true }),
write(
join(publishDir, "package.json"),
JSON.stringify({
name: "publish-pkg-bins",
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-bins": "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-bins@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-bins.bunx" : "publish-pkg-bins")),
]);
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 publishDir = tmpdirSync();
const bunfig = await authBunfig("manydeps");
await Promise.all([
rm(join(verdaccio.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 bunfig = await authBunfig("workspace");
const pkgJson = {
name: "publish-pkg-3",
version: "3.3.3",
dependencies: {
"publish-pkg-3": "3.3.3",
},
};
await Promise.all([
rm(join(verdaccio.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 bunfig = await authBunfig("dryrun");
await Promise.all([
rm(join(verdaccio.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(verdaccio.packagesPath, "dry-run-1"))).toBeFalse();
});
test("does not publish from tarball path", async () => {
const bunfig = await authBunfig("dryruntarball");
await Promise.all([
rm(join(verdaccio.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(verdaccio.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 ? " (--dry-run)" : ""}`, async () => {
const bunfig = await authBunfig("lifecycle" + (arg ? "dry" : ""));
await Promise.all([
rm(join(verdaccio.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 bunfig = await authBunfig("ignorescripts");
await Promise.all([
rm(join(verdaccio.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 bunfig = await authBunfig("privatepackage");
await Promise.all([
rm(join(verdaccio.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(verdaccio.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 bunfig = await authBunfig("accessflag");
await Promise.all([
rm(join(verdaccio.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(verdaccio.packagesPath, "publish-pkg-7"))).toBeTrue();
});
for (const access of ["restricted", "public"]) {
test(`access ${access}`, async () => {
const bunfig = await 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(verdaccio.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 bunfig = await authBunfig("simpletag");
const pkgJson = {
name: "publish-pkg-9",
version: "9.9.9",
dependencies: {
"publish-pkg-9": "simpletag",
},
};
await Promise.all([
rm(join(verdaccio.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);
});
});
});
describe("package.json indentation", async () => {
test("works for root and workspace packages", async () => {
await Promise.all([
@@ -5367,7 +4654,7 @@ describe("hoisting", async () => {
});
});
describe.only("transitive file dependencies", () => {
describe("transitive file dependencies", () => {
async function checkHoistedFiles() {
const aliasedFileDepFilesPackageJson = join(
packageDir,
@@ -5379,28 +4666,36 @@ describe.only("transitive file dependencies", () => {
"package.json",
);
const results = await Promise.all([
(await lstat(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json"))).isSymbolicLink(),
(
await lstat(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json"))
).isSymbolicLink(),
readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")),
exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")),
isWindows
? file(await readlink(aliasedFileDepFilesPackageJson)).json()
: file(aliasedFileDepFilesPackageJson).json(),
(await lstat(
join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"),
)).isSymbolicLink(),
(await lstat(
join(
packageDir,
"node_modules",
"@another-scope",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)).isSymbolicLink(),
(await lstat(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"))).isSymbolicLink(),
(
await lstat(
join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"),
)
).isSymbolicLink(),
(
await lstat(
join(
packageDir,
"node_modules",
"@another-scope",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)
).isSymbolicLink(),
(
await lstat(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"))
).isSymbolicLink(),
]);
expect(results).toEqual([
@@ -5430,38 +4725,46 @@ describe.only("transitive file dependencies", () => {
file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(),
file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(),
(await lstat(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json"))).isSymbolicLink(),
(
await lstat(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json"))
).isSymbolicLink(),
readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // []
exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false
(await lstat(
join(
packageDir,
"pkg1",
"node_modules",
"@scoped",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)).isSymbolicLink(),
(await lstat(
join(
packageDir,
"pkg1",
"node_modules",
"@another-scope",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)).isSymbolicLink(),
(await lstat(
join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"),
)).isSymbolicLink(),
(
await lstat(
join(
packageDir,
"pkg1",
"node_modules",
"@scoped",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)
).isSymbolicLink(),
(
await lstat(
join(
packageDir,
"pkg1",
"node_modules",
"@another-scope",
"file-dep",
"node_modules",
"@scoped",
"files",
"package.json",
),
)
).isSymbolicLink(),
(
await lstat(
join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"),
)
).isSymbolicLink(),
readdirSorted(join(packageDir, "pkg1", "node_modules")),
]);
@@ -9683,79 +8986,3 @@ registry = "http://localhost:${port}/"
});
}
});
it("$npm_command is accurate during publish", async () => {
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 authBunfig("npm_command"));
await rm(join(verdaccio.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:${port}/`,
``,
` + publish-pkg-10@1.0.0`,
`publish`,
``,
]);
expect(exitCode).toBe(0);
});
it("$npm_lifecycle_event is accurate during publish", async () => {
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 authBunfig("npm_lifecycle_event"));
await rm(join(verdaccio.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:${port}/`,
``,
` + publish-pkg-11@1.0.0`,
`2 publish`,
`3 postpublish`,
``,
]);
expect(exitCode).toBe(0);
});

View File

@@ -0,0 +1,850 @@
import { describe, expect, test, beforeAll, afterAll, it } from "bun:test";
import { spawn, file, write } from "bun";
import { rm, exists } from "fs/promises";
import { join } from "path";
import {
VerdaccioRegistry,
bunExe,
bunEnv as env,
stderrForInstall,
runBunInstall,
pack,
tmpdirSync,
isWindows,
} from "harness";
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 Bun.readableStreamToText(stdout);
const err = stderrForInstall(await Bun.readableStreamToText(stderr));
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({ 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 ? " (--dry-run)" : ""}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const bunfig = await registry.authBunfig("lifecycle" + (arg ? "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);
});

View File

@@ -1056,6 +1056,7 @@ export async function runBunInstall(
frozenLockfile?: boolean;
saveTextLockfile?: boolean;
packages?: string[];
verbose?: boolean;
},
) {
const production = options?.production ?? false;
@@ -1072,6 +1073,9 @@ export async function runBunInstall(
if (options?.saveTextLockfile) {
args.push("--save-text-lockfile");
}
if (options?.verbose) {
args.push("--verbose");
}
const { stdout, stderr, exited } = Bun.spawn({
cmd: args,
cwd,
@@ -1452,6 +1456,7 @@ export class VerdaccioRegistry {
process: ChildProcess | undefined;
configPath: string;
packagesPath: string;
users: Record<string, string> = {};
constructor(opts?: { configPath?: string; packagesPath?: string; verbose?: boolean }) {
this.port = randomPort();
@@ -1504,10 +1509,53 @@ export class VerdaccioRegistry {
this.process?.kill(0);
}
/**
* returns auth token
*/
async generateUser(username: string, password: string): Promise<string> {
if (this.users[username]) {
throw new Error(`User ${username} already exists`);
} else this.users[username] = password;
const url = `http://localhost:${this.port}/-/user/org.couchdb.user:${username}`;
const user = {
name: username,
password: password,
email: `${username}@example.com`,
};
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(user),
});
if (response.ok) {
const data = await response.json();
return data.token;
}
throw new Error("Failed to create user:", response.statusText);
}
async authBunfig(user: string) {
const authToken = await this.generateUser(user, user);
return `
[install]
cache = false
registry = { url = "http://localhost:${this.port}/", token = "${authToken}" }
`;
}
async createTestDir(bunfigOpts: BunfigOpts = {}) {
await rm(join(dirname(this.configPath), "htpasswd"), { force: true });
await rm(join(this.packagesPath, "private-pkg-dont-touch"), { force: true });
const packageDir = tmpdirSync();
const packageJson = join(packageDir, "package.json");
this.writeBunfig(packageDir, bunfigOpts);
this.users = {};
return { packageDir, packageJson };
}