diff options
| author | 2021-12-13 04:45:59 -0800 | |
|---|---|---|
| committer | 2021-12-16 19:18:51 -0800 | |
| commit | e3a12ea1a9fc59188c8bb32d2ee229645db1ca05 (patch) | |
| tree | 2bc423b6ad89e29344cc016cef8e8ff8d14ae65b /src | |
| parent | 5dd6fb85479bc79b21e1e536b44463972157b1da (diff) | |
| download | bun-e3a12ea1a9fc59188c8bb32d2ee229645db1ca05.tar.gz bun-e3a12ea1a9fc59188c8bb32d2ee229645db1ca05.tar.zst bun-e3a12ea1a9fc59188c8bb32d2ee229645db1ca05.zip | |
[bun install] Fix `bun add` so it writes the resolved version to package.json
Diffstat (limited to 'src')
| -rw-r--r-- | src/install/dependency.zig | 2 | ||||
| -rw-r--r-- | src/install/install.zig | 635 |
2 files changed, 413 insertions, 224 deletions
diff --git a/src/install/dependency.zig b/src/install/dependency.zig index c644aa6e6..665d427ae 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -437,7 +437,7 @@ pub fn eqlResolved(a: Dependency, b: Dependency) bool { return @as(Dependency.Version.Tag, a.version) == @as(Dependency.Version.Tag, b.version) and a.resolution == b.resolution; } -pub fn parse(allocator: *std.mem.Allocator, dependency_: string, sliced: *const SlicedString, log: *logger.Log) ?Version { +pub fn parse(allocator: *std.mem.Allocator, dependency_: string, sliced: *const SlicedString, log: ?*logger.Log) ?Version { var dependency = std.mem.trimLeft(u8, dependency_, " \t\n\r"); if (dependency.len == 0) return null; diff --git a/src/install/install.zig b/src/install/install.zig index 79839510d..ab06ff930 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -496,6 +496,7 @@ pub const Lockfile = struct { { // For now, the dumb way + // (the smart way is avoiding this copy) this.path_buf[0.."node_modules".len].* = "node_modules".*; var parent_id = tree.id; var path_written: usize = "node_modules".len; @@ -677,7 +678,7 @@ pub const Lockfile = struct { } }; - pub fn clean(old: *Lockfile, deduped: *u32, options: *const PackageManager.Options) !*Lockfile { + pub fn clean(old: *Lockfile, deduped: *u32, updates: []PackageManager.UpdateRequest, options: *const PackageManager.Options) !*Lockfile { // We will only shrink the number of packages here. // never grow @@ -755,8 +756,89 @@ pub const Lockfile = struct { }; // try clone_queue.ensureUnusedCapacity(root.dependencies.len); _ = try root.clone(old, new, package_id_mapping, &cloner); + + // When you run `"bun add react" + // This is where we update it in the lockfile from "latest" to "^17.0.2" + + if (updates.len > 0) { + var root_deps: []Dependency = new.packages.items(.dependencies)[0].mut(new.buffers.dependencies.items); + const old_root_deps: []const Dependency = old.packages.items(.dependencies)[0].get(old.buffers.dependencies.items); + const old_resolutions: []const PackageID = old.packages.items(.resolutions)[0].get(old.buffers.resolutions.items); + const resolutions_of_yore: []const Resolution = old.packages.items(.resolution); + const old_names = old.packages.items(.name); + var string_builder = new.stringBuilder(); + for (updates) |update| { + if (update.version.tag == .uninitialized) { + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + const old_resolution = old_resolutions[i]; + if (old_resolution > old.packages.len) continue; + const res = resolutions_of_yore[old_resolution]; + const len = std.fmt.count("^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}); + if (len > String.max_inline_len) { + string_builder.cap += len; + } + } + } + } + } + + try string_builder.allocate(); + defer string_builder.clamp(); + var full_buf = string_builder.ptr.?[0 .. string_builder.cap + new.buffers.string_bytes.items.len]; + var temp_buf: [513]u8 = undefined; + + for (updates) |update, update_i| { + if (update.version.tag == .uninitialized) { + // prevent the deduping logic + updates[update_i].e_string = null; + + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + const old_resolution = old_resolutions[i]; + if (old_resolution > old.packages.len) continue; + const res = resolutions_of_yore[old_resolution]; + var buf = std.fmt.bufPrint(&temp_buf, "^{}", .{res.value.npm.fmt(old.buffers.string_bytes.items)}) catch break; + const external_version = string_builder.append(ExternalString, buf); + const sliced = external_version.value.sliced( + new.buffers.string_bytes.items, + ); + + updates[update_i].missing_version = true; + updates[update_i].version = Dependency.parse( + old.allocator, + sliced.slice, + &sliced, + null, + ) orelse Dependency.Version{}; + } + } + } + } + } + try cloner.flush(); + // Don't allow invalid memory to happen + if (updates.len > 0) { + const dep_list = new.packages.items(.dependencies)[0]; + const res_list = new.packages.items(.resolutions)[0]; + const root_deps: []const Dependency = dep_list.get(new.buffers.dependencies.items); + const new_resolutions: []const PackageID = res_list.get(new.buffers.resolutions.items); + + for (updates) |update, update_i| { + if (update.missing_version) { + for (root_deps) |dep, i| { + if (dep.name_hash == String.Builder.stringHash(update.name)) { + if (new_resolutions[i] > new.packages.len) continue; + updates[update_i].version_buf = new.buffers.string_bytes.items; + updates[update_i].resolved_version_buf = new.buffers.string_bytes.items; + } + } + } + } + } + return new; } @@ -1736,6 +1818,7 @@ pub const Lockfile = struct { builder.count(this.name.slice(old_string_buf)); this.resolution.count(old_string_buf, *Lockfile.StringBuilder, builder); this.meta.count(old_string_buf, *Lockfile.StringBuilder, builder); + this.bin.count(old_string_buf, *Lockfile.StringBuilder, builder); const old_dependencies: []const Dependency = this.dependencies.get(old.buffers.dependencies.items); const old_resolutions: []const PackageID = this.resolutions.get(old.buffers.resolutions.items); @@ -1768,6 +1851,7 @@ pub const Lockfile = struct { this.name.slice(old_string_buf), this.name_hash, ), + .bin = this.bin.clone(old_string_buf, *Lockfile.StringBuilder, builder), .name_hash = this.name_hash, .meta = this.meta.clone( old_string_buf, @@ -2357,7 +2441,6 @@ pub const Lockfile = struct { man_dir: String = String{}, unpacked_size: u64 = 0, integrity: Integrity = Integrity{}, - bin: Bin = Bin{}, pub fn isDisabled(this: *const Meta) bool { return !this.arch.isMatch() or !this.os.isMatch(); @@ -2365,13 +2448,11 @@ pub const Lockfile = struct { pub fn count(this: *const Meta, buf: []const u8, comptime StringBuilderType: type, builder: StringBuilderType) void { builder.count(this.man_dir.slice(buf)); - this.bin.count(buf, StringBuilderType, builder); } pub fn clone(this: *const Meta, buf: []const u8, comptime StringBuilderType: type, builder: StringBuilderType) Meta { var new = this.*; new.id = invalid_package_id; - new.bin = this.bin.clone(buf, StringBuilderType, builder); new.man_dir = builder.append(String, this.man_dir.slice(buf)); return new; @@ -2384,6 +2465,7 @@ pub const Lockfile = struct { dependencies: DependencySlice = DependencySlice{}, resolutions: PackageIDSlice = PackageIDSlice{}, meta: Meta = Meta{}, + bin: Bin = Bin{}, pub const Serializer = struct { pub const sizes = blk: { @@ -2918,7 +3000,6 @@ const PackageInstall = struct { package_name: string, package_version: string, - expected_file_count: u32 = 0, file_count: u32 = 0, threadlocal var package_json_checker: json_parser.PackageJSONVersionChecker = undefined; @@ -2968,7 +3049,6 @@ const PackageInstall = struct { this.package_install = PackageInstall{ .cache_dir = ctx.cache_dir, .progress = ctx.progress, - .expected_file_count = meta.file_count, .cache_dir_subpath = PackageManager.cachedNPMPackageFolderNamePrint(&cache_dir_subpath_buf, name, resolution.value.npm), .destination_dir = this.destination_dir, .destination_dir_subpath = destination_dir_subpath, @@ -3001,6 +3081,8 @@ const PackageInstall = struct { pub const Method = enum { clonefile, + + // Slower than clonefile clonefile_each_dir, // Slow! @@ -3161,15 +3243,11 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); - var node = this.progress.start(this.package_name, @maximum(this.expected_file_count, 1)) catch unreachable; - defer node.completeOne(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.Dir, walker: *Walker, - node_: *Progress.Node, - progress_: *Progress, ) !u32 { var real_file_count: u32 = 0; var stackpath: [std.fs.MAX_PATH_BYTES]u8 = undefined; @@ -3195,7 +3273,7 @@ const PackageInstall = struct { else => return error.Unexpected, }, } - defer node_.completeOne(); + real_file_count += 1; }, else => {}, @@ -3212,7 +3290,10 @@ const PackageInstall = struct { defer subdir.close(); - this.file_count = FileCopier.copy(subdir, &walker_, node, this.progress) catch |err| return Result{ + this.file_count = FileCopier.copy( + subdir, + &walker_, + ) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files }, }; @@ -3222,7 +3303,7 @@ const PackageInstall = struct { } // https://www.unix.com/man-page/mojave/2/fclonefileat/ - fn installWithClonefile(this: *const PackageInstall) CloneFileError!void { + fn installWithClonefile(this: *PackageInstall) CloneFileError!Result { if (comptime !Environment.isMac) @compileError("clonefileat() is macOS only."); if (this.package_name[0] == '@') { @@ -3242,10 +3323,15 @@ const PackageInstall = struct { this.destination_dir_subpath, 0, )) { - 0 => void{}, + 0 => .{ .success = void{} }, else => |errno| switch (std.os.errno(errno)) { .OPNOTSUPP => error.NotSupported, .NOENT => error.FileNotFound, + // We first try to delete the directory + // But, this can happen if this package contains a node_modules folder + // We want to continue installing as many packages as we can, so we shouldn't block while downloading + // We use the slow path in this case + .EXIST => try this.installWithClonefileEachDir(), else => error.Unexpected, }, }; @@ -3269,14 +3355,11 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); - var node = this.progress.start(this.package_name, @maximum(this.expected_file_count, 1)) catch unreachable; - defer node.completeOne(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.Dir, walker: *Walker, - node_: *Progress.Node, progress_: *Progress, ) !u32 { var real_file_count: u32 = 0; @@ -3289,7 +3372,7 @@ const PackageInstall = struct { destination_dir_.makePath(entry_dirname) catch {}; } break :brk destination_dir_.createFile(entry.path, .{}) catch |err| { - node_.end(); + progress_.root.end(); progress_.refresh(); @@ -3299,7 +3382,6 @@ const PackageInstall = struct { }; }; defer outfile.close(); - defer node_.completeOne(); var infile = try entry.dir.openFile(entry.basename, .{ .read = true }); defer infile.close(); @@ -3309,7 +3391,7 @@ const PackageInstall = struct { CopyFile.copy(infile.handle, outfile.handle) catch { entry.dir.copyFile(entry.basename, destination_dir_, entry.path, .{}) catch |err| { - node_.end(); + progress_.root.end(); progress_.refresh(); @@ -3330,7 +3412,7 @@ const PackageInstall = struct { defer subdir.close(); - this.file_count = FileCopier.copy(subdir, &walker_, node, this.progress) catch |err| return Result{ + this.file_count = FileCopier.copy(subdir, &walker_, this.progress) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files }, }; @@ -3357,15 +3439,11 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .opening_cache_dir }, }; defer walker_.deinit(); - var node = this.progress.start(this.package_name, @maximum(this.expected_file_count, 1)) catch unreachable; - defer node.completeOne(); const FileCopier = struct { pub fn copy( destination_dir_: std.fs.Dir, walker: *Walker, - node_: *Progress.Node, - progress_: *Progress, ) !u32 { var real_file_count: u32 = 0; while (try walker.next()) |entry| { @@ -3377,8 +3455,6 @@ const PackageInstall = struct { }, else => {}, } - - defer node_.completeOne(); } return real_file_count; @@ -3391,7 +3467,10 @@ const PackageInstall = struct { defer subdir.close(); - this.file_count = FileCopier.copy(subdir, &walker_, node, this.progress) catch |err| return Result{ + this.file_count = FileCopier.copy( + subdir, + &walker_, + ) catch |err| return Result{ .fail = .{ .err = err, .step = .copying_files }, }; @@ -3411,7 +3490,9 @@ const PackageInstall = struct { .clonefile => { // First, attempt to use clonefile // if that fails due to ENOTSUP, mark it as unsupported and then fall back to copyfile - this.installWithClonefile() catch |err| { + if (this.installWithClonefile()) |result| { + return result; + } else |err| { switch (err) { error.NotSupported => { supported_method = .copyfile; @@ -3423,10 +3504,12 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .copying_files }, }, } - }; + } }, .clonefile_each_dir => { - return this.installWithClonefileEachDir() catch |err| { + if (this.installWithClonefileEachDir()) |result| { + return result; + } else |err| { switch (err) { error.NotSupported => { supported_method = .copyfile; @@ -3438,10 +3521,12 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .copying_files }, }, } - }; + } }, .hardlink => { - return this.installWithHardlink() catch |err| { + if (this.installWithHardlink()) |result| { + return result; + } else |err| { switch (err) { error.NotSupported => { supported_method = .copyfile; @@ -3453,7 +3538,7 @@ const PackageInstall = struct { .fail = .{ .err = err, .step = .copying_files }, }, } - }; + } }, else => {}, } @@ -3476,9 +3561,11 @@ const TaggedPointer = @import("../tagged_pointer.zig"); const TaskCallbackContext = union(Tag) { dependency: PackageID, request_id: PackageID, + node_modules_folder: u32, // Really, this is a file descriptor pub const Tag = enum { dependency, request_id, + node_modules_folder, }; }; @@ -3517,6 +3604,7 @@ pub const PackageManager = struct { progress_name_buf: [768]u8 = undefined, progress_name_buf_dynamic: []u8 = &[_]u8{}, cpu_count: u32 = 0, + package_json_updates: []UpdateRequest = &[_]UpdateRequest{}, root_package_json_file: std.fs.File, root_dependency_list: Lockfile.DependencySlice = .{}, @@ -3700,7 +3788,13 @@ pub const PackageManager = struct { // Do we need to download the tarball? .extract => { - var network_task = (try this.generateNetworkTaskForTarball(package)).?; + const task_id = Task.Id.forNPMPackage( + Task.Tag.extract, + name.slice(this.lockfile.buffers.string_bytes.items), + package.resolution.value.npm, + ); + + var network_task = (try this.generateNetworkTaskForTarball(task_id, package)).?; return ResolvedPackageResult{ .package = package, @@ -3714,8 +3808,7 @@ pub const PackageManager = struct { return ResolvedPackageResult{ .package = package }; } - pub fn generateNetworkTaskForTarball(this: *PackageManager, package: Lockfile.Package) !?*NetworkTask { - const task_id = Task.Id.forNPMPackage(Task.Tag.extract, this.lockfile.str(package.name), package.resolution.value.npm); + pub fn generateNetworkTaskForTarball(this: *PackageManager, task_id: u64, package: Lockfile.Package) !?*NetworkTask { const dedupe_entry = try this.network_dedupe_map.getOrPut(this.allocator, task_id); if (dedupe_entry.found_existing) return null; @@ -4742,6 +4835,157 @@ pub const PackageManager = struct { } }; + const PackageJSONEditor = struct { + pub fn edit( + allocator: *std.mem.Allocator, + updates: []UpdateRequest, + current_package_json: *JSAst.Expr, + dependency_list: string, + ) !void { + const G = JSAst.G; + + var remaining: usize = updates.len; + + // There are three possible scenarios here + // 1. There is no "dependencies" (or equivalent list) or it is empty + // 2. There is a "dependencies" (or equivalent list), but the package name already exists in a separate list + // 3. There is a "dependencies" (or equivalent list), and the package name exists in multiple lists + ast_modifier: { + // Try to use the existing spot in the dependencies list if possible + for (updates) |update, i| { + outer: for (dependency_lists_to_check) |list| { + if (current_package_json.asProperty(list)) |query| { + if (query.expr.data == .e_object) { + if (query.expr.asProperty(update.name)) |value| { + if (value.expr.data == .e_string) { + updates[i].e_string = value.expr.data.e_string; + remaining -= 1; + } + break :outer; + } + } + } + } + } + + if (remaining == 0) + break :ast_modifier; + + var dependencies: []G.Property = &[_]G.Property{}; + if (current_package_json.asProperty(dependency_list)) |query| { + if (query.expr.data == .e_object) { + dependencies = query.expr.data.e_object.properties; + } + } + + var new_dependencies = try allocator.alloc(G.Property, dependencies.len + remaining); + std.mem.copy(G.Property, new_dependencies, dependencies); + std.mem.set(G.Property, new_dependencies[dependencies.len..], G.Property{}); + + outer: for (updates) |update, j| { + if (update.e_string != null) continue; + + var k: usize = 0; + + while (k < new_dependencies.len) : (k += 1) { + if (new_dependencies[k].key == null) { + new_dependencies[k].key = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + .utf8 = update.name, + }, + logger.Loc.Empty, + ); + + new_dependencies[k].value = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + // we set it later + .utf8 = "", + }, + logger.Loc.Empty, + ); + updates[j].e_string = new_dependencies[k].value.?.data.e_string; + continue :outer; + } + + // This actually is a duplicate + // like "react" appearing in both "dependencies" and "optionalDependencies" + // For this case, we'll just swap remove it + if (new_dependencies[k].key.?.data.e_string.eql(string, update.name)) { + if (new_dependencies.len > 1) { + new_dependencies[k] = new_dependencies[new_dependencies.len - 1]; + new_dependencies = new_dependencies[0 .. new_dependencies.len - 1]; + } else { + new_dependencies = &[_]G.Property{}; + } + } + } + } + + var needs_new_dependency_list = true; + var dependencies_object: JSAst.Expr = undefined; + if (current_package_json.asProperty(dependency_list)) |query| { + if (query.expr.data == .e_object) { + needs_new_dependency_list = false; + + dependencies_object = query.expr; + } + } + + if (needs_new_dependency_list) { + dependencies_object = JSAst.Expr.init( + JSAst.E.Object, + JSAst.E.Object{ + .properties = new_dependencies, + }, + logger.Loc.Empty, + ); + } + + if (current_package_json.data != .e_object or current_package_json.data.e_object.properties.len == 0) { + var root_properties = try allocator.alloc(JSAst.G.Property, 1); + root_properties[0] = JSAst.G.Property{ + .key = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + .utf8 = dependency_list, + }, + logger.Loc.Empty, + ), + .value = dependencies_object, + }; + current_package_json.* = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = root_properties }, logger.Loc.Empty); + } else if (needs_new_dependency_list) { + var root_properties = try allocator.alloc(JSAst.G.Property, current_package_json.data.e_object.properties.len + 1); + std.mem.copy(JSAst.G.Property, root_properties, current_package_json.data.e_object.properties); + root_properties[root_properties.len - 1].key = JSAst.Expr.init( + JSAst.E.String, + JSAst.E.String{ + .utf8 = dependency_list, + }, + logger.Loc.Empty, + ); + root_properties[root_properties.len - 1].value = dependencies_object; + current_package_json.* = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = root_properties }, logger.Loc.Empty); + } + + dependencies_object.data.e_object.properties = new_dependencies; + dependencies_object.data.e_object.packageJSONSort(); + } + + for (updates) |*update, j| { + var str = update.e_string.?; + + if (update.version.tag == .uninitialized) { + str.utf8 = latest; + } else { + str.utf8 = update.version.literal.slice(update.version_buf); + } + } + } + }; + fn init( ctx: Command.Context, package_json_file_: ?std.fs.File, @@ -4837,7 +5081,7 @@ pub const PackageManager = struct { Output.flush(); } - var cpu_count = @truncate(u32, ((try std.Thread.getCpuCount()) + 1) / 2); + var cpu_count = @truncate(u32, ((try std.Thread.getCpuCount()) + 1)); if (env_loader.map.get("GOMAXPROCS")) |max_procs| { if (std.fmt.parseInt(u32, max_procs, 10)) |cpu_count_| { @@ -5159,6 +5403,13 @@ pub const PackageManager = struct { } } + const dependency_lists_to_check = [_]string{ + "dependencies", + "devDependencies", + "optionalDependencies", + "peerDependencies", + }; + fn updatePackageJSONAndInstallWithManager( ctx: Command.Context, manager: *PackageManager, @@ -5305,13 +5556,6 @@ pub const PackageManager = struct { var any_changes = false; - const dependency_lists_to_check = [_]string{ - "dependencies", - "devDependencies", - "optionalDependencies", - "peerDependencies", - }; - var dependency_list: string = "dependencies"; if (manager.options.update.development) { dependency_list = "devDependencies"; @@ -5377,146 +5621,8 @@ pub const PackageManager = struct { } }, .add, .update => { - const G = JSAst.G; - - var remaining: usize = updates.len; - - // There are three possible scenarios here - // 1. There is no "dependencies" (or equivalent list) or it is empty - // 2. There is a "dependencies" (or equivalent list), but the package name already exists in a separate list - // 3. There is a "dependencies" (or equivalent list), and the package name exists in multiple lists - ast_modifier: { - // Try to use the existing spot in the dependencies list if possible - for (updates) |update, i| { - outer: for (dependency_lists_to_check) |list| { - if (current_package_json.asProperty(list)) |query| { - if (query.expr.data == .e_object) { - if (query.expr.asProperty(update.name)) |value| { - if (value.expr.data == .e_string) { - updates[i].e_string = value.expr.data.e_string; - remaining -= 1; - } - break :outer; - } - } - } - } - } - - if (remaining == 0) - break :ast_modifier; - - var dependencies: []G.Property = &[_]G.Property{}; - if (current_package_json.asProperty(dependency_list)) |query| { - if (query.expr.data == .e_object) { - dependencies = query.expr.data.e_object.properties; - } - } - - var new_dependencies = try ctx.allocator.alloc(G.Property, dependencies.len + remaining); - std.mem.copy(G.Property, new_dependencies, dependencies); - std.mem.set(G.Property, new_dependencies[dependencies.len..], G.Property{}); - - outer: for (updates) |update, j| { - if (update.e_string != null) continue; - - var k: usize = 0; - - while (k < new_dependencies.len) : (k += 1) { - if (new_dependencies[k].key == null) { - new_dependencies[k].key = JSAst.Expr.init( - JSAst.E.String, - JSAst.E.String{ - .utf8 = update.name, - }, - logger.Loc.Empty, - ); - new_dependencies[k].value = JSAst.Expr.init( - JSAst.E.String, - JSAst.E.String{ - // we set it later - .utf8 = "", - }, - logger.Loc.Empty, - ); - updates[j].e_string = new_dependencies[k].value.?.data.e_string; - continue :outer; - } - - // This actually is a duplicate - // like "react" appearing in both "dependencies" and "optionalDependencies" - // For this case, we'll just swap remove it - if (new_dependencies[k].key.?.data.e_string.eql(string, update.name)) { - if (new_dependencies.len > 1) { - new_dependencies[k] = new_dependencies[new_dependencies.len - 1]; - new_dependencies = new_dependencies[0 .. new_dependencies.len - 1]; - } else { - new_dependencies = &[_]G.Property{}; - } - } - } - } - - var needs_new_dependency_list = true; - var dependencies_object: JSAst.Expr = undefined; - if (current_package_json.asProperty(dependency_list)) |query| { - if (query.expr.data == .e_object) { - needs_new_dependency_list = false; - - dependencies_object = query.expr; - } - } - - if (needs_new_dependency_list) { - dependencies_object = JSAst.Expr.init( - JSAst.E.Object, - JSAst.E.Object{ - .properties = new_dependencies, - }, - logger.Loc.Empty, - ); - } - - if (current_package_json.data != .e_object or current_package_json.data.e_object.properties.len == 0) { - var root_properties = try ctx.allocator.alloc(JSAst.G.Property, 1); - root_properties[0] = JSAst.G.Property{ - .key = JSAst.Expr.init( - JSAst.E.String, - JSAst.E.String{ - .utf8 = dependency_list, - }, - logger.Loc.Empty, - ), - .value = dependencies_object, - }; - current_package_json = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = root_properties }, logger.Loc.Empty); - } else if (needs_new_dependency_list) { - var root_properties = try ctx.allocator.alloc(JSAst.G.Property, current_package_json.data.e_object.properties.len + 1); - std.mem.copy(JSAst.G.Property, root_properties, current_package_json.data.e_object.properties); - root_properties[root_properties.len - 1].key = JSAst.Expr.init( - JSAst.E.String, - JSAst.E.String{ - .utf8 = dependency_list, - }, - logger.Loc.Empty, - ); - root_properties[root_properties.len - 1].value = dependencies_object; - current_package_json = JSAst.Expr.init(JSAst.E.Object, JSAst.E.Object{ .properties = root_properties }, logger.Loc.Empty); - } - - dependencies_object.data.e_object.properties = new_dependencies; - dependencies_object.data.e_object.packageJSONSort(); - } - - for (updates) |*update, j| { - var str = update.e_string.?; - - if (update.version.tag == .uninitialized) { - str.utf8 = latest; - } else { - str.utf8 = update.version.literal.slice(update.version_buf); - } - } + try PackageJSONEditor.edit(ctx.allocator, updates, ¤t_package_json, dependency_list); + manager.package_json_updates = updates; }, } @@ -5524,14 +5630,53 @@ pub const PackageManager = struct { try buffer_writer.buffer.list.ensureTotalCapacity(ctx.allocator, current_package_json_buf.len + 1); var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); - const written = JSPrinter.printJSON(@TypeOf(package_json_writer), package_json_writer, current_package_json, &package_json_source) catch |err| { + var written = JSPrinter.printJSON(@TypeOf(package_json_writer), package_json_writer, current_package_json, &package_json_source) catch |err| { Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); Global.crash(); }; + // There are various tradeoffs with how we commit updates when you run `bun add` or `bun remove` + // The one we chose here is to effectively pretend a human did: + // 1. "bun add react@latest" + // 2. open lockfile, find what react resolved to + // 3. open package.json + // 4. replace "react" : "latest" with "react" : "^16.2.0" + // 5. save package.json + // The Smarterâ„¢ approach is you resolve ahead of time and write to disk once! + // But, turns out that's slower in any case where more than one package has to be resolved (most of the time!) + // Concurrent network requests are faster than doing one and then waiting until the next batch var new_package_json_source = package_json_writer.ctx.buffer.toOwnedSliceLeaky().ptr[0 .. written + 1]; try installWithManager(ctx, manager, new_package_json_source, log_level); + if (op == .update or op == .add) { + const source = logger.Source.initPathString("package.json", new_package_json_source); + // Now, we _re_ parse our in-memory edited package.json + // so we can commit the version we changed from the lockfile + current_package_json = json_parser.ParseJSON(&source, ctx.log, manager.allocator) catch |err| { + Output.prettyErrorln("<red>error<r><d>:<r> package.json failed to parse due to error {s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + return; + }; + + try PackageJSONEditor.edit(ctx.allocator, updates, ¤t_package_json, dependency_list); + var buffer_writer_two = try JSPrinter.BufferWriter.init(ctx.allocator); + try buffer_writer_two.buffer.list.ensureTotalCapacity(ctx.allocator, current_package_json_buf.len + 1); + var package_json_writer_two = JSPrinter.BufferPrinter.init(buffer_writer_two); + + written = JSPrinter.printJSON( + @TypeOf(package_json_writer_two), + package_json_writer_two, + current_package_json, + &source, + ) catch |err| { + Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); + Global.crash(); + }; + + new_package_json_source = package_json_writer_two.ctx.buffer.toOwnedSliceLeaky().ptr[0 .. written + 1]; + } + if (!manager.options.dry_run) { // Now that we've run the install step // We can save our in-memory package.json to disk @@ -5588,42 +5733,50 @@ pub const PackageManager = struct { destination_dir_subpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined, install_count: usize = 0, - pub const Context = struct { - package_id: PackageID, - tree_id: Lockfile.Tree.Id, - }; - - pub fn installPackage( + pub fn installEnqueuedPackages( this: *PackageInstaller, package_id: PackageID, comptime log_level: Options.LogLevel, ) void { - // const package_id = ctx.package_id; - // const tree = ctx.trees[ctx.tree_id]; - const meta = &this.metas[package_id]; - - if (meta.isDisabled()) { - this.summary.skipped += 1; - if (comptime log_level.showProgress()) { - this.node.completeOne(); - } - return; - } - const buf = this.lockfile.buffers.string_bytes.items; + const name = this.names[package_id].slice(buf); const resolution = this.resolutions[package_id]; + + var callbacks = this.manager.task_queue.fetchRemove(Task.Id.forNPMPackage( + Task.Tag.extract, + name, + resolution.value.npm, + )).?.value; + defer callbacks.deinit(this.manager.allocator); + + const prev_node_modules_folder = this.node_modules_folder; + defer this.node_modules_folder = prev_node_modules_folder; + for (callbacks.items) |cb| { + const node_modules_folder = cb.node_modules_folder; + this.node_modules_folder = std.fs.Dir{ .fd = @intCast(std.os.fd_t, node_modules_folder) }; + this.installPackageWithNameAndResolution(package_id, log_level, name, resolution); + } + } + + fn installPackageWithNameAndResolution( + this: *PackageInstaller, + package_id: PackageID, + comptime log_level: Options.LogLevel, + name: string, + resolution: Resolution, + ) void { std.mem.copy(u8, &this.destination_dir_subpath_buf, name); this.destination_dir_subpath_buf[name.len] = 0; var destination_dir_subpath: [:0]u8 = this.destination_dir_subpath_buf[0..name.len :0]; var resolution_buf: [512]u8 = undefined; + const buf = this.lockfile.buffers.string_bytes.items; var resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf)}) catch unreachable; switch (resolution.tag) { .npm => { var installer = PackageInstall{ .cache_dir = this.manager.cache_directory, .progress = this.progress, - .expected_file_count = meta.file_count, .cache_dir_subpath = PackageManager.cachedNPMPackageFolderName(name, resolution.value.npm), .destination_dir = this.node_modules_folder, .destination_dir_subpath = destination_dir_subpath, @@ -5647,7 +5800,20 @@ pub const PackageManager = struct { }, .fail => |cause| { if (cause.isPackageMissingFromCache()) { - if (this.manager.generateNetworkTaskForTarball(this.lockfile.packages.get(package_id)) catch unreachable) |task| { + const task_id = Task.Id.forNPMPackage(Task.Tag.extract, name, resolution.value.npm); + var task_queue = this.manager.task_queue.getOrPut(this.manager.allocator, task_id) catch unreachable; + if (!task_queue.found_existing) { + task_queue.value_ptr.* = .{}; + } + + task_queue.value_ptr.append( + this.manager.allocator, + .{ + .node_modules_folder = @intCast(u32, this.node_modules_folder.fd), + }, + ) catch unreachable; + + if (this.manager.generateNetworkTaskForTarball(task_id, this.lockfile.packages.get(package_id)) catch unreachable) |task| { task.schedule(&this.manager.network_tarball_batch); if (this.manager.network_tarball_batch.len > 6) { _ = this.manager.scheduleNetworkTasks(); @@ -5668,6 +5834,29 @@ pub const PackageManager = struct { else => {}, } } + + pub fn installPackage( + this: *PackageInstaller, + package_id: PackageID, + comptime log_level: Options.LogLevel, + ) void { + // const package_id = ctx.package_id; + // const tree = ctx.trees[ctx.tree_id]; + const meta = &this.metas[package_id]; + + if (meta.isDisabled()) { + if (comptime log_level.showProgress()) { + this.node.completeOne(); + } + return; + } + + const buf = this.lockfile.buffers.string_bytes.items; + const name = this.names[package_id].slice(buf); + const resolution = this.resolutions[package_id]; + + this.installPackageWithNameAndResolution(package_id, log_level, name, resolution); + } }; pub fn installPackages( @@ -5775,13 +5964,15 @@ pub const PackageManager = struct { if (PackageInstall.supported_method.isSync()) { sync_install: { const cwd = std.fs.cwd(); - var prev_packages: []const PackageID = &[_]PackageID{}; while (iterator.nextNodeModulesFolder()) |node_modules| { try cwd.makePath(std.mem.span(node_modules.relative_path)); + // 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.openDirZ(node_modules.relative_path, .{ .iterate = true, }); - defer folder.close(); + installer.node_modules_folder = folder; for (node_modules.packages) |package_id| { @@ -5791,32 +5982,28 @@ pub const PackageManager = struct { try this.runTasks( *PackageInstaller, &installer, - PackageInstaller.installPackage, + PackageInstaller.installEnqueuedPackages, log_level, ); } } - prev_packages = node_modules.packages; try this.runTasks( *PackageInstaller, &installer, - PackageInstaller.installPackage, + PackageInstaller.installEnqueuedPackages, log_level, ); } - if (this.pending_tasks > 0) { - while (this.pending_tasks > 0) { - try this.runTasks( - *PackageInstaller, - &installer, - PackageInstaller.installPackage, - log_level, - ); - } + while (this.pending_tasks > 0) { + try this.runTasks( + *PackageInstaller, + &installer, + PackageInstaller.installEnqueuedPackages, + log_level, + ); } - break :run_install; } } @@ -5908,6 +6095,8 @@ pub const PackageManager = struct { var needs_new_lockfile = load_lockfile_result != .ok; + // this defaults to false + // but we force allowing updates to the lockfile when you do bun add var had_any_diffs = false; manager.progress = .{}; @@ -5997,7 +6186,7 @@ pub const PackageManager = struct { ); const sum = manager.summary.add + manager.summary.remove + manager.summary.update; - had_any_diffs = sum > 0; + had_any_diffs = had_any_diffs or sum > 0; // If you changed packages, we will copy over the new package from the new lockfile const new_dependencies = new_root.dependencies.get(lockfile.buffers.dependencies.items); @@ -6149,7 +6338,7 @@ pub const PackageManager = struct { } if (had_any_diffs or needs_new_lockfile) { - manager.lockfile = try manager.lockfile.clean(&manager.summary.deduped, &manager.options); + manager.lockfile = try manager.lockfile.clean(&manager.summary.deduped, manager.package_json_updates, &manager.options); } if (manager.options.do.save_lockfile) { |
