Repository navigation
add support for "workspaces.nohoist"
#20178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2d02d7f
6570cb7
d086756
bf6bdfe
b91ef9b
92163a4
484bdec
031d329
2161995
c2851bf
fad51ad
50a64ff
4376400
e5b196d
57cc645
92187b9
08604b7
eef5bf2
450bfd7
9a8cb96
b69bdb4
16de926
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -249,13 +249,32 @@ pub fn Builder(comptime method: BuilderMethod) type { | |
| lockfile: *const Lockfile, | ||
| // unresolved optional peers that might resolve later. if they do we will want to assign | ||
| // builder.resolutions[peer.dep_id] to the resolved pkg_id. | ||
| pending_optional_peers: std.AutoHashMap(PackageNameHash, bun.collections.ArrayListDefault(DependencyID)), | ||
| pending_optional_peers: std.AutoHashMap(PackageNameHash, ArrayListDefault(DependencyID)), | ||
| manager: if (method == .filter) *const PackageManager else void, | ||
| sort_buf: std.ArrayListUnmanaged(DependencyID) = .{}, | ||
| workspace_filters: if (method == .filter) []const WorkspaceFilter else void = if (method == .filter) &.{}, | ||
| install_root_dependencies: if (method == .filter) bool else void, | ||
| packages_to_install: if (method == .filter) ?[]const PackageID else void, | ||
|
|
||
| pub const FillItem = struct { | ||
| tree_id: Tree.Id, | ||
| dependency_id: DependencyID, | ||
|
|
||
| /// If valid, dependencies will not hoist | ||
| /// beyond this tree if they're in a subtree | ||
| hoist_root_id: Tree.Id, | ||
|
|
||
| /// Relative path of the current tree. Each component is a package | ||
| /// name or part of the package name (if the name is scoped) leading | ||
| /// to the tree. Example values: | ||
| /// "" (the root subpath is empty) | ||
| /// "react/loose-envify" (react -> loose-envify) | ||
| /// "@types/bun/bun-types/@types/node" (@types/bun -> bun-types -> @types/node) | ||
| subpath: Subpath(method), | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| }; | ||
|
markovejnovic marked this conversation as resolved.
Resolved
|
||
|
|
||
| pub const TreeFiller = bun.LinearFifo(FillItem, .Dynamic); | ||
|
|
||
| pub fn maybeReportError(this: *@This(), comptime fmt: string, args: anytype) void { | ||
| this.log.addErrorFmt(null, logger.Loc.Empty, this.allocator, fmt, args) catch {}; | ||
| } | ||
|
|
@@ -429,7 +448,7 @@ pub fn isFilteredDependencyOrWorkspace( | |
| }, | ||
| }; | ||
|
|
||
| switch (bun.glob.match(pattern, name_or_path)) { | ||
| switch (glob.match(pattern, name_or_path)) { | ||
| .match, .negate_match => workspace_matched = true, | ||
|
|
||
| .negate_no_match => { | ||
|
|
@@ -447,11 +466,19 @@ pub fn isFilteredDependencyOrWorkspace( | |
| return !workspace_matched; | ||
| } | ||
|
|
||
| fn Subpath(comptime method: BuilderMethod) type { | ||
| return switch (method) { | ||
| .filter => ArrayListDefault(u8), | ||
| .resolvable => void, | ||
| }; | ||
| } | ||
|
|
||
| pub fn processSubtree( | ||
| this: *const Tree, | ||
| dependency_id: DependencyID, | ||
| hoist_root_id: Tree.Id, | ||
| comptime method: BuilderMethod, | ||
| subpath: Subpath(method), | ||
| builder: *Builder(method), | ||
| ) SubtreeError!void { | ||
| const parent_pkg_id = switch (dependency_id) { | ||
|
|
@@ -496,8 +523,11 @@ pub fn processSubtree( | |
| ); | ||
|
|
||
| for (builder.sort_buf.items) |dep_id| { | ||
| const dependency = builder.dependencies[dep_id]; | ||
| const pkg_id = builder.resolutions[dep_id]; | ||
|
|
||
| var dep_subpath: ArrayListDefault(u8) = .init(); | ||
|
Comment on lines
498
to
+529
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory leak: The Apply this diff to add proper cleanup: for (builder.sort_buf.items) |dep_id| {
const dependency = builder.dependencies[dep_id];
const pkg_id = builder.resolutions[dep_id];
var dep_subpath: ArrayListDefault(u8) = .init();
+ defer bun.memory.deinit(&dep_subpath);
// filter out disabled dependencies🤖 Prompt for AI Agents |
||
|
|
||
| // filter out disabled dependencies | ||
| if (comptime method == .filter) { | ||
| if (isFilteredDependencyOrWorkspace( | ||
|
|
@@ -534,9 +564,48 @@ pub fn processSubtree( | |
| } | ||
| } | ||
|
|
||
| const dependency = builder.dependencies[dep_id]; | ||
|
|
||
| const hoisted: HoistDependencyResult = hoisted: { | ||
| if (comptime method == .filter) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is already Here's how I would approach refactoring this:
I can identify two chunks of code that have two pretty self-contained intents:
Refactor 2.Let's start with var curr = trees[next.id].parent;
while (curr != invalid_id) {
const curr_dep_id = trees[curr].dependency_id;
const curr_pkg_id = switch (curr_dep_id) {
root_dep_id => 0,
else => builder.resolutions[curr_dep_id],
};
var curr_resolutions = builder.resolution_lists[curr_pkg_id];
for (curr_resolutions.begin()..curr_resolutions.end()) |tree_dep_id| {
const res_id = builder.resolutions[tree_dep_id];
if (res_id == pkg_id) {
break :try_nohoist;
}
}
curr = trees[curr].parent;
}This chunk of code is prefixed by a comment: // make sure it's not circularLet's use that as the name: fn makeSureTreeNotCircular(...) boolOkay, well this is a predicate, so maybe the verb should be fn isTreeCircular(...) boolGreat! That seems a little neater. What are the parameters that this function has? Now that we have the name, we can probably infer that it will have to take a tree. It shouldn't need to mutate it, so I suppose that it should take an immutable ref. fn isTreeCircular(tree: *const Tree) boolGreat, let's copy-paste the implementation fn isTreeCircular(tree: *const Tree) bool {
var curr = trees[next.id].parent;
while (curr != invalid_id) {
const curr_dep_id = trees[curr].dependency_id;
const curr_pkg_id = switch (curr_dep_id) {
root_dep_id => 0,
else => builder.resolutions[curr_dep_id],
};
var curr_resolutions = builder.resolution_lists[curr_pkg_id];
for (curr_resolutions.begin()..curr_resolutions.end()) |tree_dep_id| {
const res_id = builder.resolutions[tree_dep_id];
if (res_id == pkg_id) {
break :try_nohoist;
}
}
curr = trees[curr].parent;
}
}Wow! Far lower nesting, but this won't really compile as-is. There's a couple invalid references now. Without any compilation I see: fn isTreeCircular(pkg_id: PackageID, forest: []const Tree, tree: *const Tree) bool {
var curr = forest[tree.id].parent;
while (curr != invalid_id) {
const curr_dep_id = forest[curr].dependency_id;
const curr_pkg_id = switch (curr_dep_id) {
root_dep_id => 0,
else => builder.resolutions[curr_dep_id],
};
var curr_resolutions = builder.resolution_lists[curr_pkg_id];
for (curr_resolutions.begin()..curr_resolutions.end()) |tree_dep_id| {
const res_id = builder.resolutions[tree_dep_id];
if (res_id == pkg_id) {
return true;
}
}
curr = forest[curr].parent;
}
return false;
}We still depend on a) Make this new function a member of I lean towards the former. I ended up, after a little bit of cleanup and renaming on the following: for (builder.manager.nohoist_patterns) |nohoist_pattern| {
if (glob.match(nohoist_pattern, dep_subpath.items()).matches()) {
if (builder.pkgHasCircularReference(pkg_id, trees, next)) {
break :try_nohoist;
}
break :hoisted .{ .placement = .{ .id = next.id } };
}
}Nesting this deep is usually a code-smell, but we have a cheap way to invert the condition and reduce one level of nesting. for (builder.manager.nohoist_patterns) |nohoist_pattern| {
if (!glob.match(nohoist_pattern, dep_subpath.items()).matches()) {
continue;
}
if (builder.pkgHasCircularReference(pkg_id, trees, next)) {
break :try_nohoist;
}
break :hoisted .{ .placement = .{ .id = next.id } };
}AsideThis is very optional, but I would split up the loop, too: for (builder.manager.nohoist_patterns) |nohoist_pattern| {
if (!glob.match(nohoist_pattern, dep_subpath.items()).matches()) {
continue;
}
if (builder.pkgHasCircularReference(pkg_id, trees, next)) {
break :try_nohoist;
}
break :hoisted .{ .placement = .{ .id = next.id } };
}Like so: struct Builder {
fn shouldNoHoist(...)
};
and
```zig
if (builder.shouldNoHoist(dep_subpath.items(), pkg_id, trees, next) catch |err| {
switch (err) {
error.CircularDependency => break :try_nohoist,
}
}) {
break :hoisted .{ .placement = .{ .id = next.id } };
}Refactor 1The next thing I would do is try to isolate the changes you've made into a self-contained function. Blocks are crappier, weaker versions of functions, and you already have a pretty good name for a function here -- You could maybe attach this to pub fn tryNohoist(
self: *const @This(),
subpath: []const u8,
dependency_id: DependencyID,
) ?error{CircularDependency}!struct {
result: HoistDependencyResult,
subpath: bun.collections.ArrayListDefault(u8),
}and you end up with something like this if (try builder.tryNohoist(if (method == .filter) subpath.items() else {}, dep_id)) |r| {
dep_subpath = r.subpath;
break :hoisted r.result;
}At the call site. All my suggestions are available here: #24646 |
||
| // not filtered, but does it match a nohoist pattern? | ||
| if (!builder.manager.nohoist_patterns.isEmpty()) try_nohoist: { | ||
| const string_buf = builder.lockfile.buffers.string_bytes.items; | ||
|
|
||
| try dep_subpath.ensureTotalCapacity(subpath.items().len + @intFromBool(!subpath.isEmpty()) + dependency.name.len()); | ||
| dep_subpath.appendSliceAssumeCapacity(subpath.items()); | ||
| if (!subpath.isEmpty()) { | ||
| dep_subpath.appendAssumeCapacity('/'); | ||
| } | ||
| dep_subpath.appendSliceAssumeCapacity(dependency.name.slice(string_buf)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't completely understand
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| if (dependency.version.tag == .workspace) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couple questions from me:
pub const Tag = enum(u8) {
pub fn supportsHoisting(self: @This()) {
return switch(self) {
.uninitialized, workspace, catalog => false,
.npm, .dist_tag, ... => true,
};
}I would suggest against using
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a unique property to workspace dependencies in bun: they always have an implicit dependency appended to Always hoisting them works because if the explicit workspace dependency name has a different name than the implicit workspace dependency it won't be able to hoist anyways matching the behavior of |
||
| break :try_nohoist; | ||
| } | ||
|
|
||
| for (builder.manager.nohoist_patterns.items()) |nohoist_pattern| { | ||
| if (glob.match(nohoist_pattern, dep_subpath.items()).matches()) { | ||
| // make sure it's not circular | ||
| var curr = trees[next.id].parent; | ||
| while (curr != invalid_id) { | ||
| const curr_dep_id = trees[curr].dependency_id; | ||
| const curr_pkg_id = switch (curr_dep_id) { | ||
| root_dep_id => 0, | ||
| else => builder.resolutions[curr_dep_id], | ||
| }; | ||
| var curr_resolutions = builder.resolution_lists[curr_pkg_id]; | ||
| for (curr_resolutions.begin()..curr_resolutions.end()) |tree_dep_id| { | ||
| const res_id = builder.resolutions[tree_dep_id]; | ||
| if (res_id == pkg_id) { | ||
| break :try_nohoist; | ||
|
markovejnovic marked this conversation as resolved.
Resolved
|
||
| } | ||
| } | ||
|
|
||
| curr = trees[curr].parent; | ||
| } | ||
| break :hoisted .{ .placement = .{ .id = next.id } }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // don't hoist if it's a folder dependency or a bundled dependency. | ||
| if (dependency.behavior.isBundled()) { | ||
|
|
@@ -617,6 +686,7 @@ pub fn processSubtree( | |
| .tree_id = replace.id, | ||
| .dependency_id = dep_id, | ||
| .hoist_root_id = hoist_root_id, | ||
| .subpath = if (comptime method == .filter) dep_subpath else {}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure transferred ownership is properly managed. When After fixing the memory leak identified earlier by adding a .subpath = dep_subpath,
dep_subpath = .init(); // Reset to prevent defer from freeing transferred bufferAlternatively, use a boolean flag to track whether ownership was transferred and conditionally deinit in the defer. Also applies to: 713-713 🤖 Prompt for AI Agents |
||
| }); | ||
| } | ||
| }, | ||
|
|
@@ -640,6 +710,7 @@ pub fn processSubtree( | |
|
|
||
| // if it's bundled, start a new hoist root | ||
| .hoist_root_id = if (dest.bundled) dest.id else hoist_root_id, | ||
| .subpath = if (comptime method == .filter) dep_subpath else {}, | ||
| }); | ||
| } | ||
| }, | ||
|
|
@@ -763,17 +834,6 @@ fn hoistDependency( | |
| return .{ .placement = .{ .id = this.id } }; // 2 | ||
| } | ||
|
|
||
| pub const FillItem = struct { | ||
| tree_id: Tree.Id, | ||
| dependency_id: DependencyID, | ||
|
|
||
| /// If valid, dependencies will not hoist | ||
| /// beyond this tree if they're in a subtree | ||
| hoist_root_id: Tree.Id, | ||
| }; | ||
|
|
||
| pub const TreeFiller = bun.LinearFifo(FillItem, .Dynamic); | ||
|
|
||
| const string = []const u8; | ||
| const stringZ = [:0]const u8; | ||
|
|
||
|
|
@@ -786,7 +846,9 @@ const OOM = bun.OOM; | |
| const Output = bun.Output; | ||
| const Path = bun.path; | ||
| const assert = bun.assert; | ||
| const glob = bun.glob; | ||
| const logger = bun.logger; | ||
| const ArrayListDefault = bun.collections.ArrayListDefault; | ||
| const Bitset = bun.bit_set.DynamicBitSetUnmanaged; | ||
| const String = bun.Semver.String; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { exists } from "fs/promises"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { VerdaccioRegistry, bunEnv, readdirSorted, runBunInstall } from "harness"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var registry = new VerdaccioRegistry(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await registry.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterAll(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| registry.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("workspaces.nohoist", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("basic", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { packageDir } = await registry.createTestDir({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "package.json": JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "basic-nohoist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaces: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nohoist: ["one-dep/no-deps"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dependencies: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "one-dep": "1.0.0", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await runBunInstall(bunEnv, packageDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["one-dep"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete test validation - verify nohoist behavior. The test configures Apply this diff to add the missing assertion: await runBunInstall(bunEnv, packageDir);
expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["one-dep"]);
+ expect(await readdirSorted(join(packageDir, "node_modules/one-dep/node_modules"))).toEqual(["no-deps"]);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("can keep package in workspace node_modules", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { packageDir } = await registry.createTestDir({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "package.json": JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "workspace-nohoist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaces: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| packages: ["packages/*"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nohoist: ["**/one-dep"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dependencies: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "a-dep": "1.0.1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "packages/pkg1/package.json": JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "pkg1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dependencies: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "one-dep": "1.0.0", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await runBunInstall(bunEnv, packageDir, { linker: "hoisted" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["a-dep", "no-deps", "pkg1"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(await readdirSorted(join(packageDir, "packages/pkg1/node_modules"))).toEqual(["one-dep"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Request clarification on test package dependencies. The test expects Example: test("can keep package in workspace node_modules", async () => {
+ // Dependency graph: a-dep@1.0.1 -> no-deps, pkg1 -> one-dep@1.0.0 -> no-deps
+ // nohoist pattern "**/one-dep" keeps one-dep in workspace node_modules
const { packageDir } = await registry.createTestDir({📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("handles cycles", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { packageDir } = await registry.createTestDir({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "package.json": JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "cycles", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaces: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nohoist: ["**"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dependencies: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "a-dep-b": "1.0.0", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await runBunInstall(bunEnv, packageDir, { linker: "hoisted" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readdirSorted(join(packageDir, "node_modules")), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readdirSorted(join(packageDir, "node_modules/a-dep-b/node_modules")), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exists(join(packageDir, "node_modules/a-dep-b/node_modules/b-dep-a/node_modules")), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toEqual([["a-dep-b"], ["b-dep-a"], false]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Good edge case coverage for circular dependencies. The test correctly validates nohoist behavior with circular dependencies using the Example: test("handles cycles", async () => {
+ // Tests circular dependency: a-dep-b -> b-dep-a -> a-dep-b
+ // nohoist "**" prevents hoisting, but cycle must be broken to avoid infinite nesting
const { packageDir } = await registry.createTestDir({🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add remaining test cases per PR objectives. The PR objectives mention a TODO checklist that includes additional test scenarios. Current test coverage includes basic, workspace, and cycle cases, but consider adding:
These additional tests would help reveal remaining bugs as mentioned in the PR description. Would you like me to help generate additional test cases to cover these scenarios? 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential memory leak in error path
If parsing succeeds for some patterns but fails on a later pattern (line 1594-1596), the successfully cloned strings in
nohoist_patternsare leaked because the ArrayList is never assigned topm.nohoist_patternsand never deinitialized.Consider deferring cleanup or building directly into
pm.nohoist_patterns:🤖 Prompt for AI Agents