From bf0e5a82f7dcfe582e44b4a3ebe4f52afb099dac Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Mon, 18 Mar 2024 22:34:15 -0700 Subject: [PATCH] fix(windows): install bugfixes for workspaces, tarballs, and relative paths (#8813) * more tests * update migration.zig * fix up paths * update tests * update tests * test * test * update registry tests * comments * early exit if stream is invalid * dont pass invalid_fd to openFileAtWindows * fix merge * misc crash fix * make this use optional pointers instead of 0xaa * ensure absolute paths are propagated properly * package.json expects forward slash * this assert was invalid * add panic checks * pass bun-remove * more panic checks * test: pass bun-add * querying these hangs outside bun too * fix compile error from merge conflict resolution * use compileError instead of comptime unreachable * tidy * bunx: check for the .exe bin extension * bunx: another route to make cache path if it doesnt exist * install: another case of FolderResolution.getOrPut expecting absolute path * fix a bun install crash * dont print zig stack trace if bun install fails * test: pass bun-link * test: bunx: add more expects * test: bun-install-registry: pass * test: bun-install: pass * test: bun-pm: pass * fix merge main error * fix posix tests * fix last failing test in bun-install.test.ts symlink difference between platforms * bun-install-registry.test.ts fix * bun-run.test.ts: remove stray console log --------- Co-authored-by: Meghan Denny Co-authored-by: Meghan Denny Co-authored-by: Jarred Sumner --- src/StandaloneModuleGraph.zig | 2 +- src/bun.js/api/bun/subprocess.zig | 2 +- src/bun.js/module_loader.zig | 4 +- src/bun.js/node/node_os.zig | 4 +- src/bun.js/webcore/streams.zig | 42 +-- src/bun.zig | 4 + src/cli.zig | 2 +- src/cli/bunx_command.zig | 35 +-- src/cli/install_command.zig | 11 +- src/cli/package_manager_command.zig | 6 +- src/fmt.zig | 85 ++++-- src/install/dependency.zig | 30 +- src/install/extract_tarball.zig | 2 +- src/install/install.zig | 64 ++-- src/install/lockfile.zig | 174 ++++++++--- src/install/migration.zig | 19 +- src/install/resolution.zig | 57 ++-- src/install/resolvers/folder_resolver.zig | 17 +- src/install/windows-shim/bun_shim_impl.zig | 6 +- src/io/PipeReader.zig | 1 + src/js_lexer.zig | 13 + src/js_parser.zig | 2 +- src/json_parser.zig | 38 +++ src/report.zig | 2 +- src/resolver/resolve_path.zig | 33 ++- src/string.zig | 2 +- src/which.zig | 6 +- test/cli/install/bun-add.test.ts | 88 +++++- test/cli/install/bun-install.test.ts | 280 ++++++++++-------- test/cli/install/bun-link.test.ts | 5 +- test/cli/install/bun-pm.test.ts | 76 +++-- test/cli/install/bun-remove.test.ts | 22 +- test/cli/install/bun-run.test.ts | 1 - test/cli/install/bunx.test.ts | 30 +- .../registry/bun-install-registry.test.ts | 221 +++++++++++--- 35 files changed, 981 insertions(+), 405 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index aab3c3e48c..9eae666ebe 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -747,7 +747,7 @@ pub const StandaloneModuleGraph = struct { const nt_path = bun.strings.addNTPathPrefix(&nt_path_buf, image_path); return bun.sys.openFileAtWindows( - bun.invalid_fd, + bun.FileDescriptor.cwd(), nt_path, // access_mask w.SYNCHRONIZE | w.GENERIC_READ, diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index e2ee03c306..996588e997 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1867,7 +1867,7 @@ pub const Subprocess = struct { } } - var windows_ipc_env_buf: if (Environment.isWindows) ["BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_00000000-0000-0000-0000-000000000000".len * 2]u8 else void = undefined; + var windows_ipc_env_buf: if (Environment.isWindows) ["BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_00000000-0000-0000-0000-000000000000\x00".len]u8 else void = undefined; if (ipc_mode != .none) { if (comptime is_sync) { globalThis.throwInvalidArguments("IPC is not supported in Bun.spawnSync", .{}); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 71f2f0ef2e..c1507ad3a2 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1205,7 +1205,7 @@ pub const ModuleLoader = struct { const msg_args = .{ result.name, - result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items), + result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), }; const msg: []u8 = try switch (result.err) { @@ -1255,7 +1255,7 @@ pub const ModuleLoader = struct { .{ bun.asByteSlice(@errorName(err)), result.name, - result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items), + result.resolution.fmt(vm.packageManager().lockfile.buffers.string_bytes.items, .any), }, ), }; diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 315433d47b..0da6b9a969 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -553,7 +553,7 @@ pub const Os = struct { } else if (comptime Environment.isMac) { break @as(?*C.sockaddr_dl, @ptrCast(@alignCast(ll_iface.ifa_addr))); } else { - comptime unreachable; + @compileError("unreachable"); } } else null; @@ -561,7 +561,7 @@ pub const Os = struct { // Encode its link-layer address. We need 2*6 bytes for the // hex characters and 5 for the colon separators var mac_buf: [17]u8 = undefined; - const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else comptime unreachable; + const addr_data = if (comptime Environment.isLinux) ll_addr.addr else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] else @compileError("unreachable"); const mac = std.fmt.bufPrint(&mac_buf, "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", .{ addr_data[0], addr_data[1], addr_data[2], addr_data[3], addr_data[4], addr_data[5], diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index f8fc2f6e19..5f4df229f9 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -959,17 +959,15 @@ pub const StreamResult = union(Tag) { }; pub const Signal = struct { - ptr: *anyopaque = dead, - vtable: VTable = VTable.Dead, - - pub const dead = @as(*anyopaque, @ptrFromInt(0xaaaaaaaa)); + ptr: ?*anyopaque = null, + vtable: ?*const VTable = null, pub fn clear(this: *Signal) void { - this.ptr = dead; + this.ptr = null; } pub fn isDead(this: Signal) bool { - return this.ptr == dead; + return this.ptr == null; } pub fn initWithType(comptime Type: type, handler: *Type) Signal { @@ -977,7 +975,7 @@ pub const Signal = struct { @setRuntimeSafety(false); return .{ .ptr = handler, - .vtable = VTable.wrap(Type), + .vtable = comptime &VTable.wrap(Type), }; } @@ -985,45 +983,33 @@ pub const Signal = struct { return initWithType(std.meta.Child(@TypeOf(handler)), handler); } - pub fn close(this: Signal, err: ?Syscall.Error) void { + pub fn close(this: *Signal, err: ?Syscall.Error) void { if (this.isDead()) return; - this.vtable.close(this.ptr, err); + this.vtable.?.close(this.ptr.?, err); } - pub fn ready(this: Signal, amount: ?Blob.SizeType, offset: ?Blob.SizeType) void { + + pub fn ready(this: *Signal, amount: ?Blob.SizeType, offset: ?Blob.SizeType) void { if (this.isDead()) return; - this.vtable.ready(this.ptr, amount, offset); + this.vtable.?.ready(this.ptr.?, amount, offset); } - pub fn start(this: Signal) void { + + pub fn start(this: *Signal) void { if (this.isDead()) return; - this.vtable.start(this.ptr); + this.vtable.?.start(this.ptr.?); } pub const VTable = struct { pub const OnCloseFn = *const (fn (this: *anyopaque, err: ?Syscall.Error) void); pub const OnReadyFn = *const (fn (this: *anyopaque, amount: ?Blob.SizeType, offset: ?Blob.SizeType) void); pub const OnStartFn = *const (fn (this: *anyopaque) void); + close: OnCloseFn, ready: OnReadyFn, start: OnStartFn, - const DeadFns = struct { - pub fn close(_: *anyopaque, _: ?Syscall.Error) void { - unreachable; - } - pub fn ready(_: *anyopaque, _: ?Blob.SizeType, _: ?Blob.SizeType) void { - unreachable; - } - - pub fn start(_: *anyopaque) void { - unreachable; - } - }; - - pub const Dead = VTable{ .close = DeadFns.close, .ready = DeadFns.ready, .start = DeadFns.start }; - pub fn wrap( comptime Wrapped: type, ) VTable { diff --git a/src/bun.zig b/src/bun.zig index e3688646ce..e2b4167a1e 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -132,6 +132,10 @@ pub const FileDescriptor = enum(FileDescriptorInt) { std.debug.assert(FDImpl.decode(fd).kind == kind); } + pub fn cwd() FileDescriptor { + return toFD(std.fs.cwd().fd); + } + pub fn isStdio(fd: FileDescriptor) bool { // fd.assertValid(); const decoded = FDImpl.decode(fd); diff --git a/src/cli.zig b/src/cli.zig index 80dd184eb0..24351fa945 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -44,7 +44,7 @@ const Bunfig = @import("./bunfig.zig").Bunfig; pub const Cli = struct { var wait_group: sync.WaitGroup = undefined; - var log_: logger.Log = undefined; + pub var log_: logger.Log = undefined; pub fn startTransform(_: std.mem.Allocator, _: Api.TransformOptions, _: *logger.Log) anyerror!void {} pub fn start(allocator: std.mem.Allocator, comptime MainPanicHandler: type) void { start_time = std.time.nanoTimestamp(); diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index ad36d2e49b..c054f122f4 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -159,13 +159,7 @@ pub const BunxCommand = struct { fn getBinNameFromProjectDirectory(bundler: *bun.Bundler, dir_fd: bun.FileDescriptor, package_name: []const u8) ![]const u8 { var subpath: [bun.MAX_PATH_BYTES]u8 = undefined; - subpath[0.."node_modules/".len].* = "node_modules/".*; - @memcpy(subpath["node_modules/".len..][0..package_name.len], package_name); - subpath["node_modules/".len + package_name.len] = std.fs.path.sep; - subpath["node_modules/".len + package_name.len + 1 ..][0.."package.json".len].* = "package.json".*; - subpath["node_modules/".len + package_name.len + 1 + "package.json".len] = 0; - - const subpath_z: [:0]const u8 = subpath[0 .. "node_modules/".len + package_name.len + 1 + "package.json".len :0]; + const subpath_z = std.fmt.bufPrintZ(&subpath, "node_modules/{s}/package.json", .{package_name}) catch unreachable; return try getBinNameFromSubpath(bundler, dir_fd, subpath_z); } @@ -429,11 +423,18 @@ pub const BunxCommand = struct { debug("bunx_cache_dir: {s}", .{bunx_cache_dir}); + const bin_extension = switch (Environment.os) { + .windows => ".exe", + .mac => "", + .linux => "", + .wasm => "", + }; + var absolute_in_cache_dir_buf: bun.PathBuffer = undefined; var absolute_in_cache_dir = std.fmt.bufPrint( &absolute_in_cache_dir_buf, - bun.pathLiteral("{s}/node_modules/.bin/{s}"), - .{ bunx_cache_dir, initial_bin_name }, + bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), + .{ bunx_cache_dir, initial_bin_name, bin_extension }, ) catch return error.PathTooLong; const passthrough = passthrough_list.items; @@ -513,7 +514,7 @@ pub const BunxCommand = struct { null, ); // runBinary is noreturn - comptime unreachable; + @compileError("unreachable"); } // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use @@ -522,7 +523,7 @@ pub const BunxCommand = struct { if (getBinName(&this_bundler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| { // if we check the bin name and its actually the same, we don't need to check $PATH here again if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { - absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, package_name_for_bin }) catch unreachable; + absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bin_extension }) catch unreachable; // Only use the system-installed version if there is no version specified if (update_request.version.literal.isEmpty()) { @@ -550,7 +551,7 @@ pub const BunxCommand = struct { null, ); // runBinary is noreturn - comptime unreachable; + @compileError("unreachable"); } } } else |err| { @@ -573,8 +574,8 @@ pub const BunxCommand = struct { var args = std.BoundedArray([]const u8, 7).fromSlice(&.{ try std.fs.selfExePathAlloc(ctx.allocator), "add", - "--no-summary", install_param, + "--no-summary", }) catch unreachable; // upper bound is known @@ -634,7 +635,7 @@ pub const BunxCommand = struct { }, } - absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}"), .{ bunx_cache_dir, initial_bin_name }) catch unreachable; + absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), .{ bunx_cache_dir, initial_bin_name, bin_extension }) catch unreachable; // Similar to "npx": // @@ -656,13 +657,13 @@ pub const BunxCommand = struct { null, ); // runBinary is noreturn - comptime unreachable; + @compileError("unreachable"); } // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use if (getBinNameFromTempDirectory(&this_bundler, bunx_cache_dir, result_package_name)) |package_name_for_bin| { if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) { - absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, package_name_for_bin }) catch unreachable; + absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bin_extension }) catch unreachable; if (bun.which( &path_buf, @@ -680,7 +681,7 @@ pub const BunxCommand = struct { null, ); // runBinary is noreturn - comptime unreachable; + @compileError("unreachable"); } } } else |_| {} diff --git a/src/cli/install_command.zig b/src/cli/install_command.zig index 0ea94e8894..6183d43d86 100644 --- a/src/cli/install_command.zig +++ b/src/cli/install_command.zig @@ -4,6 +4,15 @@ const PackageManager = @import("../install/install.zig").PackageManager; pub const InstallCommand = struct { pub fn exec(ctx: Command.Context) !void { - try PackageManager.install(ctx); + PackageManager.install(ctx) catch |err| switch (err) { + error.InstallFailed, + error.InvalidPackageJSON, + => { + const log = &bun.CLI.Cli.log_; + log.printForLogLevel(bun.Output.errorWriter()) catch {}; + bun.Global.exit(1); + }, + else => |e| return e, + }; } }; diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index ca83c40805..8f28c1659a 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -326,7 +326,7 @@ pub const PackageManagerCommand = struct { const package_id = lockfile.buffers.resolutions.items[dependency_id]; if (package_id >= lockfile.packages.len) continue; const name = dependencies[dependency_id].name.slice(string_bytes); - const resolution = resolutions[package_id].fmt(string_bytes); + const resolution = resolutions[package_id].fmt(string_bytes, .auto); if (index < sorted_dependencies.len - 1) { Output.prettyln("├── {s}@{any}\n", .{ name, resolution }); @@ -421,7 +421,7 @@ fn printNodeModulesFolderStructure( } } } - const directory_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[id].fmt(string_bytes)}); + const directory_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[id].fmt(string_bytes, .auto)}); if (std.mem.indexOf(u8, path, "node_modules")) |j| { Output.prettyln("{s}@{s}", .{ path[0 .. j - 1], directory_version }); } else { @@ -496,7 +496,7 @@ fn printNodeModulesFolderStructure( } var resolution_buf: [512]u8 = undefined; - const package_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[package_id].fmt(string_bytes)}); + const package_version = try std.fmt.bufPrint(&resolution_buf, "{}", .{resolutions[package_id].fmt(string_bytes, .auto)}); Output.prettyln("{s}@{s}", .{ package_name, package_version }); } } diff --git a/src/fmt.zig b/src/fmt.zig index 4f35328075..74bf7a4ec3 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -45,7 +45,7 @@ pub fn formatUTF16Type(comptime Slice: type, slice_: Slice, writer: anytype) !vo } } -pub fn formatUTF16TypeEscapeBackslashes(comptime Slice: type, slice_: Slice, writer: anytype) !void { +pub fn formatUTF16TypeWithPathOptions(comptime Slice: type, slice_: Slice, writer: anytype, opts: PathFormatOptions) !void { var chunk = getSharedBuffer(); // Defensively ensure recursion doesn't cause the buffer to be overwritten in-place @@ -68,13 +68,27 @@ pub fn formatUTF16TypeEscapeBackslashes(comptime Slice: type, slice_: Slice, wri break; const to_write = chunk[0..result.written]; - var ptr = to_write; - while (strings.indexOfChar(ptr, '\\')) |i| { - try writer.writeAll(ptr[0 .. i + 1]); - try writer.writeAll("\\"); - ptr = ptr[i + 1 ..]; + if (!opts.escape_backslashes and opts.path_sep == .any) { + try writer.writeAll(to_write); + } else { + var ptr = to_write; + while (strings.indexOfAny(ptr, "\\/")) |i| { + const sep = switch (opts.path_sep) { + .windows => '\\', + .posix => '/', + .auto => std.fs.path.sep, + .any => ptr[i], + }; + try writer.writeAll(ptr[0..i]); + try writer.writeByte(sep); + if (opts.escape_backslashes and sep == '\\') { + try writer.writeByte(sep); + } + + ptr = ptr[i + 1 ..]; + } + try writer.writeAll(ptr); } - try writer.writeAll(ptr); slice = slice[result.read..]; } } @@ -86,9 +100,10 @@ pub inline fn utf16(slice_: []const u16) FormatUTF16 { pub const FormatUTF16 = struct { buf: []const u16, escape_backslashes: bool = false, + path_fmt_opts: ?PathFormatOptions = null, pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { - if (self.escape_backslashes) { - try formatUTF16TypeEscapeBackslashes([]const u16, self.buf, writer); + if (self.path_fmt_opts) |opts| { + try formatUTF16TypeWithPathOptions([]const u16, self.buf, writer, opts); } else { try formatUTF16Type([]const u16, self.buf, writer); } @@ -97,24 +112,56 @@ pub const FormatUTF16 = struct { pub const FormatUTF8 = struct { buf: []const u8, - escape_backslashes: bool = false, + path_fmt_opts: ?PathFormatOptions = null, pub fn format(self: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { - if (self.escape_backslashes) { + if (self.path_fmt_opts) |opts| { + if (opts.path_sep == .any and opts.escape_backslashes == false) { + try writer.writeAll(self.buf); + return; + } + var ptr = self.buf; - while (strings.indexOfChar(ptr, '\\')) |i| { - try writer.writeAll(ptr[0 .. i + 1]); - try writer.writeAll("\\"); + while (strings.indexOfAny(ptr, "\\/")) |i| { + const sep = switch (opts.path_sep) { + .windows => '\\', + .posix => '/', + .auto => std.fs.path.sep, + .any => ptr[i], + }; + try writer.writeAll(ptr[0..i]); + try writer.writeByte(sep); + if (opts.escape_backslashes and sep == '\\') { + try writer.writeByte(sep); + } ptr = ptr[i + 1 ..]; } + try writer.writeAll(ptr); - } else { - try writer.writeAll(self.buf); + return; } + + try writer.writeAll(self.buf); } }; pub const PathFormatOptions = struct { + // The path separator used when formatting the path. + path_sep: Sep = .any, + + /// Any backslashes are escaped, including backslashes + /// added through `path_sep`. escape_backslashes: bool = false, + + pub const Sep = enum { + /// Keep paths separators as is. + any, + /// Replace all path separators with the current platform path separator. + auto, + /// Replace all path separators with `/`. + posix, + /// Replace all path separators with `\`. + windows, + }; }; pub const FormatOSPath = if (Environment.isWindows) FormatUTF16 else FormatUTF8; @@ -122,7 +169,7 @@ pub const FormatOSPath = if (Environment.isWindows) FormatUTF16 else FormatUTF8; pub fn fmtOSPath(buf: bun.OSPathSlice, options: PathFormatOptions) FormatOSPath { return FormatOSPath{ .buf = buf, - .escape_backslashes = options.escape_backslashes, + .path_fmt_opts = options, }; } @@ -134,13 +181,13 @@ pub fn fmtPath( if (T == u8) { return FormatUTF8{ .buf = path, - .escape_backslashes = options.escape_backslashes, + .path_fmt_opts = options, }; } return FormatUTF16{ .buf = path, - .escape_backslashes = options.escape_backslashes, + .path_fmt_opts = options, }; } diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 63b6892271..06772b6b12 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -26,9 +26,9 @@ const URI = union(Tag) { } if (@as(Tag, lhs) == .local) { - return strings.eql(lhs.local.slice(lhs_buf), rhs.local.slice(rhs_buf)); + return strings.eqlLong(lhs.local.slice(lhs_buf), rhs.local.slice(rhs_buf), true); } else { - return strings.eql(lhs.remote.slice(lhs_buf), rhs.remote.slice(rhs_buf)); + return strings.eqlLong(lhs.remote.slice(lhs_buf), rhs.remote.slice(rhs_buf), true); } } @@ -241,7 +241,7 @@ pub inline fn isGitHubTarballPath(dependency: string) bool { while (parts.next()) |part| { n_parts += 1; if (n_parts == 3) { - return strings.eql(part, "tarball"); + return strings.eqlComptime(part, "tarball"); } } @@ -254,6 +254,11 @@ pub inline fn isTarball(dependency: string) bool { return strings.endsWithComptime(dependency, ".tgz") or strings.endsWithComptime(dependency, ".tar.gz"); } +/// the input is assumed to be either a remote or local tarball +pub inline fn isRemoteTarball(dependency: string) bool { + return strings.hasPrefixComptime(dependency, "https://") or strings.hasPrefixComptime(dependency, "http://"); +} + pub const Version = struct { tag: Tag = .uninitialized, literal: String = .{}, @@ -338,7 +343,7 @@ pub const Version = struct { return switch (lhs.tag) { // if the two versions are identical as strings, it should often be faster to compare that than the actual semver version // semver ranges involve a ton of pointer chasing - .npm => strings.eql(lhs.literal.slice(lhs_buf), rhs.literal.slice(rhs_buf)) or + .npm => strings.eqlLong(lhs.literal.slice(lhs_buf), rhs.literal.slice(rhs_buf), true) or lhs.value.npm.eql(rhs.value.npm, lhs_buf, rhs_buf), .folder, .dist_tag => lhs.literal.eql(rhs.literal, lhs_buf, rhs_buf), .git => lhs.value.git.eql(&rhs.value.git, lhs_buf, rhs_buf), @@ -930,7 +935,7 @@ pub fn parseWithTag( }; }, .tarball => { - if (strings.hasPrefixComptime(dependency, "https://") or strings.hasPrefixComptime(dependency, "http://")) { + if (isRemoteTarball(dependency)) { return .{ .tag = .tarball, .literal = sliced.value(), @@ -962,12 +967,17 @@ pub fn parseWithTag( .folder => { if (strings.indexOfChar(dependency, ':')) |protocol| { if (strings.eqlComptime(dependency[0..protocol], "file")) { - if (dependency.len <= protocol) { - if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "\"file\" dependency missing a path", .{}) catch unreachable; - return null; - } + const folder = brk: { + if (dependency[protocol + 1] == '/') { + if (dependency.len >= protocol + 2 and dependency[protocol + 2] == '/') { + break :brk dependency[protocol + 3 ..]; + } + break :brk dependency[protocol + 2 ..]; + } + break :brk dependency[protocol + 1 ..]; + }; - return .{ .literal = sliced.value(), .value = .{ .folder = sliced.sub(dependency[protocol + 1 ..]).value() }, .tag = .folder }; + return .{ .literal = sliced.value(), .value = .{ .folder = sliced.sub(folder).value() }, .tag = .folder }; } if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Unsupported protocol {s}", .{dependency}) catch unreachable; diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 8e7a26f194..1f9be3a47a 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -309,7 +309,7 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD // e.g. @next // if it's a namespace package, we need to make sure the @name folder exists if (basename.len != name.len and !this.resolution.tag.isGit()) { - cache_dir.makeDir(std.mem.trim(u8, name[0 .. name.len - basename.len], "/")) catch {}; + cache_dir.makePath(std.mem.trim(u8, name[0 .. name.len - basename.len], "/")) catch {}; } // Now that we've extracted the archive, we rename. diff --git a/src/install/install.zig b/src/install/install.zig index 26796cde97..3413702edf 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -3195,7 +3195,7 @@ pub const PackageManager = struct { "incorrect peer dependency \"{}@{}\"", .{ existing_package.name.fmt(this.lockfile.buffers.string_bytes.items), - existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items), + existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto), }, ) catch unreachable; successFn(this, dependency_id, existing_id); @@ -3232,7 +3232,7 @@ pub const PackageManager = struct { "incorrect peer dependency \"{}@{}\"", .{ existing_package.name.fmt(this.lockfile.buffers.string_bytes.items), - existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items), + existing_package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto), }, ) catch unreachable; successFn(this, dependency_id, list.items[0]); @@ -3304,7 +3304,12 @@ pub const PackageManager = struct { .folder => { // relative to cwd - const res = FolderResolution.getOrPut(.{ .relative = .folder }, version, this.lockfile.str(&version.value.folder), this); + const folder_path = this.lockfile.str(&version.value.folder); + const folder_path_abs = if (std.fs.path.isAbsolute(folder_path)) folder_path else blk: { + var buf2: bun.PathBuffer = undefined; + break :blk Path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &buf2, &[_]string{folder_path}, .auto); + }; + const res = FolderResolution.getOrPut(.{ .relative = .folder }, version, folder_path_abs, this); switch (res) { .err => |err| return err, @@ -3321,9 +3326,14 @@ pub const PackageManager = struct { }, .workspace => { // package name hash should be used to find workspace path from map - const workspace_path: *const String = this.lockfile.workspace_paths.getPtr(@truncate(name_hash)) orelse &version.value.workspace; + const workspace_path_raw: *const String = this.lockfile.workspace_paths.getPtr(@truncate(name_hash)) orelse &version.value.workspace; + const workspace_path = this.lockfile.str(workspace_path_raw); + const workspace_path_u8 = if (std.fs.path.isAbsolute(workspace_path)) workspace_path else blk: { + var buf2: bun.PathBuffer = undefined; + break :blk Path.joinAbsStringBuf(FileSystem.instance.top_level_dir, &buf2, &[_]string{workspace_path}, .auto); + }; - const res = FolderResolution.getOrPut(.{ .relative = .workspace }, version, this.lockfile.str(workspace_path), this); + const res = FolderResolution.getOrPut(.{ .relative = .workspace }, version, workspace_path_u8, this); switch (res) { .err => |err| return err, @@ -3763,7 +3773,7 @@ pub const PackageManager = struct { this.lockfile.str(&result.package.name), label, this.lockfile.str(&result.package.name), - result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items), + result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto), }); } // Resolve dependencies first @@ -4076,7 +4086,7 @@ pub const PackageManager = struct { this.lockfile.str(&result.package.name), label, this.lockfile.str(&result.package.name), - result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items), + result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items, .auto), }); } // We shouldn't see any dependencies @@ -4333,10 +4343,13 @@ pub const PackageManager = struct { false, ) catch |err| { const note = .{ - .fmt = "error occured while resolving {s}", - .args = .{ - lockfile.str(&dependency.realname()), - }, + .fmt = "error occured while resolving {}", + .args = .{bun.fmt.fmtPath(u8, lockfile.str(&dependency.realname()), .{ + .path_sep = switch (dependency.version.tag) { + .folder => .auto, + else => .any, + }, + })}, }; if (dependency.behavior.isOptional() or dependency.behavior.isPeer()) @@ -4891,7 +4904,7 @@ pub const PackageManager = struct { .{ bun.span(@errorName(err)), extract.name.slice(), - extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items), + extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items, .auto), }, ) catch unreachable; } @@ -4910,7 +4923,7 @@ pub const PackageManager = struct { const args = .{ bun.span(@errorName(err)), extract.name.slice(), - extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items), + extract.resolution.fmt(manager.lockfile.buffers.string_bytes.items, .auto), }; if (comptime log_level.showProgress()) { Output.prettyWithPrinterFn(fmt, args, Progress.log, &manager.progress); @@ -6460,9 +6473,14 @@ pub const PackageManager = struct { } var fs = try Fs.FileSystem.init(null); - const original_cwd = strings.withoutTrailingSlash(fs.top_level_dir); + const top_level_dir_no_trailing_slash = strings.withoutTrailingSlash(fs.top_level_dir); + if (comptime Environment.isWindows) { + _ = Path.pathToPosixBuf(u8, strings.withoutTrailingSlash(fs.top_level_dir), &cwd_buf); + } else { + @memcpy(cwd_buf[0..top_level_dir_no_trailing_slash.len], top_level_dir_no_trailing_slash); + } - bun.copy(u8, &cwd_buf, original_cwd); + const original_cwd: string = cwd_buf[0..top_level_dir_no_trailing_slash.len]; var workspace_names = Package.WorkspaceMap.init(ctx.allocator); @@ -7639,7 +7657,15 @@ pub const PackageManager = struct { // add // remove outer: for (positionals) |positional| { - var input = std.mem.trim(u8, positional, " \n\r\t"); + var input: []u8 = @constCast(std.mem.trim(u8, positional, " \n\r\t")); + { + var temp: [2048]u8 = undefined; + const len = std.mem.replace(u8, input, "\\\\", "/", &temp); + bun.path.platformToPosixInPlace(u8, &temp); + const input2 = temp[0 .. input.len - len]; + @memcpy(input[0..input2.len], input2); + input.len = input2.len; + } switch (op) { .link, .unlink => if (!strings.hasPrefixComptime(input, "link:")) { input = std.fmt.allocPrint(allocator, "{0s}@link:{0s}", .{input}) catch unreachable; @@ -8469,7 +8495,7 @@ pub const PackageManager = struct { var resolution_buf: [512]u8 = undefined; const extern_string_buf = this.lockfile.buffers.extern_strings.items; - const resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf)}) catch unreachable; + const resolution_label = std.fmt.bufPrint(&resolution_buf, "{}", .{resolution.fmt(buf, .posix)}) catch unreachable; var installer = PackageInstall{ .progress = this.progress, .cache_dir = undefined, @@ -8686,7 +8712,7 @@ pub const PackageManager = struct { Output.prettyError("Blocked {d} scripts for: {s}@{}\n", .{ count, alias, - resolution.fmt(this.lockfile.buffers.string_bytes.items), + resolution.fmt(this.lockfile.buffers.string_bytes.items, .posix), }); } @@ -8747,7 +8773,7 @@ pub const PackageManager = struct { // Very old versions of Bun didn't store the tarball url when it didn't seem necessary // This caused bugs. We can't assert on it because they could come from old lockfiles if (resolution.value.npm.url.isEmpty()) { - Output.debugWarn("package {s}@{} missing tarball_url", .{ name, resolution.fmt(buf) }); + Output.debugWarn("package {s}@{} missing tarball_url", .{ name, resolution.fmt(buf, .posix) }); } } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 95d6a3559b..9b161c8ce8 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -430,7 +430,7 @@ pub const Tree = struct { } pub fn packageVersion(this: *Builder, id: PackageID) Resolution.Formatter { - return this.old_lockfile.packages.items(.resolution)[id].fmt(this.old_lockfile.buffers.string_bytes.items); + return this.old_lockfile.packages.items(.resolution)[id].fmt(this.old_lockfile.buffers.string_bytes.items, .auto); } pub const Entry = struct { @@ -1193,7 +1193,7 @@ pub const Printer = struct { fmt, .{ package_name, - resolved[package_id].fmt(string_buf), + resolved[package_id].fmt(string_buf, .posix), later_version_fmt, }, ); @@ -1210,7 +1210,7 @@ pub const Printer = struct { fmt, .{ package_name, - resolved[package_id].fmt(string_buf), + resolved[package_id].fmt(string_buf, .posix), }, ); } @@ -1235,10 +1235,10 @@ pub const Printer = struct { } try writer.print( - comptime Output.prettyFmt(" {s}@{}\n", enable_ansi_colors), + comptime Output.prettyFmt(" {s}@{}", enable_ansi_colors), .{ package_name, - resolved[package_id].fmt(string_buf), + resolved[package_id].fmt(string_buf, .auto), }, ); } @@ -1268,7 +1268,7 @@ pub const Printer = struct { fmt, .{ package_name, - resolved[package_id].fmt(string_buf), + resolved[package_id].fmt(string_buf, .posix), }, ); }, @@ -1286,7 +1286,7 @@ pub const Printer = struct { fmt, .{ package_name, - resolved[package_id].fmt(string_buf), + resolved[package_id].fmt(string_buf, .posix), }, ); @@ -1304,8 +1304,6 @@ pub const Printer = struct { // updates.len > 0 is a simpler check than to keep track of a boolean // this assert ensures it is accurate. - if (bun.Environment.allow_assert) - std.debug.assert(had_printed_new_install == (this.updates.len > 0)); if (this.updates.len > 0) { try writer.writeAll("\n"); @@ -1418,7 +1416,7 @@ pub const Printer = struct { const resolution = resolved[i]; const meta = metas[i]; const dependencies: []const Dependency = dependency_lists[i].get(dependencies_buffer); - const version_formatter = resolution.fmt(string_buf); + const version_formatter = resolution.fmt(string_buf, .posix); // This prints: // "@babel/core@7.9.0": @@ -1569,7 +1567,7 @@ pub fn verifyResolutions(this: *Lockfile, local_features: Features, remote_featu remote_features, )) continue; if (log_level != .silent) { - if (failed_dep.name.isEmpty() or strings.eql(failed_dep.name.slice(string_buf), failed_dep.version.literal.slice(string_buf))) { + if (failed_dep.name.isEmpty() or strings.eqlLong(failed_dep.name.slice(string_buf), failed_dep.version.literal.slice(string_buf), true)) { Output.prettyErrorln( "error: {} failed to resolve\n", .{ @@ -2369,12 +2367,12 @@ pub const Package = extern struct { Output.pretty(".{s}{s} @{}\n", .{ std.fs.path.sep_str, strings.withoutTrailingSlash(this.cwd[i + 1 ..]), - resolution.fmt(resolution_buf), + resolution.fmt(resolution_buf, .posix), }); } else { Output.pretty("{s} @{}\n", .{ strings.withoutTrailingSlash(this.cwd), - resolution.fmt(resolution_buf), + resolution.fmt(resolution_buf, .posix), }); } @@ -2770,7 +2768,12 @@ pub const Package = extern struct { const old_extern_string_buf = old.buffers.extern_strings.items; var builder_ = new.stringBuilder(); var builder = &builder_; - debug("Clone: {s}@{any} ({s}, {d} dependencies)", .{ this.name.slice(old_string_buf), this.resolution.fmt(old_string_buf), @tagName(this.resolution.tag), this.dependencies.len }); + debug("Clone: {s}@{any} ({s}, {d} dependencies)", .{ + this.name.slice(old_string_buf), + this.resolution.fmt(old_string_buf, .auto), + @tagName(this.resolution.tag), + this.dependencies.len, + }); builder.count(this.name.slice(old_string_buf)); this.resolution.count(old_string_buf, *Lockfile.StringBuilder, builder); @@ -3488,7 +3491,7 @@ pub const Package = extern struct { comptime features: Features, ) !void { initializeStore(); - const json = json_parser.ParseJSONUTF8(&source, log, allocator) catch |err| { + const json = json_parser.ParseJSONUTF8AlwaysDecode(&source, log, allocator) catch |err| { switch (Output.enable_ansi_colors) { inline else => |enable_ansi_colors| { log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), enable_ansi_colors) catch {}; @@ -3528,10 +3531,24 @@ pub const Package = extern struct { key_loc: logger.Loc, value_loc: logger.Loc, ) !?Dependency { - const external_version = string_builder.append(String, version); + var external_version = string_builder.append(String, version); const buf = lockfile.buffers.string_bytes.items; const sliced = external_version.sliced(buf); + if (comptime Environment.isWindows) { + switch (Dependency.Version.Tag.infer(sliced.slice)) { + .workspace, .folder, .symlink, .tarball => { + if (external_version.isInline()) { + bun.path.pathToPosixInPlace(u8, &external_version.bytes); + } else { + const ptr = external_version.ptr(); + bun.path.pathToPosixInPlace(u8, buf[ptr.off..][0..ptr.len]); + } + }, + else => {}, + } + } + var dependency_version = Dependency.parseWithOptionalTag( allocator, external_alias.value, @@ -3629,8 +3646,8 @@ pub const Package = extern struct { } else { const workspace = dependency_version.value.workspace.slice(buf); const path = string_builder.append(String, if (strings.eqlComptime(workspace, "*")) "*" else brk: { - var buf2: [bun.MAX_PATH_BYTES]u8 = undefined; - break :brk Path.relative( + var buf2: bun.PathBuffer = undefined; + break :brk Path.relativePlatform( FileSystem.instance.top_level_dir, Path.joinAbsStringBuf( FileSystem.instance.top_level_dir, @@ -3641,6 +3658,8 @@ pub const Package = extern struct { }, .auto, ), + .posix, + false, ); }); if (comptime Environment.allow_assert) { @@ -3761,6 +3780,9 @@ pub const Package = extern struct { } pub fn insert(self: *WorkspaceMap, key: string, value: Entry) !void { + if (comptime Environment.allow_assert) { + std.debug.assert(!strings.containsChar(key, std.fs.path.sep_windows)); + } const entry = try self.map.getOrPut(key); if (!entry.found_existing) { entry.key_ptr.* = try self.map.allocator.dupe(u8, key); @@ -3793,7 +3815,6 @@ pub const Package = extern struct { }; const WorkspaceEntry = struct { - path: []const u8 = "", name: []const u8 = "", name_loc: logger.Loc = logger.Loc.Empty, version: ?[]const u8 = null, @@ -3810,7 +3831,7 @@ pub const Package = extern struct { ) !WorkspaceEntry { const path_to_use = if (path.len == 0) "package.json" else brk: { const paths = [_]string{ path, "package.json" }; - break :brk bun.path.joinStringBuf(path_buf, &paths, .auto); + break :brk bun.path.joinStringBuf(path_buf, &paths, .posix); }; // TODO: windows @@ -3835,7 +3856,6 @@ pub const Package = extern struct { var entry = WorkspaceEntry{ .name = name_to_copy[0..workspace_json.found_name.len], .name_loc = workspace_json.name_loc, - .path = path_to_use, }; debug("processWorkspaceName({s}) = {s}", .{ path_to_use, entry.name }); if (workspace_json.has_found_version) { @@ -3905,6 +3925,9 @@ pub const Package = extern struct { continue; } else if (string_builder == null) { input_path = Path.joinAbsStringBuf(source.path.name.dir, filepath_buf, &[_]string{input_path}, .auto); + if (comptime Environment.isWindows) { + input_path = Path.normalizeString(input_path, true, .posix); + } } const workspace_entry = processWorkspaceName( @@ -3967,11 +3990,6 @@ pub const Package = extern struct { } if (asterisked_workspace_paths.items.len > 0) { - // max path bytes is not enough in real codebases - const second_buf = allocator.create([4096]u8) catch unreachable; - var second_buf_fixed = std.heap.FixedBufferAllocator.init(second_buf); - defer allocator.destroy(second_buf); - for (asterisked_workspace_paths.items) |user_path| { var dir_prefix = if (string_builder) |_| strings.withoutLeadingSlash(user_path) @@ -4085,17 +4103,23 @@ pub const Package = extern struct { if (workspace_entry.name.len == 0) continue; const workspace_path: string = if (string_builder) |builder| brk: { - second_buf_fixed.reset(); - const relative = std.fs.path.relative( - second_buf_fixed.allocator(), + builder.count(workspace_entry.name); + const relative = Path.relativePlatform( Fs.FileSystem.instance.top_level_dir, bun.span(entry_path), - ) catch unreachable; - builder.count(workspace_entry.name); + .posix, + true, + ); builder.count(relative); builder.cap += bun.MAX_PATH_BYTES; break :brk relative; - } else bun.span(entry_path); + } else brk: { + // entry_path is contained in filepath_buf so it is safe to constCast and + // replace path separators + const entry_slice = bun.span(entry_path); + Path.pathToPosixInPlace(u8, @constCast(entry_slice)); + break :brk entry_slice; + }; try workspace_names.insert(workspace_path, .{ .name = workspace_entry.name, @@ -4546,7 +4570,7 @@ pub const Package = extern struct { const num_notes = count: { var i: usize = 0; for (workspace_names.values()) |value| { - if (strings.eql(value.name, entry.name)) + if (strings.eqlLong(value.name, entry.name, true)) i += 1; } break :count i; @@ -4556,7 +4580,7 @@ pub const Package = extern struct { var i: usize = 0; for (workspace_names.values(), workspace_names.keys()) |value, note_path| { if (note_path.ptr == path.ptr) continue; - if (strings.eql(value.name, entry.name)) { + if (strings.eqlLong(value.name, entry.name, true)) { const note_abs_path = allocator.dupeZ(u8, Path.joinAbsStringZ(cwd, &.{ note_path, "package.json" }, .auto)) catch bun.outOfMemory(); const note_src = src: { @@ -5124,16 +5148,58 @@ const Buffers = struct { } } - pub fn save(this: Buffers, allocator: Allocator, comptime StreamType: type, stream: StreamType, comptime Writer: type, writer: Writer) !void { + pub fn save( + lockfile: *Lockfile, + allocator: Allocator, + comptime StreamType: type, + stream: StreamType, + comptime Writer: type, + writer: Writer, + ) !void { + const buffers = lockfile.buffers; inline for (sizes.names) |name| { if (PackageManager.instance.options.log_level.isVerbose()) { - Output.prettyErrorln("Saving {d} {s}", .{ @field(this, name).items.len, name }); + Output.prettyErrorln("Saving {d} {s}", .{ @field(buffers, name).items.len, name }); } // Dependencies have to be converted to .toExternal first // We store pointers in Version.Value, so we can't just write it directly if (comptime strings.eqlComptime(name, "dependencies")) { - const remaining = this.dependencies.items; + const remaining = buffers.dependencies.items; + + if (comptime Environment.allow_assert) { + for (remaining) |dep| { + switch (dep.version.tag) { + .folder => { + const folder = lockfile.str(&dep.version.value.folder); + if (strings.containsChar(folder, std.fs.path.sep_windows)) { + std.debug.panic("workspace windows separator: {s}\n", .{folder}); + } + }, + .tarball => { + if (dep.version.value.tarball.uri == .local) { + const tarball = lockfile.str(&dep.version.value.tarball.uri.local); + if (strings.containsChar(tarball, std.fs.path.sep_windows)) { + std.debug.panic("tarball windows separator: {s}", .{tarball}); + } + } + }, + .workspace => { + const workspace = lockfile.str(&dep.version.value.workspace); + if (strings.containsChar(workspace, std.fs.path.sep_windows)) { + std.debug.panic("workspace windows separator: {s}\n", .{workspace}); + } + }, + .symlink => { + const symlink = lockfile.str(&dep.version.value.symlink); + if (strings.containsChar(symlink, std.fs.path.sep_windows)) { + std.debug.panic("symlink windows separator: {s}\n", .{symlink}); + } + }, + else => {}, + } + } + } // It would be faster to buffer these instead of one big allocation var to_clone = try std.ArrayListUnmanaged(Dependency.External).initCapacity(allocator, remaining.len); @@ -5145,7 +5211,7 @@ const Buffers = struct { try writeArray(StreamType, stream, Writer, writer, []Dependency.External, to_clone.items); } else { - const list = @field(this, name); + const list = @field(buffers, name); const items = list.items; const Type = @TypeOf(items); if (comptime Type == Tree) { @@ -5318,8 +5384,28 @@ pub const Serializer = struct { }; const stream = StreamType{ .bytes = bytes }; + if (comptime Environment.allow_assert) { + for (this.packages.items(.resolution)) |res| { + switch (res.tag) { + .folder => { + std.debug.assert(!strings.containsChar(this.str(&res.value.folder), std.fs.path.sep_windows)); + }, + .symlink => { + std.debug.assert(!strings.containsChar(this.str(&res.value.symlink), std.fs.path.sep_windows)); + }, + .local_tarball => { + std.debug.assert(!strings.containsChar(this.str(&res.value.local_tarball), std.fs.path.sep_windows)); + }, + .workspace => { + std.debug.assert(!strings.containsChar(this.str(&res.value.workspace), std.fs.path.sep_windows)); + }, + else => {}, + } + } + } + try Lockfile.Package.Serializer.save(this.packages, StreamType, stream, @TypeOf(writer), writer); - try Lockfile.Buffers.save(this.buffers, z_allocator, StreamType, stream, @TypeOf(writer), writer); + try Lockfile.Buffers.save(this, z_allocator, StreamType, stream, @TypeOf(writer), writer); try writer.writeInt(u64, 0, .little); // < Bun v1.0.4 stopped right here when reading the lockfile @@ -5654,13 +5740,15 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag comptime var j: usize = 0; inline while (j < 16) : (j += 1) { alphabetized_names[(i + j) - 1] = @as(PackageID, @truncate((i + j))); - string_builder.fmtCount("{s}@{}\n", .{ names[i + j].slice(bytes), resolutions[i + j].fmt(bytes) }); + // posix path separators because we only use posix in the lockfile + string_builder.fmtCount("{s}@{}\n", .{ names[i + j].slice(bytes), resolutions[i + j].fmt(bytes, .posix) }); } } while (i < packages_len) : (i += 1) { alphabetized_names[i - 1] = @as(PackageID, @truncate(i)); - string_builder.fmtCount("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes) }); + // posix path separators because we only use posix in the lockfile + string_builder.fmtCount("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes, .posix) }); } } @@ -5699,7 +5787,7 @@ pub fn generateMetaHash(this: *Lockfile, print_name_version_string: bool, packag string_builder.len += hash_prefix.len; for (alphabetized_names) |i| { - _ = string_builder.fmt("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes) }); + _ = string_builder.fmt("{s}@{}\n", .{ names[i].slice(bytes), resolutions[i].fmt(bytes, .any) }); } if (has_scripts) { @@ -6003,7 +6091,7 @@ pub fn jsonStringify(this: *const Lockfile, w: anytype) !void { if (pkg.resolution.tag == .uninitialized) { try w.write(null); } else { - const b = try std.fmt.bufPrint(&buf, "{s} {s}", .{ @tagName(pkg.resolution.tag), pkg.resolution.fmt(sb) }); + const b = try std.fmt.bufPrint(&buf, "{s} {s}", .{ @tagName(pkg.resolution.tag), pkg.resolution.fmt(sb, .auto) }); try w.write(b); } diff --git a/src/install/migration.zig b/src/install/migration.zig index 811dd180c0..fa0ad44e02 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -177,6 +177,9 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo else => return error.InvalidNPMLockfile, }; + // due to package paths and resolved properties for links and workspaces always having + // forward slashes, we depend on `processWorkspaceNamesArray` to always return workspace + // paths with forward slashes on windows const workspace_packages_count = try Lockfile.Package.processWorkspaceNamesArray( &workspaces, allocator, @@ -387,20 +390,14 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo try this.workspace_paths.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); try this.workspace_versions.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); - var path_buf: if (Environment.isWindows) bun.PathBuffer else void = undefined; for (wksp.map.keys(), wksp.map.values()) |k, v| { const name_hash = stringHash(v.name); - this.workspace_paths.putAssumeCapacity( - name_hash, - builder.append( - String, - if (comptime Environment.isWindows) - bun.path.normalizeBuf(k, &path_buf, .windows) - else - k, - ), - ); + if (comptime Environment.allow_assert) { + std.debug.assert(!strings.containsChar(k, '\\')); + } + + this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k)); if (v.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); diff --git a/src/install/resolution.zig b/src/install/resolution.zig index bc8f14eed4..f50cf2c6f0 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -109,8 +109,12 @@ pub const Resolution = extern struct { }; } - pub fn fmt(this: *const Resolution, string_bytes: []const u8) Formatter { - return Formatter{ .resolution = this, .buf = string_bytes }; + pub fn fmt(this: *const Resolution, string_bytes: []const u8, path_sep: bun.fmt.PathFormatOptions.Sep) Formatter { + return Formatter{ + .resolution = this, + .buf = string_bytes, + .path_sep = path_sep, + }; } pub fn fmtURL(this: *const Resolution, options: *const PackageManager.Options, string_bytes: []const u8) URLFormatter { @@ -188,17 +192,19 @@ pub const Resolution = extern struct { buf: []const u8, pub fn format(formatter: URLFormatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + const buf = formatter.buf; + const value = formatter.resolution.value; switch (formatter.resolution.tag) { - .npm => try writer.writeAll(formatter.resolution.value.npm.url.slice(formatter.buf)), - .local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.slice(formatter.buf)), - .folder => try writer.writeAll(formatter.resolution.value.folder.slice(formatter.buf)), - .remote_tarball => try writer.writeAll(formatter.resolution.value.remote_tarball.slice(formatter.buf)), - .git => try formatter.resolution.value.git.formatAs("git+", formatter.buf, layout, opts, writer), - .github => try formatter.resolution.value.github.formatAs("github:", formatter.buf, layout, opts, writer), - .gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab:", formatter.buf, layout, opts, writer), - .workspace => try std.fmt.format(writer, "workspace:{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}), - .symlink => try std.fmt.format(writer, "link:{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}), - .single_file_module => try std.fmt.format(writer, "module:{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}), + .npm => try writer.writeAll(value.npm.url.slice(formatter.buf)), + .local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = .posix }).format("", {}, writer), + .folder => try writer.writeAll(value.folder.slice(formatter.buf)), + .remote_tarball => try writer.writeAll(value.remote_tarball.slice(formatter.buf)), + .git => try value.git.formatAs("git+", formatter.buf, layout, opts, writer), + .github => try value.github.formatAs("github:", formatter.buf, layout, opts, writer), + .gitlab => try value.gitlab.formatAs("gitlab:", formatter.buf, layout, opts, writer), + .workspace => try std.fmt.format(writer, "workspace:{s}", .{value.workspace.slice(formatter.buf)}), + .symlink => try std.fmt.format(writer, "link:{s}", .{value.symlink.slice(formatter.buf)}), + .single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(formatter.buf)}), else => {}, } } @@ -207,19 +213,26 @@ pub const Resolution = extern struct { pub const Formatter = struct { resolution: *const Resolution, buf: []const u8, + path_sep: bun.fmt.PathFormatOptions.Sep, pub fn format(formatter: Formatter, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + const buf = formatter.buf; + const value = formatter.resolution.value; switch (formatter.resolution.tag) { - .npm => try formatter.resolution.value.npm.version.fmt(formatter.buf).format(layout, opts, writer), - .local_tarball => try writer.writeAll(formatter.resolution.value.local_tarball.slice(formatter.buf)), - .folder => try writer.writeAll(formatter.resolution.value.folder.slice(formatter.buf)), - .remote_tarball => try writer.writeAll(formatter.resolution.value.remote_tarball.slice(formatter.buf)), - .git => try formatter.resolution.value.git.formatAs("git+", formatter.buf, layout, opts, writer), - .github => try formatter.resolution.value.github.formatAs("github:", formatter.buf, layout, opts, writer), - .gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab:", formatter.buf, layout, opts, writer), - .workspace => try std.fmt.format(writer, "workspace:{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}), - .symlink => try std.fmt.format(writer, "link:{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}), - .single_file_module => try std.fmt.format(writer, "module:{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}), + .npm => try value.npm.version.fmt(buf).format(layout, opts, writer), + .local_tarball => try bun.fmt.fmtPath(u8, value.local_tarball.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer), + .folder => try bun.fmt.fmtPath(u8, value.folder.slice(buf), .{ .path_sep = formatter.path_sep }).format("", {}, writer), + .remote_tarball => try writer.writeAll(value.remote_tarball.slice(buf)), + .git => try value.git.formatAs("git+", buf, layout, opts, writer), + .github => try value.github.formatAs("github:", buf, layout, opts, writer), + .gitlab => try value.gitlab.formatAs("gitlab:", buf, layout, opts, writer), + .workspace => try std.fmt.format(writer, "workspace:{s}", .{bun.fmt.fmtPath(u8, value.workspace.slice(buf), .{ + .path_sep = formatter.path_sep, + })}), + .symlink => try std.fmt.format(writer, "link:{s}", .{bun.fmt.fmtPath(u8, value.symlink.slice(buf), .{ + .path_sep = formatter.path_sep, + })}), + .single_file_module => try std.fmt.format(writer, "module:{s}", .{value.single_file_module.slice(buf)}), else => {}, } } diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index 9dccaf6280..1026116b03 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -105,12 +105,14 @@ pub const FolderResolution = union(Tag) { abs: stringZ, rel: string, }; - fn normalizePackageJSONPath(global_or_relative: GlobalOrRelative, joined: *[bun.MAX_PATH_BYTES]u8, non_normalized_path: string) Paths { + fn normalizePackageJSONPath(global_or_relative: GlobalOrRelative, joined: *bun.PathBuffer, non_normalized_path: string) Paths { var abs: string = ""; var rel: string = ""; // We consider it valid if there is a package.json in the folder const normalized = if (non_normalized_path.len == 1 and non_normalized_path[0] == '.') non_normalized_path + else if (std.fs.path.isAbsolute(non_normalized_path)) + std.mem.trimRight(u8, non_normalized_path, std.fs.path.sep_str) else std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str); @@ -140,14 +142,14 @@ pub const FolderResolution = union(Tag) { } } }, - else => {}, + .relative => {}, } bun.copy(u8, remain, normalized); remain[normalized.len..][0.."/package.json".len].* = (std.fs.path.sep_str ++ "package.json").*; remain = remain[normalized.len + "/package.json".len ..]; abs = joined[0 .. joined.len - remain.len]; // We store the folder name without package.json - rel = abs[0 .. abs.len - "/package.json".len]; + rel = FileSystem.instance.relative(FileSystem.instance.top_level_dir, abs[0 .. abs.len - "/package.json".len]); } joined[abs.len] = 0; @@ -222,7 +224,14 @@ pub const FolderResolution = union(Tag) { const abs = paths.abs; const rel = paths.rel; - const entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable; + // replace before getting hash. rel may or may not be contained in abs + if (comptime bun.Environment.isWindows) { + bun.path.pathToPosixInPlace(u8, @constCast(abs)); + bun.path.pathToPosixInPlace(u8, @constCast(rel)); + } + const abs_hash = hash(abs); + + const entry = manager.folders.getOrPut(manager.allocator, abs_hash) catch unreachable; if (entry.found_existing) return entry.value_ptr.*; const package: Lockfile.Package = switch (global_or_relative) { diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index 1bacf8fd2a..b5699703c4 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -519,7 +519,7 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { ptr -= 1; std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); } - comptime unreachable; + @compileError("unreachable"); }; std.debug.assert(read_ptr[0] != '\\'); std.debug.assert((read_ptr - 1)[0] == '\\'); @@ -854,9 +854,9 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { _ = nt.NtClose(process.hThread); nt.RtlExitUserProcess(exit_code); - comptime unreachable; + @compileError("unreachable"); } - comptime unreachable; + @compileError("unreachable"); } pub const FromBunRunContext = struct { diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index 142ec241b6..7952e00a0f 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -1038,6 +1038,7 @@ pub const WindowsBufferedReader = struct { pub fn startWithCurrentPipe(this: *WindowsOutputReader) bun.JSC.Maybe(void) { std.debug.assert(this.source != null); + this.source.?.setData(this); this.buffer().clearRetainingCapacity(); this.flags.is_done = false; this.unpause(); diff --git a/src/js_lexer.zig b/src/js_lexer.zig index a7ac8f8bdc..69dafc97cb 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -73,6 +73,8 @@ pub const JSONOptions = struct { /// mark as originally for a macro to enable inlining was_originally_macro: bool = false, + + always_decode_escape_sequences: bool = false, }; pub fn decodeUTF8(bytes: string, allocator: std.mem.Allocator) ![]const u16 { @@ -99,6 +101,7 @@ pub fn NewLexer( json_options.ignore_trailing_escape_sequences, json_options.json_warn_duplicate_keys, json_options.was_originally_macro, + json_options.always_decode_escape_sequences, ); } @@ -110,6 +113,7 @@ fn NewLexer_( comptime json_options_ignore_trailing_escape_sequences: bool, comptime json_options_json_warn_duplicate_keys: bool, comptime json_options_was_originally_macro: bool, + comptime json_options_always_decode_escape_sequences: bool, ) type { const json_options = JSONOptions{ .is_json = json_options_is_json, @@ -119,6 +123,7 @@ fn NewLexer_( .ignore_trailing_escape_sequences = json_options_ignore_trailing_escape_sequences, .json_warn_duplicate_keys = json_options_json_warn_duplicate_keys, .was_originally_macro = json_options_was_originally_macro, + .always_decode_escape_sequences = json_options_always_decode_escape_sequences, }; return struct { const LexerType = @This(); @@ -666,11 +671,17 @@ fn NewLexer_( pub const InnerStringLiteral = packed struct { suffix_len: u3, needs_slow_path: bool }; fn parseStringLiteralInnter(lexer: *LexerType, comptime quote: CodePoint) !InnerStringLiteral { + const check_for_backslash = comptime is_json and json_options.always_decode_escape_sequences; var needs_slow_path = false; var suffix_len: u3 = if (comptime quote == 0) 0 else 1; + var has_backslash: if (check_for_backslash) bool else void = if (check_for_backslash) false else {}; stringLiteral: while (true) { switch (lexer.code_point) { '\\' => { + if (comptime check_for_backslash) { + has_backslash = true; + } + lexer.step(); // Handle Windows CRLF @@ -784,6 +795,8 @@ fn NewLexer_( lexer.step(); } + if (comptime check_for_backslash) needs_slow_path = needs_slow_path or has_backslash; + return InnerStringLiteral{ .needs_slow_path = needs_slow_path, .suffix_len = suffix_len }; } diff --git a/src/js_parser.zig b/src/js_parser.zig index 448988a5b5..8f48fc5a85 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -11070,7 +11070,7 @@ fn NewParser_( const what = switch (kind) { .k_await_using, .k_using => "declaration", .k_const => "constant", - else => comptime unreachable, + else => @compileError("unreachable"), }; for (decls) |decl| { diff --git a/src/json_parser.zig b/src/json_parser.zig index 4c434e2aa5..4ce9a12aaf 100644 --- a/src/json_parser.zig +++ b/src/json_parser.zig @@ -114,6 +114,7 @@ fn JSONLikeParser(comptime opts: js_lexer.JSONOptions) type { opts.ignore_trailing_escape_sequences, opts.json_warn_duplicate_keys, opts.was_originally_macro, + opts.always_decode_escape_sequences, ); } @@ -125,6 +126,7 @@ fn JSONLikeParser_( comptime opts_ignore_trailing_escape_sequences: bool, comptime opts_json_warn_duplicate_keys: bool, comptime opts_was_originally_macro: bool, + comptime opts_always_decode_escape_sequences: bool, ) type { const opts = js_lexer.JSONOptions{ .is_json = opts_is_json, @@ -134,6 +136,7 @@ fn JSONLikeParser_( .ignore_trailing_escape_sequences = opts_ignore_trailing_escape_sequences, .json_warn_duplicate_keys = opts_json_warn_duplicate_keys, .was_originally_macro = opts_was_originally_macro, + .always_decode_escape_sequences = opts_always_decode_escape_sequences, }; return struct { const Lexer = js_lexer.NewLexer(if (LEXER_DEBUGGER_WORKAROUND) js_lexer.JSONOptions{} else opts); @@ -767,6 +770,41 @@ pub fn ParseJSONUTF8( return try parser.parseExpr(false, true); } +pub fn ParseJSONUTF8AlwaysDecode( + source: *const logger.Source, + log: *logger.Log, + allocator: std.mem.Allocator, +) !Expr { + const len = source.contents.len; + switch (len) { + // This is to be consisntent with how disabled JS files are handled + 0 => { + return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_object_data }; + }, + // This is a fast pass I guess + 2 => { + if (strings.eqlComptime(source.contents[0..1], "\"\"") or strings.eqlComptime(source.contents[0..1], "''")) { + return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_string_data }; + } else if (strings.eqlComptime(source.contents[0..1], "{}")) { + return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_object_data }; + } else if (strings.eqlComptime(source.contents[0..1], "[]")) { + return Expr{ .loc = logger.Loc{ .start = 0 }, .data = empty_array_data }; + } + }, + else => {}, + } + + var parser = try JSONLikeParser(.{ + .is_json = true, + .always_decode_escape_sequences = true, + }).init(allocator, source.*, log); + if (comptime Environment.allow_assert) { + std.debug.assert(parser.source().contents.len > 0); + } + + return try parser.parseExpr(false, true); +} + pub fn ParseJSONForMacro(source: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator) !Expr { switch (source.contents.len) { // This is to be consisntent with how disabled JS files are handled diff --git a/src/report.zig b/src/report.zig index 60dfb0b905..bfe81d12f3 100644 --- a/src/report.zig +++ b/src/report.zig @@ -375,7 +375,7 @@ pub noinline fn globalError(err: anyerror, trace_: @TypeOf(@errorReturnTrace())) ); Global.exit(1); }, - error.InvalidArgument, error.InstallFailed, error.InvalidPackageJSON => { + error.InvalidArgument => { Global.exit(1); }, error.SystemFdQuotaExceeded => { diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index fefa000e80..349dcbcd33 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -761,7 +761,7 @@ pub fn normalizeStringGenericTZ( } else if (path_.len > 0 and options.isSeparator(path_[0])) { buf[buf_i] = options.separator; buf_i += 1; - dotdot = 1; + dotdot = buf_i; path_begin = 1; } } @@ -793,6 +793,10 @@ pub fn normalizeStringGenericTZ( r += 1; buf[buf_i] = options.separator; buf_i += 1; + + // win32.resolve("C:\\Users\\bun", "C:\\Users\\bun", "/..\\bar") + // should be "C:\\bar" not "C:bar" + dotdot = buf_i; } } @@ -922,7 +926,7 @@ pub const Platform = enum { pub fn getSeparatorFunc(comptime _platform: Platform) IsSeparatorFunc { switch (comptime _platform.resolve()) { - .auto => comptime unreachable, + .auto => @compileError("unreachable"), .loose => { return isSepAny; }, @@ -937,7 +941,7 @@ pub const Platform = enum { pub fn getSeparatorFuncT(comptime _platform: Platform) IsSeparatorFuncT { switch (comptime _platform.resolve()) { - .auto => comptime unreachable, + .auto => @compileError("unreachable"), .loose => { return isSepAnyT; }, @@ -952,7 +956,7 @@ pub const Platform = enum { pub fn getLastSeparatorFunc(comptime _platform: Platform) LastSeparatorFunction { switch (comptime _platform.resolve()) { - .auto => comptime unreachable, + .auto => @compileError("unreachable"), .loose => { return lastIndexOfSeparatorLoose; }, @@ -967,7 +971,7 @@ pub const Platform = enum { pub fn getLastSeparatorFuncT(comptime _platform: Platform) LastSeparatorFunctionT { switch (comptime _platform.resolve()) { - .auto => comptime unreachable, + .auto => @compileError("unreachable"), .loose => { return lastIndexOfSeparatorLooseT; }, @@ -986,7 +990,7 @@ pub const Platform = enum { pub inline fn isSeparatorT(comptime _platform: Platform, comptime T: type, char: T) bool { switch (comptime _platform.resolve()) { - .auto => comptime unreachable, + .auto => @compileError("unreachable"), .loose => { return isSepAnyT(T, char); }, @@ -2221,6 +2225,23 @@ pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { } } +pub fn pathToPosixInPlace(comptime T: type, path: []T) void { + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { + path[index] = '/'; + } +} + +pub fn pathToPosixBuf(comptime T: type, path: []const T, buf: []T) []T { + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path, idx, std.fs.path.sep_windows)) |index| : (idx = index + 1) { + @memcpy(buf[idx..index], path[idx..index]); + buf[index] = std.fs.path.sep_posix; + } + @memcpy(buf[idx..path.len], path[idx..path.len]); + return buf[0..path.len]; +} + pub fn platformToPosixBuf(comptime T: type, path: []const T, buf: []T) []T { if (std.fs.path.sep == '/') return; var idx: usize = 0; diff --git a/src/string.zig b/src/string.zig index c9017bbd01..b0b29b9c92 100644 --- a/src/string.zig +++ b/src/string.zig @@ -380,7 +380,7 @@ pub const String = extern struct { return switch (@TypeOf(os_path)) { []const u8 => createUTF8(os_path), []const u16 => createUTF16(os_path), - else => comptime unreachable, + else => @compileError("unreachable"), }; } diff --git a/src/which.zig b/src/which.zig index 7b214eefd6..9e476f9349 100644 --- a/src/which.zig +++ b/src/which.zig @@ -58,7 +58,11 @@ const win_extensionsW = .{ bun.strings.w("cmd"), bun.strings.w("bat"), }; -const win_extensions = .{ "exe", "cmd", "bat" }; +const win_extensions = .{ + "exe", + "cmd", + "bat", +}; pub fn endsWithExtension(str: []const u8) bool { if (str.len < 4) return false; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index a8d45ed673..65cb7a3fff 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -1,9 +1,8 @@ -// @known-failing-on-windows: 1 failing import { file, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env, toHaveBins, toBeValidBin, toBeWorkspaceLink, ospath } from "harness"; import { access, mkdir, mkdtemp, readlink, realpath, rm, writeFile, copyFile, appendFile } from "fs/promises"; -import { join, relative, normalize, win32 } from "path"; +import { join, relative } from "path"; import { tmpdir } from "os"; import { dummyAfterAll, @@ -70,13 +69,14 @@ it("should add existing package", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("bun add"); 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([ "", - ` installed foo@${add_path}`, + ` installed foo@${add_path.replace(/\\/g, "/")}`, "", " 1 package installed", ]); @@ -87,7 +87,7 @@ it("should add existing package", async () => { name: "bar", version: "0.0.2", dependencies: { - foo: dep.replace(/\\\\/g, "\\"), + foo: dep.replace(/\\\\/g, "/"), }, }, null, @@ -118,7 +118,7 @@ it("should reject missing package", async () => { const err = await new Response(stderr).text(); expect(err).toContain("bun add"); expect(err).toContain("error: MissingPackageJSON"); - expect(err).toContain(`note: error occured while resolving ${dep}`); + expect(err).toContain(`note: error occured while resolving file:${add_path}`); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -132,7 +132,49 @@ it("should reject missing package", async () => { ); }); -it("should reject invalid path without segfault", async () => { +it.each(["file://", "file:/", "file:"])("should accept file protocol with prefix %s", async protocolPrefix => { + await writeFile( + join(add_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.2.3", + }), + ); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "bar", + version: "2.3.4", + }), + ); + const add_path = relative(package_dir, add_dir); + const dep = `${protocolPrefix}${add_path.replace(/\\/g, "/")}`; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", dep], + cwd: package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + 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([ + "", + ` installed foo@${add_path.replace(/\\/g, "/")}`, + "", + " 1 package installed", + ]); + expect(await exited).toBe(0); +}); + +it.each(["fileblah://", "file:///"])("should reject invalid path without segfault: %s", async protocolPrefix => { await writeFile( join(add_dir, "package.json"), JSON.stringify({ @@ -147,8 +189,8 @@ it("should reject invalid path without segfault", async () => { version: "0.0.2", }), ); - const add_path = relative(package_dir, add_dir); - const dep = `file://${add_path}`.replace(/\\/g, "\\\\"); + const add_path = relative(package_dir, add_dir).replace(/\\/g, "\\\\"); + const dep = `${protocolPrefix}${add_path}`; const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, @@ -160,8 +202,11 @@ it("should reject invalid path without segfault", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).toContain("bun add"); - expect(err).toContain("error: MissingPackageJSON"); - expect(err).toContain(`note: error occured while resolving ${dep}`); + if (protocolPrefix === "file:///") { + expect(err).toContain("error: MissingPackageJSON"); + } else { + expect(err).toContain(`error: unrecognised dependency format: ${dep.replace(/\\\\/g, "/")}`); + } expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -282,6 +327,7 @@ it("should add dependency with capital letters", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -337,6 +383,7 @@ it("should add exact version with --exact", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -393,6 +440,7 @@ it("should add exact version with install.exact", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -510,6 +558,7 @@ it("should add dependency with specified semver", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -571,6 +620,7 @@ it("should add dependency (GitHub)", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -658,12 +708,13 @@ it("should add dependency alongside workspaces", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); 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([ "", - ospath(" + bar@workspace:packages/bar"), + " + bar@workspace:packages/bar", "", " installed baz@0.0.3 with binaries:", " - baz-run", @@ -732,6 +783,7 @@ it("should add aliased dependency (npm)", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -793,6 +845,7 @@ it("should add aliased dependency (GitHub)", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -1256,6 +1309,7 @@ it("should prefer optionalDependencies over dependencies of the same name", asyn expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -1316,6 +1370,7 @@ it("should prefer dependencies over peerDependencies of the same name", async () expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -1612,7 +1667,7 @@ it("should add dependencies to workspaces directly", async () => { ); await writeFile(join(package_dir, "moo", "bunfig.toml"), await file(join(package_dir, "bunfig.toml")).text()); const add_path = relative(join(package_dir, "moo"), add_dir); - const dep = `file:${add_path}`.replace(/\\/g, "\\\\"); + const dep = `file:${add_path}`.replace(/\\/g, "/"); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: join(package_dir, "moo"), @@ -1624,12 +1679,13 @@ it("should add dependencies to workspaces directly", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); 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([ "", - ` installed foo@${relative(package_dir, add_dir)}`, + ` installed foo@${relative(package_dir, add_dir).replace(/\\/g, "/")}`, "", " 1 package installed", ]); @@ -1647,7 +1703,7 @@ it("should add dependencies to workspaces directly", async () => { name: "moo", version: "0.3.0", dependencies: { - foo: `file:${add_path}`, + foo: `file:${add_path.replace(/\\/g, "/")}`, }, }); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "foo"]); @@ -1687,13 +1743,14 @@ async function installRedirectsToAdd(saveFlagFirst: boolean) { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("bun add"); 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([ "", - ` installed foo@${add_path}`, + ` installed foo@${add_path.replace(/\\/g, "/")}`, "", " 1 package installed", ]); @@ -1724,6 +1781,7 @@ it("should add dependency alongside peerDependencies", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 3324ff7cc8..c66bfc13c5 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { file, listen, Socket, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, describe, test } from "bun:test"; import { bunExe, bunEnv as env, toBeValidBin, toHaveBins, toBeWorkspaceLink } from "harness"; @@ -21,6 +20,26 @@ expect.extend({ toBeWorkspaceLink, toBeValidBin, toHaveBins, + toHaveWorkspaceLink: async function (package_dir: string, [link, real]: [string, string]) { + const isWindows = process.platform === "win32"; + if (!isWindows) { + // expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); + // expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join("..", real)); + return expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join("..", real)); + } else { + // expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join(package_dir, real)); + return expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join(package_dir, real)); + } + }, + toHaveWorkspaceLink2: async function (package_dir: string, [link, realPosix, realWin]: [string, string, string]) { + const isWindows = process.platform === "win32"; + if (!isWindows) { + return expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join("..", realPosix)); + } else { + // prettier-ignore + return expect(await readlink(join(package_dir, "node_modules", link))).toBeWorkspaceLink(join(package_dir, realWin)); + } + }, }); beforeAll(dummyBeforeAll); @@ -374,9 +393,9 @@ it("should handle workspaces", async () => { 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${sep}nominally-scoped`, - ` + Asterisk@workspace:packages${sep}asterisk`, - ` + AsteriskTheSecond@workspace:packages${sep}second-asterisk`, + ` + @org/nominally-scoped@workspace:packages/nominally-scoped`, + ` + Asterisk@workspace:packages/asterisk`, + ` + AsteriskTheSecond@workspace:packages/second-asterisk`, " + Bar@workspace:bar", "", " 4 packages installed", @@ -390,16 +409,11 @@ it("should handle workspaces", async () => { "AsteriskTheSecond", "Bar", ]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readlink(join(package_dir, "node_modules", "Asterisk"))).toBeWorkspaceLink( - join("..", "packages", "asterisk"), - ); - expect(await readlink(join(package_dir, "node_modules", "AsteriskTheSecond"))).toBeWorkspaceLink( - join("..", "packages", "second-asterisk"), - ); - expect(await readlink(join(package_dir, "node_modules", "@org", "nominally-scoped"))).toBeWorkspaceLink( - join("..", "..", "packages", "nominally-scoped"), - ); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); + expect(package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); + // prettier-ignore + expect(package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); await access(join(package_dir, "bun.lockb")); // Perform `bun install` again but with lockfile from before @@ -423,9 +437,9 @@ it("should handle workspaces", async () => { 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${sep}nominally-scoped`, - ` + Asterisk@workspace:packages${sep}asterisk`, - ` + AsteriskTheSecond@workspace:packages${sep}second-asterisk`, + ` + @org/nominally-scoped@workspace:packages/nominally-scoped`, + ` + Asterisk@workspace:packages/asterisk`, + ` + AsteriskTheSecond@workspace:packages/second-asterisk`, " + Bar@workspace:bar", "", " 4 packages installed", @@ -438,16 +452,11 @@ it("should handle workspaces", async () => { "AsteriskTheSecond", "Bar", ]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readlink(join(package_dir, "node_modules", "Asterisk"))).toBeWorkspaceLink( - join("..", "packages", "asterisk"), - ); - expect(await readlink(join(package_dir, "node_modules", "AsteriskTheSecond"))).toBeWorkspaceLink( - join("..", "packages", "second-asterisk"), - ); - expect(await readlink(join(package_dir, "node_modules", "@org", "nominally-scoped"))).toBeWorkspaceLink( - join("..", "..", "packages", "nominally-scoped"), - ); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); + expect(package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); + // prettier-ignore + expect(package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); await access(join(package_dir, "bun.lockb")); }); @@ -489,14 +498,14 @@ it("should handle `workspace:` specifier", async () => { const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + Bar@workspace:path${sep}to${sep}bar`, + ` + Bar@workspace:path/to/bar`, "", " 1 package 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"))).toBeWorkspaceLink(join("..", "path", "to", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "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 }); @@ -519,14 +528,14 @@ it("should handle `workspace:` specifier", async () => { const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + Bar@workspace:path${sep}to${sep}bar`, + ` + Bar@workspace:path/to/bar`, "", " 1 package 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"))).toBeWorkspaceLink(join("..", "path", "to", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "path/to/bar"]); await access(join(package_dir, "bun.lockb")); }); @@ -571,7 +580,7 @@ it("should handle workspaces with packages array", async () => { 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"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); await access(join(package_dir, "bun.lockb")); }); @@ -622,15 +631,15 @@ it("should handle inter-dependency between workspaces", async () => { expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + Bar@workspace:bar", - ` + Baz@workspace:packages${sep}baz`, + ` + Baz@workspace:packages/baz`, "", " 2 packages installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); await access(join(package_dir, "bun.lockb")); }); @@ -681,15 +690,15 @@ it("should handle inter-dependency between workspaces (devDependencies)", async expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + Bar@workspace:bar", - ` + Baz@workspace:packages${sep}baz`, + ` + Baz@workspace:packages/baz`, "", " 2 packages installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); await access(join(package_dir, "bun.lockb")); }); @@ -740,15 +749,15 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + Bar@workspace:bar", - ` + Baz@workspace:packages${sep}baz`, + ` + Baz@workspace:packages/baz`, "", " 2 packages installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); await access(join(package_dir, "bun.lockb")); }); @@ -797,14 +806,14 @@ it("should ignore peerDependencies within workspaces", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + Baz@workspace:packages${sep}baz`, + ` + Baz@workspace:packages/baz`, "", " 1 package installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual(["Baz"]); - expect(await readlink(join(package_dir, "node_modules", "Baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); + expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); await access(join(package_dir, "bun.lockb")); }); @@ -935,7 +944,7 @@ it("should handle life-cycle scripts within workspaces", async () => { 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"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1002,7 +1011,7 @@ it("should handle life-cycle scripts during re-installation", async () => { expect(await exited1).toBe(0); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1036,7 +1045,7 @@ it("should handle life-cycle scripts during re-installation", async () => { expect(await exited2).toBe(0); expect(requested).toBe(3); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1070,7 +1079,7 @@ it("should handle life-cycle scripts during re-installation", async () => { expect(await exited3).toBe(0); expect(requested).toBe(4); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(await readlink(join(package_dir, "node_modules", "Bar"))).toBe(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1129,7 +1138,7 @@ it("should use updated life-cycle scripts in root during re-installation", async 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"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1179,7 +1188,7 @@ it("should use updated life-cycle scripts in root during re-installation", async expect(await exited2).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(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo2.txt")).text()).toBe("foo2!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); expect(await file(join(package_dir, "foo-postinstall.txt")).text()).toBe("foo!"); @@ -1216,7 +1225,7 @@ it("should use updated life-cycle scripts in root during re-installation", async expect(await exited3).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(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); expect(await file(join(package_dir, "foo2.txt")).text()).toBe("foo2!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); @@ -1276,7 +1285,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", 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"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); await access(join(package_dir, "bun.lockb")); @@ -1329,7 +1338,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", expect(await exited2).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(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); expect(await file(join(package_dir, "bar", "bar-postinstall.txt")).text()).toBe("bar postinstall!"); @@ -1368,7 +1377,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", expect(await exited3).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(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); expect(await file(join(package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); expect(await file(join(package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); @@ -1415,7 +1424,7 @@ it("should ignore workspaces within workspaces", async () => { 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"))).toBeWorkspaceLink(join("..", "bar")); + expect(package_dir).toHaveWorkspaceLink(["bar", "bar"]); await access(join(package_dir, "bun.lockb")); }); @@ -1643,6 +1652,7 @@ it("should handle ^0.0.2 in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1710,13 +1720,14 @@ it("should handle matching workspaces from dependencies", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); 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([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 3 packages installed", ]); @@ -1744,6 +1755,7 @@ it("should edit package json correctly with git dependencies", async () => { var err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); expect(await file(join(package_dir, "package.json")).json()).toEqual({ name: "foo", @@ -1764,6 +1776,7 @@ it("should edit package json correctly with git dependencies", async () => { err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); expect(await file(join(package_dir, "package.json")).json()).toEqual({ name: "foo", @@ -1784,6 +1797,7 @@ it("should edit package json correctly with git dependencies", async () => { err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); expect(await file(join(package_dir, "package.json")).json()).toEqual({ name: "foo", @@ -1804,6 +1818,7 @@ it("should edit package json correctly with git dependencies", async () => { err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); expect(await file(join(package_dir, "package.json")).json()).toEqual({ name: "foo", @@ -1840,6 +1855,7 @@ it("should handle ^0.0.2-rc in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1885,6 +1901,7 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1930,6 +1947,7 @@ it("should choose the right version with prereleases", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1975,6 +1993,7 @@ it("should handle ^0.0.2rc1 in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -2020,6 +2039,7 @@ it("should handle ^0.0.2_pre3 in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -2065,6 +2085,7 @@ it("should handle ^0.0.2b_4+cafe_b0ba in dependencies", async () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -2110,6 +2131,7 @@ it("should handle caret range in dependencies when the registry has prereleased const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -4349,8 +4371,20 @@ it("should report error on invalid format for optionalDependencies", async () => env, }); expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir + sep, "[dir]/")).toMatchSnapshot(); + + let err = await new Response(stderr).text(); + err = err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir + sep, "[dir]/"); + err = err.substring(0, err.indexOf("\n", err.lastIndexOf("[dir]/package.json:"))).trim(); + expect(err.split("\n")).toEqual([ + `bun install`, + `1 | {"name":"foo","version":"0.0.1","optionalDependencies":"bar"}`, + ` ^`, + `error: optionalDependencies expects a map of specifiers, e.g.`, + ` "optionalDependencies": {`, + ` "bun": "latest"`, + ` }`, + ` at [dir]/package.json:1:33`, + ]); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -4419,13 +4453,22 @@ it("should report error on duplicated workspace packages", async () => { env, }); expect(stderr).toBeDefined(); - const err = await new Response(stderr).text(); - expect( - err - .replace(/^bun install v.+\n/, "bun install\n") - .replaceAll(package_dir, "[dir]") - .replaceAll(sep, "/"), - ).toMatchSnapshot(); + let err = await new Response(stderr).text(); + err = err.replace(/^bun install v.+\n/, "bun install\n"); + err = err.replaceAll(package_dir, "[dir]"); + err = err.replaceAll(sep, "/"); + expect(err.trim().split("\n")).toEqual([ + `bun install`, + `1 | {"name":"moo","version":"0.0.3"}`, + ` ^`, + `error: Workspace name "moo" already exists`, + ` at [dir]/baz/package.json:1:9`, + ``, + `1 | {"name":"moo","version":"0.0.2"}`, + ` ^`, + `note: Package name is also declared here`, + ` at [dir]/bar/package.json:1:9`, + ]); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -5177,15 +5220,15 @@ it("should handle tarball path", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + baz@${join(import.meta.dir, "baz-0.0.3.tgz")}`, + ` + baz@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, "", " 1 package installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); - 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("..", "baz", "index.js")); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "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", @@ -5274,15 +5317,15 @@ it("should handle tarball path with aliasing", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + bar@${join(import.meta.dir, "baz-0.0.3.tgz")}`, + ` + bar@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, "", " 1 package installed", ]); expect(await exited).toBe(0); expect(requested).toBe(0); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); - 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", ".bin"))).toHaveBins(["baz-run"]); + expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(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", @@ -5561,7 +5604,7 @@ it("should handle tarball path with existing lockfile", async () => { const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz")}`, + ` + @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, "", " 3 packages installed", ]); @@ -5622,7 +5665,7 @@ it("should handle tarball path with existing lockfile", async () => { const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz")}`, + ` + @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, "", " 3 packages installed", ]); @@ -6278,6 +6321,7 @@ it("should handle trustedDependencies", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -6555,8 +6599,8 @@ it("should handle installing packages from inside a workspace with `*`", async ( const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + swag@workspace:packages${sep}swag`, - ` + yolo@workspace:packages${sep}yolo`, + ` + swag@workspace:packages/swag`, + ` + yolo@workspace:packages/yolo`, "", " 2 packages installed", ]); @@ -6638,8 +6682,8 @@ it("should handle installing packages from inside a workspace without prefix", a const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + p1@workspace:packages${sep}p1`, - ` + p2@workspace:packages${sep}p2`, + ` + p1@workspace:packages/p1`, + ` + p2@workspace:packages/p2`, "", " 2 packages installed", ]); @@ -6810,11 +6854,11 @@ it("should handle installing packages inside workspaces with difference versions const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + package1@workspace:packages${sep}package1`, - ` + package2@workspace:packages${sep}package2`, - ` + package3@workspace:packages${sep}package3`, - ` + package4@workspace:packages${sep}package4`, - ` + package5@workspace:packages${sep}package5`, + ` + package1@workspace:packages/package1`, + ` + package2@workspace:packages/package2`, + ` + package3@workspace:packages/package3`, + ` + package4@workspace:packages/package4`, + ` + package5@workspace:packages/package5`, "", " 5 packages installed", ]); @@ -6869,11 +6913,11 @@ it("should handle installing packages inside workspaces with difference versions const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + package1@workspace:packages${sep}package1`, - ` + package2@workspace:packages${sep}package2`, - ` + package3@workspace:packages${sep}package3`, - ` + package4@workspace:packages${sep}package4`, - ` + package5@workspace:packages${sep}package5`, + ` + package1@workspace:packages/package1`, + ` + package2@workspace:packages/package2`, + ` + package3@workspace:packages/package3`, + ` + package4@workspace:packages/package4`, + ` + package5@workspace:packages/package5`, " + bar@0.0.2", "", " 6 packages installed", @@ -6924,11 +6968,11 @@ it("should handle installing packages inside workspaces with difference versions const out3 = await new Response(stdout3).text(); expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + package1@workspace:packages${sep}package1`, - ` + package2@workspace:packages${sep}package2`, - ` + package3@workspace:packages${sep}package3`, - ` + package4@workspace:packages${sep}package4`, - ` + package5@workspace:packages${sep}package5`, + ` + package1@workspace:packages/package1`, + ` + package2@workspace:packages/package2`, + ` + package3@workspace:packages/package3`, + ` + package4@workspace:packages/package4`, + ` + package5@workspace:packages/package5`, " + bar@0.0.2", "", " 6 packages installed", @@ -6979,11 +7023,11 @@ it("should handle installing packages inside workspaces with difference versions const out4 = await new Response(stdout4).text(); expect(out4.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + package1@workspace:packages${sep}package1`, - ` + package2@workspace:packages${sep}package2`, - ` + package3@workspace:packages${sep}package3`, - ` + package4@workspace:packages${sep}package4`, - ` + package5@workspace:packages${sep}package5`, + ` + package1@workspace:packages/package1`, + ` + package2@workspace:packages/package2`, + ` + package3@workspace:packages/package3`, + ` + package4@workspace:packages/package4`, + ` + package5@workspace:packages/package5`, " + bar@0.0.2", "", " 6 packages installed", @@ -7035,11 +7079,11 @@ it("should handle installing packages inside workspaces with difference versions const out5 = await new Response(stdout5).text(); expect(out5.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + package1@workspace:packages${sep}package1`, - ` + package2@workspace:packages${sep}package2`, - ` + package3@workspace:packages${sep}package3`, - ` + package4@workspace:packages${sep}package4`, - ` + package5@workspace:packages${sep}package5`, + ` + package1@workspace:packages/package1`, + ` + package2@workspace:packages/package2`, + ` + package3@workspace:packages/package3`, + ` + package4@workspace:packages/package4`, + ` + package5@workspace:packages/package5`, " + bar@0.0.2", "", " 6 packages installed", @@ -7199,7 +7243,7 @@ it("should override @scoped npm dependency by matching workspace", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @bar/baz@workspace:packages${sep}bar-baz`, + ` + @bar/baz@workspace:packages/bar-baz`, "", " 1 package installed", ]); @@ -7426,8 +7470,8 @@ it("should override @scoped child npm dependency by matching workspace", async ( const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @moo/bar@workspace:packages${sep}moo-bar`, - ` + @moo/baz@workspace:packages${sep}moo-baz`, + ` + @moo/bar@workspace:packages/moo-bar`, + ` + @moo/baz@workspace:packages/moo-baz`, "", " 2 packages installed", ]); @@ -7489,8 +7533,8 @@ it("should override aliased child npm dependency by matching workspace", async ( const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @moo/bar@workspace:packages${sep}bar`, - ` + baz@workspace:packages${sep}baz`, + ` + @moo/bar@workspace:packages/bar`, + ` + baz@workspace:packages/baz`, "", " 2 packages installed", ]); @@ -7611,8 +7655,8 @@ it("should handle `workspace:` with alias & @scope", async () => { const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + @moo/bar@workspace:packages${sep}bar`, - ` + @moz/baz@workspace:packages${sep}baz`, + ` + @moo/bar@workspace:packages/bar`, + ` + @moz/baz@workspace:packages/baz`, "", " 2 packages installed", ]); @@ -7686,8 +7730,8 @@ it("should handle `workspace:*` on both root & child", async () => { const out1 = await new Response(stdout1).text(); expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + baz@workspace:packages${sep}baz`, - ` + bar@workspace:packages${sep}bar`, + ` + baz@workspace:packages/baz`, + ` + bar@workspace:packages/bar`, "", " 2 packages installed", ]); @@ -7724,8 +7768,8 @@ it("should handle `workspace:*` on both root & child", async () => { const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + baz@workspace:packages${sep}baz`, - ` + bar@workspace:packages${sep}bar`, + ` + baz@workspace:packages/baz`, + ` + bar@workspace:packages/bar`, "", " 2 packages installed", ]); @@ -7834,18 +7878,18 @@ describe("Registry URLs", () => { ["http://[www.example.com]/", true], ["c:a", true], ["https://registry.npmjs.org/", false], - ["https://artifactory.xxx.yyy/artifactory/api/npm/my-npm/", false], // https://github.com/oven-sh/bun/issues/3899 - ["https://artifactory.xxx.yyy/artifactory/api/npm/my-npm", false], // https://github.com/oven-sh/bun/issues/5368 + ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm/", false], // https://github.com/oven-sh/bun/issues/3899 + ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm", false], // https://github.com/oven-sh/bun/issues/5368 // ["", true], ["https:example.org", false], ["https://////example.com///", false], ["https://example.com/https:example.org", false], ["https://example.com/[]?[]#[]", false], - ["https://example/%?%#%", false], + ["http://example/%?%#%", false], ["c:", true], ["c:/", false], - ["https://點看", false], // gets converted to punycode - ["https://xn--c1yn36f/", false], + ["http://點看", false], // gets converted to punycode + ["http://xn--c1yn36f/", false], ]; for (const entry of registryURLs) { diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index b3417fdeaa..851ea71da6 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawn, file } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; @@ -73,8 +72,8 @@ it("should link and unlink workspace package", async () => { var out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + boba@workspace:packages${sep}boba`, - ` + moo@workspace:packages${sep}moo`, + ` + boba@workspace:packages/boba`, + ` + moo@workspace:packages/moo`, "", " 2 packages installed", ]); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index 050dff32dd..005f25138c 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { hash, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunEnv, bunExe, bunEnv as env } from "harness"; @@ -47,16 +46,23 @@ it("should list top-level dependency", async () => { }, }), ); - expect( - await spawn({ + { + const { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, - }).exited, - ).toBe(0); + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + expect(await exited).toBe(0); + } expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); urls.length = 0; @@ -103,16 +109,23 @@ it("should list all dependencies", async () => { }, }), ); - expect( - await spawn({ + { + const { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, - }).exited, - ).toBe(0); + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + expect(await exited).toBe(0); + } expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); urls.length = 0; @@ -160,16 +173,23 @@ it("should list top-level aliased dependency", async () => { }, }), ); - expect( - await spawn({ + { + const { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, - }).exited, - ).toBe(0); + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + expect(await exited).toBe(0); + } expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); urls.length = 0; @@ -216,16 +236,23 @@ it("should list aliased dependencies", async () => { }, }), ); - expect( - await spawn({ + { + const { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, - }).exited, - ).toBe(0); + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + expect(await exited).toBe(0); + } expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); urls.length = 0; @@ -274,8 +301,8 @@ it("should remove all cache", async () => { }), ); let cache_dir: string = join(package_dir, "node_modules", ".cache"); - expect( - await spawn({ + { + const { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, stdout: "pipe", @@ -285,8 +312,15 @@ it("should remove all cache", async () => { ...env, BUN_INSTALL_CACHE_DIR: cache_dir, }, - }).exited, - ).toBe(0); + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + expect(await exited).toBe(0); + } expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); expect(requested).toBe(2); expect(await readdirSorted(cache_dir)).toContain("bar"); diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index da8d7d5a39..caaca48dc4 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -75,8 +75,8 @@ it("should remove existing package", async () => { name: "foo", version: "0.0.2", dependencies: { - pkg1: `file:${pkg1_path}`, - pkg2: `file:${pkg2_path}`, + pkg1: `file:${pkg1_path.replace(/\\/g, "/")}`, + pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`, }, }, null, @@ -103,7 +103,7 @@ it("should remove existing package", async () => { expect(out1.replace(/\s*\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg2@${pkg2_path}`, + ` + pkg2@${pkg2_path.replace(/\\/g, "/")}`, "", " 1 package installed", " Removed: 1", @@ -117,7 +117,7 @@ it("should remove existing package", async () => { name: "foo", version: "0.0.2", dependencies: { - pkg2: `file:${pkg2_path}`, + pkg2: `file:${pkg2_path.replace(/\\/g, "/")}`, }, }, null, @@ -256,6 +256,19 @@ it("should retain a new line in the end of package.json", async () => { expect(await addExited).toBe(0); const content_before_remove = await file(join(package_dir, "package.json")).text(); expect(content_before_remove.endsWith("}")).toBe(true); + expect(content_before_remove).toEqual( + JSON.stringify( + { + name: "foo", + version: "0.0.1", + dependencies: { + pkg: `file:${pkg_path.replace(/\\/g, "/")}`, + }, + }, + null, + 2, + ), + ); await writeFile(join(package_dir, "package.json"), content_before_remove + "\n"); const { exited } = spawn({ @@ -302,6 +315,7 @@ it("should remove peerDependencies", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]/, "").split(/\r?\n/)).toEqual([" done", ""]); diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 39b5f1931d..160efd1141 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -188,7 +188,6 @@ logLevel = "debug" cwd: run_dir, env: bunEnv, }); - console.log(run_dir); if (withLogLevel) { expect(stderr.toString().trim()).toContain("ENOENT loading tsconfig.json extends"); } else { diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index d7364397a5..61574d4eb8 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawn } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env } from "harness"; @@ -47,7 +46,8 @@ it("should install and run default (latest) version", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.split(/\r?\n/)).toEqual(["console.log(42);", ""]); @@ -65,7 +65,8 @@ it("should install and run specified version", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.split(/\r?\n/)).toEqual(["uglify-js 3.14.1", ""]); @@ -84,6 +85,8 @@ it("should output usage if no arguments are passed", async () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Usage: "); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); @@ -105,7 +108,8 @@ it("should work for @scoped packages", async () => { expect(withoutCache.stderr).toBeDefined(); let err = await new Response(withoutCache.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(withoutCache.stdout).toBeDefined(); let out = await new Response(withoutCache.stdout).text(); expect(out.trim()).toContain("Usage: @withfig/autocomplete-tool"); @@ -123,7 +127,8 @@ it("should work for @scoped packages", async () => { expect(cached.stderr).toBeDefined(); err = await new Response(cached.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(cached.stdout).toBeDefined(); out = await new Response(cached.stdout).text(); expect(out.trim()).toContain("Usage: @withfig/autocomplete-tool"); @@ -150,7 +155,8 @@ console.log( }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out.split(/\r?\n/)).toEqual(["console.log(42);", ""]); @@ -172,7 +178,8 @@ it("should work for github repository", async () => { expect(withoutCache.stderr).toBeDefined(); let err = await new Response(withoutCache.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(withoutCache.stdout).toBeDefined(); let out = await new Response(withoutCache.stdout).text(); expect(out.trim()).toContain("Usage: cowsay"); @@ -190,7 +197,8 @@ it("should work for github repository", async () => { expect(cached.stderr).toBeDefined(); err = await new Response(cached.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(cached.stdout).toBeDefined(); out = await new Response(cached.stdout).text(); expect(out.trim()).toContain("Usage: cowsay"); @@ -210,7 +218,8 @@ it("should work for github repository with committish", async () => { expect(withoutCache.stderr).toBeDefined(); let err = await new Response(withoutCache.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(withoutCache.stdout).toBeDefined(); let out = await new Response(withoutCache.stdout).text(); expect(out.trim()).toContain("hello bun!"); @@ -228,7 +237,8 @@ it("should work for github repository with committish", async () => { expect(cached.stderr).toBeDefined(); err = await new Response(cached.stderr).text(); - expect(err).not.toContain("error"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(cached.stdout).toBeDefined(); out = await new Response(cached.stdout).text(); expect(out.trim()).toContain("hello bun!"); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 56778799e5..4c3259c629 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -1,6 +1,5 @@ -// @known-failing-on-windows: 1 failing import { file, spawn } from "bun"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; import { join, sep } from "path"; import { mkdtempSync, realpathSync } from "fs"; import { rm, writeFile, mkdir, exists, cp, readdir } from "fs/promises"; @@ -9,6 +8,11 @@ import { tmpdir } from "os"; import { fork, ChildProcess } from "child_process"; import { beforeAll, afterAll, beforeEach, afterEach, test, expect, describe } from "bun:test"; +expect.extend({ + toBeValidBin, + toHaveBins, +}); + var verdaccioServer: ChildProcess; var testCounter: number = 0; var port: number = 4873; @@ -268,6 +272,7 @@ test("basic 1", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + basic-1@1.0.0", @@ -296,6 +301,7 @@ test("basic 1", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + basic-1@1.0.0", @@ -334,6 +340,7 @@ test("dependency from root satisfies range from dependency", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + no-deps@1.0.0", @@ -363,6 +370,7 @@ test("dependency from root satisfies range from dependency", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + no-deps@1.0.0", @@ -402,6 +410,7 @@ test("peerDependency in child npm dependency should not maintain old version whe expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + no-deps@1.0.0", @@ -440,6 +449,7 @@ test("peerDependency in child npm dependency should not maintain old version whe out = await new Response(stdout).text(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.1", @@ -482,6 +492,7 @@ test("package added after install", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + one-range-dep@1.0.0", @@ -524,6 +535,7 @@ test("package added after install", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + no-deps@1.0.0", @@ -560,6 +572,7 @@ test("package added after install", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + no-deps@1.0.0", @@ -595,6 +608,7 @@ test("it should correctly link binaries after deleting node_modules", async () = expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + uses-what-bin@1.5.0", @@ -623,6 +637,7 @@ test("it should correctly link binaries after deleting node_modules", async () = expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + uses-what-bin@1.5.0", @@ -667,6 +682,7 @@ test("it should re-symlink binaries that become invalid when updating package ve expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + bin-change-dir@1.0.0", @@ -707,6 +723,7 @@ test("it should re-symlink binaries that become invalid when updating package ve expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + bin-change-dir@1.0.1", @@ -756,6 +773,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dep-loop-entry@1.0.0", @@ -795,6 +813,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dep-loop-entry@1.0.0", @@ -847,6 +866,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), @@ -872,6 +892,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), @@ -898,6 +919,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), @@ -1008,6 +1030,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); for (const dep of Object.keys(dependencies)) { expect(out).toContain(` + ${dep}@${dependencies[dep]}`); } @@ -1030,6 +1053,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out).not.toContain("package installed"); expect(out).toContain(`Checked ${Object.keys(dependencies).length * 2} installs across`); expect(await exited).toBe(0); @@ -1166,6 +1190,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); for (const dep of Object.keys(dependencies)) { expect(out).toContain(` + ${dep}@${dependencies[dep]}`); } @@ -1188,6 +1213,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); if (out.includes("installed")) { console.log("stdout:", out); } @@ -1211,6 +1237,7 @@ describe("hoisting", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out).not.toContain("package installed"); expect(await exited).toBe(0); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); @@ -1244,6 +1271,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1293,6 +1321,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -1342,6 +1371,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -1391,6 +1421,7 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -1508,10 +1539,11 @@ describe("workspaces", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 2 packages installed", ]); @@ -1531,10 +1563,11 @@ describe("workspaces", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 2 packages installed", ]); @@ -1557,10 +1590,11 @@ describe("workspaces", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 2 packages installed", ]); @@ -1580,10 +1614,11 @@ describe("workspaces", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 2 packages installed", ]); @@ -1630,9 +1665,10 @@ describe("workspaces", async () => { expect(err).not.toContain("Duplicate dependency"); expect(err).not.toContain('workspace dependency "workspace-1" not found'); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + workspace-1@workspace:packages${sep}workspace-1`, + ` + workspace-1@workspace:packages/workspace-1`, "", " 1 package installed", ]); @@ -1655,9 +1691,10 @@ describe("workspaces", async () => { expect(err).not.toContain("Duplicate dependency"); expect(err).not.toContain('workspace dependency "workspace-1" not found'); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + workspace-1@workspace:packages${sep}workspace-1`, + ` + workspace-1@workspace:packages/workspace-1`, "", " 1 package installed", ]); @@ -1684,9 +1721,10 @@ describe("workspaces", async () => { expect(err).not.toContain("Duplicate dependency"); expect(err).not.toContain('workspace dependency "workspace-1" not found'); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + workspace-1@workspace:packages${sep}workspace-1`, + ` + workspace-1@workspace:packages/workspace-1`, "", " 1 package installed", ]); @@ -1709,9 +1747,10 @@ describe("workspaces", async () => { expect(err).not.toContain("Duplicate dependency"); expect(err).not.toContain('workspace dependency "workspace-1" not found'); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + workspace-1@workspace:packages${sep}workspace-1`, + ` + workspace-1@workspace:packages/workspace-1`, "", " 1 package installed", ]); @@ -1745,6 +1784,7 @@ test("it should re-populate .bin folder if package is reinstalled", async () => expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + what-bin@1.5.0", @@ -1752,10 +1792,15 @@ test("it should re-populate .bin folder if package is reinstalled", async () => " 1 package installed", ]); expect(await exited).toBe(0); + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", "what-bin"), + join(packageDir, "node_modules", ".bin", bin), ); - expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); + } await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); @@ -1774,6 +1819,7 @@ test("it should re-populate .bin folder if package is reinstalled", async () => expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + what-bin@1.5.0", @@ -1782,9 +1828,13 @@ test("it should re-populate .bin folder if package is reinstalled", async () => ]); expect(await exited).toBe(0); expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", "what-bin"), + join(packageDir, "node_modules", ".bin", bin), ); - expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); + if (process.platform === "win32") { + expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); + } else { + expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); + } }); test("missing package on reinstall, some with binaries", async () => { @@ -1823,6 +1873,7 @@ test("missing package on reinstall, some with binaries", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dep-loop-entry@1.0.0", @@ -1875,6 +1926,7 @@ test("missing package on reinstall, some with binaries", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dep-loop-entry@1.0.0", @@ -1896,12 +1948,13 @@ test("missing package on reinstall, some with binaries", async () => { expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); + const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( - join(packageDir, "node_modules", ".bin", "what-bin"), + join(packageDir, "node_modules", ".bin", bin), ); expect( Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), - ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", "what-bin")); + ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); }); for (const forceWaiterThread of [false, true]) { @@ -1956,13 +2009,14 @@ for (const forceWaiterThread of [false, true]) { stderr: "pipe", env: testEnv, }); - expect(await exited).toBe(0); expect(stderr).toBeDefined(); var err = await new Response(stderr).text(); expect(stdout).toBeDefined(); var out = await new Response(stdout).text(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + expect(await exited).toBe(0); expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue(); expect(await exists(join(packageDir, "install.txt"))).toBeTrue(); expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); @@ -2006,7 +2060,6 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, })); - expect(await exited).toBe(0); expect(stderr).toBeDefined(); err = await new Response(stderr).text(); expect(stdout).toBeDefined(); @@ -2014,12 +2067,14 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + all-lifecycle-scripts@1.0.0", "", expect.stringContaining("1 package installed"), ]); + expect(await exited).toBe(0); expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!"); expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!"); expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!"); @@ -2067,6 +2122,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + all-lifecycle-scripts@1.0.0", @@ -2153,12 +2209,13 @@ for (const forceWaiterThread of [false, true]) { expect(stdout).toBeDefined(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("Saved lockfile"); var out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + pkg1@workspace:packages${sep}pkg1`, - ` + pkg2@workspace:packages${sep}pkg2`, + ` + pkg1@workspace:packages/pkg1`, + ` + pkg2@workspace:packages/pkg2`, "", " 2 packages installed", ]); @@ -2225,6 +2282,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); }); @@ -2257,6 +2315,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + all-lifecycle-scripts@1.0.0", @@ -2306,6 +2365,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 1 install across 2 packages (no changes)"), @@ -2348,6 +2408,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + what-bin@1.0.0", @@ -2356,7 +2417,10 @@ for (const forceWaiterThread of [false, true]) { ]); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", ".cache", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(["what-bin"]); + const isWindows = process.platform === "win32"; + const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"]; + // prettier-ignore + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], @@ -2372,6 +2436,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", "Checked 1 install across 2 packages (no changes)", @@ -2404,6 +2469,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + what-bin@1.0.0", @@ -2412,7 +2478,7 @@ for (const forceWaiterThread of [false, true]) { ]); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", ".cache", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(["what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], @@ -2428,13 +2494,14 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", "Checked 1 install across 2 packages (no changes)", ]); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", ".cache", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(["what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); // add it to trusted dependencies await writeFile( @@ -2463,13 +2530,14 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", "Checked 1 install across 2 packages (no changes)", ]); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", ".cache", "what-bin"]); - expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(["what-bin"]); + expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins); }); test("lifecycle scripts run if node_modules is deleted", async () => { @@ -2506,6 +2574,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); expect(await exited).toBe(0); await rm(join(packageDir, "node_modules"), { force: true, recursive: true }); @@ -2530,6 +2599,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue(); expect(await exited).toBe(0); }); @@ -2578,6 +2648,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + another-init-cwd@1.0.0", @@ -2613,8 +2684,6 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, }); - expect(await exited).toBe(1); - const err = await new Response(stderr).text(); expect(err).toContain("hello"); expect(await exited).toBe(1); @@ -2646,6 +2715,7 @@ for (const forceWaiterThread of [false, true]) { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("hello"); const out = await new Response(stdout).text(); @@ -2684,6 +2754,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2714,10 +2785,11 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, }); - const err = await new Response(stderr).text(); + let err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2743,6 +2815,13 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, })); + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); + expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue(); }); @@ -2774,6 +2853,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2831,6 +2911,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2870,6 +2951,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2903,10 +2985,11 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, }); - const err = await new Response(stderr).text(); + let err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -2946,6 +3029,13 @@ for (const forceWaiterThread of [false, true]) { env: testEnv, })); + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); + expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue(); expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue(); @@ -2989,6 +3079,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3036,6 +3127,7 @@ for (const forceWaiterThread of [false, true]) { // expect(err).toContain("Saved lockfile"); // expect(err).not.toContain("not found"); // expect(err).not.toContain("error:"); + // expect(err).not.toContain("panic:"); // await rm(join(packageDir, "node_modules", ".cache"), { recursive: true, force: true }); // expect((await readdir(join(packageDir, "node_modules"), { recursive: true })).sort()).toMatchSnapshot(); @@ -3072,6 +3164,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); var out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3117,6 +3210,13 @@ for (const forceWaiterThread of [false, true]) { env, })); + err = await Bun.readableStreamToText(stderr); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); + expect(await exited).toBe(0); expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.0.0"); expect( @@ -3139,6 +3239,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + uses-what-bin@1.5.0", @@ -3173,6 +3274,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); // if node-gyp isn't available, it would return a non-zero exit code @@ -3204,6 +3306,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).toContain("v"); }); @@ -3273,6 +3376,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); var out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3316,6 +3420,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3358,6 +3463,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + electron@1.0.0", @@ -3398,6 +3504,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); var out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3434,6 +3541,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3524,6 +3632,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3556,6 +3665,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3587,6 +3697,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); const out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3624,6 +3735,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3661,6 +3773,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -3706,6 +3819,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3738,6 +3852,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3771,6 +3886,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3815,6 +3931,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3854,6 +3971,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3900,6 +4018,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -3925,7 +4044,7 @@ for (const forceWaiterThread of [false, true]) { const originalPath = env.PATH; env.PATH = ""; - let { exited } = spawn({ + let { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", @@ -3936,6 +4055,12 @@ for (const forceWaiterThread of [false, true]) { env.PATH = originalPath; + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("No packages! Deleted empty lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); expect(await exited).toBe(0); expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue(); @@ -4041,6 +4166,7 @@ for (const forceWaiterThread of [false, true]) { expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -4212,6 +4338,7 @@ describe("pm trust", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out).toContain("Default trusted dependencies"); @@ -4262,6 +4389,7 @@ describe("pm trust", async () => { let err = await Bun.readableStreamToText(stderr); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("warn:"); let out = await Bun.readableStreamToText(stdout); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -4289,6 +4417,7 @@ describe("pm trust", async () => { expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); + expect(err).not.toContain("panic:"); out = await Bun.readableStreamToText(stdout); expect(out).toContain("1 script ran across 1 package"); @@ -4339,6 +4468,7 @@ require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT"); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); const out = await new Response(stdout).text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -4478,6 +4608,7 @@ describe("semver", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", ` + dep-with-tags@${expected}`, @@ -4644,6 +4775,7 @@ for (let i = 0; i < prereleaseTests.length; i++) { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", ` + ${depName}@${expected}`, @@ -4821,6 +4953,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dragon-test-1-d@1.0.0", @@ -4912,6 +5045,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dragon-test-2-b@workspace:dragon-test-2-b", @@ -4965,6 +5099,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dragon-test-3-a@1.0.0", @@ -5033,6 +5168,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + my-workspace@workspace:my-workspace", @@ -5113,10 +5249,11 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", - ` + a@workspace:packages${sep}a`, - ` + b@workspace:packages${sep}b`, + ` + a@workspace:packages/a`, + ` + b@workspace:packages/b`, "", " 5 packages installed", ]); @@ -5248,6 +5385,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + a@workspace:packages/a", @@ -5294,6 +5432,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dragon-test-7-a@1.0.0", @@ -5378,6 +5517,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + dragon-test-8-a@1.0.0", @@ -5419,6 +5559,7 @@ describe("yarn tests", () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", " + first@1.0.0", @@ -5496,6 +5637,7 @@ describe("yarn tests", () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -5559,6 +5701,7 @@ describe("yarn tests", () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -5612,6 +5755,7 @@ describe("yarn tests", () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5653,6 +5797,7 @@ describe("yarn tests", () => { const err = await new Response(stderr).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5692,6 +5837,7 @@ describe("yarn tests", () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5797,6 +5943,7 @@ describe("yarn tests", () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5858,6 +6005,7 @@ describe("yarn tests", () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5921,6 +6069,7 @@ describe("yarn tests", () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -5978,6 +6127,7 @@ describe("yarn tests", () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", @@ -6004,6 +6154,7 @@ describe("yarn tests", () => { out = await new Response(stdout).text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); expect(await exited).toBe(0);