diff options
author | 2023-02-09 00:37:14 +0200 | |
---|---|---|
committer | 2023-02-08 14:37:14 -0800 | |
commit | 18807cef03ac83a6a3dabded7a9735b87792f52e (patch) | |
tree | d40b8968840d16a7e1a96695b46e5415b3c4176a | |
parent | cbc28afd51547f7a90161741cf826ee7f1485ae6 (diff) | |
download | bun-18807cef03ac83a6a3dabded7a9735b87792f52e.tar.gz bun-18807cef03ac83a6a3dabded7a9735b87792f52e.tar.zst bun-18807cef03ac83a6a3dabded7a9735b87792f52e.zip |
[install] assorted fixes & improvements (#2011)
- take `peerDependencies` into account during package placement
- do not recursively resolve `workspaces` (matches `npm`)
- link binaries to non-root packages correctly
- prune empty nodes during dependency tree construction
- support non-standard `workspace:` specifier
-rw-r--r-- | src/install/bin.zig | 27 | ||||
-rw-r--r-- | src/install/dependency.zig | 52 | ||||
-rw-r--r-- | src/install/install.zig | 205 | ||||
-rw-r--r-- | src/install/lockfile.zig | 138 | ||||
-rw-r--r-- | src/install/npm.zig | 16 | ||||
-rw-r--r-- | src/install/semver.zig | 4 | ||||
-rw-r--r-- | test/bun.js/install/bar-0.0.2.tgz (renamed from test/bun.js/install/bar.tgz) | bin | 192 -> 192 bytes | |||
-rw-r--r-- | test/bun.js/install/baz-0.0.3.tgz (renamed from test/bun.js/install/baz.tgz) | bin | 283 -> 283 bytes | |||
-rw-r--r-- | test/bun.js/install/baz-0.0.5.tgz | bin | 0 -> 286 bytes | |||
-rw-r--r-- | test/bun.js/install/bun-add.test.ts | 39 | ||||
-rw-r--r-- | test/bun.js/install/bun-install.test.ts | 237 | ||||
-rw-r--r-- | test/bun.js/install/bun-link.test.ts | 6 | ||||
-rw-r--r-- | test/bun.js/install/dummy.registry.ts | 30 |
13 files changed, 476 insertions, 278 deletions
diff --git a/src/install/bin.zig b/src/install/bin.zig index 09b7201d5..2d1113556 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -257,6 +257,7 @@ pub const Bin = extern struct { pub const Linker = struct { bin: Bin, + package_installed_path: stringZ = "", package_installed_node_modules: std.os.fd_t = std.math.maxInt(std.os.fd_t), root_node_modules_folder: std.os.fd_t = std.math.maxInt(std.os.fd_t), @@ -320,15 +321,19 @@ pub const Bin = extern struct { if (!link_global) { target_buf[0..".bin/".len].* = ".bin/".*; from_remain = target_buf[".bin/".len..]; - dest_buf[0.."../".len].* = "../".*; - remain = dest_buf["../".len..]; + dest_buf[0.."..".len].* = "..".*; + remain = dest_buf["..".len..]; + std.mem.copy(u8, remain, this.package_installed_path); + remain = remain[this.package_installed_path.len..]; + remain[0] = std.fs.path.sep; + remain = remain[1..]; } else { if (this.global_bin_dir.fd >= std.math.maxInt(std.os.fd_t)) { this.err = error.MissingGlobalBinDir; return; } - @memcpy(&target_buf, this.global_bin_path.ptr, this.global_bin_path.len); + std.mem.copy(u8, &target_buf, this.global_bin_path); from_remain = target_buf[this.global_bin_path.len..]; from_remain[0] = std.fs.path.sep; from_remain = from_remain[1..]; @@ -362,8 +367,8 @@ pub const Bin = extern struct { .file => { var target = this.bin.value.file.slice(this.string_buf); - if (strings.hasPrefix(target, "./")) { - target = target[2..]; + if (strings.hasPrefixComptime(target, "./")) { + target = target["./".len..]; } std.mem.copy(u8, remain, target); remain = remain[target.len..]; @@ -384,8 +389,8 @@ pub const Bin = extern struct { }, .named_file => { var target = this.bin.value.named_file[1].slice(this.string_buf); - if (strings.hasPrefix(target, "./")) { - target = target[2..]; + if (strings.hasPrefixComptime(target, "./")) { + target = target["./".len..]; } std.mem.copy(u8, remain, target); remain = remain[target.len..]; @@ -414,8 +419,8 @@ pub const Bin = extern struct { const name_in_filesystem = this.extern_string_buf[extern_string_i + 1]; var target = name_in_filesystem.slice(this.string_buf); - if (strings.hasPrefix(target, "./")) { - target = target[2..]; + if (strings.hasPrefixComptime(target, "./")) { + target = target["./".len..]; } std.mem.copy(u8, remain, target); remain = remain[target.len..]; @@ -435,8 +440,8 @@ pub const Bin = extern struct { }, .dir => { var target = this.bin.value.dir.slice(this.string_buf); - if (strings.hasPrefix(target, "./")) { - target = target[2..]; + if (strings.hasPrefixComptime(target, "./")) { + target = target["./".len..]; } var parts = [_][]const u8{ name, target }; diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 98a62ac87..315c22894 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -473,12 +473,24 @@ pub const Version = struct { } }, // v1.2.3 + // verilog // verilog.tar.gz // verilog/repo 'v' => { if (isTarball(dependency)) return .tarball; if (isGitHubRepoPath(dependency)) return .github; - return .npm; + if (dependency.len == 1) return .dist_tag; + return switch (dependency[1]) { + '0'...'9' => .npm, + else => .dist_tag, + }; + }, + // workspace:* + // w00t + // w00t.tar.gz + // w00t/repo + 'w' => { + if (strings.hasPrefixComptime(dependency, "workspace:")) return .workspace; }, // x // xyz.tar.gz @@ -626,7 +638,7 @@ pub fn parseWithTag( return null; }; - return Version{ + return .{ .literal = sliced.value(), .value = .{ .npm = .{ @@ -638,12 +650,12 @@ pub fn parseWithTag( }; }, .dist_tag => { - var tag_to_use: String = sliced.value(); + var tag_to_use = sliced.value(); const actual = if (strings.hasPrefixComptime(dependency, "npm:") and dependency.len > "npm:".len) // npm:@foo/bar@latest sliced.sub(brk: { - var i: usize = "npm:".len; + var i = "npm:".len; // npm:@foo/bar@latest // ^ @@ -673,7 +685,7 @@ pub fn parseWithTag( // tag should never be empty if (Environment.allow_assert) std.debug.assert(!tag_to_use.isEmpty()); - return Version{ + return .{ .literal = sliced.value(), .value = .{ .dist_tag = .{ @@ -739,7 +751,7 @@ pub fn parseWithTag( repo = repo[0 .. repo.len - ".git".len]; } - return Version{ + return .{ .literal = sliced.value(), .value = .{ .github = .{ @@ -753,26 +765,26 @@ pub fn parseWithTag( }, .tarball => { if (strings.hasPrefixComptime(dependency, "https://") or strings.hasPrefixComptime(dependency, "http://")) { - return Version{ + return .{ .tag = .tarball, .literal = sliced.value(), - .value = .{ .tarball = URI{ .remote = sliced.sub(dependency).value() } }, + .value = .{ .tarball = .{ .remote = sliced.sub(dependency).value() } }, }; } else if (strings.hasPrefixComptime(dependency, "file://")) { - return Version{ + return .{ .tag = .tarball, .literal = sliced.value(), - .value = .{ .tarball = URI{ .local = sliced.sub(dependency[7..]).value() } }, + .value = .{ .tarball = .{ .local = sliced.sub(dependency[7..]).value() } }, }; } else if (strings.contains(dependency, "://")) { if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "invalid or unsupported dependency \"{s}\"", .{dependency}) catch unreachable; return null; } - return Version{ + return .{ .literal = sliced.value(), .value = .{ - .tarball = URI{ + .tarball = .{ .local = sliced.value(), }, }, @@ -787,14 +799,14 @@ pub fn parseWithTag( return null; } - return Version{ .literal = sliced.value(), .value = .{ .folder = sliced.sub(dependency[protocol + 1 ..]).value() }, .tag = .folder }; + return .{ .literal = sliced.value(), .value = .{ .folder = sliced.sub(dependency[protocol + 1 ..]).value() }, .tag = .folder }; } if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Unsupported protocol {s}", .{dependency}) catch unreachable; return null; } - return Version{ + return .{ .value = .{ .folder = sliced.value() }, .tag = .folder, .literal = sliced.value(), @@ -803,22 +815,26 @@ pub fn parseWithTag( .uninitialized => return null, .symlink => { if (strings.indexOfChar(dependency, ':')) |colon| { - return Version{ + return .{ .value = .{ .symlink = sliced.sub(dependency[colon + 1 ..]).value() }, .tag = .symlink, .literal = sliced.value(), }; } - return Version{ + return .{ .value = .{ .symlink = sliced.value() }, .tag = .symlink, .literal = sliced.value(), }; }, .workspace => { - return Version{ - .value = .{ .workspace = sliced.value() }, + var input = dependency; + if (strings.hasPrefixComptime(input, "workspace:")) { + input = input["workspace:".len..]; + } + return .{ + .value = .{ .workspace = sliced.sub(input).value() }, .tag = .workspace, .literal = sliced.value(), }; diff --git a/src/install/install.zig b/src/install/install.zig index db12c2f3a..d23c1d757 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -429,6 +429,15 @@ pub const Features = struct { return @intToEnum(Behavior, out); } + pub const main = Features{ + .check_for_duplicate_dependencies = true, + .dev_dependencies = true, + .is_main = true, + .optional_dependencies = true, + .scripts = true, + .workspaces = true, + }; + pub const folder = Features{ .dev_dependencies = true, .optional_dependencies = true, @@ -438,7 +447,6 @@ pub const Features = struct { .dev_dependencies = true, .optional_dependencies = true, .scripts = true, - .workspaces = true, }; pub const link = Features{ @@ -2180,7 +2188,7 @@ pub const PackageManager = struct { manifest: *const Npm.PackageManifest, find_result: Npm.PackageManifest.FindResult, comptime successFn: SuccessFn, - ) !ResolvedPackageResult { + ) !?ResolvedPackageResult { // Was this package already allocated? Let's reuse the existing one. if (this.lockfile.getPackageID( @@ -2197,10 +2205,12 @@ pub const PackageManager = struct { }, )) |id| { successFn(this, dependency_id, id); - return ResolvedPackageResult{ + return .{ .package = this.lockfile.packages.get(id), .is_first_time = false, }; + } else if (behavior.isPeer()) { + return null; } // appendPackage sets the PackageID on the package @@ -2215,19 +2225,10 @@ pub const PackageManager = struct { Features.npm, )); - if (!behavior.isEnabled(if (this.isRootDependency(dependency_id)) - this.options.local_package_features - else - this.options.remote_package_features)) - { - this.setPreinstallState(package.meta.id, this.lockfile, .done); - } - - const preinstall = this.determinePreinstallState(package, this.lockfile); - if (comptime Environment.allow_assert) std.debug.assert(package.meta.id != invalid_package_id); defer successFn(this, dependency_id, package.meta.id); - return switch (preinstall) { + + return switch (this.determinePreinstallState(package, this.lockfile)) { // Is this package already in the cache? // We don't need to download the tarball, but we should enqueue dependencies .done => .{ .package = package, .is_first_time = true }, @@ -2352,7 +2353,7 @@ pub const PackageManager = struct { name.assertDefined(); if (resolution < this.lockfile.packages.len) { - return ResolvedPackageResult{ .package = this.lockfile.packages.get(resolution) }; + return .{ .package = this.lockfile.packages.get(resolution) }; } switch (version.tag) { @@ -2363,7 +2364,7 @@ pub const PackageManager = struct { .dist_tag => manifest.findByDistTag(this.lockfile.str(&version.value.dist_tag.tag)), .npm => manifest.findBestVersion(version.value.npm.version), else => unreachable, - } orelse return switch (version.tag) { + } orelse return if (behavior.isPeer()) null else switch (version.tag) { .npm => error.NoMatchingVersion, .dist_tag => error.DistTagNotFound, else => unreachable, @@ -2389,12 +2390,12 @@ pub const PackageManager = struct { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + return .{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + return .{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, @@ -2406,12 +2407,12 @@ pub const PackageManager = struct { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + return .{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + return .{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, @@ -2422,12 +2423,12 @@ pub const PackageManager = struct { .err => |err| return err, .package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + return .{ .package = this.lockfile.packages.get(package_id) }; }, .new_package_id => |package_id| { successFn(this, dependency_id, package_id); - return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + return .{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } }, @@ -2594,7 +2595,7 @@ pub const PackageManager = struct { if (!this.isRootDependency(id)) if (!dependency.behavior.isEnabled(switch (dependency.version.tag) { .dist_tag, .folder, .npm => this.options.remote_package_features, - else => Features{}, + else => .{}, })) return; } @@ -2705,71 +2706,74 @@ pub const PackageManager = struct { this.enqueueNetworkTask(network_task); } } - } else if (!dependency.behavior.isPeer() and dependency.version.tag.isNPM()) { + } else if (dependency.version.tag.isNPM()) { const name_str = this.lockfile.str(&name); const task_id = Task.Id.forManifest(Task.Tag.package_manifest, name_str); - var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); - if (!network_entry.found_existing) { - if (this.options.enable.manifest_cache) { - if (Npm.PackageManifest.Serializer.load(this.allocator, this.getCacheDirectory(), name_str) catch null) |manifest_| { - const manifest: Npm.PackageManifest = manifest_; - loaded_manifest = manifest; - - if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { - try this.manifests.put(this.allocator, @truncate(PackageNameHash, manifest.pkg.name.hash), manifest); - } - // If it's an exact package version already living in the cache - // We can skip the network request, even if it's beyond the caching period - if (dependency.version.tag == .npm and dependency.version.value.npm.version.isExact()) { - if (loaded_manifest.?.findByVersion(dependency.version.value.npm.version.head.head.range.left.version)) |find_result| { - if (this.getOrPutResolvedPackageWithFindResult( - name_hash, - name, - version, - id, - dependency.behavior, - &loaded_manifest.?, - find_result, - successFn, - ) catch null) |new_resolve_result| { - resolve_result_ = new_resolve_result; - _ = this.network_dedupe_map.remove(task_id); - continue :retry_with_new_resolve_result; + if (comptime Environment.allow_assert) std.debug.assert(task_id != 0); + + if (!dependency.behavior.isPeer()) { + var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); + if (!network_entry.found_existing) { + if (this.options.enable.manifest_cache) { + if (Npm.PackageManifest.Serializer.load(this.allocator, this.getCacheDirectory(), name_str) catch null) |manifest_| { + const manifest: Npm.PackageManifest = manifest_; + loaded_manifest = manifest; + + if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { + try this.manifests.put(this.allocator, @truncate(PackageNameHash, manifest.pkg.name.hash), manifest); + } + + // If it's an exact package version already living in the cache + // We can skip the network request, even if it's beyond the caching period + if (dependency.version.tag == .npm and dependency.version.value.npm.version.isExact()) { + if (loaded_manifest.?.findByVersion(dependency.version.value.npm.version.head.head.range.left.version)) |find_result| { + if (this.getOrPutResolvedPackageWithFindResult( + name_hash, + name, + version, + id, + dependency.behavior, + &loaded_manifest.?, + find_result, + successFn, + ) catch null) |new_resolve_result| { + resolve_result_ = new_resolve_result; + _ = this.network_dedupe_map.remove(task_id); + continue :retry_with_new_resolve_result; + } } } - } - // Was it recent enough to just load it without the network call? - if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { - _ = this.network_dedupe_map.remove(task_id); - continue :retry_from_manifests_ptr; + // Was it recent enough to just load it without the network call? + if (this.options.enable.manifest_cache_control and manifest.pkg.public_max_age > this.timestamp_for_manifest_cache_control) { + _ = this.network_dedupe_map.remove(task_id); + continue :retry_from_manifests_ptr; + } } } - } - if (PackageManager.verbose_install) { - Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str}); - } + if (PackageManager.verbose_install) { + Output.prettyErrorln("Enqueue package manifest for download: {s}", .{name_str}); + } - var network_task = this.getNetworkTask(); - network_task.* = NetworkTask{ - .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 - .callback = undefined, - .task_id = task_id, - .allocator = this.allocator, - }; - try network_task.forManifest( - name_str, - this.allocator, - this.scopeForPackageName(name_str), - loaded_manifest, - ); - this.enqueueNetworkTask(network_task); + var network_task = this.getNetworkTask(); + network_task.* = .{ + .package_manager = &PackageManager.instance, // https://github.com/ziglang/zig/issues/14005 + .callback = undefined, + .task_id = task_id, + .allocator = this.allocator, + }; + try network_task.forManifest( + name_str, + this.allocator, + this.scopeForPackageName(name_str), + loaded_manifest, + ); + this.enqueueNetworkTask(network_task); + } } - if (comptime Environment.allow_assert) std.debug.assert(task_id != 0); - var manifest_entry_parse = try this.task_queue.getOrPutContext(this.allocator, task_id, .{}); if (!manifest_entry_parse.found_existing) { manifest_entry_parse.value_ptr.* = TaskCallbackList{}; @@ -2784,8 +2788,6 @@ pub const PackageManager = struct { return; }, .github => { - if (dependency.behavior.isPeer()) return; - const dep = &dependency.version.value.github; const res = Resolution{ .tag = .github, @@ -2816,6 +2818,7 @@ 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 (try this.generateNetworkTaskForTarball(task_id, url, id, package)) |network_task| { this.enqueueNetworkTask(network_task); } @@ -3620,6 +3623,15 @@ pub const PackageManager = struct { }, } } + } else if (manager.task_queue.getEntry(Task.Id.forManifest( + Task.Tag.package_manifest, + manager.lockfile.str(&manager.lockfile.packages.items(.name)[package_id]), + ))) |dependency_list_entry| { + // Peer dependencies do not initiate any downloads of their own, thus need to be resolved here instead + var dependency_list = dependency_list_entry.value_ptr.*; + dependency_list_entry.value_ptr.* = .{}; + + try manager.processDependencyList(dependency_list, void, {}, {}); } manager.setPreinstallState(package_id, manager.lockfile, .done); @@ -3690,11 +3702,9 @@ pub const PackageManager = struct { dry_run: bool = false, remote_package_features: Features = .{ .optional_dependencies = true, - .peer_dependencies = false, }, local_package_features: Features = .{ .dev_dependencies = true, - .peer_dependencies = false, }, // The idea here is: // 1. package has a platform-specific binary to install @@ -5123,7 +5133,7 @@ pub const PackageManager = struct { peer: bool = false, pub inline fn toFeatures(this: Omit) Features { - return Features{ + return .{ .dev_dependencies = this.dev, .optional_dependencies = this.optional, .peer_dependencies = this.peer, @@ -5809,6 +5819,7 @@ pub const PackageManager = struct { manager: *PackageManager, lockfile: *Lockfile, progress: *std.Progress, + node_modules_path: stringZ, node_modules_folder: std.fs.IterableDir, skip_verify_installed_version_number: bool, skip_delete: bool, @@ -6029,6 +6040,7 @@ pub const PackageManager = struct { var bin_linker = Bin.Linker{ .bin = bin, + .package_installed_path = this.node_modules_path["node_modules".len..], .package_installed_node_modules = this.node_modules_folder.dir.fd, .global_bin_path = this.options.bin_path, .global_bin_dir = this.options.global_bin_dir.dir, @@ -6273,7 +6285,6 @@ pub const PackageManager = struct { var parts = lockfile.packages.slice(); var metas = parts.items(.meta); var names = parts.items(.name); - var dependency_lists: []const Lockfile.DependencySlice = parts.items(.dependencies); var dependencies = lockfile.buffers.dependencies.items; const resolutions_buffer: []const PackageID = lockfile.buffers.resolutions.items; const resolution_lists: []const Lockfile.PackageIDSlice = parts.items(.resolutions); @@ -6291,6 +6302,7 @@ pub const PackageManager = struct { .resolutions = resolutions, .lockfile = lockfile, .node = &install_node, + .node_modules_path = "node_modules", .node_modules_folder = node_modules_folder, .progress = progress, .skip_verify_installed_version_number = skip_verify_installed_version_number, @@ -6312,9 +6324,8 @@ pub const PackageManager = struct { // We deliberately do not close this folder. // If the package hasn't been downloaded, we will need to install it later // We use this file descriptor to know where to put it. - var folder = try cwd.openIterableDir(node_modules.relative_path, .{}); - - installer.node_modules_folder = folder; + installer.node_modules_path = node_modules.relative_path; + installer.node_modules_folder = try cwd.openIterableDir(node_modules.relative_path, .{}); var remaining = node_modules.dependencies; @@ -6388,12 +6399,10 @@ pub const PackageManager = struct { const package_id = resolutions_buffer[dependency_id]; const folder = deferred.node_modules_folder; - const package_dependencies: []const Dependency = dependency_lists[package_id].get(dependencies); const package_resolutions: []const PackageID = resolution_lists[package_id].get(resolutions_buffer); const original_bin: Bin = installer.bins[package_id]; - for (package_dependencies) |_, i| { - const resolved_id = package_resolutions[i]; + for (package_resolutions) |resolved_id| { if (resolved_id >= names.len) continue; const meta: Lockfile.Package.Meta = metas[resolved_id]; @@ -6595,15 +6604,7 @@ pub const PackageManager = struct { ctx.allocator, ctx.log, package_json_source, - Features{ - .check_for_duplicate_dependencies = true, - .dev_dependencies = true, - .is_main = true, - .optional_dependencies = true, - .peer_dependencies = false, - .scripts = true, - .workspaces = true, - }, + Features.main, ); manager.lockfile.scripts = lockfile.scripts; var mapping = try manager.lockfile.allocator.alloc(PackageID, maybe_root.dependencies.len); @@ -6722,15 +6723,7 @@ pub const PackageManager = struct { ctx.allocator, ctx.log, package_json_source, - Features{ - .check_for_duplicate_dependencies = true, - .dev_dependencies = true, - .is_main = true, - .optional_dependencies = true, - .peer_dependencies = false, - .scripts = true, - .workspaces = true, - }, + Features.main, ); root = try manager.lockfile.appendPackage(root); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 83c3b9ffb..70e14fb82 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -362,16 +362,12 @@ pub const Tree = struct { /// Flatten the multi-dimensional ArrayList of package IDs into a single easily serializable array pub fn clean(this: *Builder) !DependencyIDList { - var end = @truncate(Id, this.list.len); + const end = @truncate(Id, this.list.len); var i: Id = 0; var total: u32 = 0; - var trees = this.list.items(.tree); var dependencies = this.list.items(.dependencies); - // var real_end: Id = 0; - - // TODO: can we cull empty trees here? while (i < end) : (i += 1) { total += trees[i].dependencies.len; } @@ -401,6 +397,14 @@ pub const Tree = struct { dependency_id: DependencyID, builder: *Builder, ) SubtreeError!void { + const package_id = switch (dependency_id) { + root_dep_id => 0, + else => |id| builder.resolutions[id], + }; + const resolution_list = builder.resolution_lists[package_id]; + + if (resolution_list.len == 0) return; + try builder.list.append(builder.allocator, .{ .tree = .{ .parent = this.id, @@ -414,15 +418,6 @@ pub const Tree = struct { const trees = list_slice.items(.tree); const dependency_lists = list_slice.items(.dependencies); const next: *Tree = &trees[builder.list.len - 1]; - - const package_id = switch (dependency_id) { - root_dep_id => 0, - else => |id| builder.resolutions[id], - }; - const resolution_list = builder.resolution_lists[package_id]; - - if (resolution_list.len == 0) return; - const name_hashes: []const PackageNameHash = builder.name_hashes; const max_package_id = @truncate(PackageID, name_hashes.len); var dep_id = resolution_list.off; @@ -430,44 +425,49 @@ pub const Tree = struct { while (dep_id < end) : (dep_id += 1) { const pid = builder.resolutions[dep_id]; + // Skip unresolved packages, e.g. "peerDependencies" if (pid >= max_package_id) continue; - const dependency = builder.dependencies[dep_id]; - - // Do not download/install "peerDependencies" - if (dependency.behavior.isPeer()) continue; + const dependency = builder.dependencies[dep_id]; // Do not hoist aliased packages - const destination = if (dependency.name_hash == name_hashes[pid]) next.addDependency( - true, - pid, - dep_id, - &dependency, - dependency_lists, - trees, - builder, - ) else brk: { - dependency_lists[next.id].append(builder.allocator, dep_id) catch unreachable; - next.dependencies.len += 1; - break :brk next.id; - }; + const destination = if (dependency.name_hash != name_hashes[pid]) + next.id + else + next.hoistDependency( + true, + pid, + dep_id, + &dependency, + dependency_lists, + trees, + builder, + ) catch |err| return err; switch (destination) { - Tree.dependency_loop => return error.DependencyLoop, - Tree.hoisted => continue, - else => if (builder.resolution_lists[pid].len > 0) { - try builder.queue.writeItem(.{ - .tree_id = destination, - .dependency_id = dep_id, - }); + Tree.dependency_loop, Tree.hoisted => continue, + else => { + dependency_lists[destination].append(builder.allocator, dep_id) catch unreachable; + trees[destination].dependencies.len += 1; + if (builder.resolution_lists[pid].len > 0) { + try builder.queue.writeItem(.{ + .tree_id = destination, + .dependency_id = dep_id, + }); + } }, } } + + if (next.dependencies.len == 0) { + if (comptime Environment.allow_assert) std.debug.assert(builder.list.len == next.id + 1); + _ = builder.list.pop(); + } } // This function does one of three things: // - de-duplicate (skip) the package // - move the package to the top directory // - leave the package at the same (relative) directory - fn addDependency( + fn hoistDependency( this: *Tree, comptime as_defined: bool, package_id: PackageID, @@ -476,16 +476,21 @@ pub const Tree = struct { dependency_lists: []Lockfile.DependencyIDList, trees: []Tree, builder: *Builder, - ) Id { + ) !Id { const this_dependencies = this.dependencies.get(dependency_lists[this.id].items); for (this_dependencies) |dep_id| { - if (builder.dependencies[dep_id].name_hash != dependency.name_hash) continue; - if (builder.resolutions[dep_id] != package_id) return dependency_loop; + const dep = builder.dependencies[dep_id]; + if (dep.name_hash != dependency.name_hash) continue; + if (builder.resolutions[dep_id] != package_id) { + if (as_defined and !dep.behavior.isPeer()) return error.DependencyLoop; + // ignore versioning conflicts caused by peer dependencies + return dependency_loop; + } return hoisted; } if (this.parent < error_id) { - const id = trees[this.parent].addDependency( + const id = trees[this.parent].hoistDependency( false, package_id, dependency_id, @@ -493,12 +498,10 @@ pub const Tree = struct { dependency_lists, trees, builder, - ); + ) catch unreachable; if (!as_defined or id != dependency_loop) return id; } - dependency_lists[this.id].append(builder.allocator, dependency_id) catch unreachable; - this.dependencies.len += 1; return this.id; } }; @@ -1279,20 +1282,20 @@ pub const Printer = struct { if (dep.behavior != behavior) { if (dep.behavior.isOptional()) { try writer.writeAll(" optionalDependencies:\n"); - if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + if (comptime Environment.allow_assert) dependency_behavior_change_count += 1; } else if (dep.behavior.isNormal()) { try writer.writeAll(" dependencies:\n"); - if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + if (comptime Environment.allow_assert) dependency_behavior_change_count += 1; } else if (dep.behavior.isDev()) { try writer.writeAll(" devDependencies:\n"); - if (comptime Environment.isDebug or Environment.isTest) dependency_behavior_change_count += 1; + if (comptime Environment.allow_assert) dependency_behavior_change_count += 1; } else { continue; } behavior = dep.behavior; // assert its sorted - if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(dependency_behavior_change_count < 3); + if (comptime Environment.allow_assert) std.debug.assert(dependency_behavior_change_count < 3); } try writer.writeAll(" "); @@ -1351,10 +1354,12 @@ pub fn verifyResolutions(this: *Lockfile, local_features: Features, remote_featu for (list.get(resolutions_buffer)) |package_id, j| { if (package_id >= end) { const failed_dep: Dependency = dependency_lists[parent_id].get(dependencies_buffer)[j]; - if (!failed_dep.behavior.isEnabled(if (root_list.contains(@truncate(PackageID, parent_id))) - local_features - else - remote_features)) continue; + if (failed_dep.behavior.isPeer() or !failed_dep.behavior.isEnabled( + if (root_list.contains(@truncate(PackageID, parent_id))) + local_features + else + remote_features, + )) continue; if (log_level != .silent) Output.prettyErrorln( "<r><red>error<r><d>:<r> <b>{s}<r><d>@<b>{}<r><d> failed to resolve<r>\n", @@ -1844,7 +1849,7 @@ pub const Package = extern struct { const id = @truncate(PackageID, new.packages.len); const new_package = try new.appendPackageWithID( - Lockfile.Package{ + .{ .name = builder.appendWithHash( String, this.name.slice(old_string_buf), @@ -2378,15 +2383,16 @@ pub const Package = extern struct { .workspace => if (workspace_path) |path| { dependency_version.value.workspace = path; } else { + const workspace = dependency_version.value.workspace.slice(buf); const path = string_builder.append( String, - Path.relative( + if (strings.eqlComptime(workspace, "*")) "*" else Path.relative( FileSystem.instance.top_level_dir, Path.joinAbsString( FileSystem.instance.top_level_dir, &[_]string{ source.path.name.dir, - dependency_version.value.workspace.slice(buf), + workspace, }, .posix, ), @@ -2401,7 +2407,7 @@ pub const Package = extern struct { } const this_dep = Dependency{ - .behavior = if (group.behavior.isPeer()) group.behavior else group.behavior.setWorkspace(in_workspace), + .behavior = group.behavior.setWorkspace(in_workspace), .name = external_name.value, .name_hash = external_name.hash, .version = dependency_version, @@ -2736,8 +2742,8 @@ pub const Package = extern struct { const bin_name = obj.properties.ptr[0].key.?.asString(allocator) orelse break :bin; const value = obj.properties.ptr[0].value.?.asString(allocator) orelse break :bin; - package.bin = Bin{ - .tag = Bin.Tag.named_file, + package.bin = .{ + .tag = .named_file, .value = .{ .named_file = .{ string_builder.append(String, bin_name), @@ -2764,8 +2770,8 @@ pub const Package = extern struct { i += 1; } std.debug.assert(i == extern_strings.len); - package.bin = Bin{ - .tag = Bin.Tag.map, + package.bin = .{ + .tag = .map, .value = .{ .map = @import("./install.zig").ExternalStringList.init(lockfile.buffers.extern_strings.items, extern_strings) }, }; }, @@ -2775,8 +2781,8 @@ pub const Package = extern struct { }, .e_string => |stri| { if (stri.data.len > 0) { - package.bin = Bin{ - .tag = Bin.Tag.file, + package.bin = .{ + .tag = .file, .value = .{ .file = string_builder.append(String, stri.data), }, @@ -2799,8 +2805,8 @@ pub const Package = extern struct { if (dirs.expr.asProperty("bin")) |bin_prop| { if (bin_prop.expr.asString(allocator)) |str_| { if (str_.len > 0) { - package.bin = Bin{ - .tag = Bin.Tag.dir, + package.bin = .{ + .tag = .dir, .value = .{ .dir = string_builder.append(String, str_), }, diff --git a/src/install/npm.zig b/src/install/npm.zig index 79dea81ce..71b892bef 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -1119,8 +1119,8 @@ pub const PackageManifest = struct { const bin_name = obj.properties.ptr[0].key.?.asString(allocator) orelse break :bin; const value = obj.properties.ptr[0].value.?.asString(allocator) orelse break :bin; - package_version.bin = Bin{ - .tag = Bin.Tag.named_file, + package_version.bin = .{ + .tag = .named_file, .value = .{ .named_file = .{ string_builder.append(String, bin_name), @@ -1174,8 +1174,8 @@ pub const PackageManifest = struct { extern_strings_bin_entries = extern_strings_bin_entries[group_slice.len..]; } - package_version.bin = Bin{ - .tag = Bin.Tag.map, + package_version.bin = .{ + .tag = .map, .value = .{ .map = ExternalStringList.init(all_extern_strings_bin_entries, group_slice) }, }; }, @@ -1185,8 +1185,8 @@ pub const PackageManifest = struct { }, .e_string => |stri| { if (stri.data.len > 0) { - package_version.bin = Bin{ - .tag = Bin.Tag.file, + package_version.bin = .{ + .tag = .file, .value = .{ .file = string_builder.append(String, stri.data), }, @@ -1209,8 +1209,8 @@ pub const PackageManifest = struct { if (dirs.expr.asProperty("bin")) |bin_prop| { if (bin_prop.expr.asString(allocator)) |str_| { if (str_.len > 0) { - package_version.bin = Bin{ - .tag = Bin.Tag.dir, + package_version.bin = .{ + .tag = .dir, .value = .{ .dir = string_builder.append(String, str_), }, diff --git a/src/install/semver.zig b/src/install/semver.zig index c4d9b5b5f..f9e16968f 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -577,13 +577,13 @@ pub const SlicedString = struct { } pub inline fn external(this: SlicedString) ExternalString { - if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); + if (comptime Environment.allow_assert) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); return ExternalString.init(this.buf, this.slice, std.hash.Wyhash.hash(0, this.slice)); } pub inline fn value(this: SlicedString) String { - if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); + if (comptime Environment.allow_assert) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); return String.init(this.buf, this.slice); } diff --git a/test/bun.js/install/bar.tgz b/test/bun.js/install/bar-0.0.2.tgz Binary files differindex 1983142d7..1983142d7 100644 --- a/test/bun.js/install/bar.tgz +++ b/test/bun.js/install/bar-0.0.2.tgz diff --git a/test/bun.js/install/baz.tgz b/test/bun.js/install/baz-0.0.3.tgz Binary files differindex 375a5e31f..375a5e31f 100644 --- a/test/bun.js/install/baz.tgz +++ b/test/bun.js/install/baz-0.0.3.tgz diff --git a/test/bun.js/install/baz-0.0.5.tgz b/test/bun.js/install/baz-0.0.5.tgz Binary files differnew file mode 100644 index 000000000..7e708fb1b --- /dev/null +++ b/test/bun.js/install/baz-0.0.5.tgz diff --git a/test/bun.js/install/bun-add.test.ts b/test/bun.js/install/bun-add.test.ts index 0729e4e52..57c76640b 100644 --- a/test/bun.js/install/bun-add.test.ts +++ b/test/bun.js/install/bun-add.test.ts @@ -2,7 +2,7 @@ import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe } from "bunExe"; import { bunEnv as env } from "bunEnv"; -import { access, mkdir, mkdtemp, readlink, rm, writeFile } from "fs/promises"; +import { access, mkdir, mkdtemp, readlink, realpath, rm, writeFile } from "fs/promises"; import { join, relative } from "path"; import { tmpdir } from "os"; import { @@ -17,7 +17,6 @@ import { root_url, setHandler, } from "./dummy.registry"; -import { realpathSync } from "fs"; beforeAll(dummyBeforeAll); afterAll(dummyAfterAll); @@ -25,7 +24,7 @@ afterAll(dummyAfterAll); let add_dir; beforeEach(async () => { - add_dir = await mkdtemp(join(realpathSync(tmpdir()), "bun-add.test")); + add_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-add.test")); await dummyBeforeEach(); }); afterEach(async () => { @@ -243,9 +242,11 @@ it("should handle @scoped names", async () => { it("should add dependency with specified semver", async () => { const urls: string[] = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -278,7 +279,7 @@ it("should add dependency with specified semver", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -304,9 +305,11 @@ it("should add dependency with specified semver", async () => { it("should add dependency alongside workspaces", async () => { const urls: string[] = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -349,7 +352,7 @@ it("should add dependency alongside workspaces", async () => { " 2 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -377,9 +380,11 @@ it("should add dependency alongside workspaces", async () => { it("should add aliased dependency (npm)", async () => { const urls: string[] = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -412,7 +417,7 @@ it("should add aliased dependency (npm)", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -510,7 +515,7 @@ it("should add aliased dependency (GitHub)", async () => { it("should let you add the same package twice", async () => { const urls: string[] = []; - setHandler(dummyRegistry(urls, "0.0.3", {})); + setHandler(dummyRegistry(urls, { "0.0.3": {} })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -540,7 +545,7 @@ it("should let you add the same package twice", async () => { expect(out1).toContain("installed baz@0.0.3"); expect(out1).toContain("1 packages installed"); expect(await exited1).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "baz"]); expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ diff --git a/test/bun.js/install/bun-install.test.ts b/test/bun.js/install/bun-install.test.ts index 048c15c6f..42d19193c 100644 --- a/test/bun.js/install/bun-install.test.ts +++ b/test/bun.js/install/bun-install.test.ts @@ -135,7 +135,7 @@ it("should handle empty string in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -462,6 +462,49 @@ it("should handle life-cycle scripts within workspaces", async () => { await access(join(package_dir, "bun.lockb")); }); +it("should ignore workspaces within workspaces", async () => { + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["bar"], + }), + ); + await mkdir(join(package_dir, "bar")); + await writeFile( + join(package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + workspaces: ["baz"], + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@workspace:bar", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(requested).toBe(0); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(package_dir, "node_modules", "bar"))).toBe(join("..", "bar")); + await access(join(package_dir, "bun.lockb")); +}); + it("should handle ^0 in dependencies", async () => { const urls: string[] = []; setHandler(dummyRegistry(urls)); @@ -494,7 +537,7 @@ it("should handle ^0 in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -574,7 +617,7 @@ it("should handle ^0.0 in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -691,7 +734,7 @@ it("should handle ^0.0.2 in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -704,7 +747,7 @@ it("should handle ^0.0.2 in dependencies", async () => { it("should handle ^0.0.2-rc in dependencies", async () => { const urls: string[] = []; - setHandler(dummyRegistry(urls, "0.0.2-rc")); + setHandler(dummyRegistry(urls, { "0.0.2-rc": { as: "0.0.2" } })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -734,7 +777,7 @@ it("should handle ^0.0.2-rc in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -747,7 +790,7 @@ it("should handle ^0.0.2-rc in dependencies", async () => { it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { const urls: string[] = []; - setHandler(dummyRegistry(urls, "0.0.2-alpha.3")); + setHandler(dummyRegistry(urls, { "0.0.2-alpha.3": { as: "0.0.2" } })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -777,7 +820,7 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); @@ -791,9 +834,11 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { it("should handle dependency aliasing", async () => { const urls = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -826,7 +871,7 @@ it("should handle dependency aliasing", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -845,9 +890,11 @@ it("should handle dependency aliasing", async () => { it("should handle dependency aliasing (versioned)", async () => { const urls: string[] = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -880,7 +927,7 @@ it("should handle dependency aliasing (versioned)", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -899,9 +946,11 @@ it("should handle dependency aliasing (versioned)", async () => { it("should handle dependency aliasing (dist-tagged)", async () => { const urls: string[] = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -934,7 +983,7 @@ it("should handle dependency aliasing (dist-tagged)", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -953,9 +1002,11 @@ it("should handle dependency aliasing (dist-tagged)", async () => { it("should not reinstall aliased dependencies", async () => { const urls = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -992,7 +1043,7 @@ it("should not reinstall aliased dependencies", async () => { " 1 packages installed", ]); expect(await exited1).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -1049,9 +1100,11 @@ it("should not reinstall aliased dependencies", async () => { it("should handle aliased & direct dependency references", async () => { const urls = []; setHandler( - dummyRegistry(urls, "0.0.3", { - bin: { - "baz-run": "index.js", + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, }, }), ); @@ -1097,7 +1150,7 @@ it("should handle aliased & direct dependency references", async () => { " 2 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz.tgz`]); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz"]); expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); @@ -1126,7 +1179,16 @@ it("should handle aliased & direct dependency references", async () => { it("should not hoist if name collides with alias", async () => { const urls = []; - setHandler(dummyRegistry(urls)); + setHandler( + dummyRegistry(urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1145,7 +1207,7 @@ it("should not hoist if name collides with alias", async () => { name: "moo", version: "0.0.4", dependencies: { - bar: "*", + bar: "0.0.2", }, }), ); @@ -1164,14 +1226,21 @@ it("should not hoist if name collides with alias", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + moo@workspace:moo", - " + bar@0.0.2", + " + bar@0.0.3", "", " 3 packages installed", ]); expect(await exited).toBe(0); - expect(urls).toEqual([`${root_url}/baz`, `${root_url}/bar`, `${root_url}/baz.tgz`, `${root_url}/bar.tgz`]); + expect(urls).toEqual([ + `${root_url}/baz`, + `${root_url}/bar`, + `${root_url}/baz-0.0.3.tgz`, + `${root_url}/bar-0.0.2.tgz`, + ]); expect(requested).toBe(4); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "moo"]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe(join("..", "bar", "index.js")); expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ name: "baz", @@ -1550,3 +1619,105 @@ it("should handle GitHub URL in dependencies (git+https://github.com/user/repo.g expect(package_json.name).toBe("uglify-js"); await access(join(package_dir, "bun.lockb")); }); + +it("should consider peerDependencies during hoisting", async () => { + const urls = []; + setHandler( + dummyRegistry(urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + }), + ); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + baz: ">0.0.3", + }, + workspaces: ["bar", "moo"], + }), + ); + await mkdir(join(package_dir, "bar")); + await writeFile( + join(package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + dependencies: { + baz: "0.0.3", + }, + }), + ); + await mkdir(join(package_dir, "moo")); + await writeFile( + join(package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.4", + dependencies: { + baz: "0.0.5", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml", "--peer"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@workspace:bar", + " + moo@workspace:moo", + " + baz@0.0.5", + "", + " 4 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`, `${root_url}/baz-0.0.5.tgz`]); + 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("..", "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", "bar"))).toBe(join("..", "bar")); + expect(await readdirSorted(join(package_dir, "bar"))).toEqual(["node_modules", "package.json"]); + expect(await readdirSorted(join(package_dir, "bar", "node_modules"))).toEqual(["baz"]); + expect(await readdirSorted(join(package_dir, "bar", "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + 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.5", + bin: { + "baz-exec": "index.js", + }, + }); + expect(await readlink(join(package_dir, "node_modules", "moo"))).toBe(join("..", "moo")); + expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["package.json"]); + await access(join(package_dir, "bun.lockb")); +}); diff --git a/test/bun.js/install/bun-link.test.ts b/test/bun.js/install/bun-link.test.ts index 5e80216bd..b892c00e0 100644 --- a/test/bun.js/install/bun-link.test.ts +++ b/test/bun.js/install/bun-link.test.ts @@ -2,15 +2,15 @@ import { spawn } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; import { bunExe } from "bunExe"; import { bunEnv as env } from "bunEnv"; -import { mkdtemp, rm, writeFile } from "fs/promises"; +import { mkdtemp, realpath, rm, writeFile } from "fs/promises"; import { basename, join } from "path"; import { tmpdir } from "os"; let package_dir, link_dir; beforeEach(async () => { - link_dir = await mkdtemp(join(tmpdir(), "bun-link.test")); - package_dir = await mkdtemp(join(tmpdir(), "bun-link.pkg")); + link_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-link.test")); + package_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-link.pkg")); }); afterEach(async () => { await rm(link_dir, { force: true, recursive: true }); diff --git a/test/bun.js/install/dummy.registry.ts b/test/bun.js/install/dummy.registry.ts index 9738591cc..179231ed1 100644 --- a/test/bun.js/install/dummy.registry.ts +++ b/test/bun.js/install/dummy.registry.ts @@ -1,14 +1,13 @@ import { file } from "bun"; import { expect } from "bun:test"; -import { realpathSync } from "fs"; -import { mkdtemp, readdir, rm } from "fs/promises"; +import { mkdtemp, readdir, realpath, rm } from "fs/promises"; import { tmpdir } from "os"; import { basename, join } from "path"; let handler, server; export let package_dir, requested, root_url; -export function dummyRegistry(urls, version = "0.0.2", props = {}) { +export function dummyRegistry(urls, info: object = { "0.0.2": {} }) { return async request => { urls.push(request.url); expect(request.method).toBe("GET"); @@ -21,19 +20,22 @@ export function dummyRegistry(urls, version = "0.0.2", props = {}) { expect(request.headers.get("npm-auth-type")).toBe(null); expect(await request.text()).toBe(""); const name = request.url.slice(request.url.lastIndexOf("/") + 1); + const versions = {}; + let version; + for (version in info) { + versions[version] = { + name, + version, + dist: { + tarball: `${request.url}-${info[version].as ?? version}.tgz`, + }, + ...info[version], + }; + } return new Response( JSON.stringify({ name, - versions: { - [version]: { - name, - version, - dist: { - tarball: `${request.url}.tgz`, - }, - ...props, - }, - }, + versions, "dist-tags": { latest: version, }, @@ -74,7 +76,7 @@ export function dummyAfterAll() { export async function dummyBeforeEach() { resetHanlder(); requested = 0; - package_dir = realpathSync(await mkdtemp(join(tmpdir(), "bun-install.test"))); + package_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-install.test")); } export async function dummyAfterEach() { |