Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b2bfd2feb2 fix(workspaces): treat leading slashes as relative paths for npm compatibility
Workspace patterns with leading slashes (e.g., "/packages/*") were being
treated as absolute filesystem paths, causing ENOENT errors. npm treats
these as relative to the workspace root.

This change strips leading slashes from workspace patterns in both:
- bun install (WorkspaceMap.zig)
- bun run --filter (filter_arg.zig)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:45:10 +00:00
3 changed files with 156 additions and 4 deletions

View File

@@ -69,10 +69,12 @@ pub fn getCandidatePackagePatterns(allocator: std.mem.Allocator, log: *bun.logge
for (json_array.slice()) |expr| {
switch (expr.data) {
.e_string => |pattern_expr| {
const size = pattern_expr.data.len + "/package.json".len;
// Strip leading slashes to treat workspace patterns as relative paths (npm compatibility)
const workspace_pattern = strings.withoutLeadingSlash(pattern_expr.data);
const size = workspace_pattern.len + "/package.json".len;
var pattern = try allocator.alloc(u8, size);
@memcpy(pattern[0..pattern_expr.data.len], pattern_expr.data);
@memcpy(pattern[pattern_expr.data.len..size], "/package.json");
@memcpy(pattern[0..workspace_pattern.len], workspace_pattern);
@memcpy(pattern[workspace_pattern.len..size], "/package.json");
try out_patterns.append(pattern);
},

View File

@@ -118,7 +118,7 @@ pub fn processNamesArray(
for (arr.slice()) |item| {
// TODO: when does this get deallocated?
const input_path = try item.asStringZ(allocator) orelse {
const input_path_raw = try item.asStringZ(allocator) orelse {
log.addErrorFmt(source, item.loc, allocator,
\\Workspaces expects an array of strings, like:
\\ <r><green>"workspaces"<r>: [
@@ -128,6 +128,9 @@ pub fn processNamesArray(
return error.InvalidPackageJSON;
};
// Strip leading slashes to treat workspace patterns as relative paths (npm compatibility)
const input_path = strings.withoutLeadingSlash(input_path_raw);
if (input_path.len == 0 or input_path.len == 1 and input_path[0] == '.' or strings.eqlComptime(input_path, "./") or strings.eqlComptime(input_path, ".\\")) continue;
if (glob.detectGlobSyntax(input_path)) {

View File

@@ -0,0 +1,147 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("bun install works with leading slash in workspace pattern", async () => {
using dir = tempDir("ws-leading-slash", {
"packages/foo/package.json": JSON.stringify({
name: "foo",
version: "1.0.0",
}),
"package.json": JSON.stringify({
name: "leading-slash-test",
private: true,
workspaces: ["/packages/*"],
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stderr).not.toContain("ENOENT");
expect(exitCode).toBe(0);
});
test("bun run --filter works with leading slash in workspace pattern", async () => {
using dir = tempDir("ws-leading-slash-filter", {
"packages/bar/package.json": JSON.stringify({
name: "bar",
version: "1.0.0",
scripts: {
build: "echo building bar",
},
}),
"package.json": JSON.stringify({
name: "leading-slash-filter-test",
private: true,
workspaces: ["/packages/*"],
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--filter", "*", "build"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stderr).not.toContain("ENOENT");
expect(stdout).toContain("building bar");
expect(exitCode).toBe(0);
});
test("bun install works with multiple leading slashes in workspace pattern", async () => {
using dir = tempDir("ws-multi-slash", {
"packages/baz/package.json": JSON.stringify({
name: "baz",
version: "1.0.0",
}),
"package.json": JSON.stringify({
name: "multi-slash-test",
private: true,
workspaces: ["///packages/*"],
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stderr).not.toContain("ENOENT");
expect(exitCode).toBe(0);
});
test("normal workspace patterns still work (regression check)", async () => {
using dir = tempDir("ws-normal", {
"packages/pkg/package.json": JSON.stringify({
name: "pkg",
version: "1.0.0",
scripts: {
test: "echo testing pkg",
},
}),
"package.json": JSON.stringify({
name: "normal-test",
private: true,
workspaces: ["packages/*"],
}),
});
// Test install
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const installExitCode = await installProc.exited;
expect(installExitCode).toBe(0);
// Test filter
await using filterProc = Bun.spawn({
cmd: [bunExe(), "run", "--filter", "*", "test"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [filterStdout, filterStderr, filterExitCode] = await Promise.all([
new Response(filterProc.stdout).text(),
new Response(filterProc.stderr).text(),
filterProc.exited,
]);
expect(filterStderr).not.toContain("ENOENT");
expect(filterStdout).toContain("testing pkg");
expect(filterExitCode).toBe(0);
});