Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/install/lockfile/Package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2263,8 +2263,11 @@ impl Package<u64> {
}
if bin.expr.is_string() {
if let Some(str_) = bin.expr.as_utf8(&bump) {
string_builder.count(str_);
break 'bin;
// The build pass reads `directories.bin` when `bin` is empty.
if !str_.is_empty() {
string_builder.count(str_);
break 'bin;
}
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/install/npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2182,8 +2182,12 @@ impl PackageManifest {
}
}
JSON::E::JsonValue::String(str_) => {
string_builder.count(str_.slice());
break 'bin;
// The build pass reads `directories.bin` when `bin` is empty.
let str_ = str_.slice();
if !str_.is_empty() {
string_builder.count(str_);
break 'bin;
}
}
_ => {}
}
Expand Down
96 changes: 96 additions & 0 deletions test/cli/install/bun-install-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,102 @@ describe("binaries", () => {
expect(err).not.toContain("error:");
expect(await exited).toBe(0);
});

// An empty `bin` names no file, so the bins come from `directories.bin`. Each path below is
// longer than the room that a parsed package.json or manifest has for a string that its
// counting pass missed. The `./` segments drop out of the resolved path, so each one still
// names the folder in its last segment.
describe("`directories.bin` next to an empty `bin`", () => {
const longDirOf = (folder: string) => Buffer.alloc(512, "./").toString() + folder;

/** Runs `bun install` in `packageDir`, expects it to succeed, and returns `bun.lock`. */
async function install(expectedOut: string) {
await using proc = spawn({
cmd: [bunExe(), "install", "--save-text-lockfile"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env,
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ out, err, exitCode, signalCode: proc.signalCode }).toEqual({
out: expect.stringContaining(expectedOut),
err: expect.not.stringContaining("error:"),
exitCode: 0,
signalCode: null,
});
const lockfile = file(join(packageDir, "bun.lock"));
return (await lockfile.exists()) ? ((Bun.JSONC.parse(await lockfile.text()) as any).packages ?? {}) : undefined;
}

test("in the root package.json", async () => {
await write(packageJson, JSON.stringify({ name: "foo", bin: "", directories: { bin: longDirOf("bins") } }));

expect(await install("done")).toBeUndefined();
});

test("in a folder dependency", async () => {
const binDir = longDirOf("executables");
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { "dir-bin": "./dir-bin" } })),
write(
join(packageDir, "dir-bin", "package.json"),
JSON.stringify({ name: "dir-bin", version: "1.1.1", bin: "", directories: { bin: binDir } }),
),
write(
join(packageDir, "dir-bin", "executables", "dir-bin-1.js"),
`#!/usr/bin/env node\nconsole.log("dir-bin-1")`,
),
]);

expect(await install("1 package installed")).toEqual({
"dir-bin": ["dir-bin@file:dir-bin", { binDir }],
});
expect(join(packageDir, "node_modules", ".bin", "dir-bin-1.js")).toBeValidBin(
join("..", "dir-bin", "executables", "dir-bin-1.js"),
);
});

test("in a registry manifest", async () => {
const name = "dep-with-directory-bins";
const binDir = longDirOf("bins");
await using server = Bun.serve({
port: 0,
fetch(request) {
const { origin, pathname } = new URL(request.url);
if (pathname.endsWith(".tgz")) {
return new Response(file(join(import.meta.dir, "registry", "packages", name, `${name}-1.0.0.tgz`)));
}
return Response.json({
name,
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name,
version: "1.0.0",
bin: "",
directories: { bin: binDir },
dist: { tarball: `${origin}/${name}-1.0.0.tgz` },
},
},
});
},
});
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", dependencies: { [name]: "1.0.0" } })),
write(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({ install: { cache: false, registry: server.url.href, linker: "hoisted" } }),
),
]);

expect(await install("1 package installed")).toEqual({
[name]: [`${name}@1.0.0`, `${server.url.origin}/${name}-1.0.0.tgz`, { binDir }, ""],
});
await runBin("directory-bin-1", "directory-bin-1\n", false);
await runBin("directory-bin-2", "directory-bin-2\n", false);
});
});
});

test("--config cli flag works", async () => {
Expand Down
Loading