diff options
Diffstat (limited to '')
-rw-r--r-- | src/install/install.zig | 154 | ||||
-rw-r--r-- | src/install/lockfile.zig | 248 | ||||
-rw-r--r-- | test/cli/install/bun-install.test.ts | 87 | ||||
-rw-r--r-- | test/cli/install/bun-remove.test.ts | 29 |
4 files changed, 368 insertions, 150 deletions
diff --git a/src/install/install.zig b/src/install/install.zig index 6ff76d421..ab3bb3eef 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -574,17 +574,16 @@ const Task = struct { } this.err = err; this.status = Status.fail; + this.data = .{ .package_manifest = .{} }; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }; - this.data = .{ .package_manifest = .{} }; - switch (package_manifest) { .cached => unreachable, .fresh => |manifest| { - this.data = .{ .package_manifest = manifest }; this.status = Status.success; + this.data = .{ .package_manifest = manifest }; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }, @@ -593,6 +592,7 @@ const Task = struct { this.request.package_manifest.name.slice(), }) catch unreachable; this.status = Status.fail; + this.data = .{ .package_manifest = .{} }; this.package_manager.resolve_tasks.writeItem(this.*) catch unreachable; return; }, @@ -6649,6 +6649,63 @@ pub const PackageManager = struct { } } } + + var scripts = this.lockfile.packages.items(.scripts)[package_id]; + if (scripts.hasAny()) { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path_str = Path.joinAbsString( + bun.getFdPath(this.node_modules_folder.dir.fd, &path_buf) catch unreachable, + &[_]string{destination_dir_subpath}, + .posix, + ); + + scripts.enqueue(this.lockfile, buf, path_str); + } else if (!scripts.filled and switch (resolution.tag) { + .folder => Features.folder.scripts, + .npm => Features.npm.scripts, + .git, .github, .gitlab, .local_tarball, .remote_tarball => Features.tarball.scripts, + .symlink => Features.link.scripts, + .workspace => Features.workspace.scripts, + else => false, + }) { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path_str = Path.joinAbsString( + bun.getFdPath(this.node_modules_folder.dir.fd, &path_buf) catch unreachable, + &[_]string{destination_dir_subpath}, + .posix, + ); + + scripts.enqueueFromPackageJSON( + this.manager.log, + this.lockfile, + this.node_modules_folder.dir, + destination_dir_subpath, + path_str, + ) catch |err| { + if (comptime log_level != .silent) { + const fmt = "\n<r><red>error:<r> failed to parse life-cycle scripts for <b>{s}<r>: {s}\n"; + const args = .{ name, @errorName(err) }; + + if (comptime log_level.showProgress()) { + if (Output.enable_ansi_colors) { + this.progress.log(comptime Output.prettyFmt(fmt, true), args); + } else { + this.progress.log(comptime Output.prettyFmt(fmt, false), args); + } + } else { + Output.prettyErrorln(fmt, args); + } + } + + if (this.manager.options.enable.fail_early) { + Global.exit(1); + } + + Output.flush(); + this.summary.fail += 1; + return; + }; + } }, .fail => |cause| { if (cause.isPackageMissingFromCache()) { @@ -7522,6 +7579,22 @@ pub const PackageManager = struct { } } + if (root.scripts.hasAny()) { + root.scripts.enqueue( + manager.lockfile, + manager.lockfile.buffers.string_bytes.items, + strings.withoutTrailingSlash(Fs.FileSystem.instance.top_level_dir), + ); + } + + var install_summary = PackageInstall.Summary{}; + if (manager.options.do.install_packages) { + install_summary = try manager.installPackages( + manager.lockfile, + log_level, + ); + } + // Install script order for npm 8.3.0: // 1. preinstall // 2. install @@ -7529,40 +7602,27 @@ pub const PackageManager = struct { // 4. preprepare // 5. prepare // 6. postprepare - const run_lifecycle_scripts = manager.options.do.run_scripts and manager.lockfile.scripts.hasAny() and manager.options.do.install_packages; - const has_pre_lifecycle_scripts = manager.lockfile.scripts.preinstall.items.len > 0; - const needs_configure_bundler_for_run = run_lifecycle_scripts and !has_pre_lifecycle_scripts; - - if (run_lifecycle_scripts and has_pre_lifecycle_scripts) { + if (run_lifecycle_scripts) { // We need to figure out the PATH and other environment variables // to do that, we re-use the code from bun run // this is expensive, it traverses the entire directory tree going up to the root // so we really only want to do it when strictly necessary - { - var this_bundler: bundler.Bundler = undefined; - var ORIGINAL_PATH: string = ""; - _ = try RunCommand.configureEnvForRun( - ctx, - &this_bundler, - manager.env, - &ORIGINAL_PATH, - log_level != .silent, - false, - ); - } + var this_bundler: bundler.Bundler = undefined; + var ORIGINAL_PATH: string = ""; + _ = try RunCommand.configureEnvForRun( + ctx, + &this_bundler, + manager.env, + &ORIGINAL_PATH, + log_level != .silent, + false, + ); + // 1. preinstall try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "preinstall"); } - var install_summary = PackageInstall.Summary{}; - if (manager.options.do.install_packages) { - install_summary = try manager.installPackages( - manager.lockfile, - log_level, - ); - } - if (needs_new_lockfile) { manager.summary.add = @truncate(u32, manager.lockfile.packages.len); } @@ -7666,44 +7726,6 @@ pub const PackageManager = struct { } if (run_lifecycle_scripts and install_summary.fail == 0) { - // We need to figure out the PATH and other environment variables - // to do that, we re-use the code from bun run - // this is expensive, it traverses the entire directory tree going up to the root - // so we really only want to do it when strictly necessary - if (needs_configure_bundler_for_run) { - var this_bundler: bundler.Bundler = undefined; - var ORIGINAL_PATH: string = ""; - _ = try RunCommand.configureEnvForRun( - ctx, - &this_bundler, - manager.env, - &ORIGINAL_PATH, - log_level != .silent, - false, - ); - } else { - // bun install may have installed new bins, so we need to update the PATH - // this can happen if node_modules/.bin didn't previously exist - // note: it is harmless to have the same directory in the PATH multiple times - const current_path = manager.env.map.get("PATH") orelse ""; - - // TODO: windows - const cwd_without_trailing_slash = if (Fs.FileSystem.instance.top_level_dir.len > 1 and Fs.FileSystem.instance.top_level_dir[Fs.FileSystem.instance.top_level_dir.len - 1] == '/') - Fs.FileSystem.instance.top_level_dir[0 .. Fs.FileSystem.instance.top_level_dir.len - 1] - else - Fs.FileSystem.instance.top_level_dir; - - try manager.env.map.put("PATH", try std.fmt.allocPrint( - ctx.allocator, - "{s}:{s}/node_modules/.bin", - .{ - current_path, - cwd_without_trailing_slash, - }, - )); - } - - // 1. preinstall // 2. install // 3. postinstall try manager.lockfile.scripts.run(manager.allocator, manager.env, log_level != .silent, "install"); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index bef058bf1..f5ad3681b 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -63,22 +63,22 @@ const Dependency = @import("./dependency.zig"); const Behavior = Dependency.Behavior; const FolderResolution = @import("./resolvers/folder_resolver.zig").FolderResolution; const Install = @import("./install.zig"); +const Aligner = Install.Aligner; +const alignment_bytes_to_repeat_buffer = Install.alignment_bytes_to_repeat_buffer; const PackageManager = Install.PackageManager; +const DependencyID = Install.DependencyID; const ExternalSlice = Install.ExternalSlice; const ExternalSliceAligned = Install.ExternalSliceAligned; -const PackageID = Install.PackageID; -const DependencyID = Install.DependencyID; -const Features = Install.Features; -const PackageInstall = Install.PackageInstall; -const PackageNameHash = Install.PackageNameHash; -const Aligner = Install.Aligner; +const ExternalStringList = Install.ExternalStringList; const ExternalStringMap = Install.ExternalStringMap; -const alignment_bytes_to_repeat_buffer = Install.alignment_bytes_to_repeat_buffer; +const Features = Install.Features; const initializeStore = Install.initializeStore; const invalid_package_id = Install.invalid_package_id; -const ExternalStringList = Install.ExternalStringList; -const Resolution = @import("./resolution.zig").Resolution; const Origin = Install.Origin; +const PackageID = Install.PackageID; +const PackageInstall = Install.PackageInstall; +const PackageNameHash = Install.PackageNameHash; +const Resolution = @import("./resolution.zig").Resolution; const Crypto = @import("../sha.zig").Hashers; const PackageJSON = @import("../resolver/package_json.zig").PackageJSON; @@ -124,35 +124,28 @@ pub const Scripts = struct { postprepare: StringArrayList = .{}, pub fn hasAny(this: *Scripts) bool { - return (this.preinstall.items.len + - this.install.items.len + - this.postinstall.items.len + - this.preprepare.items.len + - this.prepare.items.len + - this.postprepare.items.len) > 0; + inline for (Package.Scripts.Hooks) |hook| { + if (@field(this, hook).items.len > 0) return true; + } + return false; } pub fn run(this: *Scripts, allocator: Allocator, env: *DotEnv.Loader, silent: bool, comptime hook: []const u8) !void { for (@field(this, hook).items) |entry| { if (comptime Environment.allow_assert) std.debug.assert(Fs.FileSystem.instance_loaded); - const cwd = Path.joinAbsString( - FileSystem.instance.top_level_dir, - &[_]string{ - entry.cwd, - }, - .posix, - ); - _ = try RunCommand.runPackageScript(allocator, entry.script, hook, cwd, env, &.{}, silent); + _ = try RunCommand.runPackageScript(allocator, entry.script, hook, entry.cwd, env, &.{}, silent); } } pub fn deinit(this: *Scripts, allocator: Allocator) void { - this.preinstall.deinit(allocator); - this.install.deinit(allocator); - this.postinstall.deinit(allocator); - this.preprepare.deinit(allocator); - this.prepare.deinit(allocator); - this.postprepare.deinit(allocator); + inline for (Package.Scripts.Hooks) |hook| { + const list = &@field(this, hook); + for (list.items) |entry| { + allocator.free(entry.cwd); + allocator.free(entry.script); + } + list.deinit(allocator); + } } }; @@ -1737,6 +1730,124 @@ pub const Package = extern struct { meta: Meta = .{}, bin: Bin = .{}, + scripts: Package.Scripts = .{}, + + pub const Scripts = extern struct { + preinstall: String = .{}, + install: String = .{}, + postinstall: String = .{}, + preprepare: String = .{}, + prepare: String = .{}, + postprepare: String = .{}, + filled: bool = false, + + pub const Hooks = .{ + "preinstall", + "install", + "postinstall", + "preprepare", + "prepare", + "postprepare", + }; + + pub fn clone(this: *const Package.Scripts, buf: []const u8, comptime Builder: type, builder: Builder) Package.Scripts { + if (!this.filled) return .{}; + var scripts = Package.Scripts{ + .filled = true, + }; + inline for (Package.Scripts.Hooks) |hook| { + @field(scripts, hook) = builder.append(String, @field(this, hook).slice(buf)); + } + return scripts; + } + + pub fn count(this: *const Package.Scripts, buf: []const u8, comptime Builder: type, builder: Builder) void { + inline for (Package.Scripts.Hooks) |hook| { + builder.count(@field(this, hook).slice(buf)); + } + } + + pub fn hasAny(this: *const Package.Scripts) bool { + inline for (Package.Scripts.Hooks) |hook| { + if (!@field(this, hook).isEmpty()) return true; + } + return false; + } + + pub fn enqueue(this: *const Package.Scripts, lockfile: *Lockfile, buf: []const u8, cwd: string) void { + inline for (Package.Scripts.Hooks) |hook| { + const script = @field(this, hook); + if (!script.isEmpty()) { + @field(lockfile.scripts, hook).append(lockfile.allocator, .{ + .cwd = lockfile.allocator.dupe(u8, cwd) catch unreachable, + .script = lockfile.allocator.dupe(u8, script.slice(buf)) catch unreachable, + }) catch unreachable; + } + } + } + + pub fn parseCount(allocator: Allocator, builder: *Lockfile.StringBuilder, json: Expr) void { + if (json.asProperty("scripts")) |scripts_prop| { + if (scripts_prop.expr.data == .e_object) { + inline for (Package.Scripts.Hooks) |script_name| { + if (scripts_prop.expr.get(script_name)) |script| { + if (script.asString(allocator)) |input| { + builder.count(input); + } + } + } + } + } + } + + pub fn parseAlloc(this: *Package.Scripts, allocator: Allocator, builder: *Lockfile.StringBuilder, json: Expr) void { + if (json.asProperty("scripts")) |scripts_prop| { + if (scripts_prop.expr.data == .e_object) { + inline for (Package.Scripts.Hooks) |script_name| { + if (scripts_prop.expr.get(script_name)) |script| { + if (script.asString(allocator)) |input| { + @field(this, script_name) = builder.append(String, input); + } + } + } + } + } + } + + pub fn enqueueFromPackageJSON( + this: *Package.Scripts, + log: *logger.Log, + lockfile: *Lockfile, + node_modules: std.fs.Dir, + subpath: [:0]const u8, + cwd: string, + ) !void { + var pkg_dir = try bun.openDir(node_modules, subpath); + defer pkg_dir.close(); + const json_file = try pkg_dir.dir.openFileZ("package.json", .{ .mode = .read_only }); + defer json_file.close(); + const json_stat = try json_file.stat(); + const json_buf = try lockfile.allocator.alloc(u8, json_stat.size + 64); + const json_len = try json_file.preadAll(json_buf, 0); + const json_src = logger.Source.initPathString(cwd, json_buf[0..json_len]); + initializeStore(); + const json = try json_parser.ParseJSONUTF8( + &json_src, + log, + lockfile.allocator, + ); + + var tmp: Lockfile = undefined; + try tmp.initEmpty(lockfile.allocator); + defer tmp.deinit(); + var builder = tmp.stringBuilder(); + Lockfile.Package.Scripts.parseCount(lockfile.allocator, &builder, json); + try builder.allocate(); + this.parseAlloc(lockfile.allocator, &builder, json); + + this.enqueue(lockfile, tmp.buffers.string_bytes.items, cwd); + } + }; pub fn verify(this: *const Package, externs: []const ExternalString) void { if (comptime !Environment.allow_assert) @@ -1796,6 +1907,7 @@ pub const Package = extern 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.scripts.count(old_string_buf, *Lockfile.StringBuilder, builder); const new_extern_string_count = this.bin.count(old_string_buf, old_extern_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); @@ -1832,7 +1944,14 @@ pub const Package = extern struct { this.name.slice(old_string_buf), this.name_hash, ), - .bin = this.bin.clone(old_string_buf, old_extern_string_buf, new.buffers.extern_strings.items, new_extern_strings, *Lockfile.StringBuilder, builder), + .bin = this.bin.clone( + old_string_buf, + old_extern_string_buf, + new.buffers.extern_strings.items, + new_extern_strings, + *Lockfile.StringBuilder, + builder, + ), .name_hash = this.name_hash, .meta = this.meta.clone( id, @@ -1845,6 +1964,11 @@ pub const Package = extern struct { *Lockfile.StringBuilder, builder, ), + .scripts = this.scripts.clone( + old_string_buf, + *Lockfile.StringBuilder, + builder, + ), .dependencies = .{ .off = prev_len, .len = end - prev_len }, .resolutions = .{ .off = prev_len, .len = end - prev_len }, }, @@ -2762,18 +2886,11 @@ pub const Package = extern struct { if (json.asProperty("bin")) |bin| { switch (bin.expr.data) { .e_object => |obj| { - switch (obj.properties.len) { - 0 => { - break :bin; - }, - 1 => {}, - else => {}, - } - for (obj.properties.slice()) |bin_prop| { string_builder.count(bin_prop.key.?.asString(allocator) orelse break :bin); string_builder.count(bin_prop.value.?.asString(allocator) orelse break :bin); } + break :bin; }, .e_string => { if (bin.expr.asString(allocator)) |str_| { @@ -2796,33 +2913,7 @@ pub const Package = extern struct { } if (comptime features.scripts) { - if (json.asProperty("scripts")) |scripts_prop| { - if (scripts_prop.expr.data == .e_object) { - const scripts = .{ - "install", - "postinstall", - "postprepare", - "preinstall", - "prepare", - "preprepare", - }; - var cwd: string = ""; - - inline for (scripts) |script_name| { - if (scripts_prop.expr.get(script_name)) |script| { - if (script.asString(allocator)) |input| { - if (cwd.len == 0 and source.path.name.dir.len > 0) { - cwd = try allocator.dupe(u8, source.path.name.dir); - } - try @field(lockfile.scripts, script_name).append(allocator, .{ - .cwd = cwd, - .script = input, - }); - } - } - } - } - } + Package.Scripts.parseCount(allocator, &string_builder, json); } if (comptime ResolverContext != void) { @@ -3098,6 +3189,11 @@ pub const Package = extern struct { } } + if (comptime features.scripts) { + package.scripts.parseAlloc(allocator, &string_builder, json); + } + package.scripts.filled = true; + // It is allowed for duplicate dependencies to exist in optionalDependencies and regular dependencies if (comptime features.check_for_duplicate_dependencies) { lockfile.scratch.duplicate_checker_map.clearRetainingCapacity(); @@ -3328,9 +3424,14 @@ pub const Package = extern struct { } const field_count = try reader.readIntLittle(u64); - - if (field_count != sizes.Types.len) { - return error.@"Lockfile validation failed: unexpected number of package fields"; + switch (field_count) { + sizes.Types.len => {}, + // "scripts" field is absent before v0.6.8 + // we will back-fill from each package.json + sizes.Types.len - 1 => {}, + else => { + return error.@"Lockfile validation failed: unexpected number of package fields"; + }, } const begin_at = try reader.readIntLittle(u64); @@ -3345,8 +3446,15 @@ pub const Package = extern struct { inline for (FieldsEnum.fields) |field| { var bytes = std.mem.sliceAsBytes(sliced.items(@field(Lockfile.Package.List.Field, field.name))); - @memcpy(bytes.ptr, stream.buffer[stream.pos..].ptr, bytes.len); - stream.pos += bytes.len; + const end_pos = stream.pos + bytes.len; + if (end_pos <= end_at) { + @memcpy(bytes.ptr, stream.buffer[stream.pos..].ptr, bytes.len); + stream.pos = end_pos; + } else if (comptime strings.eqlComptime(field.name, "scripts")) { + @memset(bytes.ptr, 0, bytes.len); + } else { + return error.@"Lockfile validation failed: invalid package list range"; + } } return list; diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 8dd3d3cba..f44dc5a7e 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -610,6 +610,93 @@ it("should handle life-cycle scripts within workspaces", async () => { await access(join(package_dir, "bun.lockb")); }); +it("should handle life-cycle scripts during re-installation", async () => { + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + scripts: { + install: [bunExe(), "index.js"].join(" "), + }, + workspaces: ["bar"], + }), + ); + await writeFile(join(package_dir, "index.js"), 'console.log("[scripts:run] Foo");'); + await mkdir(join(package_dir, "bar")); + await writeFile( + join(package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + scripts: { + preinstall: [bunExe(), "index.js"].join(" "), + }, + }), + ); + await writeFile(join(package_dir, "bar", "index.js"), 'console.log("[scripts:run] Bar");'); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + 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([ + "[scripts:run] Bar", + " + Bar@workspace:bar", + "[scripts:run] Foo", + "", + " 1 packages installed", + ]); + 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")); + 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([ + "[scripts:run] Bar", + " + Bar@workspace:bar", + "[scripts:run] Foo", + "", + " 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")); + await access(join(package_dir, "bun.lockb")); +}); + it("should ignore workspaces within workspaces", async () => { await writeFile( join(package_dir, "package.json"), diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index 5a6f612c4..bdf0873e9 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -1,21 +1,10 @@ import { bunExe, bunEnv as env } from "harness"; -import { access, mkdir, mkdtemp, readlink, realpath, rm, writeFile } from "fs/promises"; +import { mkdir, mkdtemp, realpath, rm, writeFile } from "fs/promises"; import { join, relative } from "path"; import { tmpdir } from "os"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; -import { - dummyAfterAll, - dummyAfterEach, - dummyBeforeAll, - dummyBeforeEach, - dummyRegistry, - package_dir, - readdirSorted, - requested, - root_url, - setHandler, -} from "./dummy.registry"; -import { spawn, write } from "bun"; +import { dummyAfterAll, dummyAfterEach, dummyBeforeAll, dummyBeforeEach, package_dir } from "./dummy.registry"; +import { spawn } from "bun"; import { file } from "bun"; beforeAll(dummyBeforeAll); @@ -65,12 +54,18 @@ it("should remove existing package", async () => { const { exited: exited1 } = spawn({ cmd: [bunExe(), "add", `file:${pkg1_path}`], cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", env, }); expect(await exited1).toBe(0); const { exited: exited2 } = spawn({ cmd: [bunExe(), "add", `file:${pkg2_path}`], cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", env, }); expect(await exited2).toBe(0); @@ -182,6 +177,9 @@ it("should reject missing package", async () => { const { exited: addExited } = spawn({ cmd: [bunExe(), "add", `file:${pkg_path}`], cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", env, }); expect(await addExited).toBe(0); @@ -257,6 +255,9 @@ it("should retain a new line in the end of package.json", async () => { const { exited: addExited } = spawn({ cmd: [bunExe(), "add", `file:${pkg_path}`], cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", env, }); expect(await addExited).toBe(0); |