From 4afbd2b5aa68549ab2fc7c6d6d09857ade6de1b5 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Wed, 11 Jan 2023 08:08:23 +0000 Subject: [PATCH] support installation of NPM workspaces --- src/install/dependency.zig | 132 ++++----- src/install/install.zig | 104 +++++-- src/install/lockfile.zig | 327 ++++++++++++++-------- src/install/resolution.zig | 6 +- src/install/resolvers/folder_resolver.zig | 33 ++- 5 files changed, 366 insertions(+), 236 deletions(-) diff --git a/src/install/dependency.zig b/src/install/dependency.zig index e85d7cb85f85e4..c07d3444ca3e19 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -235,6 +235,7 @@ pub const Version = struct { .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), + .workspace => lhs.value.workspace.eql(rhs.value.workspace, lhs_buf, rhs_buf), else => true, }; } @@ -259,7 +260,6 @@ pub const Version = struct { /// https://stackoverflow.com/questions/51954956/whats-the-difference-between-yarn-link-and-npm-link symlink = 5, - /// TODO: workspace = 6, /// TODO: git = 7, @@ -271,20 +271,24 @@ pub const Version = struct { } pub inline fn isGitHubRepoPath(dependency: string) bool { - var slash_count: u8 = 0; + if (dependency.len < 3) return false; + if (dependency[0] == '/') return false; - for (dependency) |c| { - slash_count += @as(u8, @boolToInt(c == '/')); - if (slash_count > 1 or c == '#') break; + var slash_index: usize = 0; + for (dependency) |c, i| { // Must be alphanumeric switch (c) { - '\\', '/', 'a'...'z', 'A'...'Z', '0'...'9', '%' => {}, + '/' => { + if (slash_index > 0) return false; + slash_index = i; + }, + '\\', 'a'...'z', 'A'...'Z', '0'...'9', '%' => {}, else => return false, } } - return (slash_count == 1); + return slash_index != dependency.len - 1; } // this won't work for query string params @@ -331,32 +335,17 @@ pub const Version = struct { // git://, git@, git+ssh 'g' => { - if (strings.eqlComptime( - dependency[0..@min("git://".len, dependency.len)], - "git://", - ) or strings.eqlComptime( - dependency[0..@min("git@".len, dependency.len)], - "git@", - ) or strings.eqlComptime( - dependency[0..@min("git+ssh".len, dependency.len)], - "git+ssh", - ) or strings.eqlComptime( - dependency[0..@min("git+file".len, dependency.len)], - "git+file", - ) or strings.eqlComptime( - dependency[0..@min("git+http".len, dependency.len)], - "git+http", - ) or strings.eqlComptime( - dependency[0..@min("git+https".len, dependency.len)], - "git+https", - )) { + if (strings.hasPrefixComptime(dependency, "git://") or + strings.hasPrefixComptime(dependency, "git@") or + strings.hasPrefixComptime(dependency, "git+ssh") or + strings.hasPrefixComptime(dependency, "git+file") or + strings.hasPrefixComptime(dependency, "git+http") or + strings.hasPrefixComptime(dependency, "git+https")) + { return .git; } - if (strings.eqlComptime( - dependency[0..@min("github".len, dependency.len)], - "github", - ) or isGitHubRepoPath(dependency)) { + if (strings.hasPrefixComptime(dependency, "github:") or isGitHubRepoPath(dependency)) { return .github; } @@ -378,24 +367,15 @@ pub const Version = struct { } var remainder = dependency; - if (strings.eqlComptime( - remainder[0..@min("https://".len, remainder.len)], - "https://", - )) { + if (strings.hasPrefixComptime(remainder, "https://")) { remainder = remainder["https://".len..]; } - if (strings.eqlComptime( - remainder[0..@min("http://".len, remainder.len)], - "http://", - )) { + if (strings.hasPrefixComptime(remainder, "http://")) { remainder = remainder["http://".len..]; } - if (strings.eqlComptime( - remainder[0..@min("github".len, remainder.len)], - "github", - ) or isGitHubRepoPath(remainder)) { + if (strings.hasPrefixComptime(remainder, "github.com/") or isGitHubRepoPath(remainder)) { return .github; } @@ -422,10 +402,7 @@ pub const Version = struct { if (isTarball(dependency)) return .tarball; - if (strings.eqlComptime( - dependency[0..@min("file:".len, dependency.len)], - "file:", - )) { + if (strings.hasPrefixComptime(dependency, "file:")) { return .folder; } @@ -441,10 +418,7 @@ pub const Version = struct { if (isTarball(dependency)) return .tarball; - if (strings.eqlComptime( - dependency[0..@min("link:".len, dependency.len)], - "link:", - )) { + if (strings.hasPrefixComptime(dependency, "link:")) { return .symlink; } @@ -455,25 +429,6 @@ pub const Version = struct { return .dist_tag; }, - // workspace:// - 'w' => { - if (strings.eqlComptime( - dependency[0..@min("workspace://".len, dependency.len)], - "workspace://", - )) { - return .workspace; - } - - if (isTarball(dependency)) - return .tarball; - - if (isGitHubRepoPath(dependency)) { - return .github; - } - - return .dist_tag; - }, - else => {}, } @@ -499,8 +454,7 @@ pub const Version = struct { /// Equivalent to npm link symlink: String, - /// Unsupported, but still parsed so an error can be thrown - workspace: void, + workspace: String, /// Unsupported, but still parsed so an error can be thrown git: void, /// Unsupported, but still parsed so an error can be thrown @@ -525,16 +479,26 @@ pub fn eqlResolved(a: Dependency, b: Dependency) bool { return @as(Dependency.Version.Tag, a.version) == @as(Dependency.Version.Tag, b.version) and a.resolution == b.resolution; } -pub fn parse( +pub inline fn parse( + allocator: std.mem.Allocator, + dependency: string, + sliced: *const SlicedString, + log: ?*logger.Log, +) ?Version { + return parseWithOptionalTag(allocator, dependency, null, sliced, log); +} + +pub fn parseWithOptionalTag( allocator: std.mem.Allocator, dependency_: string, + tag_or_null: ?Dependency.Version.Tag, sliced: *const SlicedString, log: ?*logger.Log, ) ?Version { var dependency = std.mem.trimLeft(u8, dependency_, " \t\n\r"); if (dependency.len == 0) return null; - const tag = Version.Tag.infer(dependency); + const tag = tag_or_null orelse Version.Tag.infer(dependency); if (tag == .npm and strings.hasPrefixComptime(dependency, "npm:")) { dependency = dependency[4..]; @@ -652,7 +616,14 @@ pub fn parseWithTag( .literal = sliced.value(), }; }, - .workspace, .git, .github => { + .workspace => { + return Version{ + .value = .{ .workspace = sliced.value() }, + .tag = .workspace, + .literal = sliced.value(), + }; + }, + .git, .github => { if (log_) |log| log.addErrorFmt(null, logger.Loc.Empty, allocator, "Support for dependency type \"{s}\" is not implemented yet (\"{s}\")", .{ @tagName(tag), dependency }) catch unreachable; return null; }, @@ -667,6 +638,7 @@ pub const Behavior = enum(u8) { pub const optional: u8 = 1 << 2; pub const dev: u8 = 1 << 3; pub const peer: u8 = 1 << 4; + pub const workspace: u8 = 1 << 5; pub inline fn isOptional(this: Behavior) bool { return (@enumToInt(this) & Behavior.optional) != 0 and !this.isPeer(); @@ -684,6 +656,10 @@ pub const Behavior = enum(u8) { return (@enumToInt(this) & Behavior.normal) != 0; } + pub inline fn isWorkspace(this: Behavior) bool { + return (@enumToInt(this) & Behavior.workspace) != 0; + } + pub inline fn setOptional(this: Behavior, value: bool) Behavior { return @intToEnum(Behavior, @enumToInt(this) | (@as(u8, @boolToInt(value))) << 2); } @@ -704,6 +680,13 @@ pub const Behavior = enum(u8) { .lt; } + if (lhs.isWorkspace() != rhs.isWorkspace()) { + return if (lhs.isWorkspace()) + .gt + else + .lt; + } + if (lhs.isDev() != rhs.isDev()) { return if (lhs.isDev()) .gt @@ -734,6 +717,7 @@ pub const Behavior = enum(u8) { pub fn isEnabled(this: Behavior, features: Features) bool { return this.isNormal() or + this.isWorkspace() or (features.dev_dependencies and this.isDev()) or (features.peer_dependencies and this.isPeer()) or (features.optional_dependencies and this.isOptional()); diff --git a/src/install/install.zig b/src/install/install.zig index cd915a900240bf..53c9a33b0fad21 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -429,12 +429,13 @@ pub const Origin = enum(u8) { }; pub const Features = struct { - optional_dependencies: bool = false, + dependencies: bool = true, dev_dependencies: bool = false, - scripts: bool = false, - peer_dependencies: bool = true, is_main: bool = false, - dependencies: bool = true, + optional_dependencies: bool = false, + peer_dependencies: bool = true, + scripts: bool = false, + workspaces: bool = false, check_for_duplicate_dependencies: bool = false, @@ -444,25 +445,18 @@ pub const Features = struct { out |= @as(u8, @boolToInt(this.optional_dependencies)) << 2; out |= @as(u8, @boolToInt(this.dev_dependencies)) << 3; out |= @as(u8, @boolToInt(this.peer_dependencies)) << 4; + out |= @as(u8, @boolToInt(this.workspaces)) << 5; return @intToEnum(Behavior, out); } pub const folder = Features{ - .optional_dependencies = true, .dev_dependencies = true, - .scripts = false, - .peer_dependencies = true, - .is_main = false, - .dependencies = true, + .optional_dependencies = true, }; pub const link = Features{ - .optional_dependencies = false, - .dev_dependencies = false, - .scripts = false, - .peer_dependencies = false, - .is_main = false, .dependencies = false, + .peer_dependencies = false, }; pub const npm = Features{ @@ -713,6 +707,15 @@ const PackageInstall = struct { this.package_install.cache_dir_subpath = folder_buf[0 .. "../".len + folder.len :0]; this.package_install.cache_dir = std.fs.cwd(); }, + .workspace => { + var folder_buf = &cache_dir_subpath_buf; + const folder = resolution.value.workspace.slice(ctx.string_buf); + std.mem.copy(u8, folder_buf, "../" ++ std.fs.path.sep_str); + std.mem.copy(u8, folder_buf["../".len..], folder); + folder_buf["../".len + folder.len] = 0; + this.package_install.cache_dir_subpath = folder_buf[0 .. "../".len + folder.len :0]; + this.package_install.cache_dir = std.fs.cwd(); + }, else => return, } @@ -1265,15 +1268,19 @@ const PackageInstall = struct { } pub fn installFromLink(this: *PackageInstall, skip_delete: bool) Result { - + const dest_path = this.destination_dir_subpath; // 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 {}; + if (!skip_delete and !strings.eqlComptime(dest_path, ".")) 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.dir.fd, this.destination_dir_subpath) catch |err| { + std.os.symlinkat( + std.fs.path.relative(this.allocator, "node_modules", symlinked_path[0..symlinked_path.len]) catch unreachable, + this.destination_dir.dir.fd, + dest_path[0..dest_path.len], + ) catch |err| { return Result{ .fail = .{ .err = err, @@ -2349,7 +2356,24 @@ pub const PackageManager = struct { .folder => { // relative to cwd - const res = FolderResolution.getOrPut(.{ .relative = void{} }, version, version.value.folder.slice(this.lockfile.buffers.string_bytes.items), this); + const res = FolderResolution.getOrPut(.{ .relative = .folder }, version, version.value.folder.slice(this.lockfile.buffers.string_bytes.items), this); + + switch (res) { + .err => |err| return err, + .package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id) }; + }, + + .new_package_id => |package_id| { + successFn(this, dependency_id, package_id); + return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; + }, + } + }, + .workspace => { + // relative to cwd + const res = FolderResolution.getOrPut(.{ .relative = .workspace }, version, version.value.workspace.slice(this.lockfile.buffers.string_bytes.items), this); switch (res) { .err => |err| return err, @@ -2370,12 +2394,12 @@ pub const PackageManager = struct { switch (res) { .err => |err| return err, .package_id => |package_id| { - this.lockfile.buffers.resolutions.items[dependency_id] = package_id; + successFn(this, 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; + successFn(this, dependency_id, package_id); return ResolvedPackageResult{ .package = this.lockfile.packages.get(package_id), .is_first_time = true }; }, } @@ -2550,7 +2574,7 @@ pub const PackageManager = struct { } switch (dependency.version.tag) { - .folder, .npm, .dist_tag => { + .dist_tag, .folder, .npm => { retry_from_manifests_ptr: while (true) { var resolve_result_ = this.getOrPutResolvedPackage( name_hash, @@ -2734,7 +2758,7 @@ pub const PackageManager = struct { } return; }, - .symlink => { + .symlink, .workspace => { const _result = this.getOrPutResolvedPackage( name_hash, name, @@ -3472,8 +3496,14 @@ pub const PackageManager = struct { positionals: []const string = &[_]string{}, update: Update = Update{}, dry_run: bool = false, - remote_package_features: Features = Features{ .peer_dependencies = false, .optional_dependencies = true }, - local_package_features: Features = Features{ .peer_dependencies = false, .dev_dependencies = true }, + remote_package_features: Features = Features{ + .optional_dependencies = true, + .peer_dependencies = false, + }, + local_package_features: Features = Features{ + .dev_dependencies = true, + .peer_dependencies = false, + }, // The idea here is: // 1. package has a platform-specific binary to install // 2. To prevent downloading & installing incompatible versions, they stick the "real" one in optionalDependencies @@ -5657,13 +5687,24 @@ pub const PackageManager = struct { // "mineflayer": "file:." if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { installer.cache_dir_subpath = "."; - installer.cache_dir = .{ .dir = std.fs.cwd() }; } else { @memcpy(&this.folder_path_buf, folder.ptr, folder.len); this.folder_path_buf[folder.len] = 0; installer.cache_dir_subpath = std.meta.assumeSentinel(this.folder_path_buf[0..folder.len], 0); - installer.cache_dir = .{ .dir = std.fs.cwd() }; } + installer.cache_dir = .{ .dir = std.fs.cwd() }; + }, + .workspace => { + const folder = resolution.value.workspace.slice(buf); + // Handle when a package depends on itself + if (folder.len == 0 or (folder.len == 1 and folder[0] == '.')) { + installer.cache_dir_subpath = "."; + } else { + @memcpy(&this.folder_path_buf, folder.ptr, folder.len); + this.folder_path_buf[folder.len] = 0; + installer.cache_dir_subpath = std.meta.assumeSentinel(this.folder_path_buf[0..folder.len], 0); + } + installer.cache_dir = .{ .dir = std.fs.cwd() }; }, .symlink => { const directory = this.manager.globalLinkDir() catch |err| { @@ -5726,6 +5767,7 @@ pub const PackageManager = struct { if (needs_install) { const result: PackageInstall.Result = switch (resolution.tag) { .symlink => installer.installFromLink(this.skip_delete), + .workspace => installer.installFromLink(this.skip_delete), else => installer.install(this.skip_delete), }; switch (result) { @@ -6284,12 +6326,13 @@ pub const PackageManager = struct { ctx.log, package_json_source, Features{ - .optional_dependencies = true, + .check_for_duplicate_dependencies = true, .dev_dependencies = true, .is_main = true, - .check_for_duplicate_dependencies = true, + .optional_dependencies = true, .peer_dependencies = false, .scripts = true, + .workspaces = true, }, ); manager.lockfile.scripts = lockfile.scripts; @@ -6409,12 +6452,13 @@ pub const PackageManager = struct { ctx.log, package_json_source, Features{ - .optional_dependencies = true, + .check_for_duplicate_dependencies = true, .dev_dependencies = true, .is_main = true, - .check_for_duplicate_dependencies = true, + .optional_dependencies = true, .peer_dependencies = false, .scripts = true, + .workspaces = true, }, ); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 29a5b8e50a3d0f..ea1e9c4f8fcb34 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -92,10 +92,6 @@ pub const SmallExternalStringList = ExternalSlice(String); /// The version of the lockfile format, intended to prevent data corruption for format changes. format: FormatVersion = .v1, -/// Not used yet. -/// Eventually, this will be a relative path to a parent lockfile -workspace_path: string = "", - meta_hash: MetaHash = zero_hash, packages: Lockfile.Package.List = Lockfile.Package.List{}, @@ -112,10 +108,10 @@ scratch: Scratch = Scratch{}, scripts: Scripts = .{}, const Stream = std.io.FixedBufferStream([]u8); +const StringArrayList = std.ArrayListUnmanaged(string); pub const default_filename = "bun.lockb"; pub const Scripts = struct { - const StringArrayList = std.ArrayListUnmanaged(string); const RunCommand = @import("../cli/run_command.zig").RunCommand; preinstall: StringArrayList = .{}, @@ -187,7 +183,6 @@ pub fn loadFromDisk(this: *Lockfile, allocator: std.mem.Allocator, log: *logger. pub fn loadFromBytes(this: *Lockfile, buf: []u8, allocator: std.mem.Allocator, log: *logger.Log) LoadFromDiskResult { var stream = Stream{ .buffer = buf, .pos = 0 }; - this.workspace_path = ""; this.format = FormatVersion.current; this.scripts = .{}; @@ -1813,6 +1808,7 @@ pub const Package = extern struct { pub const dev = DependencyGroup{ .prop = "devDependencies", .field = "dev_dependencies", .behavior = @intToEnum(Behavior, Behavior.dev) }; pub const optional = DependencyGroup{ .prop = "optionalDependencies", .field = "optional_dependencies", .behavior = @intToEnum(Behavior, Behavior.optional) }; pub const peer = DependencyGroup{ .prop = "peerDependencies", .field = "peer_dependencies", .behavior = @intToEnum(Behavior, Behavior.peer) }; + pub const workspaces = DependencyGroup{ .prop = "workspaces", .field = "workspaces", .behavior = @intToEnum(Behavior, Behavior.workspace) }; }; pub inline fn isDisabled(this: *const Lockfile.Package) bool { @@ -2348,7 +2344,7 @@ pub const Package = extern struct { source: logger.Source, comptime features: Features, ) !void { - return try parse(lockfile, package, allocator, log, source, void, void{}, features); + return parse(lockfile, package, allocator, log, source, void, void{}, features); } pub fn parse( @@ -2390,6 +2386,116 @@ pub const Package = extern struct { ); } + fn parseDependency( + lockfile: *Lockfile, + allocator: std.mem.Allocator, + log: *logger.Log, + source: logger.Source, + comptime group: DependencyGroup, + string_builder: *StringBuilder, + comptime features: Features, + package_dependencies: []Dependency, + dependencies: []Dependency, + tag: ?Dependency.Version.Tag, + name: string, + version: string, + key: Expr, + value: Expr, + ) !?Dependency { + const external_name = string_builder.append(ExternalString, name); + const external_version = string_builder.append(String, version); + const sliced = external_version.sliced(lockfile.buffers.string_bytes.items); + + var dependency_version = Dependency.parseWithOptionalTag( + allocator, + sliced.slice, + tag, + &sliced, + log, + ) orelse Dependency.Version{}; + + switch (dependency_version.tag) { + .folder => { + const folder_path = dependency_version.value.folder.slice(lockfile.buffers.string_bytes.items); + dependency_version.value.folder = string_builder.append( + String, + Path.relative( + FileSystem.instance.top_level_dir, + Path.joinAbsString( + FileSystem.instance.top_level_dir, + &[_]string{ + source.path.name.dir, + folder_path, + }, + .posix, + ), + ), + ); + }, + .workspace => { + const folder_path = dependency_version.value.workspace.slice(lockfile.buffers.string_bytes.items); + dependency_version.value.workspace = string_builder.append( + String, + Path.relative( + FileSystem.instance.top_level_dir, + Path.joinAbsString( + FileSystem.instance.top_level_dir, + &[_]string{ + source.path.name.dir, + folder_path, + }, + .posix, + ), + ), + ); + }, + else => {}, + } + + const this_dep = Dependency{ + .behavior = group.behavior, + .name = external_name.value, + .name_hash = external_name.hash, + .version = dependency_version, + }; + + if (comptime features.check_for_duplicate_dependencies) { + var entry = lockfile.scratch.duplicate_checker_map.getOrPutAssumeCapacity(external_name.hash); + if (entry.found_existing) { + // duplicate dependencies are allowed in optionalDependencies + if (comptime group.behavior.isOptional()) { + for (package_dependencies[0 .. package_dependencies.len - dependencies.len]) |package_dep, j| { + if (package_dep.name_hash == this_dep.name_hash) { + package_dependencies[j] = this_dep; + break; + } + } + return null; + } else { + var notes = try allocator.alloc(logger.Data, 1); + + notes[0] = logger.Data{ + .text = try std.fmt.allocPrint(lockfile.allocator, "\"{s}\" originally specified here", .{name}), + .location = logger.Location.init_or_nil(&source, source.rangeOfString(entry.value_ptr.*)), + }; + + try log.addRangeErrorFmtWithNotes( + &source, + source.rangeOfString(key.loc), + lockfile.allocator, + notes, + "Duplicate dependency: \"{s}\" specified in package.json", + .{name}, + ); + } + } + + entry.value_ptr.* = value.loc; + } + + return this_dep; + } + pub fn parseWithJSON( package: *Lockfile.Package, lockfile: *Lockfile, @@ -2500,7 +2606,8 @@ pub const Package = extern struct { @as(usize, @boolToInt(features.dependencies)) + @as(usize, @boolToInt(features.dev_dependencies)) + @as(usize, @boolToInt(features.optional_dependencies)) + - @as(usize, @boolToInt(features.peer_dependencies)) + @as(usize, @boolToInt(features.peer_dependencies)) + + @as(usize, @boolToInt(features.workspaces)) ]DependencyGroup = undefined; var out_group_i: usize = 0; if (features.dependencies) { @@ -2522,25 +2629,59 @@ pub const Package = extern struct { out_group_i += 1; } + if (features.workspaces) { + out_groups[out_group_i] = DependencyGroup.workspaces; + out_group_i += 1; + } + break :brk out_groups; }; + var workspace_names: []string = undefined; inline for (dependency_groups) |group| { if (json.asProperty(group.prop)) |dependencies_q| { - if (dependencies_q.expr.data == .e_object) { - for (dependencies_q.expr.data.e_object.properties.slice()) |item| { - const key = item.key.?.asString(allocator) orelse ""; - const value = item.value.?.asString(allocator) orelse ""; + switch (dependencies_q.expr.data) { + .e_array => |arr| { + workspace_names = try allocator.alloc(string, arr.items.len); + for (arr.slice()) |item, i| { + const path = item.asString(allocator) orelse return error.InvalidPackageJSON; + + const workspace_dir = try std.fs.cwd().openDir(path, .{}); + const workspace_file = try workspace_dir.openFile("package.json", .{ .mode = .read_only }); + const workspace_stat = try workspace_file.stat(); + var workspace_buf = try allocator.alloc(u8, workspace_stat.size + 64); + const workspace_contents_len = try workspace_file.preadAll(workspace_buf, 0); + const workspace_source = logger.Source.initPathString(path, workspace_buf[0..workspace_contents_len]); + const workspace_json = try json_parser.ParseJSONUTF8(&workspace_source, log, allocator); + + if (workspace_json.asProperty("name")) |workspace_name_q| { + if (workspace_name_q.expr.asString(allocator)) |workspace_name| { + string_builder.count(workspace_name); + workspace_names[i] = workspace_name; + } else return error.InvalidPackageJSON; + } else return error.InvalidPackageJSON; + + string_builder.count(path); + string_builder.cap += bun.MAX_PATH_BYTES; + } + total_dependencies_count += @truncate(u32, arr.items.len); + }, + .e_object => |obj| { + for (obj.properties.slice()) |item| { + const key = item.key.?.asString(allocator) orelse return error.InvalidPackageJSON; + const value = item.value.?.asString(allocator) orelse return error.InvalidPackageJSON; - string_builder.count(key); - string_builder.count(value); + string_builder.count(key); + string_builder.count(value); - // If it's a folder, pessimistically assume we will need a maximum path - if (Dependency.Version.Tag.infer(value) == .folder) { - string_builder.cap += bun.MAX_PATH_BYTES; + // If it's a folder, pessimistically assume we will need a maximum path + if (Dependency.Version.Tag.infer(value) == .folder) { + string_builder.cap += bun.MAX_PATH_BYTES; + } } - } - total_dependencies_count += @truncate(u32, dependencies_q.expr.data.e_object.properties.len); + total_dependencies_count += @truncate(u32, obj.properties.len); + }, + else => {}, } } } @@ -2670,95 +2811,56 @@ pub const Package = extern struct { inline for (dependency_groups) |group| { if (json.asProperty(group.prop)) |dependencies_q| { - if (dependencies_q.expr.data == .e_object) { - const dependency_props: []const JSAst.G.Property = dependencies_q.expr.data.e_object.properties.slice(); - var i: usize = 0; - outer: while (i < dependency_props.len) { - const item = dependency_props[i]; - - const name_ = item.key.?.asString(allocator) orelse ""; - const version_ = item.value.?.asString(allocator) orelse ""; - - const external_name = string_builder.append(ExternalString, name_); - - const external_version = string_builder.append(String, version_); - - const sliced = external_version.sliced( - lockfile.buffers.string_bytes.items, - ); - - var dependency_version = Dependency.parse( - allocator, - sliced.slice, - &sliced, - log, - ) orelse Dependency.Version{}; - - if (dependency_version.tag == .folder) { - const folder_path = dependency_version.value.folder.slice(lockfile.buffers.string_bytes.items); - dependency_version.value.folder = string_builder.append( - String, - Path.relative( - FileSystem.instance.top_level_dir, - Path.joinAbsString( - FileSystem.instance.top_level_dir, - &[_]string{ - source.path.name.dir, - folder_path, - }, - .posix, - ), - ), - ); + switch (dependencies_q.expr.data) { + .e_array => |arr| { + for (arr.slice()) |item, i| { + const this_dep = try parseDependency( + lockfile, + allocator, + log, + source, + group, + &string_builder, + features, + package_dependencies, + dependencies, + .workspace, + workspace_names[i], + item.asString(allocator).?, + item, + item, + ) orelse continue; + + dependencies[0] = this_dep; + dependencies = dependencies[1..]; } - - const this_dep = Dependency{ - .behavior = group.behavior, - .name = external_name.value, - .name_hash = external_name.hash, - .version = dependency_version, - }; - - if (comptime features.check_for_duplicate_dependencies) { - var entry = lockfile.scratch.duplicate_checker_map.getOrPutAssumeCapacity(external_name.hash); - if (entry.found_existing) { - // duplicate dependencies are allowed in optionalDependencies - if (comptime group.behavior.isOptional()) { - for (package_dependencies[0 .. package_dependencies.len - dependencies.len]) |package_dep, j| { - if (package_dep.name_hash == this_dep.name_hash) { - package_dependencies[j] = this_dep; - break; - } - } - - i += 1; - continue :outer; - } else { - var notes = try allocator.alloc(logger.Data, 1); - - notes[0] = logger.Data{ - .text = try std.fmt.allocPrint(lockfile.allocator, "\"{s}\" originally specified here", .{name_}), - .location = logger.Location.init_or_nil(&source, source.rangeOfString(entry.value_ptr.*)), - }; - - try log.addRangeErrorFmtWithNotes( - &source, - source.rangeOfString(item.key.?.loc), - lockfile.allocator, - notes, - "Duplicate dependency: \"{s}\" specified in package.json", - .{name_}, - ); - } - } - - entry.value_ptr.* = item.value.?.loc; + }, + .e_object => |obj| { + for (obj.properties.slice()) |item| { + const key = item.key.?; + const value = item.value.?; + const this_dep = try parseDependency( + lockfile, + allocator, + log, + source, + group, + &string_builder, + features, + package_dependencies, + dependencies, + null, + key.asString(allocator) orelse "", + value.asString(allocator) orelse "", + key, + value, + ) orelse continue; + + dependencies[0] = this_dep; + dependencies = dependencies[1..]; } - - dependencies[0] = this_dep; - dependencies = dependencies[1..]; - i += 1; - } + }, + else => {}, } } } @@ -3215,10 +3317,6 @@ pub const Serializer = struct { try writer.writeIntLittle(u64, 0); const end = try stream.getPos(); - try writer.writeIntLittle(u64, this.workspace_path.len); - if (this.workspace_path.len > 0) - try writer.writeAll(this.workspace_path); - try writer.writeAll(&alignment_bytes_to_repeat_buffer); _ = try std.os.pwrite(stream.handle, std.mem.asBytes(&end), pos); @@ -3265,15 +3363,6 @@ pub const Serializer = struct { std.debug.assert(stream.pos == total_buffer_size); - load_workspace: { - const workspace_path_len = reader.readIntLittle(u64) catch break :load_workspace; - if (workspace_path_len > 0 and workspace_path_len < bun.MAX_PATH_BYTES) { - var workspace_path = try allocator.alloc(u8, workspace_path_len); - const len = reader.readAll(workspace_path) catch break :load_workspace; - lockfile.workspace_path = workspace_path[0..len]; - } - } - lockfile.scratch = Lockfile.Scratch.init(allocator); { diff --git a/src/install/resolution.zig b/src/install/resolution.zig index d21855d7c46c00..12fb53449d1c6d 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -191,7 +191,7 @@ pub const Resolution = extern struct { .gitlab => try formatter.resolution.value.gitlab.formatAs("gitlab", formatter.buf, layout, opts, writer), .workspace => try std.fmt.format(writer, "workspace://{s}", .{formatter.resolution.value.workspace.slice(formatter.buf)}), .symlink => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}), - .single_file_module => try std.fmt.format(writer, "link://{s}", .{formatter.resolution.value.symlink.slice(formatter.buf)}), + .single_file_module => try std.fmt.format(writer, "module://{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}), else => {}, } } @@ -212,8 +212,8 @@ 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)}), - .single_file_module => 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, "module://{s}", .{formatter.resolution.value.single_file_module.slice(formatter.buf)}), else => {}, } } diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index b25623cfe8c2ec..267137baf180dd 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -51,6 +51,7 @@ pub const FolderResolution = union(Tag) { pub const Resolver = NewResolver(Resolution.Tag.folder); pub const SymlinkResolver = NewResolver(Resolution.Tag.symlink); + pub const WorkspaceResolver = NewResolver(Resolution.Tag.workspace); pub const CacheFolderResolver = struct { folder_path: []const u8 = "", version: Semver.Version, @@ -160,7 +161,7 @@ pub const FolderResolution = union(Tag) { pub const GlobalOrRelative = union(enum) { global: []const u8, - relative: void, + relative: Dependency.Version.Tag, cache_folder: []const u8, }; @@ -185,15 +186,27 @@ pub const FolderResolution = union(Tag) { SymlinkResolver, SymlinkResolver{ .folder_path = non_normalized_path }, ), - .relative => readPackageJSONFromDisk( - manager, - joinedZ, - abs, - version, - Features.folder, - Resolver, - Resolver{ .folder_path = rel }, - ), + .relative => |tag| switch (tag) { + .folder => readPackageJSONFromDisk( + manager, + joinedZ, + abs, + version, + Features.folder, + Resolver, + Resolver{ .folder_path = rel }, + ), + .workspace => readPackageJSONFromDisk( + manager, + joinedZ, + abs, + version, + Features.folder, + WorkspaceResolver, + WorkspaceResolver{ .folder_path = rel }, + ), + else => unreachable, + }, .cache_folder => readPackageJSONFromDisk( manager, joinedZ,