Files
bun.sh/test/cli/install/bun-pack.test.ts
Dylan Conway fb2bf3fe83 fix(pack): always include bin even if not included by files (#23606)
### What does this PR do?
Fixes #23521
### How did you verify your code works?
Added 3 previously failing tests for `"bin"`, `"directories.bin"`, and
deduplicating entry in both `"bin.directories"` and `"files"`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-19 23:28:59 -07:00

1469 lines
44 KiB
TypeScript

import { file, spawn, write } from "bun";
import { readTarball } from "bun:internal-for-testing";
import { beforeEach, describe, expect, test } from "bun:test";
import { exists, mkdir, rm } from "fs/promises";
import { bunEnv, bunExe, pack, runBunInstall, tempDirWithFiles, tmpdirSync } from "harness";
import fs from "node:fs/promises";
import { join } from "path";
var packageDir: string;
beforeEach(() => {
packageDir = tmpdirSync();
});
async function packExpectError(cwd: string, env: NodeJS.Dict<string>, ...args: string[]) {
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "pack", ...args],
cwd,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env,
});
const err = await stderr.text();
expect(err).not.toContain("panic:");
const out = await stdout.text();
const exitCode = await exited;
expect(exitCode).toBeGreaterThan(0);
return { out, err };
}
test("basic", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-basic",
version: "1.2.3",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-basic-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]);
});
test("in subdirectory", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-from-subdir",
version: "7.7.7",
}),
),
mkdir(join(packageDir, "subdir1", "subdir2"), { recursive: true }),
write(join(packageDir, "root.js"), "console.log(`hello ./root.js`);"),
write(join(packageDir, "subdir1", "subdir2", "index.js"), "console.log(`hello ./subdir1/subdir2/index.js`);"),
]);
await pack(join(packageDir, "subdir1", "subdir2"), bunEnv);
const first = readTarball(join(packageDir, "pack-from-subdir-7.7.7.tgz"));
expect(first.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/root.js" },
{ "pathname": "package/subdir1/subdir2/index.js" },
]);
await rm(join(packageDir, "pack-from-subdir-7.7.7.tgz"));
await pack(join(packageDir, "subdir1"), bunEnv);
const second = readTarball(join(packageDir, "pack-from-subdir-7.7.7.tgz"));
expect(first).toEqual(second);
});
describe("package.json names and versions", () => {
const tests = [
{
desc: "missing name",
expectedError: "package.json must have `name` and `version` fields",
packageJson: {
version: "1.1.1",
},
},
{
desc: "missing version",
expectedError: "package.json must have `name` and `version` fields",
packageJson: {
name: "pack-invalid",
},
},
{
desc: "missing name and version",
expectedError: "package.json must have `name` and `version` fields",
packageJson: {
description: "ooops",
},
},
{
desc: "empty name",
expectedError: "package.json `name` and `version` fields must be non-empty strings",
packageJson: {
name: "",
version: "1.1.1",
},
},
{
desc: "empty version",
expectedError: "package.json `name` and `version` fields must be non-empty strings",
packageJson: {
name: "pack-invalid",
version: "",
},
},
{
desc: "empty name and version",
expectedError: "package.json `name` and `version` fields must be non-empty strings",
packageJson: {
name: "",
version: "",
},
},
];
for (const { desc, expectedError, packageJson } of tests) {
test(desc, async () => {
await Promise.all([
write(join(packageDir, "package.json"), JSON.stringify(packageJson)),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
const { err } = await packExpectError(packageDir, bunEnv);
expect(err).toContain(expectedError);
});
}
test("missing", async () => {
await write(join(packageDir, "index.js"), "console.log('hello ./index.js')");
const { err } = await packExpectError(packageDir, bunEnv);
expect(err).toContain(`error: No package.json was found for directory "${packageDir}`);
});
const scopedNames = [
{
input: "@scoped/pkg",
output: "scoped-pkg-1.1.1.tgz",
},
{
input: "@",
output: "-1.1.1.tgz",
},
{
input: "@/",
output: "--1.1.1.tgz",
},
{
input: "//",
output: "-1.1.1.tgz",
},
{
input: "@//",
fail: true,
output: "",
},
{
input: "@/s",
output: "-s-1.1.1.tgz",
},
{
input: "@s",
output: "s-1.1.1.tgz",
},
];
for (const { input, output, fail } of scopedNames) {
test(`scoped name: ${input}`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: input,
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
fail ? await packExpectError(packageDir, bunEnv) : await pack(packageDir, bunEnv);
if (!fail) {
const tarball = readTarball(join(packageDir, output));
expect(tarball.entries).toHaveLength(2);
}
});
}
});
describe("flags", () => {
test("--dry-run", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-dry-run",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
const { out } = await pack(packageDir, bunEnv, "--dry-run");
expect(out).toContain("files: 2");
expect(await exists(join(packageDir, "pack-dry-run-1.1.1.tgz"))).toBeFalse();
});
test("--gzip", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-gzip-test",
version: "111111.1.11111111111111",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
for (const invalidGzipLevel of ["-1", "10", "kjefj"]) {
const { err } = await packExpectError(packageDir, bunEnv, `--gzip-level=${invalidGzipLevel}`);
expect(err).toContain(`error: compression level must be between 0 and 9, received ${invalidGzipLevel}\n`);
}
await pack(packageDir, bunEnv, "--gzip-level=0");
const largerTarball = readTarball(join(packageDir, "pack-gzip-test-111111.1.11111111111111.tgz"));
expect(largerTarball.entries).toHaveLength(2);
await rm(join(packageDir, "pack-gzip-test-111111.1.11111111111111.tgz"));
await pack(packageDir, bunEnv, "--gzip-level=9");
const smallerTarball = readTarball(join(packageDir, "pack-gzip-test-111111.1.11111111111111.tgz"));
expect(smallerTarball.entries).toHaveLength(2);
expect(smallerTarball.size).toBeLessThan(largerTarball.size);
});
const destinationTests = [
{
"path": "",
},
{
"path": "dest-dir",
},
{
"path": "more/dir",
},
];
for (const { path } of destinationTests) {
test(`--destination="${path}"`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-dest-test",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
const dest = join(packageDir, path);
await pack(packageDir, bunEnv, `--destination=${dest}`);
const tarball = readTarball(join(dest, "pack-dest-test-1.1.1.tgz"));
expect(tarball.entries).toHaveLength(2);
});
}
const filenameTests = [
{
filename: "test.tgz",
error: false,
},
{
filename: "no-extension",
error: false,
},
{
filename: "no-extension.tar",
error: false,
},
{
filename: "out/foo.tgz",
error: true,
},
{
filename: "out/foo.tar",
mkdir: "out",
error: false,
},
];
for (const { filename, error, mkdir } of filenameTests) {
test(`--filename="${filename}"`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-dest-test",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
const dest = join(packageDir, filename);
if (mkdir) await fs.mkdir(join(packageDir, mkdir));
try {
await pack(packageDir, bunEnv, `--filename=${filename}`);
const tarball = readTarball(dest);
expect(tarball.entries).toHaveLength(2);
} catch (packError) {
if (!error) expect(packError).toBeNil();
}
});
}
test("--filename and --destination", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-dest-test",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
expect(async () => await pack(packageDir, bunEnv, "--filename=test.tgz", "--destination=packed")).toThrowError();
});
test("--ignore-scripts", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-ignore-scripts",
version: "1.1.1",
scripts: {
prepack: "touch prepack.txt",
postpack: "touch postpack.txt",
preprepare: "touch preprepare.txt",
prepare: "touch prepare.txt",
postprepare: "touch postprepare.txt",
},
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
await pack(packageDir, bunEnv, "--ignore-scripts");
let results = await Promise.all([
exists(join(packageDir, "prepack.txt")),
exists(join(packageDir, "postpack.txt")),
exists(join(packageDir, "preprepare.txt")),
exists(join(packageDir, "prepare.txt")),
exists(join(packageDir, "postprepare.txt")),
]);
expect(results).toEqual([false, false, false, false, false]);
await pack(packageDir, bunEnv);
results = await Promise.all([
exists(join(packageDir, "prepack.txt")),
exists(join(packageDir, "postpack.txt")),
exists(join(packageDir, "preprepare.txt")),
exists(join(packageDir, "prepare.txt")),
exists(join(packageDir, "postprepare.txt")),
]);
expect(results).toEqual([true, true, false, true, false]);
});
test("--quiet", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-quiet-test",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
const { out } = await pack(packageDir, bunEnv, "--quiet");
// Should not contain verbose output
expect(out).not.toContain("Total files:");
expect(out).not.toContain("Shasum:");
expect(out).not.toContain("Integrity:");
expect(out).not.toContain("Unpacked size:");
expect(out).not.toContain("Packed size:");
expect(out).not.toContain("bun pack v");
// Should only contain the tarball name
expect(out.trim()).toBe("pack-quiet-test-1.1.1.tgz");
// Should still create the tarball
expect(await exists(join(packageDir, "pack-quiet-test-1.1.1.tgz"))).toBeTrue();
});
});
test("shasum and integrity are consistent", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-shasum",
version: "1.1.1",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
]);
let { out } = await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-shasum-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([
{
"pathname": "package/package.json",
},
{
"pathname": "package/index.js",
},
]);
expect(out).toContain(`Shasum: ${tarball.shasum}`);
await rm(join(packageDir, "pack-shasum-1.1.1.tgz"));
({ out } = await pack(packageDir, bunEnv));
const secondTarball = readTarball(join(packageDir, "pack-shasum-1.1.1.tgz"));
expect(secondTarball.entries).toMatchObject([
{
"pathname": "package/package.json",
},
{
"pathname": "package/index.js",
},
]);
expect(out).toContain(`Shasum: ${secondTarball.shasum}`);
expect(tarball.shasum).toBe(secondTarball.shasum);
expect(tarball.integrity).toBe(secondTarball.integrity);
});
describe("workspaces", () => {
async function createBasicWorkspace() {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-workspace",
version: "2.2.2",
workspaces: ["pkgs/*"],
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.1.1" })),
write(join(packageDir, "pkgs", "pkg1", "index.js"), "console.log('hello ./index.js')"),
]);
}
test("in a workspace", async () => {
await createBasicWorkspace();
await pack(join(packageDir, "pkgs", "pkg1"), bunEnv);
const tarball = readTarball(join(packageDir, "pkgs", "pkg1", "pkg1-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]);
});
test("in a workspace subdirectory", async () => {
await createBasicWorkspace();
await mkdir(join(packageDir, "pkgs", "pkg1", "subdir"));
await pack(join(packageDir, "pkgs", "pkg1", "subdir"), bunEnv);
const tarball = readTarball(join(packageDir, "pkgs", "pkg1", "pkg1-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]);
});
test("replaces workspace: protocol without lockfile", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-workspace-protocol",
version: "2.3.4",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": "workspace:1.1.1",
},
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.1.1" })),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-workspace-protocol-2.3.4.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/pkgs/pkg1/package.json" },
{ "pathname": "package/root.js" },
]);
expect(JSON.parse(tarball.entries[0].contents)).toEqual({
name: "pack-workspace-protocol",
version: "2.3.4",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": "1.1.1",
},
});
});
const withLockfileWorkspaceProtocolTests = [
{ input: "workspace:^", expected: "^1.1.1" },
{ input: "workspace:~", expected: "~1.1.1" },
{ input: "workspace:1.x", expected: "1.x" },
{ input: "workspace:1.1.x", expected: "1.1.x" },
{ input: "workspace:*", expected: "1.1.1" },
{ input: "workspace:-", expected: "-" },
];
for (const { input, expected } of withLockfileWorkspaceProtocolTests) {
test(`replaces workspace: protocol with lockfile: ${input}`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-workspace-protocol-with-lockfile",
version: "2.5.6",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": input,
},
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.1.1" })),
]);
await runBunInstall(bunEnv, packageDir);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-workspace-protocol-with-lockfile-2.5.6.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/pkgs/pkg1/package.json" },
{ "pathname": "package/root.js" },
]);
expect(JSON.parse(tarball.entries[0].contents)).toEqual({
name: "pack-workspace-protocol-with-lockfile",
version: "2.5.6",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": expected,
},
});
});
}
test("fails gracefully when workspace version fails to resolve", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-workspace-protocol-fail",
version: "2.2.3",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": "workspace:*",
},
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.1.1" })),
]);
const { err } = await packExpectError(packageDir, bunEnv);
expect(err).toContain(
'error: Failed to resolve workspace version for "pkg1" in `dependencies`. Run `bun install` and try again.',
);
await runBunInstall(bunEnv, packageDir);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-workspace-protocol-fail-2.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/pkgs/pkg1/package.json" },
{ "pathname": "package/root.js" },
]);
});
});
test("lifecycle scripts execution order", async () => {
const script = `const fs = require("fs");
fs.writeFileSync(\`\${process.argv[2]}.txt\`, \`
prepack: \${fs.existsSync("prepack.txt")}
prepare: \${fs.existsSync("prepare.txt")}
postpack: \${fs.existsSync("postpack.txt")}
tarball: \${fs.existsSync("pack-lifecycle-order-1.1.1.tgz")}\`)`;
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-lifecycle-order",
version: "1.1.1",
scripts: {
prepack: `${bunExe()} script.js prepack`,
postpack: `${bunExe()} script.js postpack`,
prepare: `${bunExe()} script.js prepare`,
},
}),
),
write(join(packageDir, "script.js"), script),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-lifecycle-order-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/prepack.txt" },
{ "pathname": "package/prepare.txt" },
{ "pathname": "package/script.js" },
]);
const results = await Promise.all([
file(join(packageDir, "prepack.txt")).text(),
file(join(packageDir, "postpack.txt")).text(),
file(join(packageDir, "prepare.txt")).text(),
]);
expect(results).toEqual([
"\nprepack: false\nprepare: false\npostpack: false\ntarball: false",
"\nprepack: true\nprepare: true\npostpack: false\ntarball: true",
"\nprepack: true\nprepare: false\npostpack: false\ntarball: false",
]);
});
describe("bundledDependnecies", () => {
for (const bundledDependencies of ["bundledDependencies", "bundleDependencies"]) {
test(`basic (${bundledDependencies})`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bundled",
version: "4.4.4",
dependencies: {
"dep1": "1.1.1",
},
[bundledDependencies]: ["dep1"],
}),
),
write(
join(packageDir, "node_modules", "dep1", "package.json"),
JSON.stringify({
name: "dep1",
version: "1.1.1",
}),
),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bundled-4.4.4.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/node_modules/dep1/package.json" },
]);
});
}
test(`basic (bundledDependencies: true)`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bundled",
version: "4.4.4",
dependencies: {
"dep1": "1.1.1",
},
devDependencies: {
"dep2": "1.1.1",
},
bundledDependencies: true,
}),
),
write(
join(packageDir, "node_modules", "dep1", "package.json"),
JSON.stringify({
name: "dep1",
version: "1.1.1",
}),
),
write(
join(packageDir, "node_modules", "dep2", "package.json"),
JSON.stringify({
name: "dep2",
version: "1.1.1",
}),
),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bundled-4.4.4.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/node_modules/dep1/package.json" },
]);
});
test(`scoped bundledDependencies`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bundled",
version: "4.4.4",
dependencies: {
"@oven/bun": "1.1.1",
},
bundledDependencies: ["@oven/bun"],
}),
),
write(
join(packageDir, "node_modules", "@oven", "bun", "package.json"),
JSON.stringify({
name: "@oven/bun",
version: "1.1.1",
}),
),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bundled-4.4.4.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/node_modules/@oven/bun/package.json" },
]);
});
test(`invalid bundledDependencies value should throw`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bundled",
version: "4.4.4",
bundledDependencies: "a",
}),
),
]);
const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "pm", "pack"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const err = await stderr.text();
expect(err).toContain("error:");
expect(err).toContain("to be a boolean or an array of strings");
expect(err).not.toContain("warning:");
expect(err).not.toContain("failed");
expect(err).not.toContain("panic:");
const exitCode = await exited;
expect(exitCode).toBe(1);
});
test("resolve dep of bundled dep", async () => {
// Test that a bundled dep can have it's dependencies resolved without
// needing to add them to `bundledDependencies`. Also test that only
// the bundled deps are included, the other files in node_modules are excluded.
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-resolved-bundled-dep",
version: "5.5.5",
dependencies: {
dep1: "1.1.1",
},
bundledDependencies: ["dep1"],
}),
),
write(
join(packageDir, "node_modules", "dep1", "package.json"),
JSON.stringify({
name: "dep1",
version: "1.1.1",
dependencies: {
dep2: "2.2.2",
dep3: "3.3.3",
},
}),
),
write(
join(packageDir, "node_modules", "dep2", "package.json"),
JSON.stringify({
name: "dep2",
version: "2.2.2",
}),
),
write(join(packageDir, "node_modules", "dep1", "node_modules", "excluded.txt"), "do not add to tarball!"),
write(
join(packageDir, "node_modules", "dep1", "node_modules", "dep3", "package.json"),
JSON.stringify({
name: "dep3",
version: "3.3.3",
}),
),
]);
const { out } = await pack(packageDir, bunEnv);
expect(out).toContain("Total files: 4");
expect(out).toContain("Bundled deps: 3");
const tarball = readTarball(join(packageDir, "pack-resolved-bundled-dep-5.5.5.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/node_modules/dep1/node_modules/dep3/package.json" },
{ "pathname": "package/node_modules/dep1/package.json" },
{ "pathname": "package/node_modules/dep2/package.json" },
]);
});
test("scoped names", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-resolve-scoped",
version: "6.6.6",
dependencies: {
"@scoped/dep1": "1.1.1",
},
bundledDependencies: ["@scoped/dep1"],
}),
),
write(
join(packageDir, "node_modules", "@scoped", "dep1", "package.json"),
JSON.stringify({
name: "@scoped/dep1",
version: "1.1.1",
dependencies: {
"@scoped/dep2": "2.2.2",
"@scoped/dep3": "3.3.3",
},
}),
),
write(
join(packageDir, "node_modules", "@scoped", "dep2", "package.json"),
JSON.stringify({
name: "@scoped/dep2",
version: "2.2.2",
}),
),
write(
join(packageDir, "node_modules", "@scoped", "dep1", "node_modules", "@scoped", "dep3", "package.json"),
JSON.stringify({
name: "@scoped/dep3",
version: "3.3.3",
}),
),
]);
const { out } = await pack(packageDir, bunEnv);
expect(out).toContain("Total files: 4");
expect(out).toContain("Bundled deps: 3");
const tarball = readTarball(join(packageDir, "pack-resolve-scoped-6.6.6.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/node_modules/@scoped/dep1/node_modules/@scoped/dep3/package.json" },
{ "pathname": "package/node_modules/@scoped/dep1/package.json" },
{ "pathname": "package/node_modules/@scoped/dep2/package.json" },
]);
});
test("ignore deps that aren't directories", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bundled-dep-not-dir",
version: "4.5.6",
dependencies: {
dep1: "1.1.1",
},
}),
),
write(join(packageDir, "node_modules", "dep1"), "hi. this is a file, not a directory"),
]);
const { out } = await pack(packageDir, bunEnv);
expect(out).toContain("Total files: 1");
expect(out).not.toContain("Bundled deps");
const tarball = readTarball(join(packageDir, "pack-bundled-dep-not-dir-4.5.6.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }]);
});
});
describe("files", () => {
test("CHANGELOG is not included by default", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-changelog",
version: "1.1.1",
files: ["lib"],
}),
),
write(join(packageDir, "CHANGELOG.md"), "hello"),
write(join(packageDir, "lib", "index.js"), "console.log('hello ./lib/index.js')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-changelog-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/lib/index.js" },
]);
});
test(".npmignore cannot exclude CHANGELOG", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-changelog",
version: "1.1.2",
}),
),
write(join(packageDir, ".npmignore"), "CHANGELOG\nCHANGELOG.*"),
write(join(packageDir, "CHANGELOG"), "hello"),
write(join(packageDir, "CHANGELOG.md"), "hello"),
write(join(packageDir, "CHANGELOG.txt"), "hello"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-changelog-1.1.2.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/CHANGELOG" },
{ "pathname": "package/CHANGELOG.md" },
{ "pathname": "package/CHANGELOG.txt" },
]);
});
test("'files' field cannot exclude LICENSE", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-license",
version: "1.1.1",
files: ["lib", "!LICENSE"],
}),
),
write(join(packageDir, "LICENSE"), "hello"),
write(join(packageDir, "lib", "index.js"), "console.log('hello ./lib/index.js')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-license-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/LICENSE" },
{ "pathname": "package/lib/index.js" },
]);
});
test(".npmignore cannot exclude LICENSE", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-license",
version: "1.1.2",
}),
),
write(join(packageDir, ".npmignore"), "LICENSE"),
write(join(packageDir, "LICENSE"), "hello"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-license-1.1.2.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/LICENSE" }]);
});
test("can include files and directories", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-1",
version: "1.1.1",
files: ["root.js", "subdir", "subdir2/subdir"],
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "subdir", "index.js"), "console.log('hello ./subdir/index.js')"),
write(join(packageDir, "subdir", "anotherdir", "index.js"), "console.log('hello ./subdir/anotherdir/index.js')"),
write(join(packageDir, "subdir2", "subdir", "index.js"), "console.log('hello ./subdir2/subdir/index.js')"),
// should not be included
write(join(packageDir, "subdir2", "index.js"), "console.log('hello, dont include me!')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-1-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/root.js" },
{ "pathname": "package/subdir/anotherdir/index.js" },
{ "pathname": "package/subdir/index.js" },
{ "pathname": "package/subdir2/subdir/index.js" },
]);
});
test("matches relative to root by default", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-2",
version: "1.2.3",
files: ["index.js"],
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
write(join(packageDir, "subdir", "index.js"), "console.log('hello ./subdir/index.js')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-2-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]);
});
test("matches './' as the root", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-3",
version: "1.2.3",
files: ["./dist", "!./subdir", "!./dist/index.js", "./////src//index.ts"],
}),
),
write(join(packageDir, "dist", "index.js"), "console.log('hello ./dist/index.js')"),
write(join(packageDir, "subdir", "index.js"), "console.log('hello ./subdir/index.js')"),
write(join(packageDir, "src", "dist", "index.js"), "console.log('hello ./src/dist/index.js')"),
write(join(packageDir, "src", "index.ts"), "console.log('hello ./src/index.ts')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-3-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/dist/index.js" },
{ "pathname": "package/src/index.ts" },
]);
});
test("recursive only if leading **/", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-files-4",
version: "1.2.123",
files: ["**/index.js", "!**/index.test.ts"],
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "subdir", "index.js"), "console.log('hello ./subdir/index.js')"),
write(join(packageDir, "subdir", "anotherdir", "index.js"), "console.log('hello ./subdir/anotherdir/index.js')"),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
write(join(packageDir, "index.test.ts"), "console.log('hello ./index.test.ts')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-files-4-1.2.123.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/index.js" },
{ "pathname": "package/subdir/anotherdir/index.js" },
{ "pathname": "package/subdir/index.js" },
]);
});
test("excluded entries within included directories are not included", async () => {
const dir = tempDirWithFiles("bun-pack-files-excluded-entries", {
"package.json": `
{
"name": "pack-excluded-entries-from-files",
"version": "1.0.0",
"files": ["src/**", "!src/**/*.test.ts"]
}
`,
src: {
"index.ts": "console.log('hello ./src/index.js')",
"index.test.ts": "test('foo', () => expect(1).toBe(1))",
},
});
const { out } = await pack(dir, bunEnv);
expect(out).toContain("Total files: 2");
const tarball = readTarball(join(dir, "pack-excluded-entries-from-files-1.0.0.tgz"));
expect(tarball.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/src/index.ts" },
]);
});
});
describe(".gitignore/.npmignore", () => {
for (const ignoreFile of [".gitignore", ".npmignore"]) {
test(`can ignore and un-ignore a file (${ignoreFile})`, async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-ignore-1",
version: "0.0.0",
}),
),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
write(join(packageDir, ignoreFile), "index.js"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-ignore-1-0.0.0.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }]);
await Promise.all([
rm(join(packageDir, "pack-ignore-1-0.0.0.tgz")),
write(join(packageDir, ignoreFile), "index.js\n!index.js"),
]);
await pack(packageDir, bunEnv);
const tarball2 = readTarball(join(packageDir, "pack-ignore-1-0.0.0.tgz"));
expect(tarball2.entries).toMatchObject([
{ "pathname": "package/package.json" },
{ "pathname": "package/index.js" },
]);
await Promise.all([
rm(join(packageDir, "pack-ignore-1-0.0.0.tgz")),
write(join(packageDir, ignoreFile), "!index.js\nindex.js"),
]);
await pack(packageDir, bunEnv);
const tarball3 = readTarball(join(packageDir, "pack-ignore-1-0.0.0.tgz"));
expect(tarball3.entries).toMatchObject([{ "pathname": "package/package.json" }]);
});
}
test("excludes files recursively", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-ignore-2",
version: "1.2.1",
}),
),
write(join(packageDir, ".npmignore"), "index.js"),
write(join(packageDir, "index.js"), "console.log('hello ./index.js')"),
write(join(packageDir, "subdir", "index.js"), "console.log('hello ./subdir/index.js')"),
write(join(packageDir, "subdir", "subsubdir", "index.js"), "console.log('hello ./subdir/subsubdir/index.js')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-ignore-2-1.2.1.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }]);
});
});
describe("bins", () => {
test("basic", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bins",
version: "1.2.3",
bin: "bin.js",
}),
),
write(join(packageDir, "bin.js"), `#!/usr/bin/env bun\n`),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bins-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{
pathname: "package/package.json",
},
{
pathname: "package/bin.js",
},
]);
expect(tarball.entries[0].perm & 0o644).toBe(0o644);
expect(tarball.entries[1].perm & (0o644 | 0o111)).toBe(0o644 | 0o111);
});
test("directory", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bins-dir",
version: "1.2.3",
directories: {
bin: "bins",
},
}),
),
write(join(packageDir, "bins", "bin1.js"), `#!/usr/bin/env bun\n`),
write(join(packageDir, "bins", "bin2.js"), `#!/usr/bin/env bun\n`),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bins-dir-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{
pathname: "package/package.json",
},
{
pathname: "package/bins/bin1.js",
},
{
pathname: "package/bins/bin2.js",
},
]);
expect(tarball.entries[0].perm & 0o644).toBe(0o644);
expect(tarball.entries[1].perm & (0o644 | 0o111)).toBe(0o644 | 0o111);
expect(tarball.entries[2].perm & (0o644 | 0o111)).toBe(0o644 | 0o111);
});
test('are included even if not included in "files"', async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bins-and-files-1",
version: "2.2.2",
files: ["dist"],
bin: "bin.js",
}),
),
write(join(packageDir, "dist", "hi.js"), "console.log('hi!')"),
write(join(packageDir, "bin.js"), "console.log('hello')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bins-and-files-1-2.2.2.tgz"));
expect(tarball.entries).toMatchObject([
{
pathname: "package/package.json",
},
{
pathname: "package/bin.js",
},
{
pathname: "package/dist/hi.js",
},
]);
});
test('"directories" works with "files"', async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bins-and-files-2",
version: "1.2.3",
files: ["dist"],
directories: {
bin: "bins",
},
}),
),
write(join(packageDir, "dist", "hi.js"), "console.log('hi!')"),
write(join(packageDir, "bins", "bin.js"), "console.log('hello')"),
write(join(packageDir, "bins", "what", "what.js"), "console.log('hello')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bins-and-files-2-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{
pathname: "package/package.json",
},
{
pathname: "package/bins/bin.js",
},
{
pathname: "package/bins/what/what.js",
},
{
pathname: "package/dist/hi.js",
},
]);
});
test('deduplicate with "files"', async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-bins-and-files-2",
version: "1.2.3",
files: ["dist", "bins/bin.js"],
directories: {
bin: "bins",
},
}),
),
write(join(packageDir, "dist", "hi.js"), "console.log('hi!')"),
write(join(packageDir, "bins", "bin.js"), "console.log('hello')"),
write(join(packageDir, "bins", "what", "what.js"), "console.log('hello')"),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-bins-and-files-2-1.2.3.tgz"));
expect(tarball.entries).toMatchObject([
{
pathname: "package/package.json",
},
{
pathname: "package/bins/bin.js",
},
{
pathname: "package/bins/what/what.js",
},
{
pathname: "package/dist/hi.js",
},
]);
});
});
test("unicode", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-unicode",
version: "1.1.1",
}),
),
write(join(packageDir, "äöüščří.js"), `console.log('hello ./äöüščří.js');`),
]);
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-unicode-1.1.1.tgz"));
expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/äöüščří.js" }]);
});
test("$npm_command is accurate", async () => {
await write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-command",
version: "1.1.1",
scripts: {
postpack: "echo $npm_command",
},
}),
);
const p = await pack(packageDir, bunEnv);
expect(p.out.split("\n")).toEqual([
`bun pack ${Bun.version_with_sha}`,
``,
`packed 94B package.json`,
``,
`pack-command-1.1.1.tgz`,
``,
`Total files: 1`,
expect.stringContaining(`Shasum: `),
expect.stringContaining(`Integrity: sha512-`),
`Unpacked size: 94B`,
expect.stringContaining(`Packed size: `),
``,
`pack`,
``,
]);
expect(p.err).toEqual(`$ echo $npm_command\n`);
});
test("$npm_lifecycle_event is accurate", async () => {
await write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-lifecycle",
version: "1.1.1",
scripts: {
postpack: "echo $npm_lifecycle_event",
},
}),
);
const p = await pack(packageDir, bunEnv);
expect(p.out.split("\n")).toEqual([
`bun pack ${Bun.version_with_sha}`,
``,
`packed 104B package.json`,
``,
`pack-lifecycle-1.1.1.tgz`,
``,
`Total files: 1`,
expect.stringContaining(`Shasum: `),
expect.stringContaining(`Integrity: sha512-`),
`Unpacked size: 104B`,
expect.stringContaining(`Packed size: `),
``,
`postpack`,
``,
]);
expect(p.err).toEqual(`$ echo $npm_lifecycle_event\n`);
});