mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
fix(publish): "tarball" and "_attachment" path fix (#16630)
This commit is contained in:
@@ -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| {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
850
test/cli/install/bun-publish.test.ts
Normal file
850
test/cli/install/bun-publish.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user