aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/install/bin.zig27
-rw-r--r--src/install/dependency.zig52
-rw-r--r--src/install/install.zig205
-rw-r--r--src/install/lockfile.zig138
-rw-r--r--src/install/npm.zig16
-rw-r--r--src/install/semver.zig4
-rw-r--r--test/bun.js/install/bar-0.0.2.tgz (renamed from test/bun.js/install/bar.tgz)bin192 -> 192 bytes
-rw-r--r--test/bun.js/install/baz-0.0.3.tgz (renamed from test/bun.js/install/baz.tgz)bin283 -> 283 bytes
-rw-r--r--test/bun.js/install/baz-0.0.5.tgzbin0 -> 286 bytes
-rw-r--r--test/bun.js/install/bun-add.test.ts39
-rw-r--r--test/bun.js/install/bun-install.test.ts237
-rw-r--r--test/bun.js/install/bun-link.test.ts6
-rw-r--r--test/bun.js/install/dummy.registry.ts30
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
index 1983142d7..1983142d7 100644
--- a/test/bun.js/install/bar.tgz
+++ b/test/bun.js/install/bar-0.0.2.tgz
Binary files differ
diff --git a/test/bun.js/install/baz.tgz b/test/bun.js/install/baz-0.0.3.tgz
index 375a5e31f..375a5e31f 100644
--- a/test/bun.js/install/baz.tgz
+++ b/test/bun.js/install/baz-0.0.3.tgz
Binary files differ
diff --git a/test/bun.js/install/baz-0.0.5.tgz b/test/bun.js/install/baz-0.0.5.tgz
new file mode 100644
index 000000000..7e708fb1b
--- /dev/null
+++ b/test/bun.js/install/baz-0.0.5.tgz
Binary files differ
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() {