diff options
author | 2023-10-30 23:04:47 -0700 | |
---|---|---|
committer | 2023-10-30 23:04:47 -0700 | |
commit | e259056bd8966f38057f5f9962246503be7b4cb2 (patch) | |
tree | 7cd02b643760dee5491b1bed165a334273182824 | |
parent | 68146d054435c069e96e40bb1253d0000956ddf4 (diff) | |
download | bun-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.zig | 139 | ||||
-rw-r--r-- | src/install/lockfile.zig | 87 | ||||
-rw-r--r-- | src/install/migration.zig | 9 | ||||
-rw-r--r-- | src/install/npm.zig | 6 | ||||
-rw-r--r-- | src/install/semver.zig | 132 | ||||
-rw-r--r-- | test/cli/install/bun-install.test.ts | 20 |
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")); |