aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Dylan Conway <35280289+dylan-conway@users.noreply.github.com> 2023-10-30 23:04:47 -0700
committerGravatar GitHub <noreply@github.com> 2023-10-30 23:04:47 -0700
commite259056bd8966f38057f5f9962246503be7b4cb2 (patch)
tree7cd02b643760dee5491b1bed165a334273182824
parent68146d054435c069e96e40bb1253d0000956ddf4 (diff)
downloadbun-e259056bd8966f38057f5f9962246503be7b4cb2.tar.gz
bun-e259056bd8966f38057f5f9962246503be7b4cb2.tar.zst
bun-e259056bd8966f38057f5f9962246503be7b4cb2.zip
peer dependency and semver prerelease bug fixes (#6814)
* `order` and `satisfy` prerelease numbers * remove sorter * use existing package for peer dep if possible * fix test, remove loop * count workspace versions, compare each part of prerelease * other peer dependencies * use existing packages if possible * don't install peer more than once * fix update tests
-rw-r--r--src/install/install.zig139
-rw-r--r--src/install/lockfile.zig87
-rw-r--r--src/install/migration.zig9
-rw-r--r--src/install/npm.zig6
-rw-r--r--src/install/semver.zig132
-rw-r--r--test/cli/install/bun-install.test.ts20
6 files changed, 326 insertions, 67 deletions
diff --git a/src/install/install.zig b/src/install/install.zig
index 57ef430c1..9fba86e7d 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -1759,7 +1759,7 @@ pub const PackageManager = struct {
package_json_updates: []UpdateRequest = &[_]UpdateRequest{},
// used for looking up workspaces that aren't loaded into Lockfile.workspace_paths
- workspaces: std.StringArrayHashMap(?Semver.Version),
+ workspaces: std.StringArrayHashMap(Semver.Version),
// progress bar stuff when not stack allocated
root_progress_node: *std.Progress.Node = undefined,
@@ -1802,7 +1802,7 @@ pub const PackageManager = struct {
onWake: WakeHandler = .{},
ci_mode: bun.LazyBool(computeIsContinuousIntegration, @This(), "ci_mode") = .{},
- peer_dependencies: std.ArrayListUnmanaged(DependencyID) = .{},
+ peer_dependencies: std.fifo.LinearFifo(DependencyID, .Dynamic) = std.fifo.LinearFifo(DependencyID, .Dynamic).init(default_allocator),
// name hash from alias package name -> aliased package dependency version info
known_npm_aliases: NpmAliasMap = .{},
@@ -2539,7 +2539,7 @@ pub const PackageManager = struct {
Semver.Version.sortGt,
);
for (installed_versions.items) |installed_version| {
- if (version.value.npm.version.satisfies(installed_version)) {
+ if (version.value.npm.version.satisfies(installed_version, this.lockfile.buffers.string_bytes.items)) {
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
var npm_package_path = this.pathForCachedNPMPath(&buf, package_name, installed_version) catch |err| {
Output.debug("error getting path for cached npm path: {s}", .{bun.span(@errorName(err))});
@@ -2600,7 +2600,7 @@ pub const PackageManager = struct {
// Was this package already allocated? Let's reuse the existing one.
if (this.lockfile.getPackageID(
name_hash,
- if (behavior.isPeer() and !install_peer) version else null,
+ if (this.to_update) null else version,
&.{
.tag = .npm,
.value = .{
@@ -2748,6 +2748,23 @@ pub const PackageManager = struct {
}
}
+ fn resolutionSatisfiesDependency(this: *PackageManager, resolution: Resolution, dependency: Dependency.Version) bool {
+ const buf = this.lockfile.buffers.string_bytes.items;
+ if (resolution.tag == .npm and dependency.tag == .npm) {
+ return dependency.value.npm.version.satisfies(resolution.value.npm.version, buf);
+ }
+
+ if (resolution.tag == .git and dependency.tag == .git) {
+ return resolution.value.git.eql(&dependency.value.git, buf, buf);
+ }
+
+ if (resolution.tag == .github and dependency.tag == .github) {
+ return resolution.value.github.eql(&dependency.value.github, buf, buf);
+ }
+
+ return false;
+ }
+
fn getOrPutResolvedPackage(
this: *PackageManager,
name_hash: PackageNameHash,
@@ -2765,12 +2782,57 @@ pub const PackageManager = struct {
return .{ .package = this.lockfile.packages.get(resolution) };
}
+ if (install_peer and behavior.isPeer()) {
+ if (this.lockfile.package_index.get(name_hash)) |index| {
+ const resolutions: []Resolution = this.lockfile.packages.items(.resolution);
+ switch (index) {
+ .PackageID => |existing_id| {
+ if (existing_id < resolutions.len) {
+ const res_tag = resolutions[existing_id].tag;
+ const ver_tag = version.tag;
+ if ((res_tag == .npm and ver_tag == .npm) or (res_tag == .git and ver_tag == .git) or (res_tag == .github and ver_tag == .github)) {
+ successFn(this, dependency_id, existing_id);
+ return .{
+ .package = this.lockfile.packages.get(existing_id),
+ };
+ }
+ }
+ },
+ .PackageIDMultiple => |list| {
+ for (list.items) |existing_id| {
+ if (existing_id < resolutions.len) {
+ const existing_resolution = resolutions[existing_id];
+ if (this.resolutionSatisfiesDependency(existing_resolution, version)) {
+ successFn(this, dependency_id, existing_id);
+ return .{
+ .package = this.lockfile.packages.get(existing_id),
+ };
+ }
+ }
+ }
+
+ if (list.items[0] < resolutions.len) {
+ const res_tag = resolutions[list.items[0]].tag;
+ const ver_tag = version.tag;
+ if ((res_tag == .npm and ver_tag == .npm) or (res_tag == .git and ver_tag == .git) or (res_tag == .github and ver_tag == .github)) {
+ successFn(this, dependency_id, list.items[0]);
+ return .{
+ .package = this.lockfile.packages.get(list.items[0]),
+ };
+ }
+ }
+ },
+ }
+ }
+ }
+
switch (version.tag) {
.npm, .dist_tag => {
if (version.tag == .npm) {
if (this.lockfile.workspace_versions.count() > 0) resolve_from_workspace: {
if (this.lockfile.workspace_versions.get(name_hash)) |workspace_version| {
- if (version.value.npm.version.satisfies(workspace_version)) {
+ const buf = this.lockfile.buffers.string_bytes.items;
+ if (version.value.npm.version.satisfies(workspace_version, buf)) {
const root_package = this.lockfile.rootPackage() orelse break :resolve_from_workspace;
const root_dependencies = root_package.dependencies.get(this.lockfile.buffers.dependencies.items);
const root_resolutions = root_package.resolutions.get(this.lockfile.buffers.resolutions.items);
@@ -2778,7 +2840,7 @@ pub const PackageManager = struct {
for (root_dependencies, root_resolutions) |root_dep, workspace_package_id| {
if (workspace_package_id != invalid_package_id and root_dep.version.tag == .workspace and root_dep.name_hash == name_hash) {
// make sure verifyResolutions sees this resolution as a valid package id
- this.lockfile.buffers.resolutions.items[dependency_id] = workspace_package_id;
+ successFn(this, dependency_id, workspace_package_id);
return .{
.package = this.lockfile.packages.get(workspace_package_id),
.is_first_time = false,
@@ -3121,11 +3183,12 @@ pub const PackageManager = struct {
if (dependency.version.tag == .npm) {
if (this.known_npm_aliases.get(name_hash)) |aliased| {
const group = dependency.version.value.npm.version;
+ const buf = this.lockfile.buffers.string_bytes.items;
var curr_list: ?*const Semver.Query.List = &aliased.value.npm.version.head;
while (curr_list) |queries| {
var curr: ?*const Semver.Query = &queries.head;
while (curr) |query| {
- if (group.satisfies(query.range.left.version) or group.satisfies(query.range.right.version)) {
+ if (group.satisfies(query.range.left.version, buf) or group.satisfies(query.range.right.version, buf)) {
name = aliased.value.npm.name;
name_hash = String.Builder.stringHash(this.lockfile.str(&name));
break :version aliased;
@@ -3363,13 +3426,13 @@ pub const PackageManager = struct {
this.allocator,
this.scopeForPackageName(name_str),
loaded_manifest,
- dependency.behavior.isOptional() or dependency.behavior.isPeer(),
+ dependency.behavior.isOptional() or !this.options.do.install_peer_dependencies,
);
this.enqueueNetworkTask(network_task);
}
} else {
if (this.options.do.install_peer_dependencies and !dependency.behavior.isOptionalPeer()) {
- try this.peer_dependencies.append(this.allocator, id);
+ try this.peer_dependencies.writeItem(id);
}
}
@@ -3439,7 +3502,14 @@ pub const PackageManager = struct {
try entry.value_ptr.append(this.allocator, ctx);
}
- if (dependency.behavior.isPeer()) return;
+ if (dependency.behavior.isPeer()) {
+ if (!install_peer) {
+ if (this.options.do.install_peer_dependencies and !dependency.behavior.isOptionalPeer()) {
+ try this.peer_dependencies.writeItem(id);
+ }
+ return;
+ }
+ }
const network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, checkout_id, .{});
if (network_entry.found_existing) return;
@@ -3503,7 +3573,15 @@ pub const PackageManager = struct {
const callback_tag = comptime if (successFn == assignRootResolution) "root_dependency" else "dependency";
try entry.value_ptr.append(this.allocator, @unionInit(TaskCallbackContext, callback_tag, id));
- if (dependency.behavior.isPeer()) return;
+ if (dependency.behavior.isPeer()) {
+ if (!install_peer) {
+ if (this.options.do.install_peer_dependencies and !dependency.behavior.isOptionalPeer()) {
+ try this.peer_dependencies.writeItem(id);
+ }
+ return;
+ }
+ }
+
if (try this.generateNetworkTaskForTarball(task_id, url, id, .{
.name = dependency.name,
.name_hash = dependency.name_hash,
@@ -3675,7 +3753,15 @@ pub const PackageManager = struct {
const callback_tag = comptime if (successFn == assignRootResolution) "root_dependency" else "dependency";
try entry.value_ptr.append(this.allocator, @unionInit(TaskCallbackContext, callback_tag, id));
- if (dependency.behavior.isPeer()) return;
+ if (dependency.behavior.isPeer()) {
+ if (!install_peer) {
+ if (this.options.do.install_peer_dependencies and !dependency.behavior.isOptionalPeer()) {
+ try this.peer_dependencies.writeItem(id);
+ }
+ return;
+ }
+ }
+
switch (version.value.tarball.uri) {
.local => {
const network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{});
@@ -3878,10 +3964,10 @@ pub const PackageManager = struct {
fn processPeerDependencyList(
this: *PackageManager,
) !void {
- while (this.peer_dependencies.popOrNull()) |peer_dependency_id| {
- try this.processDependencyListItem(.{ .dependency = peer_dependency_id }, null, true);
+ while (this.peer_dependencies.readItem()) |peer_dependency_id| {
const dependency = this.lockfile.buffers.dependencies.items[peer_dependency_id];
const resolution = this.lockfile.buffers.resolutions.items[peer_dependency_id];
+
try this.enqueueDependencyWithMain(
peer_dependency_id,
&dependency,
@@ -4298,7 +4384,13 @@ pub const PackageManager = struct {
var dependency_list = dependency_list_entry.value_ptr.*;
dependency_list_entry.value_ptr.* = .{};
- try manager.processDependencyList(dependency_list, ExtractCompletionContext, extract_ctx, callbacks, install_peer);
+ try manager.processDependencyList(
+ dependency_list,
+ ExtractCompletionContext,
+ extract_ctx,
+ callbacks,
+ install_peer,
+ );
continue;
}
@@ -5712,9 +5804,16 @@ pub const PackageManager = struct {
} else |_| {}
}
- var workspaces = std.StringArrayHashMap(?Semver.Version).init(ctx.allocator);
+ var workspaces = std.StringArrayHashMap(Semver.Version).init(ctx.allocator);
for (workspace_names.values()) |entry| {
- try workspaces.put(entry.name, entry.version);
+ if (entry.version) |version_string| {
+ const sliced_version = SlicedString.init(version_string, version_string);
+ const result = Semver.Version.parse(sliced_version);
+ if (result.valid and result.wildcard == .none) {
+ try workspaces.put(entry.name, result.version.fill());
+ continue;
+ }
+ }
}
workspace_names.map.deinit();
@@ -5816,7 +5915,7 @@ pub const PackageManager = struct {
.lockfile = undefined,
.root_package_json_file = undefined,
.waiter = if (Environment.isPosix) try Waker.init(allocator) else bun.uws.Loop.get(),
- .workspaces = std.StringArrayHashMap(?Semver.Version).init(allocator),
+ .workspaces = std.StringArrayHashMap(Semver.Version).init(allocator),
};
manager.lockfile = try allocator.create(Lockfile);
@@ -8196,7 +8295,7 @@ pub const PackageManager = struct {
manager.drainDependencyList();
}
- if (manager.pending_tasks > 0 or manager.peer_dependencies.items.len > 0) {
+ if (manager.pending_tasks > 0 or manager.peer_dependencies.readableLength() > 0) {
if (root.dependencies.len > 0) {
_ = manager.getCacheDirectory();
_ = manager.getTemporaryDirectory();
@@ -8233,7 +8332,7 @@ pub const PackageManager = struct {
}
if (manager.options.do.install_peer_dependencies) {
- while (manager.pending_tasks > 0 or manager.peer_dependencies.items.len > 0) {
+ while (manager.pending_tasks > 0 or manager.peer_dependencies.readableLength() > 0) {
try manager.processPeerDependencyList();
manager.drainDependencyList();
diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig
index fbc3e09d4..7ce33105c 100644
--- a/src/install/lockfile.zig
+++ b/src/install/lockfile.zig
@@ -1664,7 +1664,8 @@ pub fn initEmpty(this: *Lockfile, allocator: Allocator) !void {
pub fn getPackageID(
this: *Lockfile,
name_hash: u64,
- // if it's a peer dependency, a folder, or a symlink
+ // If non-null, attempt to use an existing package
+ // that satisfies this version range.
version: ?Dependency.Version,
resolution: *const Resolution,
) ?PackageID {
@@ -1674,29 +1675,30 @@ pub fn getPackageID(
.npm => v.value.npm.version,
else => null,
} else null;
+ const buf = this.buffers.string_bytes.items;
switch (entry) {
.PackageID => |id| {
if (comptime Environment.allow_assert) std.debug.assert(id < resolutions.len);
- if (resolutions[id].eql(resolution, this.buffers.string_bytes.items, this.buffers.string_bytes.items)) {
+ if (resolutions[id].eql(resolution, buf, buf)) {
return id;
}
if (npm_version) |range| {
- if (range.satisfies(resolutions[id].value.npm.version)) return id;
+ if (range.satisfies(resolutions[id].value.npm.version, buf)) return id;
}
},
.PackageIDMultiple => |ids| {
for (ids.items) |id| {
if (comptime Environment.allow_assert) std.debug.assert(id < resolutions.len);
- if (resolutions[id].eql(resolution, this.buffers.string_bytes.items, this.buffers.string_bytes.items)) {
+ if (resolutions[id].eql(resolution, buf, buf)) {
return id;
}
if (npm_version) |range| {
- if (range.satisfies(resolutions[id].value.npm.version)) return id;
+ if (range.satisfies(resolutions[id].value.npm.version, buf)) return id;
}
}
},
@@ -1712,16 +1714,35 @@ pub fn getOrPutID(this: *Lockfile, id: PackageID, name_hash: PackageNameHash) !v
var index: *PackageIndex.Entry = gpe.value_ptr;
switch (index.*) {
- .PackageID => |single| {
+ .PackageID => |existing_id| {
var ids = try PackageIDList.initCapacity(this.allocator, 8);
- ids.appendAssumeCapacity(single);
- ids.appendAssumeCapacity(id);
+ ids.items.len = 2;
+
+ const resolutions = this.packages.items(.resolution);
+ const buf = this.buffers.string_bytes.items;
+
+ ids.items[0..2].* = if (resolutions[id].order(&resolutions[existing_id], buf, buf) == .gt)
+ .{ id, existing_id }
+ else
+ .{ existing_id, id };
+
index.* = .{
.PackageIDMultiple = ids,
};
},
- .PackageIDMultiple => {
- try index.PackageIDMultiple.append(this.allocator, id);
+ .PackageIDMultiple => |*existing_ids| {
+ const resolutions = this.packages.items(.resolution);
+ const buf = this.buffers.string_bytes.items;
+
+ for (existing_ids.items, 0..) |existing_id, i| {
+ if (resolutions[id].order(&resolutions[existing_id], buf, buf) == .gt) {
+ try existing_ids.insert(this.allocator, i, id);
+ return;
+ }
+ }
+
+ // append to end because it's the smallest or equal to the smallest
+ try existing_ids.append(this.allocator, id);
},
}
} else {
@@ -3109,7 +3130,7 @@ pub const Package = extern struct {
.npm => if (comptime tag != null)
unreachable
else if (workspace_version) |ver| {
- if (dependency_version.value.npm.version.satisfies(ver)) {
+ if (dependency_version.value.npm.version.satisfies(ver, buf)) {
for (package_dependencies[0..dependencies_count]) |dep| {
// `dependencies` & `workspaces` defined within the same `package.json`
if (dep.version.tag == .workspace and dep.name_hash == name_hash) {
@@ -3134,7 +3155,7 @@ pub const Package = extern struct {
.workspace => if (workspace_path) |path| {
if (workspace_range) |range| {
if (workspace_version) |ver| {
- if (range.satisfies(ver)) {
+ if (range.satisfies(ver, buf)) {
dependency_version.literal = path;
dependency_version.value.workspace = path;
}
@@ -3190,7 +3211,7 @@ pub const Package = extern struct {
if (switch (package_dep.version.tag) {
// `dependencies` & `workspaces` defined within the same `package.json`
.npm => String.Builder.stringHash(package_dep.realname().slice(buf)) == name_hash and
- package_dep.version.value.npm.version.satisfies(ver),
+ package_dep.version.value.npm.version.satisfies(ver, buf),
// `workspace:*`
.workspace => workspace_entry.found_existing and
String.Builder.stringHash(package_dep.realname().slice(buf)) == name_hash,
@@ -3266,7 +3287,7 @@ pub const Package = extern struct {
const Map = bun.StringArrayHashMap(Entry);
pub const Entry = struct {
name: string,
- version: ?Semver.Version,
+ version: ?string,
};
pub fn init(allocator: std.mem.Allocator) WorkspaceMap {
@@ -3321,7 +3342,7 @@ pub const Package = extern struct {
const WorkspaceEntry = struct {
path: []const u8 = "",
name: []const u8 = "",
- version: ?Semver.Version = null,
+ version: ?[]const u8 = null,
};
fn processWorkspaceName(
@@ -3356,18 +3377,14 @@ pub const Package = extern struct {
if (!workspace_json.has_found_name) {
return error.MissingPackageName;
}
- bun.copy(u8, name_to_copy[0..], workspace_json.found_name);
+ @memcpy(name_to_copy[0..workspace_json.found_name.len], workspace_json.found_name);
var entry = WorkspaceEntry{
.name = name_to_copy[0..workspace_json.found_name.len],
.path = path_to_use,
};
debug("processWorkspaceName({s}) = {s}", .{ path_to_use, entry.name });
if (workspace_json.has_found_version) {
- const version = SlicedString.init(workspace_json.found_version, workspace_json.found_version);
- const result = Semver.Version.parse(version);
- if (result.valid and result.wildcard == .none) {
- entry.version = result.version.fill();
- }
+ entry.version = try allocator.dupe(u8, workspace_json.found_version);
}
return entry;
}
@@ -3482,6 +3499,9 @@ pub const Package = extern struct {
builder.count(workspace_entry.name);
builder.count(input_path);
builder.cap += bun.MAX_PATH_BYTES;
+ if (workspace_entry.version) |version_string| {
+ builder.count(version_string);
+ }
}
try workspace_names.insert(input_path, .{
@@ -4051,6 +4071,20 @@ pub const Package = extern struct {
for (workspace_names.values(), workspace_names.keys()) |entry, path| {
const external_name = string_builder.append(ExternalString, entry.name);
+ const workspace_version = brk: {
+ if (entry.version) |version_string| {
+ const external_version = string_builder.append(ExternalString, version_string);
+ allocator.free(version_string);
+ const sliced = external_version.value.sliced(lockfile.buffers.string_bytes.items);
+ const result = Semver.Version.parse(sliced);
+ if (result.valid and result.wildcard == .none) {
+ break :brk result.version.fill();
+ }
+ }
+
+ break :brk null;
+ };
+
if (try parseDependency(
lockfile,
allocator,
@@ -4063,7 +4097,7 @@ pub const Package = extern struct {
total_dependencies_count,
in_workspace,
.workspace,
- entry.version,
+ workspace_version,
external_name,
path,
logger.Loc.Empty,
@@ -4078,8 +4112,8 @@ pub const Package = extern struct {
total_dependencies_count += 1;
try lockfile.workspace_paths.put(allocator, external_name.hash, dep.version.value.workspace);
- if (entry.version) |v| {
- try lockfile.workspace_versions.put(allocator, external_name.hash, v);
+ if (workspace_version) |version| {
+ try lockfile.workspace_versions.put(allocator, external_name.hash, version);
}
}
}
@@ -5073,6 +5107,7 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool) !MetaH
pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Version) ?PackageID {
const name_hash = String.Builder.stringHash(package_name);
const entry = this.package_index.get(name_hash) orelse return null;
+ const buf = this.buffers.string_bytes.items;
switch (version.tag) {
.npm => switch (entry) {
@@ -5080,7 +5115,7 @@ pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Ve
const resolutions = this.packages.items(.resolution);
if (comptime Environment.allow_assert) std.debug.assert(id < resolutions.len);
- if (version.value.npm.version.satisfies(resolutions[id].value.npm.version)) {
+ if (version.value.npm.version.satisfies(resolutions[id].value.npm.version, buf)) {
return id;
}
},
@@ -5089,7 +5124,7 @@ pub fn resolve(this: *Lockfile, package_name: []const u8, version: Dependency.Ve
for (ids.items) |id| {
if (comptime Environment.allow_assert) std.debug.assert(id < resolutions.len);
- if (version.value.npm.version.satisfies(resolutions[id].value.npm.version)) {
+ if (version.value.npm.version.satisfies(resolutions[id].value.npm.version, buf)) {
return id;
}
}
diff --git a/src/install/migration.zig b/src/install/migration.zig
index 6e32c0e5e..e606f6ddb 100644
--- a/src/install/migration.zig
+++ b/src/install/migration.zig
@@ -331,7 +331,14 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo
for (wksp.map.keys(), wksp.map.values()) |k, v| {
const name_hash = stringHash(v.name);
this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k));
- if (v.version) |version| this.workspace_versions.putAssumeCapacity(name_hash, version);
+
+ if (v.version) |version_string| {
+ const sliced_version = Semver.SlicedString.init(version_string, version_string);
+ const result = Semver.Version.parse(sliced_version);
+ if (result.valid and result.wildcard == .none) {
+ this.workspace_versions.putAssumeCapacity(name_hash, result.version.fill());
+ }
+ }
}
}
diff --git a/src/install/npm.zig b/src/install/npm.zig
index 1ab140beb..fdf66f333 100644
--- a/src/install/npm.zig
+++ b/src/install/npm.zig
@@ -809,7 +809,7 @@ pub const PackageManifest = struct {
}
if (this.findByDistTag("latest")) |result| {
- if (group.satisfies(result.version)) {
+ if (group.satisfies(result.version, this.string_buf)) {
if (group.flags.isSet(Semver.Query.Group.Flags.pre)) {
if (left.version.order(result.version, this.string_buf, this.string_buf) == .eq) {
// if prerelease, use latest if semver+tag match range exactly
@@ -829,7 +829,7 @@ pub const PackageManifest = struct {
while (i > 0) : (i -= 1) {
const version = releases[i - 1];
- if (group.satisfies(version)) {
+ if (group.satisfies(version, this.string_buf)) {
return .{ .version = version, .package = &this.pkg.releases.values.get(this.package_versions)[i - 1] };
}
}
@@ -842,7 +842,7 @@ pub const PackageManifest = struct {
const version = prereleases[i - 1];
// This list is sorted at serialization time.
- if (group.satisfies(version)) {
+ if (group.satisfies(version, this.string_buf)) {
const packages = this.pkg.prereleases.values.get(this.package_versions);
return .{ .version = version, .package = &packages[i - 1] };
}
diff --git a/src/install/semver.zig b/src/install/semver.zig
index 9572b85e2..e9fa11ef8 100644
--- a/src/install/semver.zig
+++ b/src/install/semver.zig
@@ -767,7 +767,56 @@ pub const Version = extern struct {
pre: ExternalString = ExternalString{},
build: ExternalString = ExternalString{},
+ pub fn orderPre(lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order {
+ const lhs_str = lhs.pre.slice(lhs_buf);
+ const rhs_str = rhs.pre.slice(rhs_buf);
+
+ // 1. split each by '.', iterating through each one looking for integers
+ // 2. compare as integers, or if not possible compare as string
+ // 3. whichever is greater is the greater one
+ //
+ // 1.0.0-canary.0.0.0.0.0.0 < 1.0.0-canary.0.0.0.0.0.1
+
+ var lhs_itr = strings.split(lhs_str, ".");
+ var rhs_itr = strings.split(rhs_str, ".");
+
+ while (true) {
+ var lhs_part = lhs_itr.next();
+ var rhs_part = rhs_itr.next();
+
+ if (lhs_part == null and rhs_part == null) return .eq;
+
+ // not having a prerelease part is greater than having one
+ if (lhs_part == null) return .gt;
+ if (rhs_part == null) return .lt;
+
+ const lhs_uint: ?u32 = std.fmt.parseUnsigned(u32, lhs_part.?, 10) catch null;
+ const rhs_uint: ?u32 = std.fmt.parseUnsigned(u32, rhs_part.?, 10) catch null;
+
+ if (lhs_uint == null or rhs_uint == null) {
+ switch (strings.order(lhs_part.?, rhs_part.?)) {
+ .eq => {
+ // continue to the next part
+ continue;
+ },
+ else => |not_equal| return not_equal,
+ }
+ }
+
+ switch (std.math.order(lhs_uint.?, rhs_uint.?)) {
+ .eq => continue,
+ else => |not_equal| return not_equal,
+ }
+ }
+
+ unreachable;
+ }
+
pub fn order(lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order {
+ if (!lhs.pre.isEmpty() and !rhs.pre.isEmpty()) {
+ return lhs.orderPre(rhs, lhs_buf, rhs_buf);
+ }
+
const pre_order = lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf);
if (pre_order != .eq) return pre_order;
@@ -1300,7 +1349,7 @@ pub const Range = struct {
}
};
- pub fn satisfies(this: Range, version: Version) bool {
+ pub fn satisfies(this: Range, version: Version, string_buf: string) bool {
const has_left = this.hasLeft();
const has_right = this.hasRight();
@@ -1344,6 +1393,75 @@ pub const Range = struct {
return false;
}
+ if (version.tag.hasPre() and this.left.version.tag.hasPre()) {
+ // make sure strings leading up to first number are the same
+ const lhs_str = this.left.version.tag.pre.slice(string_buf);
+
+ const rhs_str = if (this.right.version.tag.hasPre()) this.right.version.tag.pre.slice(string_buf) else null;
+ const has_rhs_pre = rhs_str != null;
+ const ver_str = version.tag.pre.slice(string_buf);
+
+ var lhs_itr = strings.split(lhs_str, ".");
+ var rhs_itr = if (has_rhs_pre) strings.split(rhs_str.?, ".") else null;
+ var ver_itr = strings.split(ver_str, ".");
+
+ while (true) {
+ const lhs_part = lhs_itr.next();
+ const rhs_part = if (has_rhs_pre) rhs_itr.?.next() else null;
+ const ver_part = ver_itr.next();
+
+ // it's a match
+ if (lhs_part == null and ver_part == null and rhs_part == null) return true;
+
+ // parts do not have equal length
+ if (lhs_part == null or ver_part == null) return false;
+ if (Environment.allow_assert) {
+ if (has_rhs_pre) {
+ std.debug.assert(rhs_part != null);
+ }
+ }
+
+ const lhs_uint = std.fmt.parseUnsigned(u32, lhs_part.?, 10) catch null;
+ const rhs_uint = if (has_rhs_pre) std.fmt.parseUnsigned(u32, rhs_part.?, 10) catch null else null;
+ const ver_uint = std.fmt.parseUnsigned(u32, ver_part.?, 10) catch null;
+
+ if (lhs_uint != null and ver_uint != null) {
+ if (has_rhs_pre and rhs_uint == null) return false;
+
+ if (lhs_uint.? <= ver_uint.?) {
+ if (!has_rhs_pre) continue;
+
+ if (ver_uint.? <= rhs_uint.?) {
+ // between lhs and rhs
+ continue;
+ }
+
+ // is not between lhs and rhs.
+ return false;
+ }
+
+ // falls below lhs
+ return false;
+ }
+
+ if (lhs_uint != null or ver_uint != null) return false;
+ if (Environment.allow_assert) {
+ if (has_rhs_pre) {
+ std.debug.assert(rhs_uint == null);
+ }
+ }
+
+ if (!strings.eqlLong(lhs_part.?, ver_part.?, true)) return false;
+ if (Environment.allow_assert) {
+ if (has_rhs_pre) {
+ std.debug.assert(strings.eqlLong(ver_part.?, rhs_part.?, true));
+ }
+ }
+
+ // continue to next part
+ }
+ }
+
return true;
}
};
@@ -1375,8 +1493,8 @@ pub const Query = struct {
// OR
next: ?*List = null,
- pub fn satisfies(this: *const List, version: Version) bool {
- return this.head.satisfies(version) or (this.next orelse return false).satisfies(version);
+ pub fn satisfies(this: *const List, version: Version, string_buf: string) bool {
+ return this.head.satisfies(version, string_buf) or (this.next orelse return false).satisfies(version, string_buf);
}
pub fn eql(lhs: *const List, rhs: *const List) bool {
@@ -1501,8 +1619,8 @@ pub const Query = struct {
self.tail = new_tail;
}
- pub inline fn satisfies(this: *const Group, version: Version) bool {
- return this.head.satisfies(version);
+ pub inline fn satisfies(this: *const Group, version: Version, string_buf: string) bool {
+ return this.head.satisfies(version, string_buf);
}
};
@@ -1515,8 +1633,8 @@ pub const Query = struct {
return lhs_next.eql(rhs_next);
}
- pub fn satisfies(this: *const Query, version: Version) bool {
- return this.range.satisfies(version) and (this.next orelse return true).satisfies(version);
+ pub fn satisfies(this: *const Query, version: Version, string_buf: string) bool {
+ return this.range.satisfies(version, string_buf) and (this.next orelse return true).satisfies(version, string_buf);
}
const Token = struct {
diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts
index 5aae47a26..df247c48d 100644
--- a/test/cli/install/bun-install.test.ts
+++ b/test/cli/install/bun-install.test.ts
@@ -4096,27 +4096,27 @@ it("should install peerDependencies when needed", async () => {
expect(requested).toBe(3);
expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moo"]);
expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-exec", "baz-run"]);
- expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-exec"))).toBe(
- join("..", "..", "moo", "node_modules", "baz", "index.js"),
+ expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-exec"))).toBe(join("..", "baz", "index.js"));
+ expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe(
+ join("..", "..", "bar", "node_modules", "baz", "index.js"),
);
- expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe(join("..", "baz", "index.js"));
expect(await readlink(join(package_dir, "node_modules", "bar"))).toBe(join("..", "bar"));
- expect(await readdirSorted(join(package_dir, "bar"))).toEqual(["package.json"]);
+ expect(await readdirSorted(join(package_dir, "bar"))).toEqual(["node_modules", "package.json"]);
expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]);
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
- version: "0.0.3",
+ version: "0.0.5",
bin: {
- "baz-run": "index.js",
+ "baz-exec": "index.js",
},
});
expect(await readlink(join(package_dir, "node_modules", "moo"))).toBe(join("..", "moo"));
- expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["node_modules", "package.json"]);
- expect(await file(join(package_dir, "moo", "node_modules", "baz", "package.json")).json()).toEqual({
+ expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["package.json"]);
+ expect(await file(join(package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({
name: "baz",
- version: "0.0.5",
+ version: "0.0.3",
bin: {
- "baz-exec": "index.js",
+ "baz-run": "index.js",
},
});
await access(join(package_dir, "bun.lockb"));