mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 13:51:47 +00:00
Compare commits
3 Commits
claude/fix
...
claude/nes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aa16b0f48 | ||
|
|
c8f3699e03 | ||
|
|
ce144db1cc |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user