diff options
Diffstat (limited to 'src/install/npm.zig')
-rw-r--r-- | src/install/npm.zig | 1383 |
1 files changed, 1383 insertions, 0 deletions
diff --git a/src/install/npm.zig b/src/install/npm.zig new file mode 100644 index 000000000..9d12255d4 --- /dev/null +++ b/src/install/npm.zig @@ -0,0 +1,1383 @@ +const URL = @import("../query_string_map.zig").URL; +const std = @import("std"); +const MutableString = @import("../string_mutable.zig").MutableString; +const Semver = @import("./semver.zig"); +const ExternalString = Semver.ExternalString; +const String = Semver.String; +const string = @import("../string_types.zig").string; +const strings = @import("../string_immutable.zig"); +const PackageManager = @import("./install.zig").PackageManager; +const ExternalStringMap = @import("./install.zig").ExternalStringMap; +const ExternalStringList = @import("./install.zig").ExternalStringList; +const ExternalSlice = @import("./install.zig").ExternalSlice; +const initializeStore = @import("./install.zig").initializeStore; +const logger = @import("../logger.zig"); +const Output = @import("../global.zig").Output; +const Integrity = @import("./integrity.zig").Integrity; +const Bin = @import("./bin.zig").Bin; +const Environment = @import("../global.zig").Environment; +const Aligner = @import("./install.zig").Aligner; +const HTTPClient = @import("http"); +const json_parser = @import("../json_parser.zig"); +const default_allocator = @import("../global.zig").default_allocator; +const IdentityContext = @import("../identity_context.zig").IdentityContext; +const ArrayIdentityContext = @import("../identity_context.zig").ArrayIdentityContext; +const SlicedString = Semver.SlicedString; +const FileSystem = @import("../fs.zig").FileSystem; + +const VersionSlice = @import("./install.zig").VersionSlice; +fn ObjectPool(comptime Type: type, comptime Init: (fn (allocator: *std.mem.Allocator) anyerror!Type), comptime threadsafe: bool) type { + return struct { + const LinkedList = std.SinglyLinkedList(Type); + const Data = if (threadsafe) + struct { + pub threadlocal var list: LinkedList = undefined; + pub threadlocal var loaded: bool = false; + } + else + struct { + pub var list: LinkedList = undefined; + pub var loaded: bool = false; + }; + + const data = Data; + + pub fn get(allocator: *std.mem.Allocator) *LinkedList.Node { + if (data.loaded) { + if (data.list.popFirst()) |node| { + node.data.reset(); + return node; + } + } + + var new_node = allocator.create(LinkedList.Node) catch unreachable; + new_node.* = LinkedList.Node{ + .data = Init( + allocator, + ) catch unreachable, + }; + + return new_node; + } + + pub fn release(node: *LinkedList.Node) void { + if (data.loaded) { + data.list.prepend(node); + return; + } + + data.list = LinkedList{ .first = node }; + data.loaded = true; + } + }; +} + +const Npm = @This(); + +pub const Registry = struct { + url: URL = URL.parse("https://registry.npmjs.org/"), + scopes: Map = Map{}, + + token: string = "", + auth: string = "", + + pub const BodyPool = ObjectPool(MutableString, MutableString.init2048, true); + + pub const Scope = struct { + name: string = "", + // https://github.com/npm/npm-registry-fetch/blob/main/lib/auth.js#L96 + // base64("${username}:${password}") + auth: string = "", + // URL may contain these special suffixes in the pathname: + // :_authToken + // :username + // :_password + // :_auth + url: URL, + token: string = "", + }; + + pub const Map = std.HashMapUnmanaged(u64, Scope, IdentityContext(u64), 80); + + const PackageVersionResponse = union(Tag) { + pub const Tag = enum { + cached, + fresh, + not_found, + }; + + cached: PackageManifest, + fresh: PackageManifest, + not_found: void, + }; + + const Pico = @import("picohttp"); + pub fn getPackageMetadata( + allocator: *std.mem.Allocator, + response: Pico.Response, + body: []const u8, + log: *logger.Log, + package_name: string, + loaded_manifest: ?PackageManifest, + ) !PackageVersionResponse { + switch (response.status_code) { + 400 => return error.BadRequest, + 429 => return error.TooManyRequests, + 404 => return PackageVersionResponse{ .not_found = .{} }, + 500...599 => return error.HTTPInternalServerError, + 304 => return PackageVersionResponse{ + .cached = loaded_manifest.?, + }, + else => {}, + } + + var newly_last_modified: string = ""; + var new_etag: string = ""; + for (response.headers) |header| { + if (!(header.name.len == "last-modified".len or header.name.len == "etag".len)) continue; + + const hashed = HTTPClient.hashHeaderName(header.name); + + switch (hashed) { + HTTPClient.hashHeaderName("last-modified") => { + newly_last_modified = header.value; + }, + HTTPClient.hashHeaderName("etag") => { + new_etag = header.value; + }, + else => {}, + } + } + + initializeStore(); + var new_etag_buf: [64]u8 = undefined; + + if (new_etag.len < new_etag_buf.len) { + std.mem.copy(u8, &new_etag_buf, new_etag); + new_etag = new_etag_buf[0..new_etag.len]; + } + + if (try PackageManifest.parse( + allocator, + log, + body, + package_name, + newly_last_modified, + new_etag, + @truncate(u32, @intCast(u64, @maximum(0, std.time.timestamp()))) + 300, + )) |package| { + if (PackageManager.instance.options.enable.manifest_cache) { + var tmpdir = FileSystem.instance.tmpdir(); + + PackageManifest.Serializer.save(&package, tmpdir, PackageManager.instance.cache_directory) catch {}; + } + + return PackageVersionResponse{ .fresh = package }; + } + + return error.PackageFailedToParse; + } +}; + +const VersionMap = std.ArrayHashMapUnmanaged(Semver.Version, PackageVersion, Semver.Version.HashContext, false); +const DistTagMap = extern struct { + tags: ExternalStringList = ExternalStringList{}, + versions: VersionSlice = VersionSlice{}, +}; + +const PackageVersionList = ExternalSlice(PackageVersion); +const ExternVersionMap = extern struct { + keys: VersionSlice = VersionSlice{}, + values: PackageVersionList = PackageVersionList{}, + + pub fn findKeyIndex(this: ExternVersionMap, buf: []const Semver.Version, find: Semver.Version) ?u32 { + for (this.keys.get(buf)) |key, i| { + if (key.eql(find)) { + return @truncate(u32, i); + } + } + + return null; + } +}; + +/// https://nodejs.org/api/os.html#osplatform +pub const OperatingSystem = enum(u16) { + none = 0, + all = all_value, + + _, + + pub const aix: u16 = 1 << 1; + pub const darwin: u16 = 1 << 2; + pub const freebsd: u16 = 1 << 3; + pub const linux: u16 = 1 << 4; + pub const openbsd: u16 = 1 << 5; + pub const sunos: u16 = 1 << 6; + pub const win32: u16 = 1 << 7; + pub const android: u16 = 1 << 8; + + pub const all_value: u16 = aix | darwin | freebsd | linux | openbsd | sunos | win32 | android; + + pub fn isMatch(this: OperatingSystem) bool { + if (comptime Environment.isLinux) { + return (@enumToInt(this) & linux) != 0; + } else if (comptime Environment.isMac) { + return (@enumToInt(this) & darwin) != 0; + } else { + return false; + } + } + + const Matcher = strings.ExactSizeMatcher(8); + + pub fn apply(this_: OperatingSystem, str: []const u8) OperatingSystem { + if (str.len == 0) { + return this_; + } + const this = @enumToInt(this_); + + const is_not = str[0] == '!'; + const offset: usize = if (str[0] == '!') 1 else 0; + const input = str[offset..]; + + const field: u16 = switch (Matcher.match(input)) { + Matcher.case("aix") => aix, + Matcher.case("darwin") => darwin, + Matcher.case("freebsd") => freebsd, + Matcher.case("linux") => linux, + Matcher.case("openbsd") => openbsd, + Matcher.case("sunos") => sunos, + Matcher.case("win32") => win32, + Matcher.case("android") => android, + else => return this_, + }; + + if (is_not) { + return @intToEnum(OperatingSystem, this & ~field); + } else { + return @intToEnum(OperatingSystem, this | field); + } + } +}; + +/// https://docs.npmjs.com/cli/v8/configuring-npm/package-json#cpu +/// https://nodejs.org/api/os.html#osarch +pub const Architecture = enum(u16) { + none = 0, + all = all_value, + _, + + pub const arm: u16 = 1 << 1; + pub const arm64: u16 = 1 << 2; + pub const ia32: u16 = 1 << 3; + pub const mips: u16 = 1 << 4; + pub const mipsel: u16 = 1 << 5; + pub const ppc: u16 = 1 << 6; + pub const ppc64: u16 = 1 << 7; + pub const s390: u16 = 1 << 8; + pub const s390x: u16 = 1 << 9; + pub const x32: u16 = 1 << 10; + pub const x64: u16 = 1 << 11; + + pub const all_value: u16 = arm | arm64 | ia32 | mips | mipsel | ppc | ppc64 | s390 | s390x | x32 | x64; + + pub fn isMatch(this: Architecture) bool { + if (comptime Environment.isAarch64) { + return (@enumToInt(this) & arm64) != 0; + } else if (comptime Environment.isX64) { + return (@enumToInt(this) & x64) != 0; + } else { + return false; + } + } + + const Matcher = strings.ExactSizeMatcher(8); + + pub fn apply(this_: Architecture, str: []const u8) Architecture { + if (str.len == 0) { + return this_; + } + const this = @enumToInt(this_); + + const is_not = str[0] == '!'; + const offset: usize = if (str[0] == '!') 1 else 0; + const input = str[offset..]; + + const field: u16 = switch (Matcher.match(input)) { + Matcher.case("arm") => arm, + Matcher.case("arm64") => arm64, + Matcher.case("ia32") => ia32, + Matcher.case("mips") => mips, + Matcher.case("mipsel") => mipsel, + Matcher.case("ppc") => ppc, + Matcher.case("ppc64") => ppc64, + Matcher.case("s390") => s390, + Matcher.case("s390x") => s390x, + Matcher.case("x32") => x32, + Matcher.case("x64") => x64, + else => return this_, + }; + + if (is_not) { + return @intToEnum(Architecture, this & ~field); + } else { + return @intToEnum(Architecture, this | field); + } + } +}; +const BigExternalString = Semver.BigExternalString; + +pub const PackageVersion = extern struct { + /// `"integrity"` field || `"shasum"` field + /// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#dist + // Splitting this into it's own array ends up increasing the final size a little bit. + integrity: Integrity = Integrity{}, + + /// "dependencies"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#dependencies) + dependencies: ExternalStringMap = ExternalStringMap{}, + + /// `"optionalDependencies"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#optionaldependencies) + 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) + /// We deliberately choose not to populate this field. + /// We keep it in the data layout so that if it turns out we do need it, we can add it without invalidating everyone's history. + dev_dependencies: ExternalStringMap = ExternalStringMap{}, + + /// `"bin"` field in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bin) + bin: Bin = Bin{}, + + /// `"engines"` field in package.json + engines: ExternalStringMap = ExternalStringMap{}, + + /// `"peerDependenciesMeta"` in [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#peerdependenciesmeta) + /// 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{}, + + /// can be empty! + /// When empty, it means that the tarball URL can be inferred + tarball_url: ExternalString = ExternalString{}, + + unpacked_size: u32 = 0, + file_count: u32 = 0, + + /// `"os"` field in package.json + os: OperatingSystem = OperatingSystem.all, + /// `"cpu"` field in package.json + cpu: Architecture = Architecture.all, +}; + +pub const NpmPackage = extern struct { + + /// HTTP response headers + last_modified: String = String{}, + etag: String = String{}, + + /// "modified" in the JSON + modified: String = String{}, + public_max_age: u32 = 0, + + name: ExternalString = ExternalString{}, + + releases: ExternVersionMap = ExternVersionMap{}, + prereleases: ExternVersionMap = ExternVersionMap{}, + dist_tags: DistTagMap = DistTagMap{}, + + versions_buf: VersionSlice = VersionSlice{}, + string_lists_buf: ExternalStringList = ExternalStringList{}, + string_buf: BigExternalString = BigExternalString{}, +}; + +pub const PackageManifest = struct { + pkg: NpmPackage = NpmPackage{}, + + string_buf: []const u8 = &[_]u8{}, + versions: []const Semver.Version = &[_]Semver.Version{}, + external_strings: []const ExternalString = &[_]ExternalString{}, + // We store this in a separate buffer so that we can dedupe contiguous identical versions without an extra pass + external_strings_for_versions: []const ExternalString = &[_]ExternalString{}, + package_versions: []const PackageVersion = &[_]PackageVersion{}, + + pub inline fn name(this: *const PackageManifest) string { + return this.pkg.name.slice(this.string_buf); + } + + pub const Serializer = struct { + pub const version = "bun-npm-manifest-cache-v0.0.1\n"; + const header_bytes: string = "#!/usr/bin/env bun\n" ++ version; + + pub const sizes = blk: { + // skip name + const fields = std.meta.fields(Npm.PackageManifest); + + const Data = struct { + size: usize, + name: []const u8, + alignment: usize, + }; + var data: [fields.len]Data = undefined; + for (fields) |field_info, i| { + data[i] = .{ + .size = @sizeOf(field_info.field_type), + .name = field_info.name, + .alignment = if (@sizeOf(field_info.field_type) == 0) 1 else field_info.alignment, + }; + } + const Sort = struct { + fn lessThan(trash: *i32, lhs: Data, rhs: Data) bool { + _ = trash; + return lhs.alignment > rhs.alignment; + } + }; + var trash: i32 = undefined; // workaround for stage1 compiler bug + std.sort.sort(Data, &data, &trash, Sort.lessThan); + var sizes_bytes: [fields.len]usize = undefined; + var names: [fields.len][]const u8 = undefined; + for (data) |elem, i| { + sizes_bytes[i] = elem.size; + names[i] = elem.name; + } + break :blk .{ + .bytes = sizes_bytes, + .fields = names, + }; + }; + + pub fn writeArray(comptime Writer: type, writer: Writer, comptime Type: type, array: []const Type, pos: *u64) !void { + const bytes = std.mem.sliceAsBytes(array); + if (bytes.len == 0) { + try writer.writeIntNative(u64, 0); + pos.* += 8; + return; + } + + try writer.writeIntNative(u64, bytes.len); + pos.* += 8; + pos.* += try Aligner.write(Type, Writer, writer, pos.*); + + try writer.writeAll( + bytes, + ); + pos.* += bytes.len; + } + + pub fn readArray(stream: *std.io.FixedBufferStream([]const u8), comptime Type: type) ![]const Type { + var reader = stream.reader(); + const byte_len = try reader.readIntNative(u64); + if (byte_len == 0) { + return &[_]Type{}; + } + + stream.pos += Aligner.skipAmount(Type, stream.pos); + const result_bytes = stream.buffer[stream.pos..][0..byte_len]; + const result = @ptrCast([*]const Type, @alignCast(@alignOf([*]const Type), result_bytes.ptr))[0 .. result_bytes.len / @sizeOf(Type)]; + stream.pos += result_bytes.len; + return result; + } + + pub fn write(this: *const PackageManifest, comptime Writer: type, writer: Writer) !void { + var pos: u64 = 0; + try writer.writeAll(header_bytes); + pos += header_bytes.len; + + inline for (sizes.fields) |field_name| { + if (comptime strings.eqlComptime(field_name, "pkg")) { + const bytes = std.mem.asBytes(&this.pkg); + pos += try Aligner.write(NpmPackage, Writer, writer, pos); + try writer.writeAll( + bytes, + ); + pos += bytes.len; + } else { + const field = @field(this, field_name); + try writeArray(Writer, writer, std.meta.Child(@TypeOf(field)), field, &pos); + } + } + } + + pub fn save(this: *const PackageManifest, tmpdir: std.fs.Dir, cache_dir: std.fs.Dir) !void { + const file_id = std.hash.Wyhash.hash(0, this.name()); + var dest_path_buf: [512 + 64]u8 = undefined; + var out_path_buf: ["-18446744073709551615".len + ".npm".len + 1]u8 = undefined; + var dest_path_stream = std.io.fixedBufferStream(&dest_path_buf); + var dest_path_stream_writer = dest_path_stream.writer(); + try dest_path_stream_writer.print("{x}.npm-{x}", .{ file_id, @maximum(std.time.milliTimestamp(), 0) }); + try dest_path_stream_writer.writeByte(0); + var tmp_path: [:0]u8 = dest_path_buf[0 .. dest_path_stream.pos - 1 :0]; + { + var tmpfile = try tmpdir.createFileZ(tmp_path, .{ + .truncate = true, + }); + var writer = tmpfile.writer(); + try Serializer.write(this, @TypeOf(writer), writer); + std.os.fdatasync(tmpfile.handle) catch {}; + tmpfile.close(); + } + + var out_path = std.fmt.bufPrintZ(&out_path_buf, "{x}.npm", .{file_id}) catch unreachable; + try std.os.renameatZ(tmpdir.fd, tmp_path, cache_dir.fd, out_path); + } + + pub fn load(allocator: *std.mem.Allocator, cache_dir: std.fs.Dir, package_name: string) !?PackageManifest { + const file_id = std.hash.Wyhash.hash(0, package_name); + var file_path_buf: [512 + 64]u8 = undefined; + var file_path = try std.fmt.bufPrintZ(&file_path_buf, "{x}.npm", .{file_id}); + var cache_file = cache_dir.openFileZ( + file_path, + .{ + .read = true, + }, + ) catch return null; + var timer: std.time.Timer = undefined; + if (PackageManager.verbose_install) { + timer = std.time.Timer.start() catch @panic("timer fail"); + } + defer cache_file.close(); + var bytes = try cache_file.readToEndAlloc(allocator, std.math.maxInt(u32)); + errdefer allocator.free(bytes); + if (bytes.len < header_bytes.len) return null; + const result = try readAll(bytes); + if (PackageManager.verbose_install) { + Output.prettyError("\n ", .{}); + Output.printTimer(&timer); + Output.prettyErrorln("<d> [cache hit] {s}<r>", .{package_name}); + } + return result; + } + + pub fn readAll(bytes: []const u8) !PackageManifest { + if (!strings.eqlComptime(bytes[0..header_bytes.len], header_bytes)) { + return error.InvalidPackageManifest; + } + var pkg_stream = std.io.fixedBufferStream(bytes); + pkg_stream.pos = header_bytes.len; + var package_manifest = PackageManifest{}; + + inline for (sizes.fields) |field_name| { + if (comptime strings.eqlComptime(field_name, "pkg")) { + pkg_stream.pos = std.mem.alignForward(pkg_stream.pos, @alignOf(Npm.NpmPackage)); + var reader = pkg_stream.reader(); + package_manifest.pkg = try reader.readStruct(NpmPackage); + } else { + @field(package_manifest, field_name) = try readArray( + &pkg_stream, + std.meta.Child(@TypeOf(@field(package_manifest, field_name))), + ); + } + } + + return package_manifest; + } + }; + + pub fn str(self: *const PackageManifest, external: ExternalString) string { + return external.slice(self.string_buf); + } + + pub fn reportSize(this: *const PackageManifest) void { + const versions = std.mem.sliceAsBytes(this.versions); + const external_strings = std.mem.sliceAsBytes(this.external_strings); + const package_versions = std.mem.sliceAsBytes(this.package_versions); + const string_buf = std.mem.sliceAsBytes(this.string_buf); + + Output.prettyErrorln( + \\ Versions count: {d} + \\ External Strings count: {d} + \\ Package Versions count: {d} + \\ + \\ Bytes: + \\ + \\ Versions: {d} + \\ External: {d} + \\ Packages: {d} + \\ Strings: {d} + \\ Total: {d} + , .{ + this.versions.len, + this.external_strings.len, + this.package_versions.len, + + std.mem.sliceAsBytes(this.versions).len, + std.mem.sliceAsBytes(this.external_strings).len, + std.mem.sliceAsBytes(this.package_versions).len, + std.mem.sliceAsBytes(this.string_buf).len, + std.mem.sliceAsBytes(this.versions).len + + std.mem.sliceAsBytes(this.external_strings).len + + std.mem.sliceAsBytes(this.package_versions).len + + std.mem.sliceAsBytes(this.string_buf).len, + }); + Output.flush(); + } + + pub const FindResult = struct { + version: Semver.Version, + package: *const PackageVersion, + }; + + pub fn findByString(this: *const PackageManifest, version: string) ?FindResult { + switch (Dependency.Version.Tag.infer(version)) { + .npm => { + const group = Semver.Query.parse(default_allocator, version, SlicedString.init( + version, + version, + )) catch return null; + return this.findBestVersion(group); + }, + .dist_tag => { + return this.findByDistTag(version); + }, + else => return null, + } + } + + pub fn findByVersion(this: *const PackageManifest, version: Semver.Version) ?FindResult { + const list = if (!version.tag.hasPre()) this.pkg.releases else this.pkg.prereleases; + const values = list.values.get(this.package_versions); + const keys = list.keys.get(this.versions); + const index = list.findKeyIndex(this.versions, version) orelse return null; + return FindResult{ + // Be sure to use the struct from the list in the NpmPackage + // That is the one we can correctly recover the original version string for + .version = keys[index], + .package = &values[index], + }; + } + + pub fn findByDistTag(this: *const PackageManifest, tag: string) ?FindResult { + const versions = this.pkg.dist_tags.versions.get(this.versions); + for (this.pkg.dist_tags.tags.get(this.external_strings)) |tag_str, i| { + if (strings.eql(tag_str.slice(this.string_buf), tag)) { + return this.findByVersion(versions[i]); + } + } + + return null; + } + + pub fn findBestVersion(this: *const PackageManifest, group: Semver.Query.Group) ?FindResult { + const left = group.head.head.range.left; + // Fast path: exact version + if (left.op == .eql) { + return this.findByVersion(left.version); + } + + const releases = this.pkg.releases.keys.get(this.versions); + + if (group.flags.isSet(Semver.Query.Group.Flags.pre)) { + const prereleases = this.pkg.prereleases.keys.get(this.versions); + var i = prereleases.len; + while (i > 0) : (i -= 1) { + const version = prereleases[i - 1]; + const packages = this.pkg.prereleases.values.get(this.package_versions); + + if (group.satisfies(version)) { + return FindResult{ .version = version, .package = &packages[i - 1] }; + } + } + } + + { + var i = releases.len; + // // For now, this is the dumb way + while (i > 0) : (i -= 1) { + const version = releases[i - 1]; + const packages = this.pkg.releases.values.get(this.package_versions); + + if (group.satisfies(version)) { + return FindResult{ .version = version, .package = &packages[i - 1] }; + } + } + } + + return null; + } + + const ExternalStringMapDeduper = std.HashMap(u64, ExternalStringList, IdentityContext(u64), 80); + + threadlocal var string_pool_: String.Builder.StringPool = undefined; + threadlocal var string_pool_loaded: bool = false; + + 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, + log: *logger.Log, + json_buffer: []const u8, + expected_name: []const u8, + last_modified: []const u8, + etag: []const u8, + public_max_age: u32, + ) !?PackageManifest { + const source = logger.Source.initPathString(expected_name, json_buffer); + initializeStore(); + const json = json_parser.ParseJSON(&source, log, allocator) catch |err| { + return null; + }; + + if (json.asProperty("error")) |error_q| { + if (error_q.expr.asString(allocator)) |err| { + log.addErrorFmt(&source, logger.Loc.Empty, allocator, "npm error: {s}", .{err}) catch unreachable; + return null; + } + } + + var result = PackageManifest{}; + + if (!string_pool_loaded) { + string_pool_ = String.Builder.StringPool.init(default_allocator); + string_pool_loaded = true; + } + + if (!external_string_maps_loaded) { + external_string_maps_ = ExternalStringMapDeduper.initContext(default_allocator, .{}); + 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, + }; + + if (json.asProperty("name")) |name_q| { + const field = name_q.expr.asString(allocator) orelse return null; + + if (!strings.eql(field, expected_name)) { + Output.panic("<r>internal: <red>package name mismatch<r> expected <b>\"{s}\"<r> but received <red>\"{s}\"<r>", .{ expected_name, field }); + return null; + } + + string_builder.count(field); + } + + if (json.asProperty("modified")) |name_q| { + const field = name_q.expr.asString(allocator) orelse return null; + + string_builder.count(field); + } + + const DependencyGroup = struct { prop: string, field: string }; + const dependency_groups = comptime [_]DependencyGroup{ + .{ .prop = "dependencies", .field = "dependencies" }, + .{ .prop = "optionalDependencies", .field = "optional_dependencies" }, + .{ .prop = "peerDependencies", .field = "peer_dependencies" }, + }; + + var release_versions_len: usize = 0; + var pre_versions_len: usize = 0; + var dependency_sum: usize = 0; + var extern_string_count: usize = 0; + get_versions: { + if (json.asProperty("versions")) |versions_q| { + if (versions_q.expr.data != .e_object) break :get_versions; + + const versions = versions_q.expr.data.e_object.properties; + for (versions) |prop| { + const version_name = prop.key.?.asString(allocator) orelse continue; + + if (std.mem.indexOfScalar(u8, version_name, '-') != null) { + pre_versions_len += 1; + extern_string_count += 1; + } else { + extern_string_count += @as(usize, @boolToInt(std.mem.indexOfScalar(u8, version_name, '+') != null)); + release_versions_len += 1; + } + + string_builder.count(version_name); + + bin: { + if (prop.value.?.asProperty("bin")) |bin| { + switch (bin.expr.data) { + .e_object => |obj| { + if (obj.properties.len > 0) { + string_builder.count(obj.properties[0].key.?.asString(allocator) orelse break :bin); + string_builder.count(obj.properties[0].value.?.asString(allocator) orelse break :bin); + } + }, + .e_string => |str| { + if (bin.expr.asString(allocator)) |str_| { + string_builder.count(str_); + break :bin; + } + }, + else => {}, + } + } + + if (prop.value.?.asProperty("directories")) |dirs| { + if (dirs.expr.asProperty("bin")) |bin_prop| { + if (bin_prop.expr.asString(allocator)) |str_| { + string_builder.count(str_); + break :bin; + } + } + } + } + + inline for (dependency_groups) |pair| { + if (prop.value.?.asProperty(pair.prop)) |versioned_deps| { + if (versioned_deps.expr.data == .e_object) { + dependency_sum += versioned_deps.expr.data.e_object.properties.len; + const properties = versioned_deps.expr.data.e_object.properties; + for (properties) |property| { + if (property.key.?.asString(allocator)) |key| { + string_builder.count(key); + string_builder.count(property.value.?.asString(allocator) orelse ""); + } + } + } + } + } + } + } + } + + extern_string_count += dependency_sum; + + var dist_tags_count: usize = 0; + if (json.asProperty("dist-tags")) |dist| { + if (dist.expr.data == .e_object) { + const tags = dist.expr.data.e_object.properties; + for (tags) |tag| { + if (tag.key.?.asString(allocator)) |key| { + string_builder.count(key); + extern_string_count += 2; + + string_builder.count((tag.value.?.asString(allocator) orelse "")); + dist_tags_count += 1; + } + } + } + } + + if (last_modified.len > 0) { + string_builder.count(last_modified); + } + + if (etag.len > 0) { + string_builder.count(etag); + } + + var versioned_packages = try allocator.allocAdvanced(PackageVersion, null, release_versions_len + pre_versions_len, .exact); + var all_semver_versions = try allocator.allocAdvanced(Semver.Version, null, release_versions_len + pre_versions_len + dist_tags_count, .exact); + var all_extern_strings = try allocator.allocAdvanced(ExternalString, null, extern_string_count, .exact); + var version_extern_strings = try allocator.allocAdvanced(ExternalString, null, dependency_sum, .exact); + + if (versioned_packages.len > 0) { + var versioned_packages_bytes = std.mem.sliceAsBytes(versioned_packages); + @memset(versioned_packages_bytes.ptr, 0, versioned_packages_bytes.len); + } + if (all_semver_versions.len > 0) { + var all_semver_versions_bytes = std.mem.sliceAsBytes(all_semver_versions); + @memset(all_semver_versions_bytes.ptr, 0, all_semver_versions_bytes.len); + } + if (all_extern_strings.len > 0) { + var all_extern_strings_bytes = std.mem.sliceAsBytes(all_extern_strings); + @memset(all_extern_strings_bytes.ptr, 0, all_extern_strings_bytes.len); + } + if (version_extern_strings.len > 0) { + var version_extern_strings_bytes = std.mem.sliceAsBytes(version_extern_strings); + @memset(version_extern_strings_bytes.ptr, 0, version_extern_strings_bytes.len); + } + + var versioned_package_releases = versioned_packages[0..release_versions_len]; + var all_versioned_package_releases = versioned_package_releases; + var versioned_package_prereleases = versioned_packages[release_versions_len..][0..pre_versions_len]; + var all_versioned_package_prereleases = versioned_package_prereleases; + var _versions_open = all_semver_versions; + var all_release_versions = _versions_open[0..release_versions_len]; + _versions_open = _versions_open[release_versions_len..]; + var all_prerelease_versions = _versions_open[0..pre_versions_len]; + _versions_open = _versions_open[pre_versions_len..]; + var dist_tag_versions = _versions_open[0..dist_tags_count]; + var release_versions = all_release_versions; + var prerelease_versions = all_prerelease_versions; + + var extern_strings = all_extern_strings; + string_builder.cap += (string_builder.cap % 64) + 64; + string_builder.cap *= 2; + + try string_builder.allocate(allocator); + + var string_buf: string = ""; + if (string_builder.ptr) |ptr| { + // 0 it out for better determinism + @memset(ptr, 0, string_builder.cap); + + string_buf = ptr[0..string_builder.cap]; + } + + if (json.asProperty("name")) |name_q| { + const field = name_q.expr.asString(allocator) orelse return null; + result.pkg.name = string_builder.append(ExternalString, field); + } + + var string_slice = SlicedString.init(string_buf, string_buf); + get_versions: { + if (json.asProperty("versions")) |versions_q| { + if (versions_q.expr.data != .e_object) break :get_versions; + + const versions = versions_q.expr.data.e_object.properties; + + var all_dependency_names_and_values = all_extern_strings[0..dependency_sum]; + + // versions change more often than names + // so names go last because we are better able to dedupe at the end + var dependency_values = version_extern_strings; + var dependency_names = all_dependency_names_and_values; + + var version_string__: String = String{}; + for (versions) |prop, version_i| { + const version_name = prop.key.?.asString(allocator) orelse continue; + + var sliced_string = SlicedString.init(version_name, version_name); + + // We only need to copy the version tags if it's a pre/post + if (std.mem.indexOfAny(u8, version_name, "-+") != null) { + version_string__ = string_builder.append(String, version_name); + sliced_string = version_string__.sliced(string_buf); + } + + const parsed_version = Semver.Version.parse(sliced_string, allocator); + std.debug.assert(parsed_version.valid); + + if (!parsed_version.valid) { + log.addErrorFmt(&source, prop.value.?.loc, allocator, "Failed to parse dependency {s}", .{version_name}) catch unreachable; + continue; + } + + var package_version = PackageVersion{}; + + if (prop.value.?.asProperty("cpu")) |cpu| { + package_version.cpu = Architecture.all; + + switch (cpu.expr.data) { + .e_array => |arr| { + if (arr.items.len > 0) { + package_version.cpu = Architecture.none; + for (arr.items) |item| { + if (item.asString(allocator)) |cpu_str_| { + package_version.cpu = package_version.cpu.apply(cpu_str_); + } + } + } + }, + .e_string => |str| { + package_version.cpu = Architecture.apply(Architecture.none, str.utf8); + }, + else => {}, + } + } + + if (prop.value.?.asProperty("os")) |os| { + package_version.os = OperatingSystem.all; + + switch (os.expr.data) { + .e_array => |arr| { + if (arr.items.len > 0) { + package_version.os = OperatingSystem.none; + for (arr.items) |item| { + if (item.asString(allocator)) |cpu_str_| { + package_version.os = package_version.os.apply(cpu_str_); + } + } + } + }, + .e_string => |str| { + package_version.os = OperatingSystem.apply(OperatingSystem.none, str.utf8); + }, + else => {}, + } + } + + bin: { + if (prop.value.?.asProperty("bin")) |bin| { + switch (bin.expr.data) { + .e_object => |obj| { + if (obj.properties.len > 0) { + const bin_name = obj.properties[0].key.?.asString(allocator) orelse break :bin; + const value = obj.properties[0].value.?.asString(allocator) orelse break :bin; + // For now, we're only supporting the first bin + // We'll fix that later + package_version.bin = Bin{ + .tag = Bin.Tag.named_file, + .value = .{ + .named_file = .{ + string_builder.append(String, bin_name), + string_builder.append(String, value), + }, + }, + }; + break :bin; + + // for (arr.items) |item| { + // if (item.asString(allocator)) |bin_str_| { + // package_version.bin = + // } + // } + } + }, + .e_string => |str| { + if (str.utf8.len > 0) { + package_version.bin = Bin{ + .tag = Bin.Tag.file, + .value = .{ + .file = string_builder.append(String, str.utf8), + }, + }; + break :bin; + } + }, + else => {}, + } + } + + if (prop.value.?.asProperty("directories")) |dirs| { + if (dirs.expr.asProperty("bin")) |bin_prop| { + if (bin_prop.expr.asString(allocator)) |str_| { + if (str_.len > 0) { + package_version.bin = Bin{ + .tag = Bin.Tag.dir, + .value = .{ + .dir = string_builder.append(String, str_), + }, + }; + break :bin; + } + } + } + } + } + + integrity: { + if (prop.value.?.asProperty("dist")) |dist| { + if (dist.expr.data == .e_object) { + if (dist.expr.asProperty("fileCount")) |file_count_| { + if (file_count_.expr.data == .e_number) { + package_version.file_count = file_count_.expr.data.e_number.toU32(); + } + } + + if (dist.expr.asProperty("unpackedSize")) |file_count_| { + if (file_count_.expr.data == .e_number) { + package_version.unpacked_size = file_count_.expr.data.e_number.toU32(); + } + } + + if (dist.expr.asProperty("integrity")) |shasum| { + if (shasum.expr.asString(allocator)) |shasum_str| { + package_version.integrity = Integrity.parse(shasum_str) catch Integrity{}; + if (package_version.integrity.tag.isSupported()) break :integrity; + } + } + + if (dist.expr.asProperty("shasum")) |shasum| { + if (shasum.expr.asString(allocator)) |shasum_str| { + package_version.integrity = Integrity.parseSHASum(shasum_str) catch Integrity{}; + } + } + } + } + } + + 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; + var count = items.len; + + var this_names = dependency_names[0..count]; + var this_versions = dependency_values[0..count]; + + 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; + + this_names[i] = string_builder.append(ExternalString, name_str); + this_versions[i] = string_builder.append(ExternalString, version_str); + + 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; + } + + count = i; + + var name_list = ExternalStringList.init(all_extern_strings, this_names); + var version_list = ExternalStringList.init(version_extern_strings, this_versions); + + if (comptime is_peer) { + package_version.optional_peer_dependencies_len = @truncate(u32, peer_dependency_len); + } + + if (count > 0 and + ((comptime !is_peer) or + optional_peer_dep_names.items.len == 0)) + { + const name_map_hash = name_hasher.final(); + const version_map_hash = version_hasher.final(); + + var name_entry = try external_string_maps.getOrPut(name_map_hash); + if (name_entry.found_existing) { + name_list = name_entry.value_ptr.*; + this_names = name_list.mut(all_extern_strings); + } else { + name_entry.value_ptr.* = name_list; + dependency_names = dependency_names[count..]; + } + + var version_entry = try external_string_maps.getOrPut(version_map_hash); + if (version_entry.found_existing) { + version_list = version_entry.value_ptr.*; + this_versions = version_list.mut(version_extern_strings); + } else { + version_entry.value_ptr.* = version_list; + dependency_values = dependency_values[count..]; + } + } + + if (comptime is_peer) { + if (optional_peer_dep_names.items.len > 0) { + dependency_names = dependency_names[count..]; + dependency_values = dependency_values[count..]; + } + } + + @field(package_version, pair.field) = ExternalStringMap{ + .name = name_list, + .value = version_list, + }; + + if (comptime Environment.allow_assert) { + const dependencies_list = @field(package_version, pair.field); + + std.debug.assert(dependencies_list.name.off < all_extern_strings.len); + std.debug.assert(dependencies_list.value.off < all_extern_strings.len); + std.debug.assert(dependencies_list.name.off + dependencies_list.name.len < all_extern_strings.len); + std.debug.assert(dependencies_list.value.off + dependencies_list.value.len < all_extern_strings.len); + + std.debug.assert(std.meta.eql(dependencies_list.name.get(all_extern_strings), this_names)); + std.debug.assert(std.meta.eql(dependencies_list.value.get(version_extern_strings), this_versions)); + var j: usize = 0; + const name_dependencies = dependencies_list.name.get(all_extern_strings); + + if (comptime is_peer) { + if (optional_peer_dep_names.items.len == 0) { + while (j < name_dependencies.len) : (j += 1) { + const dep_name = name_dependencies[j]; + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), this_names[j].slice(string_buf))); + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), items[j].key.?.asString(allocator).?)); + } + + j = 0; + while (j < dependencies_list.value.len) : (j += 1) { + const dep_name = dependencies_list.value.get(version_extern_strings)[j]; + + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), this_versions[j].slice(string_buf))); + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), items[j].value.?.asString(allocator).?)); + } + } + } else { + while (j < name_dependencies.len) : (j += 1) { + const dep_name = name_dependencies[j]; + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), this_names[j].slice(string_buf))); + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), items[j].key.?.asString(allocator).?)); + } + + j = 0; + while (j < dependencies_list.value.len) : (j += 1) { + const dep_name = dependencies_list.value.get(version_extern_strings)[j]; + + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), this_versions[j].slice(string_buf))); + std.debug.assert(std.mem.eql(u8, dep_name.slice(string_buf), items[j].value.?.asString(allocator).?)); + } + } + } + } + } + + if (!parsed_version.version.tag.hasPre()) { + release_versions[0] = parsed_version.version; + versioned_package_releases[0] = package_version; + release_versions = release_versions[1..]; + versioned_package_releases = versioned_package_releases[1..]; + } else { + prerelease_versions[0] = parsed_version.version; + versioned_package_prereleases[0] = package_version; + prerelease_versions = prerelease_versions[1..]; + versioned_package_prereleases = versioned_package_prereleases[1..]; + } + } + + extern_strings = all_extern_strings[all_dependency_names_and_values.len - dependency_names.len ..]; + version_extern_strings = version_extern_strings[0 .. version_extern_strings.len - dependency_values.len]; + } + } + + if (json.asProperty("dist-tags")) |dist| { + if (dist.expr.data == .e_object) { + const tags = dist.expr.data.e_object.properties; + var extern_strings_slice = extern_strings[0..dist_tags_count]; + var dist_tag_i: usize = 0; + + for (tags) |tag, i| { + if (tag.key.?.asString(allocator)) |key| { + extern_strings_slice[dist_tag_i] = string_builder.append(ExternalString, key); + + const version_name = tag.value.?.asString(allocator) orelse continue; + + const dist_tag_value_literal = string_builder.append(ExternalString, version_name); + const dist_tag_value_literal_slice = dist_tag_value_literal.slice(string_buf); + + const sliced_string = dist_tag_value_literal.value.sliced(string_buf); + + dist_tag_versions[dist_tag_i] = Semver.Version.parse(sliced_string, allocator).version; + dist_tag_i += 1; + } + } + + result.pkg.dist_tags = DistTagMap{ + .tags = ExternalStringList.init(all_extern_strings, extern_strings_slice[0..dist_tag_i]), + .versions = VersionSlice.init(all_semver_versions, dist_tag_versions[0..dist_tag_i]), + }; + + if (comptime Environment.allow_assert) { + std.debug.assert(std.meta.eql(result.pkg.dist_tags.versions.get(all_semver_versions), dist_tag_versions[0..dist_tag_i])); + std.debug.assert(std.meta.eql(result.pkg.dist_tags.tags.get(all_extern_strings), extern_strings_slice[0..dist_tag_i])); + } + + extern_strings = extern_strings[dist_tag_i..]; + } + } + + if (last_modified.len > 0) { + result.pkg.last_modified = string_builder.append(String, last_modified); + } + + if (etag.len > 0) { + result.pkg.etag = string_builder.append(String, etag); + } + + if (json.asProperty("modified")) |name_q| { + const field = name_q.expr.asString(allocator) orelse return null; + + result.pkg.modified = string_builder.append(String, field); + } + + result.pkg.releases.keys = VersionSlice.init(all_semver_versions, all_release_versions); + result.pkg.releases.values = PackageVersionList.init(versioned_packages, all_versioned_package_releases); + + result.pkg.prereleases.keys = VersionSlice.init(all_semver_versions, all_prerelease_versions); + result.pkg.prereleases.values = PackageVersionList.init(versioned_packages, all_versioned_package_prereleases); + + if (extern_strings.len > 0) { + all_extern_strings = all_extern_strings[0 .. all_extern_strings.len - extern_strings.len]; + } + + result.pkg.string_lists_buf.off = 0; + result.pkg.string_lists_buf.len = @truncate(u32, all_extern_strings.len); + + result.pkg.versions_buf.off = 0; + result.pkg.versions_buf.len = @truncate(u32, all_semver_versions.len); + + result.versions = all_semver_versions; + result.external_strings = all_extern_strings; + result.external_strings_for_versions = version_extern_strings; + result.package_versions = versioned_packages; + result.pkg.public_max_age = public_max_age; + + if (string_builder.ptr) |ptr| { + result.string_buf = ptr[0..string_builder.len]; + result.pkg.string_buf = BigExternalString{ + .off = 0, + .len = @truncate(u32, string_builder.len), + .hash = 0, + }; + } + + return result; + } +}; |