mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
8 Commits
patch-1
...
riskymh/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c1177f9e | ||
|
|
8820043ce6 | ||
|
|
ee0af66e4a | ||
|
|
3829b62c8e | ||
|
|
943794d8ba | ||
|
|
f2e85408ab | ||
|
|
0bff5aa266 | ||
|
|
ab6a51ccd0 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -168,5 +168,5 @@
|
||||
"WebKit/WebInspectorUI": true,
|
||||
},
|
||||
"git.detectSubmodules": false,
|
||||
// "bun.test.customScript": "./build/debug/bun-debug test"
|
||||
"bun.test.customScript": "./build/debug/bun-debug test"
|
||||
}
|
||||
|
||||
@@ -350,6 +350,7 @@ src/cli/install_command.zig
|
||||
src/cli/install_completions_command.zig
|
||||
src/cli/link_command.zig
|
||||
src/cli/list-of-yarn-commands.zig
|
||||
src/cli/node_command.zig
|
||||
src/cli/outdated_command.zig
|
||||
src/cli/pack_command.zig
|
||||
src/cli/package_manager_command.zig
|
||||
|
||||
17
src/cli.zig
17
src/cli.zig
@@ -91,6 +91,7 @@ pub const PackCommand = @import("./cli/pack_command.zig").PackCommand;
|
||||
pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
|
||||
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
|
||||
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
|
||||
pub const NodeCommand = @import("./cli/node_command.zig").NodeCommand;
|
||||
|
||||
pub const Arguments = @import("./cli/Arguments.zig");
|
||||
|
||||
@@ -566,6 +567,8 @@ pub const Command = struct {
|
||||
RootCommandMatcher.case("run") => .RunCommand,
|
||||
RootCommandMatcher.case("help") => .HelpCommand,
|
||||
|
||||
RootCommandMatcher.case("node") => .NodeCommand,
|
||||
|
||||
RootCommandMatcher.case("exec") => .ExecCommand,
|
||||
|
||||
RootCommandMatcher.case("outdated") => .OutdatedCommand,
|
||||
@@ -612,6 +615,8 @@ pub const Command = struct {
|
||||
"x",
|
||||
"repl",
|
||||
"info",
|
||||
"why",
|
||||
"node",
|
||||
};
|
||||
|
||||
const reject_list = default_completions_list ++ [_]string{
|
||||
@@ -803,6 +808,13 @@ pub const Command = struct {
|
||||
try TestCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.NodeCommand => {
|
||||
if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .NodeCommand) unreachable;
|
||||
const ctx = try Command.init(allocator, log, .NodeCommand);
|
||||
|
||||
try NodeCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.GetCompletionsCommand => {
|
||||
if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .GetCompletionsCommand) unreachable;
|
||||
try @"bun getcompletes"(allocator, log);
|
||||
@@ -920,6 +932,7 @@ pub const Command = struct {
|
||||
InstallCommand,
|
||||
InstallCompletionsCommand,
|
||||
LinkCommand,
|
||||
NodeCommand,
|
||||
PackageManagerCommand,
|
||||
RemoveCommand,
|
||||
RunCommand,
|
||||
@@ -957,6 +970,7 @@ pub const Command = struct {
|
||||
.InstallCommand => 'i',
|
||||
.InstallCompletionsCommand => 'C',
|
||||
.LinkCommand => 'l',
|
||||
.NodeCommand => 'N',
|
||||
.PackageManagerCommand => 'P',
|
||||
.RemoveCommand => 'R',
|
||||
.RunCommand => 'r',
|
||||
@@ -985,6 +999,7 @@ pub const Command = struct {
|
||||
.BuildCommand => Arguments.build_params,
|
||||
.TestCommand => Arguments.test_params,
|
||||
.BunxCommand => Arguments.run_params,
|
||||
.NodeCommand => Arguments.auto_only_params,
|
||||
else => Arguments.base_params_ ++ Arguments.runtime_params_ ++ Arguments.transpiler_params_,
|
||||
};
|
||||
}
|
||||
@@ -1340,6 +1355,7 @@ pub const Command = struct {
|
||||
.AutoCommand = true,
|
||||
.RunCommand = true,
|
||||
.RunAsNodeCommand = true,
|
||||
.NodeCommand = true,
|
||||
.OutdatedCommand = true,
|
||||
.UpdateInteractiveCommand = true,
|
||||
.PublishCommand = true,
|
||||
@@ -1380,6 +1396,7 @@ pub const Command = struct {
|
||||
.RemoveCommand = false,
|
||||
.UnlinkCommand = false,
|
||||
.UpdateCommand = false,
|
||||
.NodeCommand = false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
.stop_after_positional_at = switch (cmd) {
|
||||
.RunCommand => 2,
|
||||
.AutoCommand, .RunAsNodeCommand => 1,
|
||||
.NodeCommand => 0,
|
||||
else => 0,
|
||||
},
|
||||
}) catch |err| {
|
||||
|
||||
988
src/cli/node_command.zig
Normal file
988
src/cli/node_command.zig
Normal file
@@ -0,0 +1,988 @@
|
||||
pub const NodeCommand = struct {
|
||||
pub fn exec(ctx: Command.Context) !void {
|
||||
const relevant_args = bun.argv[2..];
|
||||
|
||||
if (relevant_args.len == 0) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const first_arg = relevant_args[0];
|
||||
|
||||
if (strings.eqlComptime(first_arg, "bun")) {
|
||||
try handleNodeAlias(ctx, "bun");
|
||||
return;
|
||||
}
|
||||
|
||||
if (looksLikeVersion(first_arg)) {
|
||||
if (relevant_args.len == 1) {
|
||||
try installNodeVersion(ctx, first_arg, true);
|
||||
} else {
|
||||
try runWithNodeVersion(ctx, first_arg, relevant_args[1..]);
|
||||
}
|
||||
} else {
|
||||
try runWithDefaultNode(ctx, relevant_args);
|
||||
}
|
||||
}
|
||||
|
||||
fn printHelp() void {
|
||||
Output.prettyln("<r><b>bun node<r> <d>v" ++ Global.package_json_version_with_sha ++ "<r>", .{});
|
||||
|
||||
Output.prettyln(
|
||||
\\Install & manage Node.js versions or configure node to use Bun instead.
|
||||
\\
|
||||
\\<b>Examples:<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <cyan>latest<r> <d>Install latest Node.js and set as default<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <cyan>lts<r> <d>Install latest LTS Node.js and set as default<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <blue>24<r> <d>Install latest Node.js v24.x and set as default<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <blue>foo.js<r> <d>Run foo.js with default Node.js<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <blue>24.0.0 foo.js<r> <d>Run foo.js with Node.js v24.0.0<r>
|
||||
\\ <d>$<r> <b><green>bun node<r> <cyan>bun<r> <d>Make 'node' command run Bun instead<r>
|
||||
\\
|
||||
\\<d><b>Note:<r><d> Latest version information is cached for 24 hours.<r>
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
|
||||
fn looksLikeVersion(arg: []const u8) bool {
|
||||
if (arg.len == 0) return false;
|
||||
|
||||
if (strings.eqlComptime(arg, "latest") or
|
||||
strings.eqlComptime(arg, "lts") or
|
||||
strings.eqlComptime(arg, "current"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (arg[0] != 'v' and !std.ascii.isDigit(arg[0])) return false;
|
||||
|
||||
const start: usize = if (arg[0] == 'v') 1 else 0;
|
||||
for (arg[start..]) |c| {
|
||||
if (!std.ascii.isDigit(c) and c != '.' and c != '-') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn getNodeCacheDir(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var global_dir = try Options.openGlobalDir("");
|
||||
defer global_dir.close();
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
const path = try bun.getFdPath(bun.FD.fromStdDir(global_dir), &path_buf);
|
||||
const result = try std.fs.path.join(allocator, &.{ path, "node" });
|
||||
return result;
|
||||
}
|
||||
|
||||
fn getNodeBinDir(allocator: std.mem.Allocator) ![]const u8 {
|
||||
if (bun.getenvZ("BUN_INSTALL_BIN")) |bin_dir| {
|
||||
return try allocator.dupe(u8, bin_dir);
|
||||
}
|
||||
|
||||
const install_dir = bun.getenvZ("BUN_INSTALL") orelse brk: {
|
||||
const home = bun.getenvZ("HOME") orelse bun.getenvZ("USERPROFILE") orelse {
|
||||
Output.prettyErrorln("<r><red>error:<r> unable to find home directory", .{});
|
||||
Global.crash();
|
||||
};
|
||||
break :brk try std.fs.path.join(allocator, &.{ home, ".bun" });
|
||||
};
|
||||
|
||||
if (bun.getenvZ("BUN_INSTALL") != null) {
|
||||
return try std.fs.path.join(allocator, &.{ install_dir, "bin" });
|
||||
} else {
|
||||
defer allocator.free(install_dir);
|
||||
return try std.fs.path.join(allocator, &.{ install_dir, "bin" });
|
||||
}
|
||||
}
|
||||
|
||||
fn normalizeVersion(version: []const u8) []const u8 {
|
||||
if (version.len > 0 and version[0] == 'v') {
|
||||
return version[1..];
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
fn resolveVersion(allocator: std.mem.Allocator, version_spec: []const u8) ![]const u8 {
|
||||
const normalized = normalizeVersion(version_spec);
|
||||
|
||||
if (strings.eqlComptime(normalized, "latest")) {
|
||||
return try fetchNodeVersion(allocator, .latest, null);
|
||||
} else if (strings.eqlComptime(normalized, "lts")) {
|
||||
return try fetchNodeVersion(allocator, .lts, null);
|
||||
} else if (strings.eqlComptime(normalized, "current")) {
|
||||
return try fetchNodeVersion(allocator, .latest, null);
|
||||
}
|
||||
|
||||
if (strings.indexOf(normalized, ".")) |_| {
|
||||
return try allocator.dupe(u8, normalized);
|
||||
}
|
||||
|
||||
return try fetchNodeVersion(allocator, .major, normalized);
|
||||
}
|
||||
|
||||
fn getCachedVersionInfo(allocator: std.mem.Allocator) !?[]const u8 {
|
||||
const cache_dir = try getNodeCacheDir(allocator);
|
||||
defer allocator.free(cache_dir);
|
||||
|
||||
const cache_file = try std.fs.path.join(allocator, &.{ cache_dir, ".version-cache" });
|
||||
defer allocator.free(cache_file);
|
||||
|
||||
var file = std.fs.openFileAbsolute(cache_file, .{}) catch return null;
|
||||
defer file.close();
|
||||
|
||||
const stat = try file.stat();
|
||||
const now = std.time.timestamp();
|
||||
const age = now - @divFloor(stat.mtime, std.time.ns_per_s);
|
||||
|
||||
if (age > 24 * 60 * 60) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
|
||||
return content;
|
||||
}
|
||||
|
||||
fn saveCachedVersionInfo(allocator: std.mem.Allocator, data: []const u8) !void {
|
||||
const cache_dir = try getNodeCacheDir(allocator);
|
||||
defer allocator.free(cache_dir);
|
||||
|
||||
const cache_dirZ = try allocator.dupeZ(u8, cache_dir);
|
||||
defer allocator.free(cache_dirZ);
|
||||
|
||||
switch (bun.sys.mkdir(cache_dirZ, 0o755)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (err.errno != @intFromEnum(bun.sys.E.EXIST)) {
|
||||
return err.toZigErr();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const cache_file = try std.fs.path.join(allocator, &.{ cache_dir, ".version-cache" });
|
||||
defer allocator.free(cache_file);
|
||||
|
||||
const cache_fileZ = try allocator.dupeZ(u8, cache_file);
|
||||
defer allocator.free(cache_fileZ);
|
||||
|
||||
const fd = switch (bun.sys.open(cache_fileZ, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o644)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| return err.toZigErr(),
|
||||
};
|
||||
defer fd.close();
|
||||
|
||||
var file = bun.sys.File{ .handle = fd };
|
||||
switch (file.writeAll(data)) {
|
||||
.result => {},
|
||||
.err => |err| return err.toZigErr(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetchNodeVersion(allocator: std.mem.Allocator, filter: VersionFilter, major_version: ?[]const u8) ![]const u8 {
|
||||
if (try getCachedVersionInfo(allocator)) |cached| {
|
||||
defer allocator.free(cached);
|
||||
return try parseVersionFromCache(allocator, cached, filter, major_version);
|
||||
}
|
||||
|
||||
const version_data = try fetchNodeVersionsFromAPI(allocator);
|
||||
defer allocator.free(version_data);
|
||||
|
||||
saveCachedVersionInfo(allocator, version_data) catch {};
|
||||
|
||||
return try parseVersionFromCache(allocator, version_data, filter, major_version);
|
||||
}
|
||||
|
||||
fn fetchNodeVersionsFromAPI(allocator: std.mem.Allocator) ![]const u8 {
|
||||
const url = URL.parse("https://nodejs.org/dist/index.json");
|
||||
var response_buffer = try MutableString.init(allocator, 0);
|
||||
defer response_buffer.deinit();
|
||||
|
||||
var req = AsyncHTTP.initSync(allocator, .GET, url, .{}, "", &response_buffer, "", null, null, .follow);
|
||||
|
||||
const response = req.sendSync() catch |err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to fetch Node.js versions from API: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
if (response.status_code != 200) {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to fetch Node.js versions: HTTP {d}", .{response.status_code});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
return try allocator.dupe(u8, response_buffer.list.items);
|
||||
}
|
||||
|
||||
const VersionFilter = enum {
|
||||
latest,
|
||||
lts,
|
||||
major,
|
||||
};
|
||||
|
||||
fn parseVersionFromCache(allocator: std.mem.Allocator, json_data: []const u8, filter: VersionFilter, major_version: ?[]const u8) ![]const u8 {
|
||||
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_data, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const array = switch (parsed.value) {
|
||||
.array => |arr| arr,
|
||||
else => {
|
||||
Output.prettyErrorln("<r><red>error:<r> Invalid Node.js version data format", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
for (array.items) |item| {
|
||||
const obj = switch (item) {
|
||||
.object => |o| o,
|
||||
else => continue,
|
||||
};
|
||||
|
||||
const version_value = obj.get("version") orelse continue;
|
||||
const version_str = switch (version_value) {
|
||||
.string => |s| s,
|
||||
else => continue,
|
||||
};
|
||||
|
||||
var version = version_str;
|
||||
if (version.len > 0 and version[0] == 'v') {
|
||||
version = version[1..];
|
||||
}
|
||||
|
||||
switch (filter) {
|
||||
.latest => {
|
||||
return try allocator.dupe(u8, version);
|
||||
},
|
||||
.lts => {
|
||||
if (obj.get("lts")) |lts_val| {
|
||||
switch (lts_val) {
|
||||
.string => return try allocator.dupe(u8, version),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
.major => {
|
||||
if (strings.indexOf(version, ".")) |dot_idx| {
|
||||
const major = version[0..dot_idx];
|
||||
if (strings.eql(major, major_version.?)) {
|
||||
return try allocator.dupe(u8, version);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter) {
|
||||
.latest => {
|
||||
Output.prettyErrorln("<r><red>error:<r> Could not determine latest Node.js version", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
.lts => {
|
||||
Output.prettyErrorln("<r><red>error:<r> Could not find LTS version in Node.js version data", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
.major => {
|
||||
Output.prettyErrorln("<r><red>error:<r> Node.js version {s} not found", .{major_version.?});
|
||||
Global.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const node_platform = blk: {
|
||||
if (Env.isMac) {
|
||||
if (Env.isAarch64) {
|
||||
break :blk "darwin-arm64";
|
||||
} else {
|
||||
break :blk "darwin-x64";
|
||||
}
|
||||
} else if (Env.isLinux) {
|
||||
if (Env.isAarch64) {
|
||||
break :blk "linux-arm64";
|
||||
} else {
|
||||
break :blk "linux-x64";
|
||||
}
|
||||
} else if (Env.isWindows) {
|
||||
if (Env.isAarch64) {
|
||||
break :blk "win-arm64";
|
||||
} else {
|
||||
break :blk "win-x64";
|
||||
}
|
||||
}
|
||||
break :blk "unknown";
|
||||
};
|
||||
|
||||
const node_binary_name = if (Env.isWindows) "node.exe" else "node";
|
||||
const node_archive_ext = if (Env.isWindows) "zip" else "tar.gz";
|
||||
const node_archive_url_fmt = "https://nodejs.org/dist/v{s}/node-v{s}-" ++ node_platform ++ "." ++ node_archive_ext;
|
||||
|
||||
fn downloadNode(allocator: std.mem.Allocator, version: []const u8, dest_dir: []const u8) !void {
|
||||
const url_str = try std.fmt.allocPrint(allocator, node_archive_url_fmt, .{ version, version });
|
||||
defer allocator.free(url_str);
|
||||
|
||||
Output.prettyln("<r><green>Downloading<r> Node.js v{s}", .{version});
|
||||
Output.flush();
|
||||
|
||||
try downloadNodeInternal(allocator, url_str, dest_dir, version, false);
|
||||
}
|
||||
|
||||
fn downloadNodeSilent(allocator: std.mem.Allocator, version: []const u8, dest_dir: []const u8) !void {
|
||||
const url_str = try std.fmt.allocPrint(allocator, node_archive_url_fmt, .{ version, version });
|
||||
defer allocator.free(url_str);
|
||||
|
||||
if (Output.enable_ansi_colors_stderr) {
|
||||
Output.prettyError("<r><green>Downloading<r> Node.js v{s}", .{version});
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
try downloadNodeInternal(allocator, url_str, dest_dir, version, true);
|
||||
|
||||
if (Output.enable_ansi_colors_stderr) {
|
||||
Output.prettyError("\r\x1b[K", .{});
|
||||
Output.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn downloadNodeInternal(allocator: std.mem.Allocator, url_str: []const u8, dest_dir: []const u8, version: []const u8, is_quiet: bool) !void {
|
||||
const parent_dir = std.fs.path.dirname(dest_dir);
|
||||
if (parent_dir) |parent| {
|
||||
std.fs.cwd().makePath(parent) catch {};
|
||||
}
|
||||
|
||||
const dest_dirZ = try allocator.dupeZ(u8, dest_dir);
|
||||
defer allocator.free(dest_dirZ);
|
||||
|
||||
switch (bun.sys.mkdir(dest_dirZ, 0o755)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (err.errno != @intFromEnum(bun.sys.E.EXIST)) {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to create directory {s}: {}", .{ dest_dir, err });
|
||||
Global.exit(1);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const temp_archive = try std.fmt.allocPrint(allocator, "{s}.download." ++ node_archive_ext, .{dest_dir});
|
||||
defer allocator.free(temp_archive);
|
||||
defer {
|
||||
if (allocator.dupeZ(u8, temp_archive)) |temp_archiveZ| {
|
||||
defer allocator.free(temp_archiveZ);
|
||||
_ = bun.sys.unlink(temp_archiveZ);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
const url = URL.parse(url_str);
|
||||
var response_buffer = try MutableString.init(allocator, 0);
|
||||
defer response_buffer.deinit();
|
||||
|
||||
var req = AsyncHTTP.initSync(allocator, .GET, url, .{}, "", &response_buffer, "", null, null, .follow);
|
||||
|
||||
const response = req.sendSync() catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to download Node.js v{s}: {}", .{ version, err });
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
if (response.status_code != 200) {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to download Node.js v{s}: HTTP {d}", .{ version, response.status_code });
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const temp_archiveZ = try allocator.dupeZ(u8, temp_archive);
|
||||
defer allocator.free(temp_archiveZ);
|
||||
|
||||
const fd = switch (bun.sys.open(temp_archiveZ, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o644)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to create file {s}: {}", .{ temp_archive, err });
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer fd.close();
|
||||
|
||||
var file = bun.sys.File{ .handle = fd };
|
||||
switch (file.writeAll(response_buffer.list.items)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to write archive {s}: {}", .{ temp_archive, err });
|
||||
Global.exit(1);
|
||||
},
|
||||
}
|
||||
|
||||
try extractNodeArchive(allocator, temp_archive, dest_dir, version, is_quiet);
|
||||
}
|
||||
|
||||
fn extractNodeArchive(allocator: std.mem.Allocator, archive_path: []const u8, dest_dir: []const u8, version: []const u8, is_quiet: bool) !void {
|
||||
_ = version;
|
||||
const archive_data = std.fs.cwd().readFileAlloc(allocator, archive_path, std.math.maxInt(usize)) catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to read archive {s}: {}", .{ archive_path, err });
|
||||
Global.exit(1);
|
||||
};
|
||||
defer allocator.free(archive_data);
|
||||
|
||||
var dest_dir_handle = std.fs.openDirAbsolute(dest_dir, .{}) catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to open destination directory {s}: {}", .{ dest_dir, err });
|
||||
Global.exit(1);
|
||||
};
|
||||
defer dest_dir_handle.close();
|
||||
|
||||
_ = Archiver.extractToDir(
|
||||
archive_data,
|
||||
dest_dir_handle,
|
||||
null,
|
||||
void,
|
||||
{},
|
||||
.{
|
||||
.depth_to_skip = 1,
|
||||
.close_handles = true,
|
||||
},
|
||||
) catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to extract archive {s}: {}", .{ archive_path, err });
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
const src_binary = if (Env.isWindows)
|
||||
try std.fs.path.join(allocator, &.{ dest_dir, node_binary_name })
|
||||
else
|
||||
try std.fs.path.join(allocator, &.{ dest_dir, "bin", node_binary_name });
|
||||
defer allocator.free(src_binary);
|
||||
|
||||
const dest_binary = try std.fs.path.join(allocator, &.{ dest_dir, node_binary_name });
|
||||
defer allocator.free(dest_binary);
|
||||
|
||||
if (!Env.isWindows or !strings.eql(src_binary, dest_binary)) {
|
||||
if (Env.isWindows) {
|
||||
var src_buf: bun.OSPathBuffer = undefined;
|
||||
var dest_buf: bun.OSPathBuffer = undefined;
|
||||
const src_path = bun.strings.toWPathNormalized(&src_buf, src_binary);
|
||||
const dest_path = bun.strings.toWPathNormalized(&dest_buf, dest_binary);
|
||||
|
||||
bun.copyFile(src_path, dest_path).unwrap() catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy binary: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
} else {
|
||||
const src_fd = switch (bun.sys.open(try allocator.dupeZ(u8, src_binary), bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to open source binary: {}", .{err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer src_fd.close();
|
||||
|
||||
const dest_fd = switch (bun.sys.open(try allocator.dupeZ(u8, dest_binary), bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o755)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to create dest binary: {}", .{err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer dest_fd.close();
|
||||
|
||||
bun.copyFile(src_fd, dest_fd).unwrap() catch |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy binary: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Env.isPosix) {
|
||||
const dest_binaryZ = try allocator.dupeZ(u8, dest_binary);
|
||||
defer allocator.free(dest_binaryZ);
|
||||
|
||||
switch (bun.sys.chmod(dest_binaryZ, 0o755)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (is_quiet) Output.prettyErrorln("", .{});
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to chmod {s}: {}", .{ dest_binary, err });
|
||||
Global.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn installNodeVersion(ctx: Command.Context, version_spec: []const u8, set_as_default: bool) !void {
|
||||
const allocator = ctx.allocator;
|
||||
const version = try resolveVersion(allocator, version_spec);
|
||||
defer allocator.free(version);
|
||||
|
||||
const cache_dir = try getNodeCacheDir(allocator);
|
||||
defer allocator.free(cache_dir);
|
||||
|
||||
const version_dir = try std.fmt.allocPrint(allocator, "{s}/node-{s}", .{ cache_dir, version });
|
||||
defer allocator.free(version_dir);
|
||||
|
||||
const version_binary = try std.fmt.allocPrintZ(allocator, "{s}/" ++ node_binary_name, .{version_dir});
|
||||
defer allocator.free(version_binary);
|
||||
|
||||
const access_result = if (Env.isWindows) blk: {
|
||||
var buf: bun.OSPathBuffer = undefined;
|
||||
const path = bun.strings.toWPathNormalized(&buf, version_binary);
|
||||
break :blk bun.sys.access(path, 0);
|
||||
} else bun.sys.access(version_binary, 0);
|
||||
|
||||
if (access_result == .result) {
|
||||
if (set_as_default) {
|
||||
Output.prettyln("<r><green>✓<r> Node.js v{s} is already installed", .{version});
|
||||
}
|
||||
} else {
|
||||
if (set_as_default) {
|
||||
try downloadNode(allocator, version, version_dir);
|
||||
Output.prettyln("<r><green>✓<r> Successfully installed Node.js v{s}", .{version});
|
||||
} else {
|
||||
try downloadNodeSilent(allocator, version, version_dir);
|
||||
}
|
||||
}
|
||||
|
||||
if (set_as_default) {
|
||||
try updateGlobalNodeSymlink(ctx, version);
|
||||
Output.prettyln("<r><green>✓<r> Set Node.js v{s} as default", .{version});
|
||||
|
||||
try checkPathPriority(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
fn updateGlobalNodeSymlink(ctx: Command.Context, version: []const u8) !void {
|
||||
const allocator = ctx.allocator;
|
||||
|
||||
const cache_dir = try getNodeCacheDir(allocator);
|
||||
defer allocator.free(cache_dir);
|
||||
|
||||
const bin_dir = try getNodeBinDir(allocator);
|
||||
defer allocator.free(bin_dir);
|
||||
|
||||
const bin_dirZ = try allocator.dupeZ(u8, bin_dir);
|
||||
defer allocator.free(bin_dirZ);
|
||||
|
||||
switch (bun.sys.mkdir(bin_dirZ, 0o755)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (err.errno != @intFromEnum(bun.sys.E.EXIST)) {
|
||||
return err.toZigErr();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const version_binary = try std.fmt.allocPrintZ(allocator, "{s}/node-{s}/" ++ node_binary_name, .{ cache_dir, version });
|
||||
defer allocator.free(version_binary);
|
||||
|
||||
const access_result2 = if (Env.isWindows) blk: {
|
||||
var buf: bun.OSPathBuffer = undefined;
|
||||
const path = bun.strings.toWPathNormalized(&buf, version_binary);
|
||||
break :blk bun.sys.access(path, 0);
|
||||
} else bun.sys.access(version_binary, 0);
|
||||
|
||||
if (access_result2 != .result) {
|
||||
const version_dir = try std.fmt.allocPrint(allocator, "{s}/node-{s}", .{ cache_dir, version });
|
||||
defer allocator.free(version_dir);
|
||||
|
||||
Output.prettyErrorln("<r><yellow>warn:<r> Node.js v{s} binary not found, downloading...", .{version});
|
||||
try downloadNode(allocator, version, version_dir);
|
||||
}
|
||||
|
||||
const global_binary = try std.fs.path.joinZ(allocator, &.{ bin_dir, node_binary_name });
|
||||
defer allocator.free(global_binary);
|
||||
|
||||
_ = bun.sys.unlink(global_binary);
|
||||
|
||||
switch (bun.sys.link(u8, version_binary, global_binary)) {
|
||||
.result => {},
|
||||
.err => |err| switch (err.getErrno()) {
|
||||
.XDEV => {
|
||||
if (Env.isWindows) {
|
||||
var src_buf: bun.OSPathBuffer = undefined;
|
||||
var dest_buf: bun.OSPathBuffer = undefined;
|
||||
const src_path = bun.strings.toWPathNormalized(&src_buf, version_binary);
|
||||
const dest_path = bun.strings.toWPathNormalized(&dest_buf, global_binary);
|
||||
|
||||
bun.copyFile(src_path, dest_path).unwrap() catch |copy_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy Node binary: {}", .{copy_err});
|
||||
Global.exit(1);
|
||||
};
|
||||
} else {
|
||||
const src_fd = switch (bun.sys.open(version_binary, bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |open_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to open source binary: {}", .{open_err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer src_fd.close();
|
||||
|
||||
const dest_fd = switch (bun.sys.open(global_binary, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o755)) {
|
||||
.result => |fd| fd,
|
||||
.err => |open_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to create dest binary: {}", .{open_err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer dest_fd.close();
|
||||
|
||||
bun.copyFile(src_fd, dest_fd).unwrap() catch |copy_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy Node binary: {}", .{copy_err});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
},
|
||||
else => return err.toZigErr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handleNodeAlias(ctx: Command.Context, target: []const u8) !void {
|
||||
const allocator = ctx.allocator;
|
||||
|
||||
if (!strings.eqlComptime(target, "bun")) {
|
||||
Output.prettyErrorln("<r><red>error:<r> only 'bun' is supported as alias target", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
|
||||
const bin_dir = try getNodeBinDir(allocator);
|
||||
defer allocator.free(bin_dir);
|
||||
|
||||
const bin_dirZ2 = try allocator.dupeZ(u8, bin_dir);
|
||||
defer allocator.free(bin_dirZ2);
|
||||
|
||||
switch (bun.sys.mkdir(bin_dirZ2, 0o755)) {
|
||||
.result => {},
|
||||
.err => |err| {
|
||||
if (err.errno != @intFromEnum(bun.sys.E.EXIST)) {
|
||||
return err.toZigErr();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const global_binary = try std.fs.path.joinZ(allocator, &.{ bin_dir, node_binary_name });
|
||||
defer allocator.free(global_binary);
|
||||
|
||||
const bun_exe = bun.selfExePath() catch {
|
||||
Output.prettyErrorln("<r><red>error:<r> failed to determine bun executable path", .{});
|
||||
Global.crash();
|
||||
};
|
||||
|
||||
_ = bun.sys.unlink(global_binary);
|
||||
|
||||
switch (bun.sys.link(u8, bun_exe, global_binary)) {
|
||||
.result => {},
|
||||
.err => |err| switch (err.getErrno()) {
|
||||
.XDEV => {
|
||||
if (Env.isWindows) {
|
||||
var src_buf: bun.OSPathBuffer = undefined;
|
||||
var dest_buf: bun.OSPathBuffer = undefined;
|
||||
const src_path = bun.strings.toWPathNormalized(&src_buf, bun_exe);
|
||||
const dest_path = bun.strings.toWPathNormalized(&dest_buf, global_binary);
|
||||
|
||||
bun.copyFile(src_path, dest_path).unwrap() catch |copy_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy Bun binary: {}", .{copy_err});
|
||||
Global.exit(1);
|
||||
};
|
||||
} else {
|
||||
const src_fd = switch (bun.sys.open(try allocator.dupeZ(u8, bun_exe), bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |open_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to open Bun binary: {}", .{open_err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer src_fd.close();
|
||||
|
||||
const dest_fd = switch (bun.sys.open(global_binary, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o755)) {
|
||||
.result => |fd| fd,
|
||||
.err => |open_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to create dest binary: {}", .{open_err});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer dest_fd.close();
|
||||
|
||||
bun.copyFile(src_fd, dest_fd).unwrap() catch |copy_err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to copy Bun binary: {}", .{copy_err});
|
||||
Global.exit(1);
|
||||
};
|
||||
}
|
||||
},
|
||||
else => return err.toZigErr(),
|
||||
},
|
||||
}
|
||||
|
||||
Output.prettyln("<r><green>✓<r> Successfully aliased 'node' to Bun", .{});
|
||||
Output.prettyln("<r><d>The 'node' command will now run Bun<r>", .{});
|
||||
|
||||
try checkPathPriority(allocator);
|
||||
}
|
||||
|
||||
fn runWithNodeVersion(ctx: Command.Context, version_spec: []const u8, args: []const []const u8) !void {
|
||||
const allocator = ctx.allocator;
|
||||
const version = try resolveVersion(allocator, version_spec);
|
||||
defer allocator.free(version);
|
||||
|
||||
const cache_dir = try getNodeCacheDir(allocator);
|
||||
defer allocator.free(cache_dir);
|
||||
|
||||
const version_binary = try std.fmt.allocPrint(allocator, "{s}/node-{s}/" ++ node_binary_name, .{ cache_dir, version });
|
||||
defer allocator.free(version_binary);
|
||||
|
||||
const version_binaryZ3 = try allocator.dupeZ(u8, version_binary);
|
||||
defer allocator.free(version_binaryZ3);
|
||||
|
||||
const access_result3 = if (Env.isWindows) blk: {
|
||||
var buf: bun.OSPathBuffer = undefined;
|
||||
const path = bun.strings.toWPathNormalized(&buf, version_binary);
|
||||
break :blk bun.sys.access(path, 0);
|
||||
} else bun.sys.access(version_binaryZ3, 0);
|
||||
|
||||
if (access_result3 == .result) {
|
||||
try runNode(allocator, version_binary, args);
|
||||
} else {
|
||||
try installNodeVersion(ctx, version_spec, false);
|
||||
try runNode(allocator, version_binary, args);
|
||||
}
|
||||
}
|
||||
|
||||
fn runWithDefaultNode(ctx: Command.Context, args: []const []const u8) !void {
|
||||
const allocator = ctx.allocator;
|
||||
|
||||
const bin_dir = try getNodeBinDir(allocator);
|
||||
defer allocator.free(bin_dir);
|
||||
|
||||
const node_symlink = try std.fs.path.joinZ(allocator, &.{ bin_dir, node_binary_name });
|
||||
defer allocator.free(node_symlink);
|
||||
|
||||
const access_result4 = if (Env.isWindows) blk: {
|
||||
var buf: bun.OSPathBuffer = undefined;
|
||||
const path = bun.strings.toWPathNormalized(&buf, node_symlink);
|
||||
break :blk bun.sys.access(path, 0);
|
||||
} else bun.sys.access(node_symlink, 0);
|
||||
|
||||
if (access_result4 == .result) {
|
||||
try runNode(allocator, node_symlink, args);
|
||||
return;
|
||||
}
|
||||
|
||||
var path_buf2: bun.PathBuffer = undefined;
|
||||
const path_env2 = bun.getenvZ("PATH") orelse "";
|
||||
var cwd_buf: bun.PathBuffer = undefined;
|
||||
const cwd_tmp = bun.getcwd(&cwd_buf) catch "";
|
||||
|
||||
if (which(&path_buf2, path_env2, cwd_tmp, "node")) |node_path| {
|
||||
try runNode(allocator, node_path, args);
|
||||
} else {
|
||||
Output.prettyErrorln("<r><red>error:<r> Node.js not found", .{});
|
||||
Output.prettyln("Run 'bun node lts' to install Node.js", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn runNode(allocator: std.mem.Allocator, node_path: []const u8, args: []const []const u8) !void {
|
||||
var argv = try allocator.alloc([]const u8, args.len + 1);
|
||||
defer allocator.free(argv);
|
||||
|
||||
argv[0] = node_path;
|
||||
for (args, 1..) |arg, i| {
|
||||
argv[i] = arg;
|
||||
}
|
||||
|
||||
const bin_dir = try getNodeBinDir(allocator);
|
||||
defer allocator.free(bin_dir);
|
||||
|
||||
const current_path = bun.getenvZ("PATH") orelse "";
|
||||
const new_path = try std.fmt.allocPrint(allocator, "{s}{c}{s}", .{ bin_dir, if (Env.isWindows) ';' else ':', current_path });
|
||||
defer allocator.free(new_path);
|
||||
|
||||
var env_map = try std.process.getEnvMap(allocator);
|
||||
defer env_map.deinit();
|
||||
try env_map.put("PATH", new_path);
|
||||
|
||||
const envp_count = env_map.count();
|
||||
const envp_buf = try allocator.allocSentinel(?[*:0]const u8, envp_count, null);
|
||||
{
|
||||
var it = env_map.iterator();
|
||||
var i: usize = 0;
|
||||
while (it.next()) |pair| : (i += 1) {
|
||||
const env_buf = try allocator.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0);
|
||||
bun.copy(u8, env_buf, pair.key_ptr.*);
|
||||
env_buf[pair.key_ptr.len] = '=';
|
||||
bun.copy(u8, env_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*);
|
||||
envp_buf[i] = env_buf.ptr;
|
||||
}
|
||||
}
|
||||
|
||||
const node_pathZ = try allocator.dupeZ(u8, node_path);
|
||||
defer allocator.free(node_pathZ);
|
||||
|
||||
const spawn_result = bun.spawnSync(&.{
|
||||
.argv = argv,
|
||||
.argv0 = node_pathZ,
|
||||
.envp = envp_buf,
|
||||
.cwd = try bun.getcwdAlloc(allocator),
|
||||
.stderr = .inherit,
|
||||
.stdout = .inherit,
|
||||
.stdin = .inherit,
|
||||
.use_execve_on_macos = true,
|
||||
.windows = if (Env.isWindows) .{
|
||||
.loop = bun.jsc.EventLoopHandle.init(bun.jsc.MiniEventLoop.initGlobal(null)),
|
||||
.hide_window = false,
|
||||
} else {},
|
||||
}) catch |err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to run Node.js: {}", .{err});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
switch (spawn_result) {
|
||||
.result => |result| {
|
||||
switch (result.status) {
|
||||
.exited => |exit| {
|
||||
Global.exit(exit.code);
|
||||
},
|
||||
.signaled => |signal| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Node.js terminated by signal: {}", .{signal});
|
||||
Global.exit(1);
|
||||
},
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to run Node.js: {}", .{err});
|
||||
Global.exit(1);
|
||||
},
|
||||
.running => {
|
||||
Global.exit(1);
|
||||
},
|
||||
}
|
||||
},
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("<r><red>error:<r> Failed to spawn Node.js: {}", .{err.toSystemError()});
|
||||
Global.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn checkPathPriority(allocator: std.mem.Allocator) !void {
|
||||
const bin_dir = try getNodeBinDir(allocator);
|
||||
defer allocator.free(bin_dir);
|
||||
|
||||
var path_buf: bun.PathBuffer = undefined;
|
||||
const path_env = bun.getenvZ("PATH") orelse "";
|
||||
var cwd_buf: bun.PathBuffer = undefined;
|
||||
const cwd = bun.getcwd(&cwd_buf) catch "";
|
||||
|
||||
if (which(&path_buf, path_env, cwd, "node")) |node_path| {
|
||||
const node_dir = std.fs.path.dirname(node_path) orelse "";
|
||||
|
||||
if (!strings.eql(node_dir, bin_dir)) {
|
||||
const inner_path_env = bun.getenvZ("PATH") orelse "";
|
||||
var path_entries = strings.split(inner_path_env, if (Env.isWindows) ";" else ":");
|
||||
|
||||
var found_bun_dir = false;
|
||||
var found_other_dir = false;
|
||||
|
||||
while (path_entries.next()) |entry| {
|
||||
if (strings.eql(entry, bin_dir)) {
|
||||
found_bun_dir = true;
|
||||
if (found_other_dir) {
|
||||
if (Env.isWindows) {
|
||||
const msg =
|
||||
\\
|
||||
\\<r><yellow>⚠️ Warning:<r> The 'node' command may not use the Bun-managed version
|
||||
\\ Found 'node' at: {s}
|
||||
\\ Bun's bin directory ({s}) appears after another 'node' in PATH
|
||||
\\
|
||||
\\ To fix this, add Bun's bin directory earlier in your PATH:
|
||||
\\ 1. Open System Properties → Advanced → Environment Variables
|
||||
\\ 2. Edit the "Path" variable in System or User variables
|
||||
\\ 3. Move "{s}" before other directories containing 'node'
|
||||
\\ 4. Click OK and restart your terminal
|
||||
\\
|
||||
\\ Or run this in PowerShell as Administrator:
|
||||
\\ <cyan>[Environment]::SetEnvironmentVariable("Path", "{s};" + $env:Path, [System.EnvironmentVariableTarget]::User)<r>
|
||||
\\
|
||||
;
|
||||
Output.prettyln(msg, .{ node_path, bin_dir, bin_dir, bin_dir });
|
||||
} else {
|
||||
const msg =
|
||||
\\
|
||||
\\<r><yellow>⚠️ Warning:<r> The 'node' command may not use the Bun-managed version
|
||||
\\ Found 'node' at: {s}
|
||||
\\ Bun's bin directory ({s}) appears after another 'node' in PATH
|
||||
\\
|
||||
\\ To fix this, add the following to the end of your shell configuration:
|
||||
\\ <cyan>export PATH="{s}:$PATH"<r>
|
||||
\\
|
||||
;
|
||||
Output.prettyln(msg, .{ node_path, bin_dir, bin_dir });
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if (entry.len > 0) {
|
||||
const test_node = try std.fs.path.joinZ(allocator, &.{ entry, "node" });
|
||||
defer allocator.free(test_node);
|
||||
const access_result5 = if (Env.isWindows) blk: {
|
||||
var buf: bun.OSPathBuffer = undefined;
|
||||
const path = bun.strings.toWPathNormalized(&buf, test_node);
|
||||
break :blk bun.sys.access(path, 0);
|
||||
} else bun.sys.access(test_node, 0);
|
||||
|
||||
if (access_result5 == .result) {
|
||||
found_other_dir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_bun_dir) {
|
||||
if (Env.isWindows) {
|
||||
const msg =
|
||||
\\
|
||||
\\ <r><yellow>⚠️ Warning:<r> Bun's bin directory is not in PATH
|
||||
\\
|
||||
\\ The 'node' command will not be available globally
|
||||
\\
|
||||
\\ To fix this:
|
||||
\\ 1. Open System Properties → Advanced → Environment Variables
|
||||
\\ 2. Edit the "Path" variable in System or User variables
|
||||
\\ 3. Add "{s}" to the list (and ensure it's before other directories containing 'node')
|
||||
\\ 4. Click OK and restart your terminal
|
||||
\\
|
||||
\\ Or run this in PowerShell as Administrator:
|
||||
\\ <cyan>[Environment]::SetEnvironmentVariable("Path", $env:Path + ";{s}", [System.EnvironmentVariableTarget]::User)<r>
|
||||
\\
|
||||
;
|
||||
Output.prettyln(msg, .{ bin_dir, bin_dir });
|
||||
} else {
|
||||
const msg =
|
||||
\\
|
||||
\\ <r><yellow>⚠️ Warning:<r> Bun's bin directory is not in PATH
|
||||
\\
|
||||
\\ The 'node' command will not be available globally
|
||||
\\
|
||||
\\ To fix this, add the following to the end of your shell configuration:
|
||||
\\ <cyan>export PATH="{s}:$PATH"<r>
|
||||
\\
|
||||
;
|
||||
Output.prettyln(msg, .{bin_dir});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Options = @import("../install/PackageManager/PackageManagerOptions.zig");
|
||||
const std = @import("std");
|
||||
const Command = @import("../cli.zig").Command;
|
||||
const which = @import("../which.zig").which;
|
||||
|
||||
const bun = @import("bun");
|
||||
const Env = bun.Environment;
|
||||
const Global = bun.Global;
|
||||
const MutableString = bun.MutableString;
|
||||
const Output = bun.Output;
|
||||
const URL = bun.URL;
|
||||
const strings = bun.strings;
|
||||
const Archiver = bun.libarchive.Archiver;
|
||||
|
||||
const HTTP = bun.http;
|
||||
const AsyncHTTP = HTTP.AsyncHTTP;
|
||||
343
test/cli/node-command.test.ts
Normal file
343
test/cli/node-command.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import { beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// if BUN_DEBUG_QUIET_LOGS is set, we'll wait longer for tests to complete
|
||||
if (process.env.BUN_DEBUG_QUIET_LOGS) {
|
||||
setDefaultTimeout(100000);
|
||||
}
|
||||
|
||||
describe("bun node", () => {
|
||||
let sharedDir: string;
|
||||
let sharedEnv: any;
|
||||
let sharedBinDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const testDir = tempDirWithFiles("node-test-shared", {});
|
||||
const bunInstallDir = join(testDir, ".bun");
|
||||
const binDir = join(bunInstallDir, "bin");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
sharedDir = testDir;
|
||||
sharedBinDir = binDir;
|
||||
sharedEnv = {
|
||||
...bunEnv,
|
||||
BUN_INSTALL: bunInstallDir,
|
||||
BUN_INSTALL_BIN: binDir,
|
||||
PATH: `${binDir}:${process.env.PATH || ""}`,
|
||||
HOME: testDir,
|
||||
};
|
||||
});
|
||||
|
||||
test("shows help when no arguments", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(output).toInclude("Examples:");
|
||||
expect(output).toInclude("$ bun node lts");
|
||||
expect(output).toInclude("$ bun node bun");
|
||||
});
|
||||
|
||||
describe("version management", () => {
|
||||
test("installs and runs Node.js", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "24"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(stdout).toInclude("Successfully installed Node.js v24");
|
||||
|
||||
writeFileSync(join(sharedDir, "test.js"), "console.log(process.version);");
|
||||
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "test.js"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const runOutput = await new Response(runProc.stdout).text();
|
||||
expect(await runProc.exited).toBe(0);
|
||||
expect(runOutput).toMatch(/v24\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
test("handles already installed version", async () => {
|
||||
await using _ = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "24"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "24"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(output).toMatch(/Set Node\.js v\d+\.\d+\.\d+ as default/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("script execution", () => {
|
||||
test("runs scripts with arguments", async () => {
|
||||
writeFileSync(join(sharedDir, "args.js"), `console.log('Args:', process.argv.slice(2).join(' '));`);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "args.js", "arg1", "arg2", "--flag"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(stdout).toInclude("Args: arg1 arg2 --flag");
|
||||
});
|
||||
|
||||
test("runs with specific version", async () => {
|
||||
writeFileSync(join(sharedDir, "version.js"), "console.log(process.version);");
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "24", "version.js"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(stdout).toMatch(/v24\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
test("handles node flags", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "--print", "'hello'"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(stdout.trim()).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bun alias", () => {
|
||||
test("creates node -> bun alias", async () => {
|
||||
const testEnv = (() => {
|
||||
const testDir = tempDirWithFiles("node-alias", {});
|
||||
const bunInstallDir = join(testDir, ".bun");
|
||||
const binDir = join(bunInstallDir, "bin");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
return {
|
||||
dir: testDir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
BUN_INSTALL: bunInstallDir,
|
||||
BUN_INSTALL_BIN: binDir,
|
||||
PATH: `${binDir}:${process.env.PATH || ""}`,
|
||||
HOME: testDir,
|
||||
},
|
||||
binDir,
|
||||
};
|
||||
})();
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "bun"],
|
||||
env: testEnv.env,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(await proc.exited).toBe(0);
|
||||
expect(stdout).toInclude("Successfully aliased 'node' to Bun");
|
||||
|
||||
const nodePath = join(testEnv.binDir, process.platform === "win32" ? "node.exe" : "node");
|
||||
expect(existsSync(nodePath)).toBe(true);
|
||||
|
||||
await using runProc = Bun.spawn({
|
||||
cmd: ["node", "--print", "process.versions.bun"],
|
||||
env: testEnv.env,
|
||||
cwd: testEnv.dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
const runOutput = await new Response(runProc.stdout).text();
|
||||
expect(runOutput.trim()).toBe(Bun.version);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATH warnings", () => {
|
||||
test("warns when bun bin dir comes after another node", async () => {
|
||||
const testEnv = (() => {
|
||||
const testDir = tempDirWithFiles("node-path-warn", {});
|
||||
const bunInstallDir = join(testDir, ".bun");
|
||||
const binDir = join(bunInstallDir, "bin");
|
||||
const otherDir = join(testDir, "other-bin");
|
||||
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
mkdirSync(otherDir);
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
writeFileSync(join(otherDir, "node"), "#!/bin/bash\necho 'other node'");
|
||||
chmodSync(join(otherDir, "node"), 0o755);
|
||||
} else {
|
||||
writeFileSync(join(otherDir, "node.cmd"), "@echo off\necho 'other node'");
|
||||
}
|
||||
|
||||
return {
|
||||
dir: testDir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
BUN_INSTALL: bunInstallDir,
|
||||
BUN_INSTALL_BIN: binDir,
|
||||
PATH: `${otherDir}:${binDir}:${process.env.PATH || ""}`,
|
||||
HOME: testDir,
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "24"],
|
||||
env: testEnv.env,
|
||||
cwd: testEnv.dir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
await proc.exited;
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
if (process.platform !== "win32") {
|
||||
expect(output).toInclude("Warning:");
|
||||
expect(output).toInclude("appears after another 'node' in PATH");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("handles invalid version", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "99"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
expect(await proc.exited).toBe(1);
|
||||
expect(stderr).toInclude("error");
|
||||
});
|
||||
|
||||
test("handles missing script file", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "nonexistent.js"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
expect(await proc.exited).not.toBe(0);
|
||||
expect(stderr).toInclude("Cannot find module");
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment variables", () => {
|
||||
test("respects BUN_INSTALL_BIN", async () => {
|
||||
const customBin = join(sharedDir, "custom-bin");
|
||||
mkdirSync(customBin);
|
||||
|
||||
const env = {
|
||||
...sharedEnv,
|
||||
BUN_INSTALL_BIN: customBin,
|
||||
PATH: `${customBin}:${process.env.PATH || ""}`,
|
||||
};
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "bun"],
|
||||
env,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
expect(await proc.exited).toBe(0);
|
||||
|
||||
const binaryName = process.platform === "win32" ? "node.exe" : "node";
|
||||
const nodePath = join(customBin, binaryName);
|
||||
expect(existsSync(nodePath)).toBe(true);
|
||||
});
|
||||
|
||||
test("prepends bin dir to PATH for child process", async () => {
|
||||
writeFileSync(
|
||||
join(sharedDir, "check-path.js"),
|
||||
`
|
||||
const path = process.env.PATH || '';
|
||||
const binDir = '${sharedBinDir}';
|
||||
console.log(path.startsWith(binDir) ? 'PATH correct' : 'PATH wrong');
|
||||
`,
|
||||
);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "check-path.js"],
|
||||
env: sharedEnv,
|
||||
cwd: sharedDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
expect(stdout).toInclude("PATH correct");
|
||||
});
|
||||
});
|
||||
|
||||
test("downloads silently when running scripts", async () => {
|
||||
const testEnv = (() => {
|
||||
const testDir = tempDirWithFiles("node-silent-dl", {});
|
||||
const bunInstallDir = join(testDir, ".bun");
|
||||
const binDir = join(bunInstallDir, "bin");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
return {
|
||||
dir: testDir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
BUN_INSTALL: bunInstallDir,
|
||||
BUN_INSTALL_BIN: binDir,
|
||||
PATH: `${binDir}:${process.env.PATH || ""}`,
|
||||
HOME: testDir,
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
writeFileSync(join(testEnv.dir, "output.js"), "console.log('OUTPUT');");
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "node", "21", "output.js"],
|
||||
env: testEnv.env,
|
||||
cwd: testEnv.dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
||||
|
||||
expect(stdout.trim()).toBe("OUTPUT");
|
||||
expect(stderr).not.toInclude("Downloading");
|
||||
expect(stderr).not.toInclude("Successfully");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user