diff options
| author | 2021-12-09 21:21:43 -0800 | |
|---|---|---|
| committer | 2021-12-16 19:18:51 -0800 | |
| commit | 461c769ac4e5e4a45c86dcd27d04c01ac4da8756 (patch) | |
| tree | c14591d4a3c411e262270d1fb15217c03861056c /src | |
| parent | 0dc0d6c31c609eb4fce3fdb99d9b97483538d386 (diff) | |
| download | bun-461c769ac4e5e4a45c86dcd27d04c01ac4da8756.tar.gz bun-461c769ac4e5e4a45c86dcd27d04c01ac4da8756.tar.zst bun-461c769ac4e5e4a45c86dcd27d04c01ac4da8756.zip | |
Track peer dependencies but don't install them
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.zig | 12 | ||||
| -rw-r--r-- | src/cli/package_manager_command.zig | 24 | ||||
| -rw-r--r-- | src/install/dependency.zig | 10 | ||||
| -rw-r--r-- | src/install/install.zig | 276 | ||||
| -rw-r--r-- | src/install/npm.zig | 91 | ||||
| -rw-r--r-- | src/install/semver.zig | 35 |
6 files changed, 360 insertions, 88 deletions
diff --git a/src/cli.zig b/src/cli.zig index 0294ea791..f876bbf41 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -40,6 +40,7 @@ const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand; const InstallCommand = @import("./cli/install_command.zig").InstallCommand; const AddCommand = @import("./cli/add_command.zig").AddCommand; const RemoveCommand = @import("./cli/remove_command.zig").RemoveCommand; +const PackageManagerCommand = @import("./cli/package_manager_command.zig").PackageManagerCommand; const InstallCompletionsCommand = @import("./cli/install_completions_command.zig").InstallCompletionsCommand; const ShellCompletions = @import("./cli/shell_completions.zig"); @@ -490,6 +491,7 @@ const HelpCommand = struct { \\> <r> <b><green>install<r> Install dependencies for a package.json <d>(bun i)<r> \\> <r> <b><blue>add <r><d> {s:<16}<r> Add a dependency to package.json <d>(bun a)<r> \\> <r> remove <r><d> {s:<16}<r> Remove a dependency from package.json <d>(bun rm)<r> + \\> <r> pm <r> More package manager-related subcommands \\ \\> <r> <b><blue>upgrade <r> Get the latest version of Bun \\> <r> <b><d>completions<r> Install shell completions for tab-completion @@ -615,6 +617,8 @@ pub const Command = struct { RootCommandMatcher.case("i"), RootCommandMatcher.case("install") => .InstallCommand, RootCommandMatcher.case("c"), RootCommandMatcher.case("create") => .CreateCommand, + RootCommandMatcher.case("pm") => .PackageManagerCommand, + RootCommandMatcher.case("add"), RootCommandMatcher.case("update"), RootCommandMatcher.case("a") => .AddCommand, RootCommandMatcher.case("remove"), RootCommandMatcher.case("rm") => .RemoveCommand, @@ -694,6 +698,12 @@ pub const Command = struct { try RemoveCommand.exec(ctx); return; }, + .PackageManagerCommand => { + const ctx = try Command.Context.create(allocator, log, .PackageManagerCommand); + + try PackageManagerCommand.exec(ctx); + return; + }, .GetCompletionsCommand => { const ctx = try Command.Context.create(allocator, log, .GetCompletionsCommand); var filter = ctx.positionals; @@ -834,12 +844,14 @@ pub const Command = struct { InstallCompletionsCommand, RunCommand, UpgradeCommand, + PackageManagerCommand, pub const uses_global_options: std.EnumArray(Tag, bool) = std.EnumArray(Tag, bool).initDefault(true, .{ .CreateCommand = false, .InstallCommand = false, .AddCommand = false, .RemoveCommand = false, + .PackageManagerCommand = false, }); }; }; diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig new file mode 100644 index 000000000..31064948a --- /dev/null +++ b/src/cli/package_manager_command.zig @@ -0,0 +1,24 @@ +const Command = @import("../cli.zig").Command; +const PackageManager = @import("../install/install.zig").PackageManager; +const std = @import("std"); +const strings = @import("strings"); + +pub const PackageManagerCommand = struct { + pub fn printHelp(allocator: *std.mem.Allocator) void {} + pub fn exec(ctx: Command.Context) !void { + var args = try std.process.argsAlloc(ctx.allocator); + args = args[1..]; + + var first = std.mem.span(args[0]); + if (strings.eqlComptime(first, "pm")) { + args = args[1..]; + } + + if (args.len == 0) { + printHelp(ctx.allocator); + std.os.exit(0); + } + + first = std.mem.span(args[0]); + } +}; diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 9584b7b7b..5d0e53542 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -49,7 +49,9 @@ behavior: Behavior = Behavior.uninitialized, /// Sorting order for dependencies is: /// 1. [`dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`] -/// 2. name +/// 2. name ASC +/// "name" must be ASC so that later, when we rebuild the lockfile +/// we insert it back in reverse order without an extra sorting pass pub fn isLessThan(string_buf: []const u8, lhs: Dependency, rhs: Dependency) bool { const behavior = lhs.behavior.cmp(rhs.behavior); if (behavior != .eq) { @@ -535,7 +537,7 @@ pub const Behavior = enum(u8) { pub const peer: u8 = 1 << 4; pub inline fn isOptional(this: Behavior) bool { - return (@enumToInt(this) & Behavior.optional) != 0; + return (@enumToInt(this) & Behavior.optional) != 0 and (@enumToInt(this) & Behavior.peer) == 0; } pub inline fn isDev(this: Behavior) bool { @@ -550,6 +552,10 @@ pub const Behavior = enum(u8) { return (@enumToInt(this) & Behavior.normal) != 0; } + pub inline fn setOptional(this: Behavior, value: bool) Behavior { + return @intToEnum(Behavior, @enumToInt(this) | (@as(u8, @boolToInt(value))) << 2); + } + pub inline fn cmp(lhs: Behavior, rhs: Behavior) std.math.Order { if (@enumToInt(lhs) == @enumToInt(rhs)) { return .eq; diff --git a/src/install/install.zig b/src/install/install.zig index 6bb8ea5f0..f2d484457 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -339,7 +339,7 @@ pub const Features = struct { optional_dependencies: bool = false, dev_dependencies: bool = false, scripts: bool = false, - peer_dependencies: bool = false, + peer_dependencies: bool = true, is_main: bool = false, check_for_duplicate_dependencies: bool = false, @@ -485,49 +485,66 @@ pub const Lockfile = struct { try new.packages.ensureTotalCapacity(old.allocator, old.packages.len); try new.buffers.preallocate(old.buffers, old.allocator); + const InstallOrder = struct { + parent: PackageID, + children: PackageIDSlice, + }; + old.scratch.dependency_list_queue.head = 0; // Step 1. Recreate the lockfile with only the packages that are still alive const root = old.rootPackage() orelse return error.NoPackage; - var slices = old.packages.slice(); var package_id_mapping = try old.allocator.alloc(PackageID, old.packages.len); std.mem.set( PackageID, package_id_mapping, invalid_package_id, ); - var clone_queue_ = PendingResolutions.init(old.allocator); - var clone_queue = &clone_queue_; - try clone_queue.ensureUnusedCapacity(root.dependencies.len); + var clone_queue_ = PendingResolutions.init(); + var cloner = Cloner{ + .old = old, + .lockfile = new, + .mapping = package_id_mapping, + .clone_queue = clone_queue_, + }; + // try clone_queue.ensureUnusedCapacity(root.dependencies.len); - var duplicate_resolutions_bitset = try std.DynamicBitSetUnmanaged.initEmpty(old.buffers.resolutions.items.len, old.allocator); - var duplicate_resolutions_bitset_ptr = &duplicate_resolutions_bitset; - _ = try root.clone(old, new, package_id_mapping, clone_queue, duplicate_resolutions_bitset_ptr); + _ = try root.clone(old, new, package_id_mapping, &cloner); + try cloner.flush(); - while (clone_queue.readItem()) |to_clone_| { - const to_clone: PendingResolution = to_clone_; + return new; + } - const mapping = package_id_mapping[to_clone.old_resolution]; - if (mapping < max_package_id) { - new.buffers.resolutions.items[to_clone.resolve_id] = package_id_mapping[to_clone.old_resolution]; + const Cloner = struct { + clone_queue: PendingResolutions, + lockfile: *Lockfile, + old: *Lockfile, + mapping: []PackageID, - continue; - } + pub fn flush(this: *Cloner) anyerror!void { + const max_package_id = this.old.packages.len; + while (this.clone_queue.readItem()) |to_clone_| { + const to_clone: PendingResolution = to_clone_; - const old_package = old.packages.get(to_clone.old_resolution); + const mapping = this.mapping[to_clone.old_resolution]; + if (mapping < max_package_id) { + this.lockfile.buffers.resolutions.items[to_clone.resolve_id] = this.mapping[to_clone.old_resolution]; - new.buffers.resolutions.items[to_clone.resolve_id] = try old_package.clone( - old, - new, - package_id_mapping, - clone_queue, - duplicate_resolutions_bitset_ptr, - ); - } + continue; + } - return new; - } + const old_package = this.old.packages.get(to_clone.old_resolution); + + this.lockfile.buffers.resolutions.items[to_clone.resolve_id] = try old_package.clone( + this.old, + this.lockfile, + this.mapping, + this, + ); + } + } + }; const PendingResolution = struct { old_resolution: PackageID, @@ -535,7 +552,7 @@ pub const Lockfile = struct { parent: PackageID, }; - const PendingResolutions = std.fifo.LinearFifo(PendingResolution, .Dynamic); + const PendingResolutions = std.fifo.LinearFifo(PendingResolution, .{ .Static = 32 }); pub const Printer = struct { lockfile: *Lockfile, @@ -657,6 +674,60 @@ pub const Lockfile = struct { } } + pub const Tree = struct { + pub fn print( + this: *Printer, + comptime Writer: type, + writer: Writer, + comptime enable_ansi_colors: bool, + ) !void { + var lockfile = this.lockfile; + + const IDDepthPair = struct { + depth: u16 = 0, + id: PackageID, + }; + + var visited = try std.DynamicBitSetUnmanaged.initEmpty(this.lockfile.packages.len, this.lockfile.allocator); + + var slice = this.lockfile.packages.slice(); + const names: []const String = slice.items(.name); + const resolved: []const Resolution = slice.items(.resolution); + const metas: []const Lockfile.Package.Meta = slice.items(.meta); + if (names.len == 0) return; + const dependency_lists = slice.items(.dependencies); + const resolutions_list = slice.items(.resolutions); + const resolutions_buffer = this.lockfile.buffers.resolutions.items; + const dependencies_buffer = this.lockfile.buffers.dependencies.items; + const package_count = @truncate(PackageID, names.len); + const string_buf = this.lockfile.buffers.string_bytes.items; + + const root = this.lockfile.rootPackage() orelse return; + visited.set(0); + + for (names) |name, package_id| { + const package_name = name.slice(string_buf); + + const dependency_list = dependency_lists[package_id]; + + try writer.print( + comptime Output.prettyFmt(" <r><b>{s}<r><d>@<b>{}<r><d> ({d} dependencies)<r>\n", enable_ansi_colors), + .{ + package_name, + resolved[package_id].fmt(string_buf), + dependency_list.len, + }, + ); + + if (visited.isSet(package_id)) { + continue; + } + + visited.set(package_id); + } + } + }; + pub const Yarn = struct { pub fn print( this: *Printer, @@ -944,10 +1015,12 @@ pub const Lockfile = struct { pub fn getPackageID( this: *Lockfile, name_hash: u64, + // if it's a peer dependency + version: ?Dependency.Version, resolution: Resolution, ) ?PackageID { const entry = this.package_index.get(name_hash) orelse return null; - const resolutions = this.packages.items(.resolution); + const resolutions: []const Resolution = this.packages.items(.resolution); switch (entry) { .PackageID => |id| { if (comptime Environment.isDebug or Environment.isTest) { @@ -961,10 +1034,23 @@ pub const Lockfile = struct { this.buffers.string_bytes.items, )) { return id; + } else if (version) |version_| { + switch (version_.tag) { + .npm => { + // is it a peerDependency satisfied by a parent package? + if (version_.value.npm.satisfies(resolutions[id].value.npm)) { + return id; + } + }, + else => return null, + } } }, .PackageIDMultiple => |multi_| { const multi = std.mem.span(multi_); + + const can_satisfy = version != null and version.?.tag == .npm; + for (multi) |id| { if (comptime Environment.isDebug or Environment.isTest) { std.debug.assert(id != invalid_package_id); @@ -975,6 +1061,10 @@ pub const Lockfile = struct { if (resolutions[id].eql(resolution, this.buffers.string_bytes.items, this.buffers.string_bytes.items)) { return id; } + + if (can_satisfy and version.?.value.npm.satisfies(resolutions[id].value.npm)) { + return id; + } } }, } @@ -1045,8 +1135,8 @@ pub const Lockfile = struct { pub fn appendPackageWithID(this: *Lockfile, package_: Lockfile.Package, id: PackageID) !Lockfile.Package { defer { if (comptime Environment.isDebug) { - std.debug.assert(this.getPackageID(package_.name_hash, package_.resolution) != null); - std.debug.assert(this.getPackageID(package_.name_hash, package_.resolution).? == id); + 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_; @@ -1227,11 +1317,19 @@ pub const Lockfile = struct { const DependencySlice = ExternalSlice(Dependency); const PackageIDSlice = ExternalSlice(PackageID); + const NodeModulesFolderSlice = ExternalSlice(NodeModulesFolder); + const PackageIDList = std.ArrayListUnmanaged(PackageID); const DependencyList = std.ArrayListUnmanaged(Dependency); const StringBuffer = std.ArrayListUnmanaged(u8); const SmallExternalStringBuffer = std.ArrayListUnmanaged(String); + const NodeModulesFolder = extern struct { + in: PackageID = 0, + packages: PackageIDSlice = PackageIDSlice{}, + children: NodeModulesFolderSlice = NodeModulesFolderSlice{}, + }; + pub const Package = extern struct { const DependencyGroup = struct { prop: string, @@ -1267,8 +1365,7 @@ pub const Lockfile = struct { old: *Lockfile, new: *Lockfile, package_id_mapping: []PackageID, - clone_queue: *PendingResolutions, - duplicate_resolutions_bitset: *std.DynamicBitSetUnmanaged, + cloner: *Cloner, ) !PackageID { const old_string_buf = old.buffers.string_bytes.items; var builder_ = new.stringBuilder(); @@ -1280,6 +1377,7 @@ pub const Lockfile = struct { const old_dependencies: []const Dependency = this.dependencies.get(old.buffers.dependencies.items); const old_resolutions: []const PackageID = this.resolutions.get(old.buffers.resolutions.items); + for (old_dependencies) |dependency, i| { dependency.count(old_string_buf, *Lockfile.StringBuilder, builder); } @@ -1292,7 +1390,7 @@ pub const Lockfile = struct { const prev_len = @truncate(u32, new.buffers.dependencies.items.len); const end = prev_len + @truncate(u32, old_dependencies.len); - const max_package_id = @truncate(u32, old.packages.len); + const max_package_id = @truncate(PackageID, old.packages.len); new.buffers.dependencies.items = new.buffers.dependencies.items.ptr[0..end]; new.buffers.resolutions.items = new.buffers.resolutions.items.ptr[0..end]; @@ -1333,29 +1431,32 @@ pub const Lockfile = struct { *Lockfile.StringBuilder, builder, ); + } - const old_resolution = old_resolutions[i]; - if (old_resolution < max_package_id) { - const mapped = package_id_mapping[old_resolution]; - const resolve_id = new_package.resolutions.off + @truncate(u32, i); + builder.clamp(); - if (!old.unique_packages.isSet(old_resolution)) duplicate_resolutions_bitset.set(resolve_id); + for (old_resolutions) |old_resolution, i| { + if (old_resolution >= max_package_id) continue; - if (mapped < max_package_id) { - resolutions[i] = mapped; - } else { - try clone_queue.writeItem( - PendingResolution{ - .old_resolution = old_resolution, - .parent = new_package.meta.id, - .resolve_id = resolve_id, - }, - ); - } + if (cloner.clone_queue.writableLength() == 0) { + try cloner.flush(); } - } - builder.clamp(); + const mapped = package_id_mapping[old_resolution]; + const resolve_id = new_package.resolutions.off + @intCast(PackageID, i); + + if (mapped < max_package_id) { + resolutions[i] = mapped; + } else { + cloner.clone_queue.writeItemAssumeCapacity( + PendingResolution{ + .old_resolution = old_resolution, + .parent = new_package.meta.id, + .resolve_id = resolve_id, + }, + ); + } + } return new_package.meta.id; } @@ -1466,16 +1567,21 @@ pub const Lockfile = struct { const version_strings = map.value.get(manifest.external_strings_for_versions); if (comptime Environment.isDebug) std.debug.assert(keys.len == version_strings.len); + const is_peer = comptime strings.eqlComptime(group.field, "peer_dependencies"); for (keys) |key, i| { const version_string_ = version_strings[i]; const name: ExternalString = string_builder.appendWithHash(ExternalString, key.slice(string_buf), key.hash); const dep_version = string_builder.appendWithHash(String, version_string_.slice(string_buf), version_string_.hash); const sliced = dep_version.sliced(lockfile.buffers.string_bytes.items); + const dependency = Dependency{ .name = name.value, .name_hash = name.hash, - .behavior = group.behavior, + .behavior = if (comptime is_peer) + group.behavior.setOptional(package_version.optional_peer_dependencies_len > i) + else + group.behavior, .version = Dependency.parse( allocator, sliced.slice, @@ -1983,6 +2089,8 @@ pub const Lockfile = struct { resolutions: PackageIDList = PackageIDList{}, dependencies: DependencyList = DependencyList{}, extern_strings: SmallExternalStringBuffer = SmallExternalStringBuffer{}, + // node_modules_folders: NodeModulesFolderList = NodeModulesFolderList{}, + // node_modules_package_ids: PackageIDList = PackageIDList{}, string_bytes: StringBuffer = StringBuffer{}, pub fn preallocate(this: *Buffers, that: Buffers, allocator: *std.mem.Allocator) !void { @@ -2941,15 +3049,20 @@ pub const PackageManager = struct { name: String, version: Dependency.Version, dependency_id: PackageID, + is_peer: bool, manifest: *const Npm.PackageManifest, find_result: Npm.PackageManifest.FindResult, ) !?ResolvedPackageResult { // Was this package already allocated? Let's reuse the existing one. - if (this.lockfile.getPackageID(name_hash, .{ - .tag = .npm, - .value = .{ .npm = find_result.version }, - })) |id| { + if (this.lockfile.getPackageID( + name_hash, + version, + .{ + .tag = .npm, + .value = .{ .npm = find_result.version }, + }, + )) |id| { return ResolvedPackageResult{ .package = this.lockfile.packages.get(id), .is_first_time = false, @@ -3105,6 +3218,7 @@ pub const PackageManager = struct { name_hash: PackageNameHash, name: String, version: Dependency.Version, + is_peer: bool, dependency_id: PackageID, resolution: PackageID, ) !?ResolvedPackageResult { @@ -3126,7 +3240,7 @@ pub const PackageManager = struct { else => unreachable, }; - return try getOrPutResolvedPackageWithFindResult(this, name_hash, name, version, dependency_id, manifest, find_result); + return try getOrPutResolvedPackageWithFindResult(this, name_hash, name, version, dependency_id, is_peer, manifest, find_result); }, else => return null, @@ -3259,7 +3373,14 @@ pub const PackageManager = struct { switch (dependency.version.tag) { .npm, .dist_tag => { retry_from_manifests_ptr: while (true) { - var resolve_result_ = this.getOrPutResolvedPackage(name_hash, name, version, id, resolution); + var resolve_result_ = this.getOrPutResolvedPackage( + name_hash, + name, + version, + dependency.behavior.isPeer(), + id, + resolution, + ); retry_with_new_resolve_result: while (true) { const resolve_result = resolve_result_ catch |err| { @@ -3327,7 +3448,7 @@ pub const PackageManager = struct { this.enqueueNetworkTask(network_task); } } - } else { + } else if (!dependency.behavior.isPeer()) { const task_id = Task.Id.forManifest(Task.Tag.package_manifest, this.lockfile.str(name)); var network_entry = try this.network_dedupe_map.getOrPutContext(this.allocator, task_id, .{}); if (!network_entry.found_existing) { @@ -3349,6 +3470,7 @@ pub const PackageManager = struct { name, version, id, + dependency.behavior.isPeer(), &loaded_manifest.?, find_result, ) catch null) |new_resolve_result| { @@ -4921,7 +5043,6 @@ pub const PackageManager = struct { const cache_dir = this.cache_directory; lockfile.unique_packages.unset(0); - var toplevel_node_modules = lockfile.unique_packages.iterator(.{}); // If there was already a valid lockfile and so we did not resolve, i.e. there was zero network activity // the packages could still not be in the cache dir @@ -4953,12 +5074,29 @@ pub const PackageManager = struct { var summary = PackageInstall.Summary{}; { - const toplevel_count = lockfile.unique_packages.count(); - var parts = lockfile.packages.slice(); var metas = parts.items(.meta); var names = parts.items(.name); + var dependency_lists = parts.items(.dependencies); + var dependencies = lockfile.buffers.dependencies.items; + var resolutions_buffer = lockfile.buffers.resolutions.items; + var resolution_lists = parts.items(.resolutions); var resolutions = parts.items(.resolution); + + const pending_task_offset = this.total_tasks; + const root_dependency_list = dependency_lists[0]; + const root_resolution_list = resolution_lists[0]; + + var toplevel_packages = try lockfile.unique_packages.clone(this.allocator); + const max_package_id = @truncate(PackageID, names.len); + for (root_resolution_list.get(resolutions_buffer)) |package_id| { + if (package_id > max_package_id) continue; + toplevel_packages.set(package_id); + } + + const toplevel_count = toplevel_packages.count(); + var toplevel_node_modules = toplevel_packages.iterator(.{}); + var installer = PackageInstaller{ .manager = this, .metas = metas, @@ -4975,8 +5113,6 @@ pub const PackageManager = struct { .install_count = toplevel_count, }; - const pending_task_offset = this.total_tasks; - // When it's a Good Idea, run the install in single-threaded // From benchmarking, apfs clonefile() is ~6x faster than copyfile() on macOS // Running it in parallel is the same or slower. @@ -5389,6 +5525,18 @@ pub const PackageManager = struct { } } + if (manager.options.log_level != .silent) { + var printer = Lockfile.Printer{ + .lockfile = manager.lockfile, + .options = manager.options, + }; + if (Output.enable_ansi_colors) { + try Lockfile.Printer.Tree.print(&printer, Output.WriterType, Output.writer(), true); + } else { + try Lockfile.Printer.Tree.print(&printer, Output.WriterType, Output.writer(), false); + } + } + Output.prettyln(" <green>+{d}<r> add | <cyan>{d}<r> update | <r><red>-{d}<r> remove | {d} installed | {d} deduped | {d} skipped | {d} failed", .{ manager.summary.add, manager.summary.update, diff --git a/src/install/npm.zig b/src/install/npm.zig index 339ac0496..b7a5904a2 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -341,6 +341,7 @@ pub const PackageVersion = extern struct { optional_dependencies: ExternalStringMap = ExternalStringMap{}, /// `"peerDependencies"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#peerdependencies) + /// if `optional_peer_dependencies_len` is > 0, then instead of alphabetical, the first N items are optional peer_dependencies: ExternalStringMap = ExternalStringMap{}, /// `"devDependencies"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#devdependencies) @@ -355,7 +356,8 @@ pub const PackageVersion = extern struct { engines: ExternalStringMap = ExternalStringMap{}, /// `"peerDependenciesMeta"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#peerdependenciesmeta) - optional_peer_dependencies: ExternalStringMap = ExternalStringMap{}, + /// if `optional_peer_dependencies_len` is > 0, then instead of alphabetical, the first N items of `peer_dependencies` are optional + optional_peer_dependencies_len: u32 = 0, man_dir: ExternalString = ExternalString{}, @@ -705,6 +707,9 @@ pub const PackageManifest = struct { threadlocal var external_string_maps_: ExternalStringMapDeduper = undefined; threadlocal var external_string_maps_loaded: bool = false; + threadlocal var optional_peer_dep_names_: std.ArrayList(u64) = undefined; + threadlocal var optional_peer_dep_names_loaded: bool = false; + /// This parses [Abbreviated metadata](https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-metadata-format) pub fn parse( allocator: *std.mem.Allocator, @@ -740,13 +745,21 @@ pub const PackageManifest = struct { external_string_maps_loaded = true; } + if (!optional_peer_dep_names_loaded) { + optional_peer_dep_names_ = std.ArrayList(u64).init(default_allocator); + optional_peer_dep_names_loaded = true; + } + var string_pool = string_pool_; string_pool.clearRetainingCapacity(); var external_string_maps = external_string_maps_; external_string_maps.clearRetainingCapacity(); + var optional_peer_dep_names = optional_peer_dep_names_; + optional_peer_dep_names.clearRetainingCapacity(); defer string_pool_ = string_pool; defer external_string_maps_ = external_string_maps; + defer optional_peer_dep_names_ = optional_peer_dep_names; var string_builder = String.Builder{ .string_pool = string_pool, @@ -1093,6 +1106,8 @@ pub const PackageManifest = struct { } } + var peer_dependency_len: usize = 0; + inline for (dependency_groups) |pair| { if (prop.value.?.asProperty(comptime pair.prop)) |versioned_deps| { const items = versioned_deps.expr.data.e_object.properties; @@ -1104,7 +1119,30 @@ pub const PackageManifest = struct { var name_hasher = std.hash.Wyhash.init(0); var version_hasher = std.hash.Wyhash.init(0); + const is_peer = comptime strings.eqlComptime(pair.prop, "peerDependencies"); + + if (comptime is_peer) { + optional_peer_dep_names.clearRetainingCapacity(); + + if (prop.value.?.asProperty("peerDependenciesMeta")) |meta| { + if (meta.expr.data == .e_object) { + const meta_props = meta.expr.data.e_object.properties; + try optional_peer_dep_names.ensureUnusedCapacity(meta_props.len); + for (meta_props) |meta_prop| { + if (meta_prop.value.?.asProperty("optional")) |optional| { + if (optional.expr.data != .e_boolean or !optional.expr.data.e_boolean.value) { + continue; + } + + optional_peer_dep_names.appendAssumeCapacity(String.Builder.stringHash(meta_prop.key.?.asString(allocator) orelse unreachable)); + } + } + } + } + } + var i: usize = 0; + for (items) |item| { const name_str = item.key.?.asString(allocator) orelse if (comptime Environment.allow_assert) unreachable else continue; const version_str = item.value.?.asString(allocator) orelse if (comptime Environment.allow_assert) unreachable else continue; @@ -1112,10 +1150,36 @@ pub const PackageManifest = struct { this_names[i] = string_builder.append(ExternalString, name_str); this_versions[i] = string_builder.append(ExternalString, version_str); - const names_hash_bytes = @bitCast([8]u8, this_names[i].hash); - name_hasher.update(&names_hash_bytes); - const versions_hash_bytes = @bitCast([8]u8, this_versions[i].hash); - version_hasher.update(&versions_hash_bytes); + if (comptime is_peer) { + if (std.mem.indexOfScalar(u64, optional_peer_dep_names.items, this_names[i].hash) != null) { + // For optional peer dependencies, we store a length instead of a whole separate array + // To make that work, we have to move optional peer dependencies to the front of the array + // + if (peer_dependency_len != i) { + const current_name = this_names[i]; + this_names[i] = this_names[peer_dependency_len]; + this_names[peer_dependency_len] = current_name; + + const current_version = this_versions[i]; + this_versions[i] = this_versions[peer_dependency_len]; + this_versions[peer_dependency_len] = current_version; + + peer_dependency_len += 1; + } + } + + if (optional_peer_dep_names.items.len == 0) { + const names_hash_bytes = @bitCast([8]u8, this_names[i].hash); + name_hasher.update(&names_hash_bytes); + const versions_hash_bytes = @bitCast([8]u8, this_versions[i].hash); + version_hasher.update(&versions_hash_bytes); + } + } else { + const names_hash_bytes = @bitCast([8]u8, this_names[i].hash); + name_hasher.update(&names_hash_bytes); + const versions_hash_bytes = @bitCast([8]u8, this_versions[i].hash); + version_hasher.update(&versions_hash_bytes); + } i += 1; } @@ -1126,6 +1190,19 @@ pub const PackageManifest = struct { var version_list = ExternalStringList.init(version_extern_strings, this_versions); if (count > 0) { + if (comptime is_peer) { + if (optional_peer_dep_names.items.len > 0) { + for (this_names[0..count]) |byte_str| { + const bytes = @bitCast([8]u8, byte_str.hash); + name_hasher.update(&bytes); + } + + for (this_versions[0..count]) |byte_str| { + const bytes = @bitCast([8]u8, byte_str.hash); + version_hasher.update(&bytes); + } + } + } const name_map_hash = name_hasher.final(); const version_map_hash = version_hasher.final(); @@ -1153,6 +1230,10 @@ pub const PackageManifest = struct { .value = version_list, }; + if (comptime is_peer) { + package_version.optional_peer_dependencies_len = @truncate(u32, peer_dependency_len); + } + if (comptime Environment.allow_assert) { const dependencies_list = @field(package_version, pair.field); diff --git a/src/install/semver.zig b/src/install/semver.zig index c6f4189d8..986564950 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -107,10 +107,28 @@ pub const String = extern struct { } } + pub const Pointer = extern struct { + off: u32 = 0, + len: u32 = 0, + + pub inline fn init( + buf: string, + in: string, + ) Pointer { + std.debug.assert(@ptrToInt(buf.ptr) <= @ptrToInt(in.ptr) and ((@ptrToInt(in.ptr) + in.len) <= (@ptrToInt(buf.ptr) + buf.len))); + + return Pointer{ + .off = @truncate(u32, @ptrToInt(in.ptr) - @ptrToInt(buf.ptr)), + .len = @truncate(u32, in.len), + }; + } + }; + pub inline fn ptr(this: String) Pointer { return @bitCast(Pointer, @as(u64, @truncate(u63, @bitCast(u64, this)))); } + // String must be a pointer because we reference it as a slice. It will become a dead pointer if it is copied. pub fn slice(this: *const String, buf: string) string { switch (this.bytes[max_inline_len - 1] & 128) { 0 => { @@ -256,23 +274,6 @@ pub const String = extern struct { } }; - pub const Pointer = extern struct { - off: u32 = 0, - len: u32 = 0, - - pub inline fn init( - buf: string, - in: string, - ) Pointer { - std.debug.assert(@ptrToInt(buf.ptr) <= @ptrToInt(in.ptr) and ((@ptrToInt(in.ptr) + in.len) <= (@ptrToInt(buf.ptr) + buf.len))); - - return Pointer{ - .off = @truncate(u32, @ptrToInt(in.ptr) - @ptrToInt(buf.ptr)), - .len = @truncate(u32, in.len), - }; - } - }; - comptime { if (@sizeOf(String) != @sizeOf(Pointer)) { @compileError("String types must be the same size"); |
