Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
8aa16b0f48 WIP: Add parent tracking infrastructure for nested overrides
Added extensive debugging and parent package tracking, but runtime application
still not working due to timing issues:

- Added findParentPackageForDependency() to search for parent package
- Modified enqueueDependencyList() to accept parent_package_id parameter
- Created enqueueDependencyWithMainAndSuccessFnAndParent() wrapper
- Added extensive debug logging throughout override lookup

Issue: Dependencies are often enqueued before being added to parent's dependency
list, so findParentPackageForDependency() returns null. Need different approach
to track parent context, possibly:
1. Store parent info in dependency buffer itself
2. Pass parent through entire resolution chain
3. Defer override application until after full dependency tree is built

Current test status: 7/13 tests passing (all basic global overrides work,
nested overrides parsed but not applied at runtime)
2025-10-03 07:35:17 +00:00
Claude Bot
c8f3699e03 Fix npm override parsing - parent is key, children are nested values
After testing actual npm behavior, discovered the correct format is:
{
  "overrides": {
    "parent-pkg": {
      "child-pkg": "version"
    }
  }
}

NOT what I originally implemented (child as key with parent nested inside).

Updated parseFromOverrides to correctly parse this structure. Override is now
stored in lockfile correctly, but runtime application still needs debugging.

Test results:
- Parsing works ✓ (override appears in lockfile)
- Runtime application ✗ (override not applied during install)

Next: Debug why parent_name_hash lookup isn't working in PackageManagerEnqueue
2025-10-03 06:23:34 +00:00
Claude Bot
ce144db1cc Implement nested overrides/resolutions in bun install
This implements support for nested dependency overrides in both npm and yarn
formats. Nested overrides allow you to override a dependency version only when
it's required by a specific parent package, rather than globally.

## NPM Format (overrides)

```json
{
  "overrides": {
    "child-pkg": {
      ".": "1.0.0",
      "parent-pkg": {
        "child-pkg": "2.0.0"
      }
    }
  }
}
```

The "." key provides a global override, while parent package names can specify
version overrides that only apply when that parent requires the child package.

## Yarn Format (resolutions)

```json
{
  "resolutions": {
    "child-pkg": "1.0.0",
    "parent-pkg/child-pkg": "2.0.0",
    "@scope/parent/child-pkg": "3.0.0"
  }
}
```

Yarn uses a slash-separated format where `parent/child` creates a nested
override. The `**/` prefix is also supported for global overrides.

## Implementation Details

- Updated `OverrideMap` to use a union type that supports both global and
  nested overrides
- Modified `parseFromOverrides` to parse the npm nested object format
- Modified `parseFromResolutions` to parse yarn's parent/child path format
- Updated `PackageManagerEnqueue` to pass parent package information when
  looking up overrides
- Added `eql` and `toExternal` methods to `OverrideValue` for lockfile
  operations
- Updated all override map serialization code to handle the new structure

## Limitations

- Binary lockfile format (bun.lockb) only stores global overrides from nested
  configurations (parent-specific overrides are lost in binary serialization)
- Tests added but some scenarios still need investigation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 03:34:25 +00:00
10 changed files with 677 additions and 119 deletions

View File

@@ -337,7 +337,7 @@ pub const AsyncModule = struct {
// we are only truly done if all the dependencies are done.
const current_tasks = pm.total_tasks;
// so if enqueuing all the dependencies produces no new tasks, we are done.
pm.enqueueDependencyList(package.dependencies);
pm.enqueueDependencyList(package_id, package.dependencies);
if (current_tasks == pm.total_tasks) {
tags[tag_i] = .done;
done_count += 1;

View File

@@ -442,7 +442,7 @@ pub fn get() *PackageManager {
pub const SuccessFn = *const fn (*PackageManager, DependencyID, PackageID) void;
pub const FailFn = *const fn (*PackageManager, *const Dependency, PackageID, anyerror) void;
pub const debug = Output.scoped(.PackageManager, .hidden);
pub const debug = Output.scoped(.PackageManager, .visible);
pub fn ensureTempNodeGypScript(this: *PackageManager) !void {
return ensureTempNodeGypScriptOnce.call(.{this});
@@ -1176,7 +1176,9 @@ pub const enqueue = @import("./PackageManager/PackageManagerEnqueue.zig");
pub const enqueueDependencyList = enqueue.enqueueDependencyList;
pub const enqueueDependencyToRoot = enqueue.enqueueDependencyToRoot;
pub const enqueueDependencyWithMain = enqueue.enqueueDependencyWithMain;
pub const enqueueDependencyWithMainAndParent = enqueue.enqueueDependencyWithMainAndParent;
pub const enqueueDependencyWithMainAndSuccessFn = enqueue.enqueueDependencyWithMainAndSuccessFn;
pub const enqueueDependencyWithMainAndSuccessFnAndParent = enqueue.enqueueDependencyWithMainAndSuccessFnAndParent;
pub const enqueueExtractNPMPackage = enqueue.enqueueExtractNPMPackage;
pub const enqueueGitCheckout = enqueue.enqueueGitCheckout;
pub const enqueueGitForCheckout = enqueue.enqueueGitForCheckout;

View File

@@ -16,10 +16,32 @@ pub fn enqueueDependencyWithMain(
);
}
pub fn enqueueDependencyWithMainAndParent(
this: *PackageManager,
id: DependencyID,
/// This must be a *const to prevent UB
dependency: *const Dependency,
resolution: PackageID,
parent_package_id: PackageID,
install_peer: bool,
) !void {
return this.enqueueDependencyWithMainAndSuccessFnAndParent(
id,
dependency,
resolution,
parent_package_id,
install_peer,
assignResolution,
null,
);
}
pub fn enqueueDependencyList(
this: *PackageManager,
parent_package_id: PackageID,
dependencies_list: Lockfile.DependencySlice,
) void {
debug("enqueueDependencyList called with parent_package_id={d}, dependencies_list.len={d}", .{ parent_package_id, dependencies_list.len });
this.task_queue.ensureUnusedCapacity(this.allocator, dependencies_list.len) catch unreachable;
const lockfile = this.lockfile;
@@ -58,10 +80,11 @@ pub fn enqueueDependencyList(
while (i < end) : (i += 1) {
const dependency = lockfile.buffers.dependencies.items[i];
const resolution = lockfile.buffers.resolutions.items[i];
this.enqueueDependencyWithMain(
this.enqueueDependencyWithMainAndParent(
i,
&dependency,
resolution,
parent_package_id,
false,
) catch |err| {
const note = .{
@@ -431,6 +454,20 @@ pub fn enqueuePatchTaskPre(this: *PackageManager, task: *PatchTask) void {
_ = this.pending_pre_calc_hashes.fetchAdd(1, .monotonic);
}
/// Find the parent package that contains a given dependency ID
fn findParentPackageForDependency(pm: *PackageManager, dependency_id: DependencyID) ?PackageID {
const packages = pm.lockfile.packages.slice();
debug("findParentPackageForDependency: looking for dependency_id={d} in {d} packages", .{ dependency_id, packages.len });
for (packages.items(.dependencies), 0..) |dep_slice, pkg_id| {
if (dep_slice.contains(dependency_id)) {
debug(" found in package {d}", .{pkg_id});
return @intCast(pkg_id);
}
}
debug(" not found in any package", .{});
return null;
}
/// Q: "What do we do with a dependency in a package.json?"
/// A: "We enqueue it!"
pub fn enqueueDependencyWithMainAndSuccessFn(
@@ -442,6 +479,32 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
install_peer: bool,
comptime successFn: SuccessFn,
comptime failFn: ?FailFn,
) !void {
debug("enqueueDependencyWithMainAndSuccessFn: id={d}, looking for parent...", .{id});
// Try to find the parent package for nested override support
const parent_package_id = findParentPackageForDependency(this, id);
debug("enqueueDependencyWithMainAndSuccessFn: parent_package_id={?d}", .{parent_package_id});
return this.enqueueDependencyWithMainAndSuccessFnAndParent(
id,
dependency,
resolution,
parent_package_id,
install_peer,
successFn,
failFn,
);
}
pub fn enqueueDependencyWithMainAndSuccessFnAndParent(
this: *PackageManager,
id: DependencyID,
/// This must be a *const to prevent UB
dependency: *const Dependency,
resolution: PackageID,
parent_package_id: ?PackageID,
install_peer: bool,
comptime successFn: SuccessFn,
comptime failFn: ?FailFn,
) !void {
if (dependency.behavior.isOptionalPeer()) return;
@@ -478,7 +541,25 @@ pub fn enqueueDependencyWithMainAndSuccessFn(
// allow overriding all dependencies unless the dependency is coming directly from an alias, "npm:<this dep>" or
// if it's a workspaceOnly dependency
if (!dependency.behavior.isWorkspace() and (dependency.version.tag != .npm or !dependency.version.value.npm.is_alias)) {
if (this.lockfile.overrides.get(name_hash)) |new| {
// Get parent package name hash for nested override lookup
const parent_name_hash: ?PackageNameHash = if (parent_package_id) |parent_id| blk: {
debug("parent_package_id = {d}, invalid_package_id = {d}, packages.len = {d}", .{ parent_id, invalid_package_id, this.lockfile.packages.len });
if (parent_id != invalid_package_id and parent_id < this.lockfile.packages.len) {
const parent_pkg = this.lockfile.packages.get(parent_id);
const parent_hash = parent_pkg.name_hash;
const parent_name = this.lockfile.str(&parent_pkg.name);
debug("parent package: {s} (hash: {x})", .{ parent_name, parent_hash });
break :blk parent_hash;
} else {
debug("parent_id is invalid or out of bounds", .{});
break :blk null;
}
} else blk: {
debug("no parent_package_id provided", .{});
break :blk null;
};
if (this.lockfile.overrides.get(name_hash, parent_name_hash)) |new| {
debug("override: {s} -> {s}", .{ this.lockfile.str(&dependency.version.literal), this.lockfile.str(&new.literal) });
name, name_hash = updateNameAndNameHashFromVersionReplacement(this.lockfile, name, name_hash, new);

View File

@@ -119,11 +119,11 @@ pub fn resolveFromDiskCache(this: *PackageManager, package_name: []const u8, ver
};
switch (FolderResolution.getOrPut(.{ .cache_folder = npm_package_path }, dependency, ".", this)) {
.new_package_id => |id| {
this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id]);
this.enqueueDependencyList(id, this.lockfile.packages.items(.dependencies)[id]);
return id;
},
.package_id => |id| {
this.enqueueDependencyList(this.lockfile.packages.items(.dependencies)[id]);
this.enqueueDependencyList(id, this.lockfile.packages.items(.dependencies)[id]);
return id;
},
.err => |err| {

View File

@@ -472,7 +472,7 @@ pub fn installWithManager(
var iter = manager.lockfile.patched_dependencies.iterator();
while (iter.next()) |entry| manager.enqueuePatchTaskPre(PatchTask.newCalcPatchHash(manager, entry.key_ptr.*, null));
}
manager.enqueueDependencyList(root.dependencies);
manager.enqueueDependencyList(invalid_package_id, root.dependencies);
} else {
{
var iter = manager.lockfile.patched_dependencies.iterator();

View File

@@ -2,55 +2,200 @@ const OverrideMap = @This();
const debug = Output.scoped(.OverrideMap, .visible);
map: std.ArrayHashMapUnmanaged(PackageNameHash, Dependency, ArrayIdentityContext.U64, false) = .{},
/// Override value can be either global (applies to all instances of a package)
/// or nested (applies only when a specific parent package depends on it)
const OverrideValue = union(enum) {
/// Global override - applies to all instances of this package
global: Dependency,
/// Nested overrides - contains both a global override (in ".") and parent-specific overrides
nested: NestedOverrides,
/// In the future, this `get` function should handle multi-level resolutions. This is difficult right
/// now because given a Dependency ID, there is no fast way to trace it to its package.
///
/// A potential approach is to add another buffer to the lockfile that maps Dependency ID to Package ID,
/// and from there `OverrideMap.map` can have a union as the value, where the union is between "override all"
/// and "here is a list of overrides depending on the package that imported" similar to PackageIndex above.
pub fn get(this: *const OverrideMap, name_hash: PackageNameHash) ?Dependency.Version {
debug("looking up override for {x}", .{name_hash});
pub fn eql(this: *const OverrideValue, other: *const OverrideValue, this_buf: []const u8, other_buf: []const u8) bool {
if (@intFromEnum(this.*) != @intFromEnum(other.*)) {
return false;
}
return switch (this.*) {
.global => |this_dep| {
const other_dep = other.global;
return this_dep.name.eql(other_dep.name, this_buf, other_buf) and
this_dep.name_hash == other_dep.name_hash and
this_dep.version.eql(&other_dep.version, this_buf, other_buf);
},
.nested => |this_nested| {
const other_nested = other.nested;
// Compare global overrides
if (this_nested.global != null and other_nested.global != null) {
const this_global = this_nested.global.?;
const other_global = other_nested.global.?;
if (!this_global.name.eql(other_global.name, this_buf, other_buf) or
this_global.name_hash != other_global.name_hash or
!this_global.version.eql(&other_global.version, this_buf, other_buf))
{
return false;
}
} else if ((this_nested.global != null) != (other_nested.global != null)) {
return false;
}
// Compare parent maps
if (this_nested.parent_map.count() != other_nested.parent_map.count()) {
return false;
}
for (this_nested.parent_map.keys(), this_nested.parent_map.values()) |key, this_dep| {
const other_dep = other_nested.parent_map.get(key) orelse return false;
if (!this_dep.name.eql(other_dep.name, this_buf, other_buf) or
this_dep.name_hash != other_dep.name_hash or
!this_dep.version.eql(&other_dep.version, this_buf, other_buf))
{
return false;
}
}
return true;
},
};
}
/// Convert to external representation for binary lockfile.
/// For nested overrides, only the global override is included (parent-specific overrides are lost in binary format)
pub fn toExternal(this: *const OverrideValue) Dependency.External {
const dep = switch (this.*) {
.global => |d| d,
.nested => |nested| blk: {
if (nested.global) |g| {
break :blk g;
}
// If there's no global, use the first parent-specific override
// This is a limitation of the binary format
if (nested.parent_map.count() > 0) {
break :blk nested.parent_map.values()[0];
} else {
// Shouldn't happen, but provide a safe default
break :blk Dependency{};
}
},
};
return dep.toExternal();
}
};
const NestedOverrides = struct {
/// Global override for this package (from the "." property in npm overrides)
global: ?Dependency = null,
/// Map from parent package name hash to the override dependency
parent_map: std.ArrayHashMapUnmanaged(PackageNameHash, Dependency, ArrayIdentityContext.U64, false) = .{},
pub fn deinit(this: *NestedOverrides, allocator: Allocator) void {
this.parent_map.deinit(allocator);
}
};
map: std.ArrayHashMapUnmanaged(PackageNameHash, OverrideValue, ArrayIdentityContext.U64, false) = .{},
/// Get the override for a package, optionally considering the parent package.
/// If parent_name_hash is provided and a nested override exists for that parent, it takes precedence.
/// Otherwise, falls back to the global override if one exists.
pub fn get(this: *const OverrideMap, name_hash: PackageNameHash, parent_name_hash: ?PackageNameHash) ?Dependency.Version {
debug("looking up override for {x} (parent: {?x})", .{ name_hash, parent_name_hash });
if (this.map.count() == 0) {
debug("override map is empty", .{});
return null;
}
return if (this.map.get(name_hash)) |dep|
dep.version
else
null;
const override_value = this.map.get(name_hash) orelse {
debug("no override found for package hash {x}", .{name_hash});
return null;
};
return switch (override_value) {
.global => |dep| {
debug("found global override", .{});
return dep.version;
},
.nested => |nested| {
debug("found nested override entry, parent_map has {d} entries", .{nested.parent_map.count()});
// If parent is provided, check for parent-specific override first
if (parent_name_hash) |parent_hash| {
if (nested.parent_map.get(parent_hash)) |dep| {
debug("found parent-specific override for parent {x}", .{parent_hash});
return dep.version;
} else {
debug("no match for parent {x} in parent_map", .{parent_hash});
// Debug: print all parent hashes in the map
for (nested.parent_map.keys()) |key| {
debug(" parent_map contains: {x}", .{key});
}
}
} else {
debug("no parent_name_hash provided", .{});
}
// Fall back to global override if present
if (nested.global) |dep| {
debug("falling back to global override", .{});
return dep.version;
}
debug("no global override available", .{});
return null;
},
};
}
pub fn sort(this: *OverrideMap, lockfile: *const Lockfile) void {
const Ctx = struct {
buf: string,
override_deps: [*]const Dependency,
override_values: [*]const OverrideValue,
pub fn lessThan(sorter: *const @This(), l: usize, r: usize) bool {
const deps = sorter.override_deps;
const l_dep = deps[l];
const r_dep = deps[r];
const values = sorter.override_values;
const l_name = switch (values[l]) {
.global => |dep| dep.name,
.nested => |nested| if (nested.global) |dep| dep.name else return false,
};
const r_name = switch (values[r]) {
.global => |dep| dep.name,
.nested => |nested| if (nested.global) |dep| dep.name else return true,
};
const buf = sorter.buf;
return l_dep.name.order(&r_dep.name, buf, buf) == .lt;
return l_name.order(&r_name, buf, buf) == .lt;
}
};
const ctx: Ctx = .{
.buf = lockfile.buffers.string_bytes.items,
.override_deps = this.map.values().ptr,
.override_values = this.map.values().ptr,
};
this.map.sort(&ctx);
}
pub fn deinit(this: *OverrideMap, allocator: Allocator) void {
for (this.map.values()) |*value| {
switch (value.*) {
.global => {},
.nested => |*nested| nested.deinit(allocator),
}
}
this.map.deinit(allocator);
}
pub fn count(this: *OverrideMap, lockfile: *Lockfile, builder: *Lockfile.StringBuilder) void {
for (this.map.values()) |dep| {
dep.count(lockfile.buffers.string_bytes.items, @TypeOf(builder), builder);
const buf = lockfile.buffers.string_bytes.items;
for (this.map.values()) |value| {
switch (value) {
.global => |dep| dep.count(buf, @TypeOf(builder), builder),
.nested => |nested| {
if (nested.global) |dep| {
dep.count(buf, @TypeOf(builder), builder);
}
for (nested.parent_map.values()) |dep| {
dep.count(buf, @TypeOf(builder), builder);
}
},
}
}
}
@@ -58,11 +203,29 @@ pub fn clone(this: *OverrideMap, pm: *PackageManager, old_lockfile: *Lockfile, n
var new = OverrideMap{};
try new.map.ensureTotalCapacity(new_lockfile.allocator, this.map.entries.len);
const old_buf = old_lockfile.buffers.string_bytes.items;
for (this.map.keys(), this.map.values()) |k, v| {
new.map.putAssumeCapacity(
k,
try v.clone(pm, old_lockfile.buffers.string_bytes.items, @TypeOf(new_builder), new_builder),
);
const new_value = switch (v) {
.global => |dep| OverrideValue{
.global = try dep.clone(pm, old_buf, @TypeOf(new_builder), new_builder),
},
.nested => |nested| blk: {
var new_nested = NestedOverrides{};
if (nested.global) |dep| {
new_nested.global = try dep.clone(pm, old_buf, @TypeOf(new_builder), new_builder);
}
try new_nested.parent_map.ensureTotalCapacity(new_lockfile.allocator, nested.parent_map.count());
for (nested.parent_map.keys(), nested.parent_map.values()) |parent_hash, dep| {
new_nested.parent_map.putAssumeCapacity(
parent_hash,
try dep.clone(pm, old_buf, @TypeOf(new_builder), new_builder),
);
}
break :blk OverrideValue{ .nested = new_nested };
},
};
new.map.putAssumeCapacity(k, new_value);
}
return new;
@@ -86,9 +249,12 @@ pub fn parseCount(
.e_string => |s| {
builder.count(s.slice(lockfile.allocator));
},
.e_object => {
if (entry.value.?.asProperty(".")) |dot| {
if (dot.expr.asString(lockfile.allocator)) |s| {
.e_object => |obj| {
// Count all nested properties
for (obj.properties.slice()) |nested_prop| {
const nested_key = nested_prop.key.?.asString(lockfile.allocator).?;
builder.count(nested_key);
if (nested_prop.value.?.asString(lockfile.allocator)) |s| {
builder.count(s);
}
}
@@ -101,7 +267,40 @@ pub fn parseCount(
return;
for (resolutions.expr.data.e_object.properties.slice()) |entry| {
builder.count(entry.key.?.asString(lockfile.allocator).?);
const key = entry.key.?.asString(lockfile.allocator).?;
// Parse "parent/child" format - need to count both parent and child names
var remaining = key;
if (strings.hasPrefixComptime(remaining, "**/")) {
remaining = remaining[3..];
}
// For scoped packages, handle @scope/pkg/child
if (remaining.len > 0 and remaining[0] == '@') {
if (strings.indexOfChar(remaining, '/')) |first_slash| {
if (strings.indexOfChar(remaining[first_slash + 1 ..], '/')) |second_slash| {
// Nested: @scope/parent/child
const parent = remaining[0 .. first_slash + 1 + second_slash];
const child = remaining[first_slash + 2 + second_slash ..];
builder.count(parent);
builder.count(child);
} else {
// Not nested: @scope/pkg
builder.count(remaining);
}
} else {
builder.count(remaining);
}
} else if (strings.indexOfChar(remaining, '/')) |slash_idx| {
// Nested: parent/child
const parent = remaining[0..slash_idx];
const child = remaining[slash_idx + 1 ..];
builder.count(parent);
builder.count(child);
} else {
// Not nested
builder.count(remaining);
}
builder.count(entry.value.?.asString(lockfile.allocator) orelse continue);
}
}
@@ -150,60 +349,107 @@ pub fn parseFromOverrides(
for (expr.data.e_object.properties.slice()) |prop| {
const key = prop.key.?;
const k = key.asString(lockfile.allocator).?;
if (k.len == 0) {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Missing overridden package name", .{});
const package_name = key.asString(lockfile.allocator).?;
if (package_name.len == 0) {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Missing package name in overrides", .{});
continue;
}
const name_hash = String.Builder.stringHash(k);
const value_expr = prop.value.?;
const value = value: {
// for one level deep, we will only support a string and { ".": value }
const value_expr = prop.value.?;
if (value_expr.data == .e_string) {
break :value value_expr;
} else if (value_expr.data == .e_object) {
if (value_expr.asProperty(".")) |dot| {
if (dot.expr.data == .e_string) {
if (value_expr.data.e_object.properties.len > 1) {
try log.addWarningFmt(source, value_expr.loc, lockfile.allocator, "Bun currently does not support nested \"overrides\"", .{});
}
break :value dot.expr;
} else {
try log.addWarningFmt(source, value_expr.loc, lockfile.allocator, "Invalid override value for \"{s}\"", .{k});
continue;
}
} else {
try log.addWarningFmt(source, value_expr.loc, lockfile.allocator, "Bun currently does not support nested \"overrides\"", .{});
// Handle simple string override: "pkg": "1.0.0" (global override)
if (value_expr.data == .e_string) {
const package_name_hash = String.Builder.stringHash(package_name);
const version_str = value_expr.data.e_string.slice(lockfile.allocator);
if (strings.hasPrefixComptime(version_str, "patch:")) {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Bun currently does not support patched package \"overrides\"", .{});
continue;
}
if (try parseOverrideValue(
"override",
lockfile,
pm,
root_package,
source,
value_expr.loc,
log,
package_name,
version_str,
builder,
)) |dep| {
this.map.putAssumeCapacity(package_name_hash, .{ .global = dep });
}
continue;
}
// Handle object: could be either "parent": { "child": "version" } or global package with "."
if (value_expr.data == .e_object) {
const parent_name = package_name;
const parent_name_hash = String.Builder.stringHash(parent_name);
const nested_props = value_expr.data.e_object.properties.slice();
// Iterate through children of this parent
for (nested_props) |child_prop| {
const child_key = child_prop.key.?;
const child_name = child_key.asString(lockfile.allocator).?;
if (child_prop.value.?.data != .e_string) {
try log.addWarningFmt(source, child_prop.value.?.loc, lockfile.allocator, "Invalid override value for \"{s}\"", .{child_name});
continue;
}
const child_version_str = child_prop.value.?.data.e_string.slice(lockfile.allocator);
if (strings.hasPrefixComptime(child_version_str, "patch:")) {
try log.addWarningFmt(source, child_key.loc, lockfile.allocator, "Bun currently does not support patched package \"overrides\"", .{});
continue;
}
const child_name_hash = String.Builder.stringHash(child_name);
if (try parseOverrideValue(
"override",
lockfile,
pm,
root_package,
source,
child_prop.value.?.loc,
log,
child_name,
child_version_str,
builder,
)) |dep| {
// Get or create the nested override entry for this child
const gop = try this.map.getOrPut(lockfile.allocator, child_name_hash);
if (!gop.found_existing) {
// Create new nested override
var nested = NestedOverrides{};
try nested.parent_map.ensureTotalCapacity(lockfile.allocator, 1);
nested.parent_map.putAssumeCapacity(parent_name_hash, dep);
gop.value_ptr.* = .{ .nested = nested };
} else {
// Update existing entry
switch (gop.value_ptr.*) {
.global => |global_dep| {
// Convert global to nested, keeping the global as fallback
var nested = NestedOverrides{};
nested.global = global_dep;
try nested.parent_map.ensureTotalCapacity(lockfile.allocator, 1);
nested.parent_map.putAssumeCapacity(parent_name_hash, dep);
gop.value_ptr.* = .{ .nested = nested };
},
.nested => |*nested| {
// Add this parent-specific override
try nested.parent_map.put(lockfile.allocator, parent_name_hash, dep);
},
}
}
}
}
try log.addWarningFmt(source, value_expr.loc, lockfile.allocator, "Invalid override value for \"{s}\"", .{k});
continue;
};
const version_str = value.data.e_string.slice(lockfile.allocator);
if (strings.hasPrefixComptime(version_str, "patch:")) {
// TODO(dylan-conway): apply .patch files to packages
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Bun currently does not support patched package \"overrides\"", .{});
continue;
}
if (try parseOverrideValue(
"override",
lockfile,
pm,
root_package,
source,
value.loc,
log,
k,
version_str,
builder,
)) |version| {
this.map.putAssumeCapacity(name_hash, version);
}
try log.addWarningFmt(source, value_expr.loc, lockfile.allocator, "Invalid override value for \"{s}\"", .{package_name});
}
}
@@ -238,22 +484,6 @@ pub fn parseFromResolutions(
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Expected string value for resolution \"{s}\"", .{k});
continue;
}
// currently we only support one level deep, so we should error if there are more than one
// - "foo/bar":
// - "@namespace/hello/world"
if (k[0] == '@') {
const first_slash = strings.indexOfChar(k, '/') orelse {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Invalid package name \"{s}\"", .{k});
continue;
};
if (strings.indexOfChar(k[first_slash + 1 ..], '/') != null) {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Bun currently does not support nested \"resolutions\"", .{});
continue;
}
} else if (strings.indexOfChar(k, '/') != null) {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Bun currently does not support nested \"resolutions\"", .{});
continue;
}
const version_str = value.data.e_string.data;
if (strings.hasPrefixComptime(version_str, "patch:")) {
@@ -262,20 +492,104 @@ pub fn parseFromResolutions(
continue;
}
if (try parseOverrideValue(
"resolution",
lockfile,
pm,
root_package,
source,
value.loc,
log,
k,
version_str,
builder,
)) |version| {
const name_hash = String.Builder.stringHash(k);
this.map.putAssumeCapacity(name_hash, version);
// Parse nested resolution format: "parent/child" or "@scope/parent/child"
var parent_name: ?[]const u8 = null;
var child_name: []const u8 = k;
// Handle scoped packages: @scope/parent/child
if (k.len > 0 and k[0] == '@') {
if (strings.indexOfChar(k, '/')) |first_slash| {
if (strings.indexOfChar(k[first_slash + 1 ..], '/')) |second_slash| {
// Nested: @scope/parent/child
parent_name = k[0 .. first_slash + 1 + second_slash];
child_name = k[first_slash + 2 + second_slash ..];
} else {
// Not nested: @scope/pkg (global override)
child_name = k;
}
} else {
try log.addWarningFmt(source, key.loc, lockfile.allocator, "Invalid package name \"{s}\"", .{k});
continue;
}
} else if (strings.indexOfChar(k, '/')) |slash_idx| {
// Nested: parent/child (non-scoped)
parent_name = k[0..slash_idx];
child_name = k[slash_idx + 1 ..];
}
if (parent_name) |pname| {
// This is a nested override - create or update NestedOverrides for child_name
const child_name_hash = String.Builder.stringHash(child_name);
const parent_name_hash = String.Builder.stringHash(pname);
if (try parseOverrideValue(
"resolution",
lockfile,
pm,
root_package,
source,
value.loc,
log,
child_name,
version_str,
builder,
)) |dep| {
// Check if we already have an entry for this child
const gop = try this.map.getOrPut(lockfile.allocator, child_name_hash);
if (!gop.found_existing) {
// Create new nested override
var nested = NestedOverrides{};
try nested.parent_map.ensureTotalCapacity(lockfile.allocator, 1);
nested.parent_map.putAssumeCapacity(parent_name_hash, dep);
gop.value_ptr.* = .{ .nested = nested };
} else {
// Update existing entry
switch (gop.value_ptr.*) {
.global => |global_dep| {
// Convert global to nested
var nested = NestedOverrides{};
nested.global = global_dep;
try nested.parent_map.ensureTotalCapacity(lockfile.allocator, 1);
nested.parent_map.putAssumeCapacity(parent_name_hash, dep);
gop.value_ptr.* = .{ .nested = nested };
},
.nested => |*nested| {
try nested.parent_map.put(lockfile.allocator, parent_name_hash, dep);
},
}
}
}
} else {
// Global override
if (try parseOverrideValue(
"resolution",
lockfile,
pm,
root_package,
source,
value.loc,
log,
child_name,
version_str,
builder,
)) |dep| {
const child_name_hash = String.Builder.stringHash(child_name);
const gop = try this.map.getOrPut(lockfile.allocator, child_name_hash);
if (!gop.found_existing) {
gop.value_ptr.* = .{ .global = dep };
} else {
// Update existing entry
switch (gop.value_ptr.*) {
.global => |*global_dep| {
global_dep.* = dep;
},
.nested => |*nested| {
// Set or update the global override
nested.global = dep;
},
}
}
}
}
}
}

View File

@@ -302,12 +302,32 @@ pub const Stringifier = struct {
\\
);
indent.* += 1;
for (lockfile.overrides.map.values()) |override_dep| {
try writeIndent(writer, indent);
try writer.print(
\\{}: {},
\\
, .{ override_dep.name.fmtJson(buf, .{}), override_dep.version.literal.fmtJson(buf, .{}) });
for (lockfile.overrides.map.values()) |override_value| {
switch (override_value) {
.global => |dep| {
try writeIndent(writer, indent);
try writer.print(
\\{}: {},
\\
, .{ dep.name.fmtJson(buf, .{}), dep.version.literal.fmtJson(buf, .{}) });
},
.nested => |nested| {
if (nested.global) |dep| {
try writeIndent(writer, indent);
try writer.print(
\\{}: {},
\\
, .{ dep.name.fmtJson(buf, .{}), dep.version.literal.fmtJson(buf, .{}) });
}
for (nested.parent_map.values()) |dep| {
try writeIndent(writer, indent);
try writer.print(
\\{}: {},
\\
, .{ dep.name.fmtJson(buf, .{}), dep.version.literal.fmtJson(buf, .{}) });
}
},
}
}
try decIndent(writer, indent);
@@ -1248,7 +1268,7 @@ pub fn parseIntoBinaryLockfile(
},
};
try lockfile.overrides.map.put(allocator, name_hash, dep);
try lockfile.overrides.map.put(allocator, name_hash, .{ .global = dep });
}
}

View File

@@ -462,7 +462,7 @@ pub fn load(
.package_manager = manager,
};
for (overrides_name_hashes.items, override_versions_external.items) |name, value| {
map.putAssumeCapacity(name, Dependency.toDependency(value, context));
map.putAssumeCapacity(name, .{ .global = Dependency.toDependency(value, context) });
}
} else {
stream.pos -= 8;

View File

@@ -180,7 +180,7 @@ pub fn migratePnpmLockfile(
},
};
try lockfile.overrides.map.put(allocator, name_hash, dep);
try lockfile.overrides.map.put(allocator, name_hash, .{ .global = dep });
}
}

View File

@@ -247,3 +247,144 @@ test("overrides do not apply to workspaces", async () => {
expect(await exited).toBe(0);
expect(await stderr.text()).not.toContain("Saved lockfile");
});
// NPM-style nested overrides tests
test("nested overrides - npm format parent-specific", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
express: "4.18.2",
},
overrides: {
express: {
bytes: "1.0.0",
},
},
}),
);
install(tmp, ["install"]);
// Express depends on bytes, so it should get the parent-specific override (1.0.0)
expect(versionOf(tmp, "node_modules/bytes/package.json")).toBe("1.0.0");
ensureLockfileDoesntChangeOnBunI(tmp);
});
test("nested overrides - npm format with global and parent-specific", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
express: "4.18.2",
},
overrides: {
bytes: "2.0.0", // global
express: {
bytes: "1.0.0", // override for express's bytes dependency
},
},
}),
);
install(tmp, ["install"]);
// Express depends on bytes, should get the parent-specific override (1.0.0)
expect(versionOf(tmp, "node_modules/bytes/package.json")).toBe("1.0.0");
ensureLockfileDoesntChangeOnBunI(tmp);
});
// Yarn-style nested resolutions tests
test("nested resolutions - yarn format parent/child", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
express: "4.18.2",
},
resolutions: {
bytes: "2.0.0", // global fallback
"express/bytes": "1.0.0", // nested override
},
}),
);
install(tmp, ["install"]);
// Express depends on bytes, should get parent-specific override
expect(versionOf(tmp, "node_modules/bytes/package.json")).toBe("1.0.0");
ensureLockfileDoesntChangeOnBunI(tmp);
});
test("nested resolutions - yarn format with wildcard prefix", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
express: "4.18.2",
},
resolutions: {
"**/bytes": "2.0.0", // global with wildcard
"express/bytes": "1.0.0", // nested override
},
}),
);
install(tmp, ["install"]);
expect(versionOf(tmp, "node_modules/bytes/package.json")).toBe("1.0.0");
ensureLockfileDoesntChangeOnBunI(tmp);
});
test("nested resolutions - yarn format with scoped packages", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {},
resolutions: {
lodash: "4.17.0", // global
"@babel/core/lodash": "4.17.21", // nested for scoped package
},
}),
);
install(tmp, ["install", "@babel/core@7.20.0"]);
// @babel/core depends on lodash, should get the nested version
expect(versionOf(tmp, "node_modules/lodash/package.json")).toBe("4.17.21");
ensureLockfileDoesntChangeOnBunI(tmp);
});
test("nested overrides with multiple parents", async () => {
const tmp = tmpdirSync();
writeFileSync(
join(tmp, "package.json"),
JSON.stringify({
dependencies: {
express: "4.18.2",
"body-parser": "1.20.1",
},
overrides: {
express: {
bytes: "1.0.0",
},
"body-parser": {
bytes: "2.0.0",
},
},
}),
);
install(tmp, ["install"]);
// Both express and body-parser depend on bytes with different overrides
// The actual version will depend on which parent is resolved first
const bytesVersion = versionOf(tmp, "node_modules/bytes/package.json");
expect(["1.0.0", "2.0.0"]).toContain(bytesVersion);
ensureLockfileDoesntChangeOnBunI(tmp);
});