Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
7c3a4b42ad feat(install): add --filter support infrastructure for bun add
This implements the CLI and infrastructure for --filter support in bun add,
enabling the foundation for workspace-specific dependency addition.

Current implementation:
-  --filter flag parsing and validation
-  CLI help text with examples
-  Infrastructure integration with existing workspace filtering
-  Multi-package.json editing (requires additional implementation)

Current behavior: bun add pkg --filter="workspace" adds to root package.json
Expected behavior: Should add to matching workspace package.json files

The core filtering infrastructure is in place. Full implementation requires
extending PackageJSONEditor.edit() to support editing multiple workspace
package.json files when filters are specified.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-25 09:35:04 +00:00
7 changed files with 226 additions and 1 deletions

26
filter-test/bun.lock Normal file
View File

@@ -0,0 +1,26 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "root",
"dependencies": {
"lodash": "^4.17.21",
},
},
"packages/pkg1": {
"name": "pkg1",
"version": "0.0.1",
},
"packages/pkg2": {
"name": "pkg2",
"version": "0.0.1",
},
},
"packages": {
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"pkg1": ["pkg1@workspace:packages/pkg1"],
"pkg2": ["pkg2@workspace:packages/pkg2"],
}
}

8
filter-test/package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "root",
"version": "1.0.0",
"workspaces": ["packages/*"],
"dependencies": {
"lodash": "^4.17.21"
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "pkg1",
"version": "0.0.1"
}
EOF < /dev/null

View File

@@ -0,0 +1,5 @@
{
"name": "pkg2",
"version": "0.0.1"
}
EOF < /dev/null

View File

@@ -176,8 +176,8 @@ pub const Subcommand = enum {
return switch (this) {
.outdated => true,
.install => true,
.add => true,
// .pack => true,
// .add => true,
else => false,
};
}

View File

@@ -93,6 +93,7 @@ pub const add_params: []const ParamType = &(shared_params ++ [_]ParamType{
clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable,
clap.parseParam("--peer Add dependency to \"peerDependencies\"") catch unreachable,
clap.parseParam("-E, --exact Add the exact version instead of the ^range") catch unreachable,
clap.parseParam("--filter <STR>... Add packages to the matching workspaces") catch unreachable,
clap.parseParam("-a, --analyze Recursively analyze & install dependencies of files passed as arguments (using Bun's bundler)") catch unreachable,
clap.parseParam("--only-missing Only add dependencies to package.json if they are not already present") catch unreachable,
clap.parseParam("<POS> ... \"name\" or \"name@version\" of package(s) to install") catch unreachable,
@@ -400,6 +401,10 @@ pub fn printHelp(subcommand: Subcommand) void {
\\ <b><green>bun add<r> <cyan>--optional<r> <blue>lodash<r>
\\ <b><green>bun add<r> <cyan>--peer<r> <blue>esbuild<r>
\\
\\ <d>Add to specific workspaces using --filter<r>
\\ <b><green>bun add<r> <blue>lodash<r> <cyan>--filter="@workspace/package"<r>
\\ <b><green>bun add<r> <blue>lodash<r> <cyan>--filter="./packages/*"<r>
\\
\\Full documentation is available at <magenta>https://bun.com/docs/cli/add<r>.
\\
;

View File

@@ -2324,3 +2324,179 @@ it("should add multiple dependencies specified on command line", async () => {
});
await access(join(package_dir, "bun.lockb"));
});
it("should add dependency to specific workspace using --filter", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"0.0.2": {},
}),
);
// Set up root package.json with workspaces
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "root",
version: "1.0.0",
workspaces: ["packages/*"],
}),
);
// Create workspace packages
await mkdir(join(package_dir, "packages", "pkg1"), { recursive: true });
await writeFile(
join(package_dir, "packages", "pkg1", "package.json"),
JSON.stringify({
name: "pkg1",
version: "0.0.1",
}),
);
await mkdir(join(package_dir, "packages", "pkg2"), { recursive: true });
await writeFile(
join(package_dir, "packages", "pkg2", "package.json"),
JSON.stringify({
name: "pkg2",
version: "0.0.1",
}),
);
// Add dependency to specific workspace using --filter
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "add", "bar", "--filter", "pkg1"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed bar@0.0.2",
"",
"3 packages installed",
]);
expect(await exited).toBe(0);
// Verify dependency was added only to pkg1
const pkg1Json = await file(join(package_dir, "packages", "pkg1", "package.json")).json();
expect(pkg1Json).toMatchObject({
name: "pkg1",
version: "0.0.1",
dependencies: {
bar: "^0.0.2",
},
});
// Verify dependency was NOT added to pkg2
const pkg2Json = await file(join(package_dir, "packages", "pkg2", "package.json")).json();
expect(pkg2Json).toEqual({
name: "pkg2",
version: "0.0.1",
});
// Verify dependency was NOT added to root
const rootJson = await file(join(package_dir, "package.json")).json();
expect(rootJson).toEqual({
name: "root",
version: "1.0.0",
workspaces: ["packages/*"],
});
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
await access(join(package_dir, "bun.lockb"));
});
it("should add dependency to multiple workspaces using path filter", async () => {
const urls: string[] = [];
setHandler(
dummyRegistry(urls, {
"0.0.2": {},
}),
);
// Set up root package.json with workspaces
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({
name: "root",
version: "1.0.0",
workspaces: ["packages/*"],
}),
);
// Create workspace packages
await mkdir(join(package_dir, "packages", "app"), { recursive: true });
await writeFile(
join(package_dir, "packages", "app", "package.json"),
JSON.stringify({
name: "@workspace/app",
version: "0.0.1",
}),
);
await mkdir(join(package_dir, "packages", "lib"), { recursive: true });
await writeFile(
join(package_dir, "packages", "lib", "package.json"),
JSON.stringify({
name: "@workspace/lib",
version: "0.0.1",
}),
);
// Add to specific workspace using path pattern filter
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "add", "bar", "--filter", "./packages/app"],
cwd: package_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed bar@0.0.2",
"",
"3 packages installed",
]);
expect(await exited).toBe(0);
// Verify dependency was added only to app
const appJson = await file(join(package_dir, "packages", "app", "package.json")).json();
expect(appJson).toMatchObject({
name: "@workspace/app",
version: "0.0.1",
dependencies: {
bar: "^0.0.2",
},
});
// Verify dependency was NOT added to lib
const libJson = await file(join(package_dir, "packages", "lib", "package.json")).json();
expect(libJson).toEqual({
name: "@workspace/lib",
version: "0.0.1",
});
expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]);
expect(requested).toBe(2);
await access(join(package_dir, "bun.lockb"));
});