diff --git a/docs/installation.md b/docs/installation.md index bada2d86b071a..cac2b33c38e5d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -232,8 +232,8 @@ If you need to remove Bun from your system, use the following commands. $ rm -rf ~/.bun # for macOS, Linux, and WSL ``` -```bash#Windows -$ Remove-Item ~\.bun -Recurse +```powershell#Windows +powershell -c ~\.bun\uninstall.ps1 ``` ```bash#NPM diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index c3712c13dcdc0..64b2fa7da7c07 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92 +Subproject commit 64b2fa7da7c077a3ac7f258d45281f25ac2ef250 diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index ee21120203255..61275d392a5a1 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -456,7 +456,7 @@ pub const RunCommand = struct { // wrapper exe. we build the full exe path even though we could do // a relative lookup, because in the case we do find it, we have to // generate this full path anyways. - if (Environment.isWindows and bun.strings.hasSuffixComptime(executable, ".exe")) { + if (Environment.isWindows and bun.FeatureFlags.windows_bunx_fast_path and bun.strings.hasSuffixComptime(executable, ".exe")) { std.debug.assert(std.fs.path.isAbsolute(executable)); // Using @constCast is safe because we know that @@ -1506,7 +1506,7 @@ pub const RunCommand = struct { } // Run absolute/relative path - if ((script_name_to_search.len > 1 and script_name_to_search[0] == '/') or + if (std.fs.path.isAbsolute(script_name_to_search) or (script_name_to_search.len > 2 and script_name_to_search[0] == '.' and script_name_to_search[1] == '/')) { Run.boot(ctx, ctx.allocator.dupe(u8, script_name_to_search) catch unreachable) catch |err| { @@ -1525,7 +1525,6 @@ pub const RunCommand = struct { if (script_name_to_search.len == 1 and script_name_to_search[0] == '-') { // read from stdin - var stack_fallback = std.heap.stackFallback(2048, bun.default_allocator); var list = std.ArrayList(u8).init(stack_fallback.get()); errdefer list.deinit(); @@ -1554,7 +1553,7 @@ pub const RunCommand = struct { return true; } - if (Environment.isWindows) try_bunx_file: { + if (Environment.isWindows and bun.FeatureFlags.windows_bunx_fast_path) try_bunx_file: { // Attempt to find a ".bunx" file on disk, and run it, skipping the // wrapper exe. we build the full exe path even though we could do // a relative lookup, because in the case we do find it, we have to @@ -1675,7 +1674,10 @@ pub const BunXFastPath = struct { var environment_buffer: bun.WPathBuffer = undefined; /// If this returns, it implies the fast path cannot be taken - fn tryLaunch(ctx: Command.Context, path_to_use: [:0]u16, env: *DotEnv.Loader, passthrough: []const []const u8) void { + fn tryLaunch(ctx_const: Command.Context, path_to_use: [:0]u16, env: *DotEnv.Loader, passthrough: []const []const u8) void { + if (!bun.FeatureFlags.windows_bunx_fast_path) return; + + var ctx = ctx_const; std.debug.assert(bun.isSliceInBufferT(u16, path_to_use, &BunXFastPath.direct_launch_buffer)); var command_line = BunXFastPath.direct_launch_buffer[path_to_use.len..]; @@ -1700,6 +1702,7 @@ pub const BunXFastPath = struct { const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[1 + i ..], str); i += result.len + 1; } + ctx.passthrough = passthrough; const run_ctx = shim_impl.FromBunRunContext{ .handle = handle, diff --git a/src/cli/uninstall.ps1 b/src/cli/uninstall.ps1 index df74be1872f7d..7fca554a9f449 100644 --- a/src/cli/uninstall.ps1 +++ b/src/cli/uninstall.ps1 @@ -12,7 +12,8 @@ $ErrorActionPreference = "Stop" function Write-Env { param([String]$Key, [String]$Value) - $EnvRegisterKey = Get-Item -Path 'HKCU:Environment' + $RegisterKey = Get-Item -Path 'HKCU:' + $EnvRegisterKey = $RegisterKey.OpenSubKey('Environment', $true) if ($null -eq $Value) { $EnvRegisterKey.DeleteValue($Key) } else { @@ -30,7 +31,8 @@ function Write-Env { function Get-Env { param([String] $Key) - $RegisterKey = Get-Item -Path 'HKCU:Environment' + $RegisterKey = Get-Item -Path 'HKCU:' + $EnvRegisterKey = $RegisterKey.OpenSubKey('Environment') $EnvRegisterKey.GetValue($Key, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } @@ -94,8 +96,9 @@ try { $Path = $Path -split ';' $Path = $Path | Where-Object { $_ -ne "${PSScriptRoot}\bin" } Write-Env -Key 'Path' -Value ($Path -join ';') -} catch { +} catch { Write-Host "Could not remove ${PSScriptRoot}\bin from PATH." + Write-Error $_ if ($PauseOnError) { pause } exit 1 } diff --git a/src/feature_flags.zig b/src/feature_flags.zig index eb9d55f3e5fd7..f0183b07cb706 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -164,6 +164,8 @@ pub const disable_auto_js_to_ts_in_node_modules = true; pub const runtime_transpiler_cache = true; +pub const windows_bunx_fast_path = false; // Disabled to simplify fixing the bunx issue + pub const breaking_changes_1_1_0 = false; // This causes strange bugs where writing via console.log (sync) has a different diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 1f9be3a47a0b7..59d782e4528c4 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -451,7 +451,7 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD /// Opens the dir if the path already exists and is a directory. /// This function is not atomic, and if it returns an error, the file system may /// have been modified regardless. -fn makeOpenPathAccessMaskW(self: std.fs.Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) std.os.OpenError!std.fs.Dir { +fn makeOpenPathAccessMaskW(self: std.fs.Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) !std.fs.Dir { var it = try std.fs.path.componentIterator(sub_path); // If there are no components in the path, then create a dummy component with the full path. var component = it.last() orelse std.fs.path.NativeUtf8ComponentIterator.Component{ @@ -483,7 +483,7 @@ const MakeOpenDirAccessMaskWOptions = struct { create_disposition: u32, }; -fn makeOpenDirAccessMaskW(self: std.fs.Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) std.os.OpenError!std.fs.Dir { +fn makeOpenDirAccessMaskW(self: std.fs.Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) !std.fs.Dir { var result = std.fs.Dir{ .fd = undefined, }; @@ -528,6 +528,7 @@ fn makeOpenDirAccessMaskW(self: std.fs.Dir, sub_path_w: [*:0]const u16, access_m // and the directory is trying to be opened for iteration. .ACCESS_DENIED => return error.AccessDenied, .INVALID_PARAMETER => return error.BadPathName, + .SHARING_VIOLATION => return error.SharingViolation, else => return w.unexpectedStatus(rc), } } diff --git a/src/install/semver.zig b/src/install/semver.zig index dff0c9ec97dc0..320837ec65a59 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -63,12 +63,6 @@ pub const String = extern struct { } } - pub fn isUndefined(this: *const String) bool { - const num: u64 = undefined; - const bytes = @as(u64, @bitCast(this.bytes)); - return @as(u63, @truncate(bytes)) == @as(u63, @truncate(num)); - } - pub const Formatter = struct { str: *const String, buf: string, diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index a229f791730eb..81dac390eba31 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -30,7 +30,7 @@ shebang: ?Shebang, /// These arbitrary numbers will probably not show up in the other fields. /// This will reveal off-by-one mistakes. pub const VersionFlag = enum(u13) { - pub const current = .v4; + pub const current = .v5; v1 = 5474, /// Fix bug where paths were not joined correctly @@ -41,6 +41,8 @@ pub const VersionFlag = enum(u13) { /// automatic fallback path where if "node" is asked for, but not present, /// it will retry the spawn with "bun". v4 = 5477, + /// Fixed bugs where passing arguments did not always work. + v5 = 5478, _, }; @@ -82,14 +84,14 @@ fn wU8(comptime s: []const u8) []const u8 { pub const Shebang = struct { launcher: []const u8, utf16_len: u32, - is_bun: bool, + is_node_or_bun: bool, - pub fn init(launcher: []const u8, is_bun: bool) !Shebang { + pub fn init(launcher: []const u8, is_node_or_bun: bool) !Shebang { return .{ .launcher = launcher, // TODO(@paperdave): what if this is invalid utf8? .utf16_len = @intCast(bun.simdutf.length.utf16.from.utf8(launcher)), - .is_bun = is_bun, + .is_node_or_bun = is_node_or_bun, }; } @@ -200,8 +202,8 @@ pub const Shebang = struct { if (eqlComptime(first, "/usr/bin/env") or eqlComptime(first, "/bin/env")) { const rest = tokenizer.rest(); const program = tokenizer.next() orelse return parseFromBinPath(bin_path); - const is_bun = eqlComptime(program, "bun") or eqlComptime(program, "node"); - return try Shebang.init(rest, is_bun); + const is_node_or_bun = eqlComptime(program, "bun") or eqlComptime(program, "node"); + return try Shebang.init(rest, is_node_or_bun); } return try Shebang.init(line, false); @@ -235,7 +237,7 @@ pub fn encodeInto(options: @This(), buf: []u8) !void { wbuf[1] = 0; wbuf = wbuf[2..]; - const is_node_or_bun = if (options.shebang) |s| s.is_bun else false; + const is_node_or_bun = if (options.shebang) |s| s.is_node_or_bun else false; var flags = Flags{ .has_shebang = options.shebang != null, .is_node_or_bun = is_node_or_bun, diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index ce5a5eb726f47..bec89b47e4a39 100755 Binary files a/src/install/windows-shim/bun_shim_impl.exe and b/src/install/windows-shim/bun_shim_impl.exe differ diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index b5699703c45ae..acbdb0b3fff9c 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -34,8 +34,10 @@ //! Prior Art: //! - https://github.com/ScoopInstaller/Shim/blob/master/src/shim.cs //! -//! The compiled binary is 12800 bytes and is `@embedFile`d into Bun itself. +//! The compiled binary is 13312 bytes and is `@embedFile`d into Bun itself. //! When this file is updated, the new binary should be compiled and BinLinkingShim.VersionFlag.current should be updated. +//! +//! Questions about this file should be directed at @paperdave. const builtin = @import("builtin"); const dbg = builtin.mode == .Debug; @@ -54,11 +56,9 @@ const Flags = @import("./BinLinkingShim.zig").Flags; pub inline fn wliteral(comptime str: []const u8) []const u16 { if (!@inComptime()) @compileError("strings.w() must be called in a comptime context"); comptime var output: [str.len]u16 = undefined; - for (str, 0..) |c, i| { output[i] = c; } - const Static = struct { pub const literal: []const u16 = output[0..output.len]; }; @@ -116,6 +116,8 @@ const k32 = struct { const GetExitCodeProcess = w.kernel32.GetExitCodeProcess; /// https://learn.microsoft.com/en-us/windows/console/getconsolemode const GetConsoleMode = w.kernel32.GetConsoleMode; + /// https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-sethandleinformation + const SetHandleInformation = w.kernel32.SetHandleInformation; /// https://learn.microsoft.com/en-us/windows/console/setconsolemode extern "kernel32" fn SetConsoleMode( hConsoleHandle: w.HANDLE, // [in] @@ -128,7 +130,7 @@ fn debug(comptime fmt: []const u8, args: anytype) void { if (!is_standalone) { bunDebugMessage(fmt, args); } else { - std.log.debug(if (fmt[fmt.len - 1] == '\n') fmt else fmt ++ "\n", args); + std.log.debug(fmt, args); } } @@ -177,7 +179,7 @@ const FailReason = enum { .CouldNotDirectLaunch => if (!is_standalone) "bin metadata is corrupt (invalid utf16)" else - // Unreachable is ok because Direct Launch is not supported in standalone mode + // unreachable is ok because Direct Launch is not supported in standalone mode unreachable, }; } @@ -192,7 +194,9 @@ const FailReason = enum { try writer.writeAll("error: "); switch (reason) { inline else => |r| { - if (is_standalone and r == .CouldNotDirectLaunch) unreachable; + if (is_standalone and r == .CouldNotDirectLaunch) + // unreachable is ok because Direct Launch is not supported in standalone mode + unreachable; const template = comptime getFormatTemplate(r) ++ "\n\n"; @@ -344,8 +348,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { assert(@intFromPtr(cmd_line_u16.ptr) % 2 == 0); // alignment assumption if (dbg) { - debug("CommandLine: {}\n", .{fmt16(cmd_line_u16[0 .. cmd_line_b_len / 2])}); - debug("ImagePathName: {}\n", .{fmt16(image_path_u16[0 .. image_path_b_len / 2])}); + debug("CommandLine: {}", .{fmt16(cmd_line_u16[0 .. cmd_line_b_len / 2])}); + debug("ImagePathName: {}", .{fmt16(image_path_u16[0 .. image_path_b_len / 2])}); } var buf1: [w.PATH_MAX_WIDE + "\"\" ".len]u16 = undefined; @@ -392,8 +396,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { .MaximumLength = path_len_bytes, .Buffer = buf1_u16, }; - if (dbg) debug("NtCreateFile({s})\n", .{fmt16(unicodeStringToU16(nt_name))}); - if (dbg) debug("NtCreateFile({any})\n", .{(unicodeStringToU16(nt_name))}); + if (dbg) debug("NtCreateFile({s})", .{fmt16(unicodeStringToU16(nt_name))}); + if (dbg) debug("NtCreateFile({any})", .{(unicodeStringToU16(nt_name))}); var attr = w.OBJECT_ATTRIBUTES{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), .RootDirectory = null, @@ -405,8 +409,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // NtCreateFile will fail for absolute paths if we do not pass an OBJECT name // so we need the prefix here. This is an extra sanity check. if (dbg) { - std.debug.assert(std.mem.startsWith(u16, unicodeStringToU16(nt_name), &nt_object_prefix)); - std.debug.assert(std.mem.endsWith(u16, unicodeStringToU16(nt_name), comptime wliteral(".bunx"))); + assert(std.mem.startsWith(u16, unicodeStringToU16(nt_name), &nt_object_prefix)); + assert(std.mem.endsWith(u16, unicodeStringToU16(nt_name), comptime wliteral(".bunx"))); } const rc = nt.NtCreateFile( &metadata_handle, @@ -422,7 +426,7 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { 0, ); if (rc != .SUCCESS) { - if (dbg) debug("error opening: {s}\n", .{@tagName(rc)}); + if (dbg) debug("error opening: {s}", .{@tagName(rc)}); if (rc == .OBJECT_NAME_NOT_FOUND) mode.fail(.ShimNotFound); mode.fail(.CouldNotOpenShim); @@ -462,11 +466,11 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { break :find_args cmd_line_u8[0..0]; }; - if (dbg) debug("UserArgs: '{s}' ({d} bytes)\n", .{ user_arguments_u8, user_arguments_u8.len }); + if (dbg) debug("UserArgs: '{s}' ({d} bytes)", .{ user_arguments_u8, user_arguments_u8.len }); - std.debug.assert(user_arguments_u8.len % 2 == 0); - std.debug.assert(user_arguments_u8.len != 2); - std.debug.assert(user_arguments_u8.len == 0 or user_arguments_u8[0] == ' '); + assert(user_arguments_u8.len % 2 == 0); + assert(user_arguments_u8.len != 2); + assert(user_arguments_u8.len == 0 or user_arguments_u8[0] == ' '); // Read the metadata file into the memory right after the image path. // @@ -482,15 +486,18 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { var read_ptr: [*]u16 = brk: { var left = image_path_b_len / 2 - (if (is_standalone) ".exe".len else ".bunx".len) - 1; var ptr: [*]u16 = buf1_u16[nt_object_prefix.len + left ..]; - if (dbg) debug("left = {d}, at {}, after {}\n", .{ left, ptr[0], ptr[1] }); + if (dbg) debug("left = {d}, at {}, after {}", .{ left, ptr[0], ptr[1] }); // if this is false, potential out of bounds memory access - std.debug.assert(@intFromPtr(ptr) - left * @sizeOf(std.meta.Child(@TypeOf(ptr))) >= @intFromPtr(buf1_u16)); + if (dbg) + assert( + @intFromPtr(ptr) - left * @sizeOf(std.meta.Child(@TypeOf(ptr))) >= @intFromPtr(buf1_u16), + ); // we start our search right before the . as we know the extension is '.bunx' - std.debug.assert(ptr[1] == '.'); + assert(ptr[1] == '.'); while (true) { - if (dbg) debug("1 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); + if (dbg) debug("1 - {}", .{std.unicode.fmtUtf16le(ptr[0..1])}); if (ptr[0] == '\\') { left -= 1; // ptr is of type [*]u16, which means -= operates on number of ITEMS, not BYTES @@ -502,12 +509,13 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { return mode.fail(.NoDirname); } ptr -= 1; - std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + if (dbg) + assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); } // inlined loop to do this again, because the completion case is different // using `inline for` caused comptime issues that made the code much harder to read while (true) { - if (dbg) debug("2 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); + if (dbg) debug("2 - {}", .{std.unicode.fmtUtf16le(ptr[0..1])}); if (ptr[0] == '\\') { // ptr is at the position marked S, so move forward one *character* break :brk ptr + 1; @@ -517,17 +525,18 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { return mode.fail(.NoDirname); } ptr -= 1; - std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + if (dbg) + assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); } - @compileError("unreachable"); + @compileError("unreachable - the loop breaks this entire block"); }; - std.debug.assert(read_ptr[0] != '\\'); - std.debug.assert((read_ptr - 1)[0] == '\\'); + assert(read_ptr[0] != '\\'); + assert((read_ptr - 1)[0] == '\\'); const read_max_len = buf1.len * 2 - (@intFromPtr(read_ptr) - @intFromPtr(buf1_u16)); - if (dbg) debug("read_ptr = buf1 + {d}\n", .{(@intFromPtr(read_ptr) - @intFromPtr(buf1_u16))}); - if (dbg) debug("max_read_len = {d}\n", .{read_max_len}); + if (dbg) debug("read_ptr = buf1 + {d}", .{(@intFromPtr(read_ptr) - @intFromPtr(buf1_u16))}); + if (dbg) debug("max_read_len = {d}", .{read_max_len}); // Do the read! // @@ -549,25 +558,25 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // In the context of this program, I don't think that is possible, but I will handle it read_max_len, else => |rc| { - if (dbg) debug("error reading: {s}\n", .{@tagName(rc)}); + if (dbg) debug("error reading: {s}", .{@tagName(rc)}); return mode.fail(.CouldNotReadShim); }, }; _ = nt.NtClose(metadata_handle); - if (dbg) debug("BufferAfterRead: '{}'\n", .{fmt16(buf1_u16[0 .. ((@intFromPtr(read_ptr) - @intFromPtr(buf1_u8)) + read_len) / 2])}); + if (dbg) debug("BufferAfterRead: '{}'", .{fmt16(buf1_u16[0 .. ((@intFromPtr(read_ptr) - @intFromPtr(buf1_u8)) + read_len) / 2])}); read_ptr = @ptrFromInt(@intFromPtr(read_ptr) + read_len - @sizeOf(Flags)); const flags: Flags = @as(*align(1) Flags, @ptrCast(read_ptr)).*; if (dbg) { const flags_u16: u16 = @as(*align(1) u16, @ptrCast(read_ptr)).*; - debug("FlagsInt: {d}\n", .{flags_u16}); + debug("FlagsInt: {d}", .{flags_u16}); - debug("Flags:\n", .{}); + debug("Flags:", .{}); inline for (comptime std.meta.fieldNames(Flags)) |name| { - debug(" {s}: {}\n", .{ name, @field(flags, name) }); + debug(" {s}: {}", .{ name, @field(flags, name) }); } } @@ -635,8 +644,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { const shebang_bin_path_len_bytes = shebang_metadata.bin_path_len_bytes; if (dbg) { - debug("bin_path_len_bytes: {}\n", .{shebang_metadata.bin_path_len_bytes}); - debug("args_len_bytes: {}\n", .{shebang_metadata.args_len_bytes}); + debug("bin_path_len_bytes: {}", .{shebang_metadata.bin_path_len_bytes}); + debug("args_len_bytes: {}", .{shebang_metadata.args_len_bytes}); } // magic number related to how BinLinkingShim.zig writes the metadata @@ -648,7 +657,7 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { (@as(u64, shebang_arg_len_u8) +| @as(u64, shebang_bin_path_len_bytes)) + validation_length_offset != read_len) { if (dbg) - debug("read_len: {}\n", .{read_len}); + debug("read_len: {}", .{read_len}); return mode.fail(.InvalidShimBounds); } @@ -663,11 +672,11 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // // This optimization can save an additional ~10-20ms depending on the machine // as we do not have to launch a second process. - if (dbg) debug("direct_launch_with_bun_js\n", .{}); + if (dbg) debug("direct_launch_with_bun_js", .{}); // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr const len = (@intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - shebang_arg_len_u8) / 2 - nt_object_prefix.len - "\"\x00".len; - const launch_slice = buf1_u16[nt_object_prefix.len..][0..len :'"']; + const launch_slice = buf1_u16[nt_object_prefix.len..][0..len :'"']; // assert we slice at the " bun_ctx.direct_launch_with_bun_js( launch_slice, bun_ctx.cli_context, @@ -678,6 +687,7 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // Copy the shebang bin path // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' // ^~~~^ + // ^ read_ptr // BUF2: 'node !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' read_ptr = @ptrFromInt(@intFromPtr(read_ptr) - shebang_arg_len_u8); @memcpy(buf2_u8, @as([*]u8, @ptrCast(read_ptr))[0..shebang_arg_len_u8]); @@ -687,34 +697,59 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // Copy the filename in. There is no leading " but there is a trailing " // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' - // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr + // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' - const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - shebang_arg_len_u8; + const length_of_filename_u8 = @intFromPtr(read_ptr) - + @intFromPtr(buf1_u8) - 2 * (nt_object_prefix.len + "\x00".len); + const filename = buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8]; + if (dbg) { + const sliced = std.mem.bytesAsSlice(u16, filename); + debug("filename and quote: '{}'", .{fmt16(@alignCast(sliced))}); + debug("last char of above is '{}'", .{sliced[sliced.len - 1]}); + assert(sliced[sliced.len - 1] == '\"'); + } + @memcpy( buf2_u8[shebang_arg_len_u8 + 2 * "\"".len ..][0..length_of_filename_u8], - buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8], + filename, ); - read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + length_of_filename_u8 + 2 * ("\"".len + nt_object_prefix.len)); + // the pointer is now going to act as a write pointer for remaining data. + // note that it points into buf2 now, not buf1. this will write arguments and the null terminator + // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' + // ^ write_ptr + if (dbg) { + debug("advance = {} + {} + {}\n", .{ shebang_arg_len_u8, "\"".len, length_of_filename_u8 }); + } + const advance = shebang_arg_len_u8 + 2 * "\"".len + length_of_filename_u8; + var write_ptr: [*]u16 = @ptrFromInt(@intFromPtr(buf2_u8) + advance); + assert((write_ptr - 1)[0] == '"'); if (user_arguments_u8.len > 0) { // Copy the user arguments in: // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags!!!!!!!!!!!' // ^~~~~X^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ - // | |filename_len where the user args go + // | |filename_len write_ptr // | the quote // shebang_arg_len - @memcpy(@as([*]u8, @ptrCast(read_ptr)), user_arguments_u8); - read_ptr = @ptrFromInt(@intFromPtr(read_ptr) + user_arguments_u8.len); + @memcpy(@as([*]u8, @ptrCast(write_ptr)), user_arguments_u8); + write_ptr = @ptrFromInt(@intFromPtr(write_ptr) + user_arguments_u8.len); } // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags#!!!!!!!!!!' // ^ null terminator - @as(*align(1) u16, @ptrCast(read_ptr)).* = 0; + write_ptr[0] = 0; break :spawn_command_line @ptrCast(buf2_u16); }, }; + if (!is_standalone) { + // Prepare stdio for the child process, as after this we are going to *immediatly* exit + // it is likely that the c-runtime's atexit will not be called as we end the process ourselves. + bun.Output.Source.Stdio.restore(); + bun.C.windows_enable_stdio_inheritance(); + } + // I attempted to use lower level methods for this, but it really seems // too difficult and not worth the stability risks. // @@ -745,9 +780,10 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { .wShowWindow = 0, .cbReserved2 = 0, .lpReserved2 = null, - .hStdInput = ProcessParameters.hStdInput, - .hStdOutput = ProcessParameters.hStdOutput, - .hStdError = ProcessParameters.hStdError, + // The standard handles outside of standalone may be tampered with. + .hStdInput = if (is_standalone) ProcessParameters.hStdInput else bun.win32.STDIN_FD.cast(), + .hStdOutput = if (is_standalone) ProcessParameters.hStdOutput else bun.win32.STDOUT_FD.cast(), + .hStdError = if (is_standalone) ProcessParameters.hStdError else bun.win32.STDERR_FD.cast(), }; inline for (.{ 0, 1 }) |attempt_number| iteration: { @@ -759,8 +795,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { null, null, 1, // true - if (is_standalone) 0 else w.CREATE_UNICODE_ENVIRONMENT, - if (is_standalone) null else @constCast(bun_ctx.environment), + 0, + null, null, &startup_info, &process, @@ -768,8 +804,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { if (did_process_spawn == 0) { const spawn_err = k32.GetLastError(); if (dbg) { - debug("CreateProcessW failed: {s}\n", .{@tagName(spawn_err)}); - debug("attempt number: {d}\n", .{attempt_number}); + debug("CreateProcessW failed: {s}", .{@tagName(spawn_err)}); + debug("attempt number: {d}", .{attempt_number}); } return switch (spawn_err) { .FILE_NOT_FOUND => if (flags.has_shebang) { @@ -849,14 +885,15 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { var exit_code: w.DWORD = 255; _ = k32.GetExitCodeProcess(process.hProcess, &exit_code); + if (dbg) debug("exit_code: {d}", .{exit_code}); _ = nt.NtClose(process.hProcess); _ = nt.NtClose(process.hThread); nt.RtlExitUserProcess(exit_code); - @compileError("unreachable"); + @compileError("unreachable - RtlExitUserProcess does not return"); } - @compileError("unreachable"); + @compileError("unreachable - above loop should not exit"); } pub const FromBunRunContext = struct { @@ -888,8 +925,9 @@ pub const FromBunRunContext = struct { /// this returns void, to which the caller should still try invoking the exe directly. This /// is to handle version mismatches where bun.exe's decoder is too new than the .bunx file. pub fn tryStartupFromBunJS(context: FromBunRunContext) void { - std.debug.assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); - comptime std.debug.assert(!is_standalone); + assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); + comptime assert(!is_standalone); + comptime assert(bun.FeatureFlags.windows_bunx_fast_path); launcher(.launch, context); } @@ -922,16 +960,17 @@ pub const ReadWithoutLaunchResult = union { /// The cost of spawning is about 5-12ms, and the unicode conversions are way /// faster than that, so this is a huge win. pub fn readWithoutLaunch(context: FromBunShellContext) ReadWithoutLaunchResult { - std.debug.assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); - comptime std.debug.assert(!is_standalone); + assert(!std.mem.startsWith(u16, context.base_path, &nt_object_prefix)); + comptime assert(!is_standalone); + comptime assert(bun.FeatureFlags.windows_bunx_fast_path); return launcher(.read_without_launch, context); } /// Main function for `bun_shim_impl.exe` pub inline fn main() noreturn { - comptime std.debug.assert(is_standalone); - comptime std.debug.assert(builtin.single_threaded); - comptime std.debug.assert(!builtin.link_libc); - comptime std.debug.assert(!builtin.link_libcpp); + comptime assert(is_standalone); + comptime assert(builtin.single_threaded); + comptime assert(!builtin.link_libc); + comptime assert(!builtin.link_libcpp); launcher(.launch, {}); } diff --git a/src/output.zig b/src/output.zig index 41b01dbb58100..341a7fea91ef3 100644 --- a/src/output.zig +++ b/src/output.zig @@ -208,6 +208,7 @@ pub const Source = struct { _ = SetConsoleCP(CP_UTF8); const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200; + const ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002; const ENABLE_PROCESSED_OUTPUT = 0x0001; var mode: w.DWORD = undefined; @@ -220,13 +221,13 @@ pub const Source = struct { if (w.kernel32.GetConsoleMode(stdout, &mode) != 0) { console_mode[1] = mode; bun_stdio_tty[1] = 1; - _ = SetConsoleMode(stdout, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + _ = SetConsoleMode(stdout, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT | mode); } if (w.kernel32.GetConsoleMode(stderr, &mode) != 0) { console_mode[2] = mode; bun_stdio_tty[2] = 1; - _ = SetConsoleMode(stderr, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + _ = SetConsoleMode(stderr, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT | mode); } } }; @@ -241,9 +242,9 @@ pub const Source = struct { const stdout = bun.sys.File.from(std.io.getStdOut()); const stderr = bun.sys.File.from(std.io.getStdErr()); - var output_source = Output.Source.init(stdout, stderr); - output_source.set(); + Output.Source.init(stdout, stderr) + .set(); if (comptime Environment.isDebug) { initScopedDebugWriterAtStartup(); @@ -259,8 +260,8 @@ pub const Source = struct { } }; - pub fn set(_source: *Source) void { - source = _source.*; + pub fn set(new_source: *const Source) void { + source = new_source.*; source_set = true; if (!stdout_stream_set) { @@ -288,8 +289,8 @@ pub const Source = struct { enable_ansi_colors = enable_ansi_colors_stdout or enable_ansi_colors_stderr; } - stdout_stream = _source.stream; - stderr_stream = _source.error_stream; + stdout_stream = new_source.stream; + stderr_stream = new_source.error_stream; } } }; diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 61574d4eb80dc..640c4dfb681f0 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -1,15 +1,23 @@ import { spawn } from "bun"; import { afterEach, beforeEach, expect, it } from "bun:test"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, isWindows } from "harness"; import { mkdtemp, realpath, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { readdirSorted } from "./dummy.registry"; +import { readdirSync } from "js/node/fs/export-star-from"; let x_dir: string; beforeEach(async () => { x_dir = await realpath(await mkdtemp(join(tmpdir(), "bun-x.test"))); + + const tmp = isWindows ? tmpdir() : "/tmp"; + readdirSync(tmp).forEach(file => { + if (file.startsWith("bunx-")) { + rm(join(tmp, file), { recursive: true, force: true }); + } + }); }); afterEach(async () => { await rm(x_dir, { force: true, recursive: true }); @@ -95,10 +103,9 @@ it("should output usage if no arguments are passed", async () => { }); it("should work for @scoped packages", async () => { - await rm(join(await realpath(tmpdir()), "@withfig"), { force: true, recursive: true }); // without cache const withoutCache = spawn({ - cmd: [bunExe(), "x", "@withfig/autocomplete-tools", "--help"], + cmd: [bunExe(), "--bun", "x", "@withfig/autocomplete-tools", "--help"], cwd: x_dir, stdout: "pipe", stdin: "pipe", @@ -112,12 +119,12 @@ it("should work for @scoped packages", async () => { 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"); + expect(out.trim()).toContain("Usage: @withfig/autocomplete-tools"); expect(await withoutCache.exited).toBe(0); // cached const cached = spawn({ - cmd: [bunExe(), "x", "@withfig/autocomplete-tools", "--help"], + cmd: [bunExe(), "--bun", "x", "@withfig/autocomplete-tools", "--help"], cwd: x_dir, stdout: "pipe", stdin: "pipe", @@ -131,7 +138,8 @@ it("should work for @scoped packages", async () => { expect(err).not.toContain("panic:"); expect(cached.stdout).toBeDefined(); out = await new Response(cached.stdout).text(); - expect(out.trim()).toContain("Usage: @withfig/autocomplete-tool"); + console.log({ out, err }); + expect(out.trim()).toContain("Usage: @withfig/autocomplete-tools"); expect(await cached.exited).toBe(0); }); @@ -165,7 +173,6 @@ console.log( }); it("should work for github repository", async () => { - await rm(join(await realpath(tmpdir()), "github:piuccio"), { force: true, recursive: true }); // without cache const withoutCache = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay", "--help"], @@ -182,7 +189,7 @@ it("should work for github repository", async () => { expect(err).not.toContain("panic:"); expect(withoutCache.stdout).toBeDefined(); let out = await new Response(withoutCache.stdout).text(); - expect(out.trim()).toContain("Usage: cowsay"); + expect(out.trim()).toContain("Usage: " + (isWindows ? "cli.js" : "cowsay")); expect(await withoutCache.exited).toBe(0); // cached @@ -201,12 +208,11 @@ it("should work for github repository", async () => { expect(err).not.toContain("panic:"); expect(cached.stdout).toBeDefined(); out = await new Response(cached.stdout).text(); - expect(out.trim()).toContain("Usage: cowsay"); + expect(out.trim()).toContain("Usage: " + (isWindows ? "cli.js" : "cowsay")); expect(await cached.exited).toBe(0); }); it("should work for github repository with committish", async () => { - await rm(join(await realpath(tmpdir()), "github:piuccio"), { force: true, recursive: true }); const withoutCache = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"], cwd: x_dir, @@ -222,6 +228,7 @@ it("should work for github repository with committish", async () => { expect(err).not.toContain("panic:"); expect(withoutCache.stdout).toBeDefined(); let out = await new Response(withoutCache.stdout).text(); + if (!out) console.log(err); expect(out.trim()).toContain("hello bun!"); expect(await withoutCache.exited).toBe(0); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index a3bfb09a96ae3..af22b87fa4934 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -1,12 +1,13 @@ import { file, spawn } from "bun"; -import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; +import { bunExe, bunEnv as env, isWindows, toBeValidBin, toHaveBins } from "harness"; import { join } from "path"; -import { mkdtempSync, realpathSync } from "fs"; +import { mkdtempSync, realpathSync, copyFileSync, mkdirSync } from "fs"; import { rm, writeFile, mkdir, exists, cp } from "fs/promises"; import { readdirSorted } from "../dummy.registry"; import { tmpdir } from "os"; import { fork, ChildProcess } from "child_process"; import { beforeAll, afterAll, beforeEach, afterEach, test, expect, describe } from "bun:test"; +import { f } from "js/bun/http/js-sink-sourmap-fixture/index.mjs"; expect.extend({ toBeValidBin, @@ -51,7 +52,7 @@ registry = "http://localhost:${port}/" }); afterEach(async () => { - await rm(packageDir, { force: true, recursive: true }); + 0 && (await rm(packageDir, { force: true, recursive: true })); }); describe.each(["--production", "without --production"])("%s", flag => { @@ -6616,3 +6617,159 @@ describe("yarn tests", () => { expect(await exited).toBe(0); }); }); + +// This test is to verify that BinLinkingShim.zig creates correct shim files as +// well as bun_shim_impl.exe works in various edge cases. There are many fast +// paths for many many cases. +test.if(isWindows)( + "windows bin linking shim should work", + async () => { + expect(process.platform).toBe("win32"); // extra check + await writeFile( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "bunx-bins": "*", + }, + }), + ); + console.log(packageDir); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--dev"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + var err = await new Response(stderr).text(); + var out = await new Response(stdout).text(); + console.log(err); + 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([ + "", + " + bunx-bins@1.0.0", + "", + expect.stringContaining("1 package installed"), + ]); + + const temp_bin_dir = join(packageDir, "temp"); + mkdirSync(temp_bin_dir); + + for (let i = 1; i <= 7; i++) { + const target = join(temp_bin_dir, "a".repeat(i) + ".exe"); + copyFileSync(bunExe(), target); + } + + copyFileSync(join(packageDir, "node_modules\\bunx-bins\\native.exe"), join(temp_bin_dir, "native.exe")); + + const PATH = process.env.PATH + ";" + temp_bin_dir; + + const bins = [ + { bin: "bin1", name: "bin1" }, + { bin: "bin2", name: "bin2" }, + { bin: "bin3", name: "bin3" }, + { bin: "bin4", name: "bin4" }, + { bin: "bin5", name: "bin5" }, + { bin: "bin6", name: "bin6" }, + { bin: "bin7", name: "bin7" }, + { bin: "bin-node", name: "bin-node" }, + { bin: "bin-bun", name: "bin-bun" }, + { bin: "bin-py", name: "bin-py" }, + { bin: "native", name: "exe" }, + { bin: "uses-native", name: `exe ${packageDir}\\node_modules\\bunx-bins\\uses-native.ts` }, + ]; + + // `bun run ${bin} arg1 arg2` + for (const { bin, name } of bins) { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "run", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: { + ...env, + Path: PATH, + }, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + } + + // `bun --bun run ${bin} arg1 arg2` + for (const { bin, name } of bins) { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "--bun", "run", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: { + ...env, + Path: PATH, + }, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + } + + // `bun --bun x ${bin} arg1 arg2` + for (const { bin, name } of bins) { + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "--bun", "x", bin, "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: { + ...env, + Path: PATH, + }, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + } + + // `${bin} arg1 arg2` + for (const { bin, name } of bins) { + var { stdout, stderr, exited } = spawn({ + cmd: [join(packageDir, "node_modules", ".bin", bin + ".exe"), "arg1", "arg2"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: { + ...env, + Path: PATH, + }, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.trim()).toBe(""); + const out = await new Response(stdout).text(); + expect(out.trim()).toBe(`i am ${name} arg1 arg2`); + expect(await exited).toBe(0); + } + }, + 60_000, +); diff --git a/test/cli/install/registry/packages/bunx-bins/bunx-bins-1.0.0.tgz b/test/cli/install/registry/packages/bunx-bins/bunx-bins-1.0.0.tgz new file mode 100644 index 0000000000000..7a49c5da0d665 Binary files /dev/null and b/test/cli/install/registry/packages/bunx-bins/bunx-bins-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/bunx-bins/package.json b/test/cli/install/registry/packages/bunx-bins/package.json new file mode 100644 index 0000000000000..689eb6a10720a --- /dev/null +++ b/test/cli/install/registry/packages/bunx-bins/package.json @@ -0,0 +1,53 @@ +{ + "name": "bunx-bins", + "versions": { + "1.0.0": { + "name": "bunx-bins", + "version": "1.0.0", + "bin": { + "bin-node": "bin-node.js", + "bin-bun": "bin-bun.ts", + "bin-bun-run": "bin-bun-run.ts", + "bin-py": "bin-py.py", + "bin1": "bin1.ts", + "bin2": "bin2.ts", + "bin3": "bin3.ts", + "bin4": "bin4.ts", + "bin5": "bin5.ts", + "bin6": "bin6.ts", + "bin7": "bin7.ts", + "native": "native.exe", + "uses-native": "uses-native.ts", + "no-shebang": "no-shebang.ts" + }, + "_id": "bunx-bins@1.0.0", + "_nodeVersion": "21.7.1", + "_npmVersion": "10.5.0", + "dist": { + "shasum": "1809cf2217354265981106f91617b87381abad75", + "tarball": "http://localhost:4873/bunx-bins/-/bunx-bins-1.0.0.tgz" + }, + "contributors": [] + } + }, + "time": { + "modified": "2024-03-26T00:02:03.432Z", + "created": "2024-03-26T00:02:03.432Z", + "1.0.0": "2024-03-26T00:02:03.432Z" + }, + "users": {}, + "dist-tags": { + "latest": "1.0.0" + }, + "_uplinks": {}, + "_distfiles": {}, + "_attachments": { + "bunx-bins-1.0.0.tgz": { + "shasum": "c23382ce43a04a94035050ac736f04179a8639ff", + "version": "1.0.0" + } + }, + "_rev": "", + "_id": "bunx-bins", + "readme": "ERROR: No README data found!" +} \ No newline at end of file diff --git a/test/js/bun/util/highlighter.test.ts b/test/js/bun/util/highlighter.test.ts index af3b7abb2f5a6..422d6bfe50ff0 100644 --- a/test/js/bun/util/highlighter.test.ts +++ b/test/js/bun/util/highlighter.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { readFileSync, writeFileSync } from "fs"; + // @ts-expect-error const highlighter: (code: string) => string = globalThis[Symbol.for("Bun.lazy")]("unstable_syntaxHighlight");