aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/cli.zig35
-rw-r--r--src/cli/link_command.zig8
-rw-r--r--src/cli/unlink_command.zig8
-rw-r--r--src/deps/zig-clap/clap.zig4
-rw-r--r--src/deps/zig-clap/clap/comptime.zig17
-rw-r--r--src/install/bin.zig154
-rw-r--r--src/install/dependency.zig28
-rw-r--r--src/install/install.zig471
-rw-r--r--src/install/lockfile.zig5
-rw-r--r--src/install/resolution.zig3
-rw-r--r--src/install/resolvers/folder_resolver.zig161
-rw-r--r--src/string_immutable.zig38
12 files changed, 853 insertions, 79 deletions
diff --git a/src/cli.zig b/src/cli.zig
index b878b31a4..743527a93 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -44,6 +44,8 @@ const CreateListExamplesCommand = @import("./cli/create_command.zig").CreateList
const DevCommand = @import("./cli/dev_command.zig").DevCommand;
const DiscordCommand = @import("./cli/discord_command.zig").DiscordCommand;
const InstallCommand = @import("./cli/install_command.zig").InstallCommand;
+const LinkCommand = @import("./cli/link_command.zig").LinkCommand;
+const UnlinkCommand = @import("./cli/unlink_command.zig").UnlinkCommand;
const InstallCompletionsCommand = @import("./cli/install_completions_command.zig").InstallCompletionsCommand;
const PackageManagerCommand = @import("./cli/package_manager_command.zig").PackageManagerCommand;
const RemoveCommand = @import("./cli/remove_command.zig").RemoveCommand;
@@ -854,6 +856,8 @@ pub const Command = struct {
RootCommandMatcher.case("upgrade") => .UpgradeCommand,
RootCommandMatcher.case("completions") => .InstallCompletionsCommand,
RootCommandMatcher.case("getcompletes") => .GetCompletionsCommand,
+ RootCommandMatcher.case("link") => .LinkCommand,
+ RootCommandMatcher.case("unlink") => .UnlinkCommand,
RootCommandMatcher.case("i"), RootCommandMatcher.case("install") => brk: {
for (args_iter.buf) |arg| {
@@ -951,6 +955,18 @@ pub const Command = struct {
try RemoveCommand.exec(ctx);
return;
},
+ .LinkCommand => {
+ const ctx = try Command.Context.create(allocator, log, .LinkCommand);
+
+ try LinkCommand.exec(ctx);
+ return;
+ },
+ .UnlinkCommand => {
+ const ctx = try Command.Context.create(allocator, log, .UnlinkCommand);
+
+ try UnlinkCommand.exec(ctx);
+ return;
+ },
.PackageManagerCommand => {
const ctx = try Command.Context.create(allocator, log, .PackageManagerCommand);
@@ -1244,6 +1260,8 @@ pub const Command = struct {
UpgradeCommand,
PackageManagerCommand,
TestCommand,
+ LinkCommand,
+ UnlinkCommand,
pub fn params(comptime cmd: Tag) []const Arguments.ParamType {
return &comptime switch (cmd) {
@@ -1261,7 +1279,7 @@ pub const Command = struct {
pub fn isNPMRelated(this: Tag) bool {
return switch (this) {
- .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand => true,
+ .LinkCommand, .UnlinkCommand, .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand => true,
else => false,
};
}
@@ -1273,12 +1291,17 @@ pub const Command = struct {
.DevCommand = true,
.RunCommand = true,
.TestCommand = true,
- .InstallCommand = true,
- .AddCommand = true,
- .RemoveCommand = true,
});
- pub const loads_config = cares_about_bun_file;
+ pub const loads_config = brk: {
+ var cares = cares_about_bun_file;
+ cares.set(.InstallCommand, true);
+ cares.set(.AddCommand, true);
+ cares.set(.RemoveCommand, true);
+ cares.set(.LinkCommand, true);
+ cares.set(.UnlinkCommand, true);
+ break :brk cares;
+ };
pub const always_loads_config: std.EnumArray(Tag, bool) = std.EnumArray(Tag, bool).initDefault(false, .{
.BuildCommand = true,
.BunCommand = true,
@@ -1296,6 +1319,8 @@ pub const Command = struct {
.AddCommand = false,
.RemoveCommand = false,
.PackageManagerCommand = false,
+ .LinkCommand = false,
+ .UnlinkCommand = false,
});
};
};
diff --git a/src/cli/link_command.zig b/src/cli/link_command.zig
new file mode 100644
index 000000000..2dca97999
--- /dev/null
+++ b/src/cli/link_command.zig
@@ -0,0 +1,8 @@
+const Command = @import("../cli.zig").Command;
+const PackageManager = @import("../install/install.zig").PackageManager;
+
+pub const LinkCommand = struct {
+ pub fn exec(ctx: Command.Context) !void {
+ try PackageManager.link(ctx);
+ }
+};
diff --git a/src/cli/unlink_command.zig b/src/cli/unlink_command.zig
new file mode 100644
index 000000000..91e97547e
--- /dev/null
+++ b/src/cli/unlink_command.zig
@@ -0,0 +1,8 @@
+const Command = @import("../cli.zig").Command;
+const PackageManager = @import("../install/install.zig").PackageManager;
+
+pub const UnlinkCommand = struct {
+ pub fn exec(ctx: Command.Context) !void {
+ try PackageManager.unlink(ctx);
+ }
+};
diff --git a/src/deps/zig-clap/clap.zig b/src/deps/zig-clap/clap.zig
index 8e1d55da2..6a49f6a15 100644
--- a/src/deps/zig-clap/clap.zig
+++ b/src/deps/zig-clap/clap.zig
@@ -264,6 +264,10 @@ pub fn Args(comptime Id: type, comptime params: []const Param(Id)) type {
pub fn positionals(a: @This()) []const []const u8 {
return a.clap.positionals();
}
+
+ pub fn hasFlag(comptime name: []const u8) bool {
+ return ComptimeClap(Id, params).hasFlag(name);
+ }
};
}
diff --git a/src/deps/zig-clap/clap/comptime.zig b/src/deps/zig-clap/clap/comptime.zig
index f55be3f1a..9631c9ca7 100644
--- a/src/deps/zig-clap/clap/comptime.zig
+++ b/src/deps/zig-clap/clap/comptime.zig
@@ -130,6 +130,23 @@ pub fn ComptimeClap(
return parser.pos;
}
+ pub fn hasFlag(comptime name: []const u8) bool {
+ comptime {
+ for (converted_params) |param| {
+ if (param.names.short) |s| {
+ if (mem.eql(u8, name, "-" ++ [_]u8{s}))
+ return true;
+ }
+ if (param.names.long) |l| {
+ if (mem.eql(u8, name, "--" ++ l))
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
fn findParam(comptime name: []const u8) clap.Param(usize) {
comptime {
for (converted_params) |param| {
diff --git a/src/install/bin.zig b/src/install/bin.zig
index d7d99d480..49462ed78 100644
--- a/src/install/bin.zig
+++ b/src/install/bin.zig
@@ -484,5 +484,159 @@ pub const Bin = extern struct {
},
}
}
+
+ pub fn unlink(this: *Linker, link_global: bool) void {
+ var target_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var dest_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ var from_remain: []u8 = &target_buf;
+ var remain: []u8 = &dest_buf;
+
+ if (!link_global) {
+ target_buf[0..".bin/".len].* = ".bin/".*;
+ from_remain = target_buf[".bin/".len..];
+ dest_buf[0.."../".len].* = "../".*;
+ remain = dest_buf["../".len..];
+ } else {
+ if (this.global_bin_dir.fd >= std.math.maxInt(std.os.fd_t)) {
+ this.err = error.MissingGlobalBinDir;
+ return;
+ }
+
+ @memcpy(&target_buf, this.global_bin_path.ptr, this.global_bin_path.len);
+ from_remain = target_buf[this.global_bin_path.len..];
+ from_remain[0] = std.fs.path.sep;
+ from_remain = from_remain[1..];
+ const abs = std.os.getFdPath(this.root_node_modules_folder, &dest_buf) catch |err| {
+ this.err = err;
+ return;
+ };
+ remain = remain[abs.len..];
+ remain[0] = std.fs.path.sep;
+ remain = remain[1..];
+
+ this.root_node_modules_folder = this.global_bin_dir.fd;
+ }
+
+ const name = this.package_name.slice();
+ std.mem.copy(u8, remain, name);
+ remain = remain[name.len..];
+ remain[0] = std.fs.path.sep;
+ remain = remain[1..];
+
+ if (comptime Environment.isWindows) {
+ @compileError("Bin.Linker.unlink() needs to be updated to generate .cmd files on Windows");
+ }
+
+ switch (this.bin.tag) {
+ .none => {
+ if (comptime Environment.isDebug) {
+ unreachable;
+ }
+ },
+ .file => {
+ // we need to use the unscoped package name here
+ // this is why @babel/parser would fail to link
+ const unscoped_name = unscopedPackageName(name);
+ std.mem.copy(u8, from_remain, unscoped_name);
+ from_remain = from_remain[unscoped_name.len..];
+ from_remain[0] = 0;
+ var dest_path: [:0]u8 = target_buf[0 .. @ptrToInt(from_remain.ptr) - @ptrToInt(&target_buf) :0];
+
+ std.os.unlinkatZ(this.root_node_modules_folder.fd, dest_path, 0) catch {};
+ },
+ .named_file => {
+ var name_to_use = this.bin.value.named_file[0].slice(this.string_buf);
+ std.mem.copy(u8, from_remain, name_to_use);
+ from_remain = from_remain[name_to_use.len..];
+ from_remain[0] = 0;
+ var dest_path: [:0]u8 = target_buf[0 .. @ptrToInt(from_remain.ptr) - @ptrToInt(&target_buf) :0];
+
+ std.os.unlinkatZ(this.root_node_modules_folder.fd, dest_path, 0) catch {};
+ },
+ .map => {
+ var extern_string_i: u32 = this.bin.value.map.off;
+ const end = this.bin.value.map.len + extern_string_i;
+ const _from_remain = from_remain;
+ const _remain = remain;
+ while (extern_string_i < end) : (extern_string_i += 2) {
+ from_remain = _from_remain;
+ remain = _remain;
+ const name_in_terminal = this.extern_string_buf[extern_string_i];
+ const name_in_filesystem = this.extern_string_buf[extern_string_i + 1];
+
+ var target = name_in_filesystem.slice(this.string_buf);
+ if (strings.hasPrefix(target, "./")) {
+ target = target[2..];
+ }
+ std.mem.copy(u8, remain, target);
+ remain = remain[target.len..];
+ remain[0] = 0;
+ remain = remain[1..];
+
+ var name_to_use = name_in_terminal.slice(this.string_buf);
+ std.mem.copy(u8, from_remain, name_to_use);
+ from_remain = from_remain[name_to_use.len..];
+ from_remain[0] = 0;
+ var dest_path: [:0]u8 = target_buf[0 .. @ptrToInt(from_remain.ptr) - @ptrToInt(&target_buf) :0];
+
+ std.os.unlinkatZ(this.root_node_modules_folder.fd, dest_path, 0) catch {};
+ }
+ },
+ .dir => {
+ var target = this.bin.value.dir.slice(this.string_buf);
+ if (strings.hasPrefix(target, "./")) {
+ target = target[2..];
+ }
+
+ var parts = [_][]const u8{ name, target };
+
+ std.mem.copy(u8, remain, target);
+ remain = remain[target.len..];
+
+ var dir = std.fs.Dir{ .fd = this.package_installed_node_modules };
+
+ var joined = Path.joinStringBuf(&target_buf, &parts, .auto);
+ @intToPtr([*]u8, @ptrToInt(joined.ptr))[joined.len] = 0;
+ var joined_: [:0]const u8 = joined.ptr[0..joined.len :0];
+ var child_dir = dir.openDirZ(joined_, .{ .iterate = true }) catch |err| {
+ this.err = err;
+ return;
+ };
+ defer child_dir.close();
+
+ var iter = child_dir.iterate();
+
+ var basedir_path = std.os.getFdPath(child_dir.fd, &target_buf) catch |err| {
+ this.err = err;
+ return;
+ };
+ target_buf[basedir_path.len] = std.fs.path.sep;
+ var target_buf_remain = target_buf[basedir_path.len + 1 ..];
+ var prev_target_buf_remain = target_buf_remain;
+
+ while (iter.next() catch null) |entry_| {
+ const entry: std.fs.Dir.Entry = entry_;
+ switch (entry.kind) {
+ std.fs.Dir.Entry.Kind.SymLink, std.fs.Dir.Entry.Kind.File => {
+ target_buf_remain = prev_target_buf_remain;
+ std.mem.copy(u8, target_buf_remain, entry.name);
+ target_buf_remain = target_buf_remain[entry.name.len..];
+ target_buf_remain[0] = 0;
+ var to_path = if (!link_global)
+ std.fmt.bufPrintZ(&dest_buf, ".bin/{s}", .{entry.name}) catch continue
+ else
+ std.fmt.bufPrintZ(&dest_buf, "{s}", .{entry.name}) catch continue;
+
+ std.os.unlinkatZ(
+ this.root_node_modules_folder.fd,
+ to_path,
+ ) catch continue;
+ },
+ else => {},
+ }
+ }
+ },
+ }
+ }
};
};
diff --git a/src/install/dependency.zig b/src/install/dependency.zig
index f83366073..f4c3c7173 100644
--- a/src/install/dependency.zig
+++ b/src/install/dependency.zig
@@ -190,6 +190,7 @@ pub const Version = struct {
lhs.value.npm.eql(rhs.value.npm),
.folder, .dist_tag => lhs.literal.eql(rhs.literal, lhs_buf, rhs_buf),
.tarball => lhs.value.tarball.eql(rhs.value.tarball, lhs_buf, rhs_buf),
+ .symlink => lhs.value.symlink.eql(rhs.value.symlink, lhs_buf, rhs_buf),
else => true,
};
}
@@ -209,8 +210,11 @@ pub const Version = struct {
/// Local folder
folder = 4,
- /// TODO:
+ /// link:path
+ /// https://docs.npmjs.com/cli/v8/commands/npm-link#synopsis
+ /// https://stackoverflow.com/questions/51954956/whats-the-difference-between-yarn-link-and-npm-link
symlink = 5,
+
/// TODO:
workspace = 6,
/// TODO:
@@ -448,8 +452,9 @@ pub const Version = struct {
tarball: URI,
folder: String,
- /// Unsupported, but still parsed so an error can be thrown
- symlink: void,
+ /// Equivalent to npm link
+ symlink: String,
+
/// Unsupported, but still parsed so an error can be thrown
workspace: void,
/// Unsupported, but still parsed so an error can be thrown
@@ -588,7 +593,22 @@ pub fn parseWithTag(
};
},
.uninitialized => return null,
- .symlink, .workspace, .git, .github => {
+ .symlink => {
+ if (strings.indexOfChar(dependency, ':')) |colon| {
+ return Version{
+ .value = .{ .symlink = sliced.sub(dependency[colon + 1 ..]).value() },
+ .tag = .symlink,
+ .literal = sliced.value(),
+ };
+ }
+
+ return Version{
+ .value = .{ .symlink = sliced.value() },
+ .tag = .symlink,
+ .literal = sliced.value(),
+ };
+ },
+ .workspace, .git, .github => {
if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Unsupported dependency type {s} for \"{s}\"", .{ @tagName(tag), dependency }) catch unreachable;
return null;
},
diff --git a/src/install/install.zig b/src/install/install.zig
index d396b2248..a60fe405f 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -47,6 +47,7 @@ const ExtractTarball = @import("./extract_tarball.zig");
const Npm = @import("./npm.zig");
const Bitset = @import("./bit_set.zig").DynamicBitSetUnmanaged;
const z_allocator = @import("../memory_allocator.zig").z_allocator;
+const Syscall = @import("javascript_core").Node.Syscall;
const RunCommand = @import("../cli/run_command.zig").RunCommand;
threadlocal var initialized_store = false;
@@ -446,6 +447,15 @@ pub const Features = struct {
.dependencies = true,
};
+ pub const link = Features{
+ .optional_dependencies = false,
+ .dev_dependencies = false,
+ .scripts = false,
+ .peer_dependencies = false,
+ .is_main = false,
+ .dependencies = false,
+ };
+
pub const npm = Features{
.optional_dependencies = true,
};
@@ -853,6 +863,7 @@ const PackageInstall = struct {
copyfile,
opening_cache_dir,
copying_files,
+ linking,
};
const CloneFileError = error{
@@ -1225,6 +1236,61 @@ const PackageInstall = struct {
pub fn uninstall(this: *PackageInstall) !void {
try this.destination_dir.deleteTree(std.mem.span(this.destination_dir_subpath));
}
+
+ fn isDanglingSymlink(path: [:0]const u8) bool {
+ if (comptime Environment.isLinux) {
+ const rc = Syscall.system.open(path, @as(u31, std.os.O.PATH | 0), @as(u31, 0));
+ switch (Syscall.getErrno(rc)) {
+ .SUCCESS => {
+ _ = Syscall.system.close(rc);
+ return false;
+ },
+ else => return true,
+ }
+ } else {
+ const rc = Syscall.system.open(path, @as(u31, 0), @as(u31, 0));
+ switch (Syscall.getErrno(rc)) {
+ .SUCCESS => {
+ _ = Syscall.system.close(rc);
+ return false;
+ },
+ else => return true,
+ }
+ }
+ }
+
+ pub fn installFromLink(this: *PackageInstall, skip_delete: bool) Result {
+
+ // If this fails, we don't care.
+ // we'll catch it the next error
+ if (!skip_delete and !strings.eqlComptime(this.destination_dir_subpath, ".")) this.uninstall() catch {};
+
+ // cache_dir_subpath in here is actually the full path to the symlink pointing to the linked package
+ const symlinked_path = this.cache_dir_subpath;
+
+ std.os.symlinkatZ(symlinked_path, this.destination_dir.fd, this.destination_dir_subpath) catch |err| {
+ return Result{
+ .fail = .{
+ .err = err,
+ .step = .linking,
+ },
+ };
+ };
+
+ if (isDanglingSymlink(symlinked_path)) {
+ return Result{
+ .fail = .{
+ .err = error.DanglingSymlink,
+ .step = .linking,
+ },
+ };
+ }
+
+ return Result{
+ .success = void{},
+ };
+ }
+
pub fn install(this: *PackageInstall, skip_delete: bool) Result {
// If this fails, we don't care.
@@ -1381,7 +1447,6 @@ pub const PackageManager = struct {
manifests: PackageManifestMap = PackageManifestMap{},
folders: FolderResolution.Map = FolderResolution.Map{},
- resolved_package_index: PackageIndex = PackageIndex{},
task_queue: TaskDependencyQueue = .{},
network_dedupe_map: NetworkTaskQueue = .{},
@@ -1398,6 +1463,10 @@ pub const PackageManager = struct {
options: Options = Options{},
preinstall_state: std.ArrayListUnmanaged(PreinstallState) = std.ArrayListUnmanaged(PreinstallState){},
+ global_link_dir: ?std.fs.Dir = null,
+ global_dir: ?std.fs.Dir = null,
+ global_link_dir_path: string = "",
+
const PreallocatedNetworkTasks = std.BoundedArray(NetworkTask, 1024);
const NetworkTaskQueue = std.HashMapUnmanaged(u64, void, IdentityContext(u64), 80);
const PackageIndex = std.AutoHashMapUnmanaged(u64, *Package);
@@ -1410,6 +1479,23 @@ pub const PackageManager = struct {
80,
);
+ pub fn globalLinkDir(this: *PackageManager) !std.fs.Dir {
+ return this.global_link_dir orelse brk: {
+ var global_dir = try Options.openGlobalDir(this.options.explicit_global_directory);
+ this.global_dir = global_dir;
+ this.global_link_dir = try global_dir.makeOpenPath("node_modules", .{ .iterate = true });
+ var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+ const _path = try std.os.getFdPath(this.global_link_dir.?.fd, &buf);
+ this.global_link_dir_path = try Fs.FileSystem.DirnameStore.instance.append([]const u8, _path);
+ break :brk this.global_link_dir.?;
+ };
+ }
+
+ pub fn globalLinkDirPath(this: *PackageManager) ![]const u8 {
+ _ = try this.globalLinkDir();
+ return this.global_link_dir_path;
+ }
+
fn ensurePreinstallStateListCapacity(this: *PackageManager, count: usize) !void {
if (this.preinstall_state.items.len >= count) {
return;
@@ -1907,7 +1993,24 @@ pub const PackageManager = struct {
},
.folder => {
- const res = FolderResolution.getOrPut(version.value.folder.slice(this.lockfile.buffers.string_bytes.items), this);
+ // relative to cwd
+ const res = FolderResolution.getOrPut(.{ .relative = void{} }, version, version.value.folder.slice(this.lockfile.buffers.string_bytes.items), this);
+
+ switch (res) {
+ .err => |err| return err,
+ .package_id => |package_id| {
+ this.lockfile.buffers.resolutions.items[dependency_id] = package_id;
+ return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) };
+ },
+
+ .new_package_id => |package_id| {
+ this.lockfile.buffers.resolutions.items[dependency_id] = package_id;
+ return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true };
+ },
+ }
+ },
+ .symlink => {
+ const res = FolderResolution.getOrPut(.{ .global = try this.globalLinkDirPath() }, version, version.value.symlink.slice(this.lockfile.buffers.string_bytes.items), this);
switch (res) {
.err => |err| return err,
@@ -2177,6 +2280,8 @@ pub const PackageManager = struct {
this.enqueueNetworkTask(network_task);
}
+ std.debug.assert(task_id != 0);
+
var manifest_entry_parse = try this.task_queue.getOrPutContext(this.allocator, task_id, .{});
if (!manifest_entry_parse.found_existing) {
manifest_entry_parse.value_ptr.* = TaskCallbackList{};
@@ -2189,6 +2294,74 @@ pub const PackageManager = struct {
}
return;
},
+ .symlink => {
+ const _result = this.getOrPutResolvedPackage(
+ name_hash,
+ name,
+ version,
+ dependency.behavior,
+ id,
+ resolution,
+ ) catch |err| brk: {
+ if (err == error.MissingPackageJSON) {
+ break :brk null;
+ }
+
+ return err;
+ };
+
+ const not_found_fmt =
+ \\package \"{[name]s}\" is not linked
+ \\
+ \\To install a linked package:
+ \\ <cyan>bun link my-pkg-name-from-package-json<r>
+ \\
+ \\Tip: the package name is from package.json, which can differ from the folder name.
+ \\
+ ;
+ if (_result) |result| {
+ // First time?
+ if (result.is_first_time) {
+ if (PackageManager.verbose_install) {
+ const label: string = this.lockfile.str(version.literal);
+
+ Output.prettyErrorln(" -> \"{s}\": \"{s}\" -> {s}@{}", .{
+ this.lockfile.str(result.package.name),
+ label,
+ this.lockfile.str(result.package.name),
+ result.package.resolution.fmt(this.lockfile.buffers.string_bytes.items),
+ });
+ }
+ // We shouldn't see any dependencies
+ if (result.package.dependencies.len > 0) {
+ try this.lockfile.scratch.dependency_list_queue.writeItem(result.package.dependencies);
+ }
+ }
+
+ // should not trigger a network call
+ std.debug.assert(result.network_task == null);
+ } else if (dependency.behavior.isRequired()) {
+ this.log.addErrorFmt(
+ null,
+ logger.Loc.Empty,
+ this.allocator,
+ not_found_fmt,
+ .{
+ .name = this.lockfile.str(name),
+ },
+ ) catch unreachable;
+ } else if (this.options.log_level.isVerbose()) {
+ this.log.addWarningFmt(
+ null,
+ logger.Loc.Empty,
+ this.allocator,
+ not_found_fmt,
+ .{
+ .name = this.lockfile.str(name),
+ },
+ ) catch unreachable;
+ }
+ },
else => {},
}
@@ -2662,6 +2835,7 @@ pub const PackageManager = struct {
global: bool = false,
global_bin_dir: std.fs.Dir = std.fs.Dir{ .fd = std.math.maxInt(std.os.fd_t) },
+ explicit_global_directory: string = "",
/// destination directory to link bins into
// must be a variable due to global installs and bunx
bin_path: stringZ = "node_modules/.bin",
@@ -2751,17 +2925,13 @@ pub const PackageManager = struct {
optional: bool = false,
};
- pub fn openGlobalDir(opts_: ?*Api.BunInstall) !std.fs.Dir {
+ pub fn openGlobalDir(explicit_global_dir: string) !std.fs.Dir {
if (std.os.getenvZ("BUN_INSTALL_GLOBAL_DIR")) |home_dir| {
return try std.fs.cwd().makeOpenPath(home_dir, .{ .iterate = true });
}
- if (opts_) |opts| {
- if (opts.global_dir) |home_dir| {
- if (home_dir.len > 0) {
- return try std.fs.cwd().makeOpenPath(home_dir, .{ .iterate = true });
- }
- }
+ if (explicit_global_dir.len > 0) {
+ return try std.fs.cwd().makeOpenPath(explicit_global_dir, .{ .iterate = true });
}
if (std.os.getenvZ("BUN_INSTALL")) |home_dir| {
@@ -2908,6 +3078,8 @@ pub const PackageManager = struct {
this.save_lockfile_path = try allocator.dupeZ(u8, save);
}
}
+
+ this.explicit_global_directory = bun_install.global_dir orelse this.explicit_global_directory;
}
const default_disable_progress_bar: bool = brk: {
@@ -3070,11 +3242,14 @@ pub const PackageManager = struct {
if (cli_) |cli| {
if (cli.no_save) {
this.do.save_lockfile = false;
+ this.do.write_package_json = false;
}
if (cli.dry_run) {
this.do.install_packages = false;
this.dry_run = true;
+ this.do.write_package_json = false;
+ this.do.save_lockfile = false;
}
if (cli.no_cache) {
@@ -3159,6 +3334,7 @@ pub const PackageManager = struct {
save_lockfile: bool = true,
load_lockfile: bool = true,
install_packages: bool = true,
+ write_package_json: bool = true,
run_scripts: bool = true,
save_yarn_lock: bool = false,
print_meta_hash_string: bool = false,
@@ -3393,7 +3569,11 @@ pub const PackageManager = struct {
try NetworkThread.warmup();
if (cli.global) {
- var global_dir = try Options.openGlobalDir(ctx.install);
+ var explicit_global_dir: string = "";
+ if (ctx.install) |opts| {
+ explicit_global_dir = opts.global_dir orelse explicit_global_dir;
+ }
+ var global_dir = try Options.openGlobalDir(explicit_global_dir);
try global_dir.setAsCwd();
}
@@ -3545,6 +3725,182 @@ pub const PackageManager = struct {
try updatePackageJSONAndInstall(ctx, .remove, &remove_params);
}
+ pub inline fn link(
+ ctx: Command.Context,
+ ) !void {
+ var manager = PackageManager.init(ctx, null, &link_params) catch |err| brk: {
+ switch (err) {
+ error.MissingPackageJSON => {
+ var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| {
+ Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err2)});
+ Global.crash();
+ };
+ try package_json_file.pwriteAll("{\"dependencies\": {}}", 0);
+
+ break :brk try PackageManager.init(ctx, package_json_file, &link_params);
+ },
+ else => return err,
+ }
+
+ unreachable;
+ };
+
+ if (manager.options.log_level != .silent) {
+ Output.prettyErrorln("<r><b>bun link <r><d>v" ++ Global.package_json_version ++ "<r>\n", .{});
+ Output.flush();
+ }
+
+ if (manager.options.positionals.len == 1) {
+ // bun link
+
+ var lockfile: Lockfile = undefined;
+ var name: string = "";
+ var package: Lockfile.Package = Lockfile.Package{};
+
+ // Step 1. parse the nearest package.json file
+ {
+ var current_package_json_stat = try manager.root_package_json_file.stat();
+ var current_package_json_buf = try ctx.allocator.alloc(u8, current_package_json_stat.size + 64);
+ const current_package_json_contents_len = try manager.root_package_json_file.preadAll(
+ current_package_json_buf,
+ 0,
+ );
+
+ const package_json_source = logger.Source.initPathString(
+ package_json_cwd_buf[0 .. FileSystem.instance.top_level_dir.len + "package.json".len],
+ current_package_json_buf[0..current_package_json_contents_len],
+ );
+ try lockfile.initEmpty(ctx.allocator);
+
+ try Lockfile.Package.parseMain(&lockfile, &package, ctx.allocator, manager.log, package_json_source, Features.folder);
+ name = lockfile.str(package.name);
+ if (name.len == 0) {
+ if (manager.options.log_level != .silent)
+ Output.prettyErrorln("<r><red>error:<r> package.json missing \"name\" <d>in \"{s}\"<r>", .{package_json_source.path.text});
+ Global.crash();
+ } else if (!strings.isNPMPackageName(name)) {
+ if (manager.options.log_level != .silent)
+ Output.prettyErrorln("<r><red>error:<r> invalid package.json name \"{s}\" <d>in \"{s}\"<r>", .{
+ name,
+ package_json_source.path.text,
+ });
+ Global.crash();
+ }
+ }
+
+ // Step 2. Setup the global directory
+ var node_modules: std.fs.Dir = brk: {
+ Bin.Linker.umask = C.umask(0);
+ var explicit_global_dir: string = "";
+ if (ctx.install) |install_| {
+ explicit_global_dir = install_.global_dir orelse explicit_global_dir;
+ }
+ manager.global_dir = try Options.openGlobalDir(explicit_global_dir);
+
+ try manager.setupGlobalDir(&ctx);
+
+ break :brk manager.global_dir.?.makeOpenPath("node_modules", .{ .iterate = true }) catch |err| {
+ if (manager.options.log_level != .silent)
+ Output.prettyErrorln("<r><red>error:<r> failed to create node_modules in global dir due to error {s}", .{@errorName(err)});
+ Global.crash();
+ };
+ };
+
+ // Step 3a. symlink to the node_modules folder
+ {
+ // delete it if it exists
+ node_modules.deleteTree(name) catch {};
+
+ // create the symlink
+ node_modules.symLink(Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), name, .{ .is_directory = true }) catch |err| {
+ if (manager.options.log_level != .silent)
+ Output.prettyErrorln("<r><red>error:<r> failed to create symlink to node_modules in global dir due to error {s}", .{@errorName(err)});
+ Global.crash();
+ };
+ }
+
+ // Step 3b. Link any global bins
+ if (package.bin.tag != .none) {
+ var bin_linker = Bin.Linker{
+ .bin = package.bin,
+ .package_installed_node_modules = node_modules.fd,
+ .global_bin_path = manager.options.bin_path,
+ .global_bin_dir = manager.options.global_bin_dir,
+
+ // .destination_dir_subpath = destination_dir_subpath,
+ .root_node_modules_folder = node_modules.fd,
+ .package_name = strings.StringOrTinyString.init(name),
+ .string_buf = lockfile.buffers.string_bytes.items,
+ .extern_string_buf = lockfile.buffers.extern_strings.items,
+ };
+ bin_linker.link(true);
+
+ if (bin_linker.err) |err| {
+ if (manager.options.log_level != .silent)
+ Output.prettyErrorln("<r><red>error:<r> failed to link bin due to error {s}", .{@errorName(err)});
+ Global.crash();
+ }
+ }
+
+ Output.flush();
+
+ // Done
+ if (manager.options.log_level != .silent)
+ Output.prettyln(
+ \\<r><green>Success!<r> Registered \"{[name]s}\"
+ \\
+ \\To use {[name]s} in a project, run:
+ \\ <cyan>bun link {[name]s}<r>
+ \\
+ \\Or add it in dependencies in your package.json file:
+ \\ <cyan>"{[name]s}": "link:{[name]s}"<r>
+ \\
+ ,
+ .{
+ .name = name,
+ },
+ );
+
+ Output.flush();
+ Global.exit(0);
+ } else {
+ // bun link lodash
+ switch (manager.options.log_level) {
+ .default => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .default),
+ .verbose => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .verbose),
+ .silent => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .silent),
+ .default_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .default_no_progress),
+ .verbose_no_progress => try updatePackageJSONAndInstallWithManager(ctx, manager, .link, .verbose_no_progress),
+ }
+ }
+ }
+
+ pub inline fn unlink(
+ ctx: Command.Context,
+ ) !void {
+ var manager = PackageManager.init(ctx, null, &unlink_params) catch |err| brk: {
+ switch (err) {
+ error.MissingPackageJSON => {
+ var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| {
+ Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err2)});
+ Global.crash();
+ };
+ try package_json_file.pwriteAll("{\"dependencies\": {}}", 0);
+
+ break :brk try PackageManager.init(ctx, package_json_file, &unlink_params);
+ },
+ else => return err,
+ }
+
+ unreachable;
+ };
+
+ if (manager.options.log_level != .silent) {
+ Output.prettyErrorln("<r><b>bun unlink <r><d>v" ++ Global.package_json_version ++ "<r>\n", .{});
+ Output.flush();
+ }
+ }
+
const ParamType = clap.Param(clap.Help);
const platform_specific_backend_label = if (Environment.isMac)
"Possible values: \"clonefile\" (default), \"hardlink\", \"symlink\", \"copyfile\""
@@ -3591,6 +3947,16 @@ pub const PackageManager = struct {
clap.parseParam("<POS> ... \"name\" of packages to remove from package.json") catch unreachable,
};
+ pub const link_params = install_params_ ++ [_]ParamType{
+ clap.parseParam("--save Save to package.json") catch unreachable,
+ clap.parseParam("<POS> ... \"name\" install package as a link") catch unreachable,
+ };
+
+ pub const unlink_params = install_params_ ++ [_]ParamType{
+ clap.parseParam("--save Save to package.json") catch unreachable,
+ clap.parseParam("<POS> ... \"name\" uninstall package as a link") catch unreachable,
+ };
+
pub const CommandLineArguments = struct {
registry: string = "",
cache_dir: string = "",
@@ -3678,6 +4044,13 @@ pub const PackageManager = struct {
cli.silent = args.flag("--silent");
cli.verbose = args.flag("--verbose");
cli.ignore_scripts = args.flag("--ignore-scripts");
+ if (comptime @TypeOf(args).hasFlag("--save")) {
+ cli.no_save = true;
+
+ if (args.flag("--save")) {
+ cli.no_save = false;
+ }
+ }
if (args.option("--config")) |opt| {
cli.config = opt;
@@ -3834,6 +4207,10 @@ pub const PackageManager = struct {
Global.exit(1);
}
+ if ((op == .link or op == .unlink) and !strings.hasPrefixComptime(request.version_buf, "link:")) {
+ request.version_buf = std.fmt.allocPrint(allocator, "link:{s}", .{request.name}) catch unreachable;
+ }
+
if (request.version_buf.len == 0) {
request.missing_version = true;
} else {
@@ -3990,6 +4367,7 @@ pub const PackageManager = struct {
Global.exit(0);
},
+ else => {},
}
}
@@ -4115,7 +4493,7 @@ pub const PackageManager = struct {
}
manager.to_remove = updates;
},
- .add, .update => {
+ .link, .unlink, .add, .update => {
try PackageJSONEditor.edit(ctx.allocator, updates, &current_package_json, dependency_list);
manager.package_json_updates = updates;
},
@@ -4147,7 +4525,7 @@ pub const PackageManager = struct {
try installWithManager(ctx, manager, new_package_json_source, log_level);
- if (op == .update or op == .add) {
+ if (op == .update or op == .add or op == .link or op == .unlink) {
for (manager.package_json_updates) |update| {
if (update.failed) {
Global.exit(1);
@@ -4183,7 +4561,7 @@ pub const PackageManager = struct {
new_package_json_source = try ctx.allocator.dupe(u8, package_json_writer_two.ctx.writtenWithoutTrailingZero());
}
- if (!manager.options.dry_run) {
+ if (manager.options.do.write_package_json) {
// Now that we've run the install step
// We can save our in-memory package.json to disk
try manager.root_package_json_file.pwriteAll(new_package_json_source, 0);
@@ -4378,13 +4756,69 @@ pub const PackageManager = struct {
installer.cache_dir = std.fs.cwd();
}
},
+ .symlink => {
+ const directory = this.manager.globalLinkDir() catch |err| {
+ if (comptime log_level != .silent) {
+ const fmt = "\n<r><red>error:<r> unable to access global directory while installing <b>{s}<r>: {s}\n";
+ const args = .{ name, @errorName(err) };
+
+ if (comptime log_level.showProgress()) {
+ if (Output.enable_ansi_colors) {
+ this.progress.log(comptime Output.prettyFmt(fmt, true), args);
+ } else {
+ this.progress.log(comptime Output.prettyFmt(fmt, false), args);
+ }
+ } else {
+ Output.prettyErrorln(fmt, args);
+ }
+ }
+
+ if (this.manager.options.enable.fail_early) {
+ Global.exit(1);
+ }
+
+ Output.flush();
+ this.summary.fail += 1;
+ return;
+ };
+
+ const folder = resolution.value.symlink.slice(buf);
+
+ if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) {
+ installer.cache_dir_subpath = ".";
+ installer.cache_dir = std.fs.cwd();
+ } else {
+ const global_link_dir = this.manager.globalLinkDirPath() catch unreachable;
+ var ptr = &this.folder_path_buf;
+ var remain: []u8 = this.folder_path_buf[0..];
+ @memcpy(ptr, global_link_dir.ptr, global_link_dir.len);
+ remain = remain[global_link_dir.len..];
+ if (global_link_dir[global_link_dir.len - 1] != std.fs.path.sep) {
+ remain[0] = std.fs.path.sep;
+ remain = remain[1..];
+ }
+ @memcpy(remain.ptr, folder.ptr, folder.len);
+ remain = remain[folder.len..];
+ remain[0] = 0;
+ const len = @ptrToInt(remain.ptr) - @ptrToInt(ptr);
+ installer.cache_dir_subpath = std.meta.assumeSentinel(
+ this.folder_path_buf[0..len :0],
+ 0,
+ );
+ installer.cache_dir = directory;
+ }
+ },
else => return,
}
+
const needs_install = this.force_install or this.skip_verify_installed_version_number or !installer.verify();
this.summary.skipped += @as(u32, @boolToInt(!needs_install));
if (needs_install) {
- const result = installer.install(this.skip_delete);
+ const result: PackageInstall.Result = switch (resolution.tag) {
+ .symlink => installer.installFromLink(this.skip_delete),
+ else => installer.install(this.skip_delete),
+ };
switch (result) {
.success => {
const is_duplicate = this.successfully_installed.isSet(package_id);
@@ -4492,6 +4926,12 @@ pub const PackageManager = struct {
this.summary.fail += 1;
},
}
+ } else if (cause.err == error.DanglingSymlink) {
+ Output.prettyErrorln(
+ "<r><red>error<r>: <b>{s}<r> \"link:{s}\" not found (try running 'bun link' in the intended package's folder)<r>",
+ .{ @errorName(cause.err), this.names[package_id].slice(buf) },
+ );
+ this.summary.fail += 1;
} else {
Output.prettyErrorln(
"<r><red>error<r>: <b><red>{s}<r> installing <b>{s}<r>",
@@ -5300,7 +5740,8 @@ pub const PackageManager = struct {
}
if (install_summary.fail > 0) {
- Output.prettyln("<r> Failed to install <red><b>{d}<r> packages\n", .{install_summary.fail});
+ Output.prettyln("<r>Failed to install <red><b>{d}<r> packages\n", .{install_summary.fail});
+ Output.flush();
}
if (run_lifecycle_scripts and install_summary.fail == 0) {
diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig
index d6b3b4824..cca34b20b 100644
--- a/src/install/lockfile.zig
+++ b/src/install/lockfile.zig
@@ -1480,7 +1480,7 @@ pub fn initEmpty(this: *Lockfile, allocator: std.mem.Allocator) !void {
pub fn getPackageID(
this: *Lockfile,
name_hash: u64,
- // if it's a peer dependency
+ // if it's a peer dependency, a folder, or a symlink
version: ?Dependency.Version,
resolution: Resolution,
) ?PackageID {
@@ -1601,7 +1601,6 @@ pub fn appendPackageWithID(this: *Lockfile, package_: Lockfile.Package, id: Pack
defer {
if (comptime Environment.isDebug) {
std.debug.assert(this.getPackageID(package_.name_hash, null, package_.resolution) != null);
- std.debug.assert(this.getPackageID(package_.name_hash, null, package_.resolution).? == id);
}
}
var package = package_;
@@ -2137,6 +2136,8 @@ pub const Package = extern struct {
add,
remove,
update,
+ unlink,
+ link,
};
pub const Summary = struct {
diff --git a/src/install/resolution.zig b/src/install/resolution.zig
index 231ff3475..baf31b2ad 100644
--- a/src/install/resolution.zig
+++ b/src/install/resolution.zig
@@ -212,7 +212,7 @@ pub const Resolution = extern struct {
.github => try formatter.resolution.value.github.formatAs("github", formatter.buf, layout, opts, writer),
.gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab", formatter.buf, layout, opts, writer),
.workspace => try std.fmt.format(writer, "workspace://{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}),
- .symlink => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
+ .symlink => try std.fmt.format(writer, "link:{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
.single_file_module => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}),
else => {},
}
@@ -242,7 +242,6 @@ pub const Resolution = extern struct {
workspace: String,
/// global link
- /// not implemented yet
symlink: String,
single_file_module: String,
diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig
index 1c82e6b4f..bf9d5a78b 100644
--- a/src/install/resolvers/folder_resolver.zig
+++ b/src/install/resolvers/folder_resolver.zig
@@ -13,6 +13,7 @@ const strings = @import("strings");
const Resolution = @import("../resolution.zig").Resolution;
const String = @import("../semver.zig").String;
const bun = @import("../../global.zig");
+const Dependency = @import("../dependency.zig");
pub const FolderResolution = union(Tag) {
package_id: PackageID,
new_package_id: PackageID,
@@ -30,30 +31,32 @@ pub const FolderResolution = union(Tag) {
return std.hash.Wyhash.hash(0, normalized_path);
}
- pub const Resolver = struct {
- folder_path: string,
+ pub fn NewResolver(comptime tag: Resolution.Tag) type {
+ return struct {
+ folder_path: string,
- pub fn resolve(this: Resolver, comptime Builder: type, builder: Builder, _: JSAst.Expr) !Resolution {
- return Resolution{
- .tag = .folder,
- .value = .{
- .folder = builder.append(String, this.folder_path),
- },
- };
- }
+ pub fn resolve(this: @This(), comptime Builder: type, builder: Builder, _: JSAst.Expr) !Resolution {
+ return Resolution{
+ .tag = tag,
+ .value = @unionInit(Resolution.Value, @tagName(tag), builder.append(String, this.folder_path)),
+ };
+ }
- pub fn count(this: Resolver, comptime Builder: type, builder: Builder, _: JSAst.Expr) void {
- builder.count(this.folder_path);
- }
- };
+ pub fn count(this: @This(), comptime Builder: type, builder: Builder, _: JSAst.Expr) void {
+ builder.count(this.folder_path);
+ }
+ };
+ }
- pub fn getOrPut(non_normalized_path: string, manager: *PackageManager) FolderResolution {
+ pub const Resolver = NewResolver(Resolution.Tag.folder);
+ pub const SymlinkResolver = NewResolver(Resolution.Tag.symlink);
- // We consider it valid if there is a package.json in the folder
- const normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str);
- var joined: [bun.MAX_PATH_BYTES]u8 = undefined;
+ pub fn normalizePackageJSONPath(global_or_relative: GlobalOrRelative, joined: *[bun.MAX_PATH_BYTES]u8, non_normalized_path: string) [2]string {
var abs: string = "";
var rel: string = "";
+ // We consider it valid if there is a package.json in the folder
+ const normalized = std.mem.trimRight(u8, normalize(non_normalized_path), std.fs.path.sep_str);
+
if (strings.startsWithChar(normalized, '.')) {
var tempcat: [bun.MAX_PATH_BYTES]u8 = undefined;
@@ -61,64 +64,120 @@ pub const FolderResolution = union(Tag) {
tempcat[normalized.len] = std.fs.path.sep;
std.mem.copy(u8, tempcat[normalized.len + 1 ..], "package.json");
var parts = [_]string{ FileSystem.instance.top_level_dir, tempcat[0 .. normalized.len + 1 + "package.json".len] };
- abs = FileSystem.instance.absBuf(&parts, &joined);
+ abs = FileSystem.instance.absBuf(&parts, joined);
rel = FileSystem.instance.relative(FileSystem.instance.top_level_dir, abs[0 .. abs.len - "/package.json".len]);
} else {
- std.mem.copy(u8, &joined, normalized);
- joined[normalized.len] = std.fs.path.sep;
- joined[normalized.len + 1 ..][0.."package.json".len].* = "package.json".*;
- abs = joined[0 .. normalized.len + 1 + "package.json".len];
+ var remain: []u8 = joined[0..];
+ switch (global_or_relative) {
+ .global => |path| {
+ const offset = path.len - @as(usize, @boolToInt(path[path.len - 1] == std.fs.path.sep));
+ @memcpy(remain.ptr, path.ptr, offset);
+ remain = remain[offset..];
+ if ((path[path.len - 1] != std.fs.path.sep) and (normalized[0] != std.fs.path.sep)) {
+ remain[0] = std.fs.path.sep;
+ remain = remain[1..];
+ }
+ },
+ .relative => {},
+ }
+ std.mem.copy(u8, remain, normalized);
+ remain[normalized.len] = std.fs.path.sep;
+ remain[normalized.len + 1 ..][0.."package.json".len].* = "package.json".*;
+ remain = remain[normalized.len + 1 + "package.json".len ..];
+ abs = joined[0 .. joined.len - remain.len];
// We store the folder name without package.json
rel = abs[0 .. abs.len - "/package.json".len];
}
- var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable;
- if (entry.found_existing) return entry.value_ptr.*;
-
- joined[abs.len] = 0;
- var joinedZ: [:0]u8 = joined[0..abs.len :0];
+ return .{ abs, rel };
+ }
- var package_json: std.fs.File = std.fs.cwd().openFileZ(joinedZ, .{ .mode = .read_only }) catch |err| {
- entry.value_ptr.* = .{ .err = err };
- return entry.value_ptr.*;
- };
+ pub fn readPackageJSONFromDisk(
+ manager: *PackageManager,
+ joinedZ: [:0]const u8,
+ abs: []const u8,
+ version: Dependency.Version,
+ comptime features: Features,
+ comptime ResolverType: type,
+ resolver: ResolverType,
+ ) !Lockfile.Package {
+ var package_json: std.fs.File = try std.fs.cwd().openFileZ(joinedZ, .{ .mode = .read_only });
+ defer package_json.close();
var package = Lockfile.Package{};
var body = Npm.Registry.BodyPool.get(manager.allocator);
-
defer Npm.Registry.BodyPool.release(body);
- const len = package_json.getEndPos() catch |err| {
- entry.value_ptr.* = .{ .err = err };
- return entry.value_ptr.*;
- };
+ const len = try package_json.getEndPos();
body.data.reset();
body.data.inflate(@maximum(len, 2048)) catch unreachable;
body.data.list.expandToCapacity();
- const source_buf = package_json.readAll(body.data.list.items) catch |err| {
- entry.value_ptr.* = .{ .err = err };
- return entry.value_ptr.*;
- };
- var resolver = Resolver{
- .folder_path = rel,
- };
+ const source_buf = try package_json.readAll(body.data.list.items);
+
const source = logger.Source.initPathString(abs, body.data.list.items[0..source_buf]);
- Lockfile.Package.parse(
+ try Lockfile.Package.parse(
manager.lockfile,
&package,
manager.allocator,
manager.log,
source,
- Resolver,
+ ResolverType,
resolver,
- Features.folder,
- ) catch |err| {
- // Folders are considered dependency-less
- entry.value_ptr.* = .{ .err = err };
+ features,
+ );
+
+ if (manager.lockfile.getPackageID(package.name_hash, version, package.resolution)) |existing_id| {
+ return manager.lockfile.packages.get(existing_id);
+ }
+
+ return manager.lockfile.appendPackage(package) catch unreachable;
+ }
+
+ pub const GlobalOrRelative = union(enum) {
+ global: []const u8,
+ relative: void,
+ };
+
+ pub fn getOrPut(global_or_relative: GlobalOrRelative, version: Dependency.Version, non_normalized_path: string, manager: *PackageManager) FolderResolution {
+ var joined: [bun.MAX_PATH_BYTES]u8 = undefined;
+ const paths = normalizePackageJSONPath(global_or_relative, &joined, non_normalized_path);
+ const abs = paths[0];
+ const rel = paths[1];
+
+ var entry = manager.folders.getOrPut(manager.allocator, hash(abs)) catch unreachable;
+ if (entry.found_existing) return entry.value_ptr.*;
+
+ joined[abs.len] = 0;
+ var joinedZ: [:0]u8 = joined[0..abs.len :0];
+ const package = switch (global_or_relative) {
+ .global => readPackageJSONFromDisk(
+ manager,
+ joinedZ,
+ abs,
+ version,
+ Features.link,
+ SymlinkResolver,
+ SymlinkResolver{ .folder_path = non_normalized_path },
+ ),
+ .relative => readPackageJSONFromDisk(
+ manager,
+ joinedZ,
+ abs,
+ version,
+ Features.folder,
+ Resolver,
+ Resolver{ .folder_path = rel },
+ ),
+ } catch |err| {
+ if (err == error.FileNotFound) {
+ entry.value_ptr.* = .{ .err = error.MissingPackageJSON };
+ } else {
+ entry.value_ptr.* = .{ .err = err };
+ }
+
return entry.value_ptr.*;
};
- package = manager.lockfile.appendPackage(package) catch unreachable;
entry.value_ptr.* = .{ .package_id = package.meta.id };
return FolderResolution{ .new_package_id = package.meta.id };
}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 4d856b966..540572b6f 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -82,6 +82,44 @@ pub inline fn containsAny(in: anytype, target: string) bool {
return false;
}
+/// https://docs.npmjs.com/cli/v8/configuring-npm/package-json
+/// - The name must be less than or equal to 214 characters. This includes the scope for scoped packages.
+/// - The names of scoped packages can begin with a dot or an underscore. This is not permitted without a scope.
+/// - New packages must not have uppercase letters in the name.
+/// - The name ends up being part of a URL, an argument on the command line, and
+/// a folder name. Therefore, the name can't contain any non-URL-safe
+/// characters.
+pub inline fn isNPMPackageName(target: string) bool {
+ if (target.len >= 215) return false;
+ switch (target[0]) {
+ 'a'...'z',
+ '0'...'9',
+ '$',
+ '@',
+ '-',
+ => {},
+ else => return false,
+ }
+ if (target.len == 1) return true;
+
+ var slash_count: usize = 0;
+
+ for (target[1..]) |c| {
+ switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '$', '-', '_', '.' => {},
+ '/' => {
+ if (slash_count > 0) {
+ return false;
+ }
+ slash_count += 1;
+ },
+ else => return false,
+ }
+ }
+
+ return true;
+}
+
pub inline fn indexAny(in: anytype, target: string) ?usize {
for (in) |str, i| if (indexOf(str, target) != null) return i;
return null;