Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/runtime/cli/filter_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use core::ptr::NonNull;

use bstr::BStr;
use bun_ast::{self, ExprData, Log};
use bun_collections::StringHashMap;
use bun_core::Global;
use bun_core::Output;
use bun_core::{ZStr, strings};
Expand Down Expand Up @@ -178,7 +179,12 @@ pub(crate) fn select_packages(

let mut iter = PackageFilterIterator::init(&glob_patterns, &root_dir)?;
let mut discovered: Vec<WorkspacePackage> = Vec::new();
// Each "workspaces" entry is walked on its own, so two entries can yield the same path.
let mut seen_paths: StringHashMap<()> = StringHashMap::default();
while let Some(package_json_path) = iter.next()? {
if seen_paths.get_or_put(&package_json_path)?.found_existing {
continue;
}
let dir = strings::without_trailing_slash(resolve_path::dirname::<platform::Auto>(
&package_json_path,
));
Expand Down
56 changes: 56 additions & 0 deletions test/cli/run/filter-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,62 @@ describe("bun", () => {
});
});

describe.concurrent('"workspaces" entries that match the same package', () => {
function fixture(workspaces: string[] | { packages: string[] }) {
return tempDir("filter-workspaces-overlap", {
packages: {
app: { "package.json": JSON.stringify({ name: "app", scripts: { build: "echo ran-app" } }) },
legacy: { "package.json": JSON.stringify({ name: "legacy", scripts: { build: "echo ran-legacy" } }) },
},
"package.json": JSON.stringify({ name: "root", private: true, workspaces }),
});
}

// `ran` has one element per script run, so a package that runs twice is listed twice.
async function run(cwd: string, args: string[]) {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", ...args, "build"],
cwd,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { ran: (stdout.match(/ran-\w+/g) ?? []).sort(), stderr, exitCode };
}

test.each([
["a glob and a path", ["packages/*", "packages/legacy"]],
["two globs", ["packages/*", "packages/l*"]],
["one path written two ways", ["packages/app", "packages/legacy", "./packages/legacy/"]],
['the { "packages": [...] } form', { packages: ["packages/*", "packages/legacy"] }],
])("%s: each package runs once", async (_, workspaces) => {
using dir = fixture(workspaces);
const { ran, exitCode } = await run(String(dir), ["--filter", "*"]);
expect({ ran, exitCode }).toEqual({ ran: ["ran-app", "ran-legacy"], exitCode: 0 });
});

test.each([[["--workspaces"]], [["--parallel", "--workspaces"]], [["--sequential", "--filter", "*"]]])(
"%j runs each package once",
async args => {
using dir = fixture(["packages/*", "packages/legacy"]);
const { ran, exitCode } = await run(String(dir), args);
expect({ ran, exitCode }).toEqual({ ran: ["ran-app", "ran-legacy"], exitCode: 0 });
},
);

test("a package.json that fails to parse is reported once", async () => {
using dir = fixture(["packages/*", "packages/legacy"]);
await Bun.write(join(String(dir), "packages", "legacy", "package.json"), "this is { not valid json");
const { ran, stderr, exitCode } = await run(String(dir), ["--filter", "*"]);
expect({ ran, warnings: stderr.split("skipping this workspace package").length - 1, exitCode }).toEqual({
ran: ["ran-app"],
warnings: 1,
exitCode: 0,
});
});
});

describe("selectors", () => {
test("'foo...' runs foo and the workspaces it depends on", () => {
runInCwdSuccess({
Expand Down
Loading