diff options
-rw-r--r-- | src/cli/package_manager_command.zig | 4 | ||||
-rw-r--r-- | src/install/install.zig | 65 | ||||
-rw-r--r-- | src/install/lockfile.zig | 36 | ||||
-rwxr-xr-x | test/bun.lockb | bin | 150672 -> 149736 bytes | |||
-rw-r--r-- | test/cli/install/bun-install.test.ts | 164 |
5 files changed, 187 insertions, 82 deletions
diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 4408568e0..cb03b9e3f 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -51,7 +51,7 @@ pub const PackageManagerCommand = struct { @memcpy(lockfile_buffer[0..lockfile_.len], lockfile_); lockfile_buffer[lockfile_.len] = 0; var lockfile = lockfile_buffer[0..lockfile_.len :0]; - var pm = try PackageManager.init(ctx, null, PackageManager.Subcommand.pm); + var pm = try PackageManager.init(ctx, PackageManager.Subcommand.pm); const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, lockfile); handleLoadLockfileErrors(load_lockfile, pm); @@ -87,7 +87,7 @@ pub const PackageManagerCommand = struct { var args = try std.process.argsAlloc(ctx.allocator); args = args[1..]; - var pm = PackageManager.init(ctx, null, PackageManager.Subcommand.pm) catch |err| { + var pm = PackageManager.init(ctx, PackageManager.Subcommand.pm) catch |err| { // TODO: error messages here // if (err == error.MissingPackageJSON) { // // TODO: error messages diff --git a/src/install/install.zig b/src/install/install.zig index 0a0c21637..2c78c81a2 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -5088,15 +5088,13 @@ pub const PackageManager = struct { pub fn init( ctx: Command.Context, - package_json_file_: ?std.fs.File, comptime subcommand: Subcommand, ) !*PackageManager { - return initMaybeInstall(ctx, package_json_file_, subcommand); + return initMaybeInstall(ctx, subcommand); } fn initMaybeInstall( ctx: Command.Context, - package_json_file_: ?std.fs.File, comptime subcommand: Subcommand, ) !*PackageManager { const cli = try CommandLineArguments.parse(ctx.allocator, subcommand); @@ -5108,12 +5106,11 @@ pub const PackageManager = struct { } var _ctx = ctx; - return initWithCLI(&_ctx, package_json_file_, cli, subcommand); + return initWithCLI(&_ctx, cli, subcommand); } fn initWithCLI( ctx: *Command.Context, - package_json_file_: ?std.fs.File, cli: CommandLineArguments, comptime subcommand: Subcommand, ) !*PackageManager { @@ -5138,7 +5135,7 @@ pub const PackageManager = struct { // // We will walk up from the cwd, calling chdir on each directory until we find a package.json // If we fail to find one, we will report an error saying no packages to install - const package_json_file = package_json_file_ orelse brk: { + const package_json_file = brk: { var this_cwd = original_cwd; const child_json = child: { while (true) { @@ -5437,13 +5434,13 @@ pub const PackageManager = struct { return manager; } - fn attemptToCreatePackageJSON() !std.fs.File { - var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err| { + fn attemptToCreatePackageJSON() !void { + const package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err| { Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err)}); Global.crash(); }; + defer package_json_file.close(); try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); - return package_json_file; } pub inline fn add( @@ -5461,16 +5458,13 @@ pub const PackageManager = struct { pub inline fn link( ctx: Command.Context, ) !void { - var manager = PackageManager.init(ctx, null, .link) catch |err| brk: { - switch (err) { - error.MissingPackageJSON => { - var package_json_file = try attemptToCreatePackageJSON(); - break :brk try PackageManager.init(ctx, package_json_file, .link); - }, - else => return err, + var manager = PackageManager.init(ctx, .link) catch |err| brk: { + if (err == error.MissingPackageJSON) { + try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, .link); } - unreachable; + return err; }; if (manager.options.shouldPrintCommandName()) { @@ -5616,16 +5610,13 @@ pub const PackageManager = struct { pub inline fn unlink( ctx: Command.Context, ) !void { - var manager = PackageManager.init(ctx, null, .unlink) catch |err| brk: { - switch (err) { - error.MissingPackageJSON => { - var package_json_file = try attemptToCreatePackageJSON(); - break :brk try PackageManager.init(ctx, package_json_file, .unlink); - }, - else => return err, + var manager = PackageManager.init(ctx, .unlink) catch |err| brk: { + if (err == error.MissingPackageJSON) { + try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, .unlink); } - unreachable; + return err; }; if (manager.options.shouldPrintCommandName()) { @@ -6091,21 +6082,18 @@ pub const PackageManager = struct { comptime op: Lockfile.Package.Diff.Op, comptime subcommand: Subcommand, ) !void { - var manager = PackageManager.init(ctx, null, subcommand) catch |err| brk: { - switch (err) { - error.MissingPackageJSON => { - if (op == .add or op == .update) { - var package_json_file = try attemptToCreatePackageJSON(); - break :brk try PackageManager.init(ctx, package_json_file, subcommand); - } - + var manager = PackageManager.init(ctx, subcommand) catch |err| brk: { + if (err == error.MissingPackageJSON) { + if (op == .remove) { Output.prettyErrorln("<r>No package.json, so nothing to remove\n", .{}); Global.crash(); - }, - else => return err, + } + + try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, subcommand); } - unreachable; + return err; }; if (manager.options.shouldPrintCommandName()) { @@ -6515,7 +6503,7 @@ pub const PackageManager = struct { var package_json_cwd: string = ""; pub inline fn install(ctx: Command.Context) !void { - var manager = initMaybeInstall(ctx, null, .install) catch |err| { + var manager = initMaybeInstall(ctx, .install) catch |err| { if (err == error.SwitchToBunAdd) { return add(ctx); } @@ -7511,8 +7499,7 @@ pub const PackageManager = struct { mapping, ); - const sum = manager.summary.add + manager.summary.remove + manager.summary.update; - had_any_diffs = had_any_diffs or sum > 0; + had_any_diffs = had_any_diffs or manager.summary.hasDiffs(); if (manager.options.enable.frozen_lockfile and had_any_diffs) { if (comptime log_level != .silent) { diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index ff4f82400..7d29084c6 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -2374,13 +2374,15 @@ pub const Package = extern struct { add: u32 = 0, remove: u32 = 0, update: u32 = 0, - deduped: u32 = 0, pub inline fn sum(this: *Summary, that: Summary) void { this.add += that.add; this.remove += that.remove; this.update += that.update; - this.deduped += that.deduped; + } + + pub inline fn hasDiffs(this: Summary) bool { + return this.add > 0 or this.remove > 0 or this.update > 0; } }; @@ -2422,6 +2424,7 @@ pub const Package = extern struct { summary.remove += 1; continue; } + defer to_i += 1; if (to_deps[to_i].eql(from_dep, to_lockfile.buffers.string_bytes.items, from_lockfile.buffers.string_bytes.items)) { if (id_mapping) |mapping| { @@ -2452,11 +2455,11 @@ pub const Package = extern struct { null, ); - break :brk diff.add == 0 and diff.remove == 0 and diff.update == 0; + break :brk !diff.hasDiffs(); } else false, else => true, }) { - mapping[to_i] = @as(PackageID, @truncate(i)); + mapping[to_i] = @truncate(i); continue; } } else { @@ -2468,15 +2471,7 @@ pub const Package = extern struct { summary.update += 1; } - outer: for (to_deps, 0..) |to_dep, i| { - if (from_deps.len > i and from_deps[i].name_hash == to_dep.name_hash) continue; - - for (from_deps) |from_dep| { - if (from_dep.name_hash == to_dep.name_hash) continue :outer; - } - - summary.add += 1; - } + summary.add = @truncate(to_deps.len - (from_deps.len - summary.remove)); return summary; } @@ -2628,16 +2623,18 @@ pub const Package = extern struct { if (workspace_range) |range| { if (workspace_version) |ver| { if (range.satisfies(ver)) { + dependency_version.literal = path; dependency_version.value.workspace = path; } } } else { + dependency_version.literal = path; dependency_version.value.workspace = path; } } else { { const workspace = dependency_version.value.workspace.slice(buf); - var path = string_builder.append( + const path = string_builder.append( String, if (strings.eqlComptime(workspace, "*")) "*" else Path.relative( FileSystem.instance.top_level_dir, @@ -2651,20 +2648,25 @@ pub const Package = extern struct { ), ), ); - defer dependency_version.value.workspace = path; + if (comptime Environment.allow_assert) { + std.debug.assert(path.len() > 0); + std.debug.assert(!std.fs.path.isAbsolute(path.slice(buf))); + } + dependency_version.literal = path; + dependency_version.value.workspace = path; + var workspace_entry = try lockfile.workspace_paths.getOrPut(allocator, @truncate(name_hash)); if (workspace_entry.found_existing) { const old_path = workspace_entry.value_ptr.*; if (strings.eqlComptime(workspace, "*")) { - path = old_path; return null; } else if (strings.eqlComptime(old_path.slice(buf), "*")) brk: { workspace_entry.value_ptr.* = path; for (package_dependencies[0..dependencies_count]) |*package_dep| { if (String.Builder.stringHash(package_dep.realname().slice(buf)) == name_hash) { if (package_dep.version.tag != .workspace) break :brk; - package_dep.version.value.workspace = path; + package_dep.version = dependency_version; return null; } } diff --git a/test/bun.lockb b/test/bun.lockb Binary files differindex 735613c54..f3d337bb0 100755 --- a/test/bun.lockb +++ b/test/bun.lockb diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index b8f2508c4..cd467c97e 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -251,7 +251,11 @@ it("should handle workspaces", async () => { }), ); - const { stdout, stderr, exited } = spawn({ + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: null, @@ -259,12 +263,12 @@ it("should handle workspaces", async () => { 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([ + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + @org/nominally-scoped@workspace:packages/nominally-scoped", " + Asterisk@workspace:packages/asterisk", " + AsteriskTheSecond@workspace:packages/second-asterisk", @@ -272,7 +276,7 @@ it("should handle workspaces", async () => { "", " 4 packages installed", ]); - expect(await exited).toBe(0); + expect(await exited1).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ ".cache", @@ -290,6 +294,51 @@ it("should handle workspaces", async () => { join("..", "..", "packages", "nominally-scoped"), ); await access(join(package_dir, "bun.lockb")); + + // Perform `bun install` again but with lockfile from before + await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + @org/nominally-scoped@workspace:packages/nominally-scoped", + " + Asterisk@workspace:packages/asterisk", + " + AsteriskTheSecond@workspace:packages/second-asterisk", + " + Bar@workspace:bar", + "", + " 4 packages installed", + ]); + expect(await exited2).toBe(0); + expect(requested).toBe(0); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + "@org", + "Asterisk", + "AsteriskTheSecond", + "Bar", + ]); + expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe(join("..", "bar")); + expect(await readlink(join(package_dir, "node_modules", "Asterisk"))).toBe(join("..", "packages", "asterisk")); + expect(await readlink(join(package_dir, "node_modules", "AsteriskTheSecond"))).toBe( + join("..", "packages", "second-asterisk"), + ); + expect(await readlink(join(package_dir, "node_modules", "@org", "nominally-scoped"))).toBe( + join("..", "..", "packages", "nominally-scoped"), + ); + await access(join(package_dir, "bun.lockb")); }); it("should handle `workspace:` specifier", async () => { @@ -311,7 +360,11 @@ it("should handle `workspace:` specifier", async () => { version: "0.0.2", }), ); - const { stdout, stderr, exited } = spawn({ + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: null, @@ -319,21 +372,50 @@ it("should handle `workspace:` specifier", async () => { 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([ + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + Bar@workspace:path/to/bar", "", " 1 packages installed", ]); - expect(await exited).toBe(0); + expect(await exited1).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("..", "path", "to", "bar")); await access(join(package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + Bar@workspace:path/to/bar", + "", + " 1 packages installed", + ]); + expect(await exited2).toBe(0); + expect(requested).toBe(0); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual(["Bar"]); + expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe(join("..", "path", "to", "bar")); + await access(join(package_dir, "bun.lockb")); }); it("should handle workspaces with packages array", async () => { @@ -4640,7 +4722,11 @@ it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { version: "0.0.1", }); await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: null, @@ -4648,22 +4734,52 @@ it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { 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([ + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + bar@workspace:bar", "", " 1 packages installed", ]); - expect(await exited).toBe(0); + expect(await exited1).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")); expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); await access(join(package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@workspace:bar", + "", + " 1 packages installed", + ]); + expect(await exited2).toBe(0); + expect(requested).toBe(0); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual(["bar"]); + expect(await readlink(join(package_dir, "node_modules", "bar"))).toBe(join("..", "bar")); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(package_dir, "bun.lockb")); }); it("should handle `workspaces:bar` and `workspace:*` gracefully", async () => { |