diff options
-rw-r--r-- | src/analytics/analytics_thread.zig | 6 | ||||
-rw-r--r-- | src/install/dependency.zig | 25 | ||||
-rw-r--r-- | src/install/install.zig | 13 | ||||
-rw-r--r-- | src/install/npm.zig | 6 | ||||
-rw-r--r-- | src/install/semver.zig | 408 | ||||
-rw-r--r-- | test/bun.js/install/bun-install.test.ts | 435 |
6 files changed, 644 insertions, 249 deletions
diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index 21c09f2f3..f4c799849 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -293,10 +293,10 @@ pub const GenerateHeader = struct { } _ = forOS(); const release = std.mem.span(&linux_os_name.release); - var sliced_string = Semver.SlicedString.init(release, release); - var result = Semver.Version.parse(sliced_string, bun.default_allocator); + const sliced_string = Semver.SlicedString.init(release, release); + const result = Semver.Version.parse(sliced_string, bun.default_allocator); // we only care about major, minor, patch so we don't care about the string - return result.version; + return result.version.fill(); } pub fn forLinux() Analytics.Platform { diff --git a/src/install/dependency.zig b/src/install/dependency.zig index d1e9e26c4..15e7b9806 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -143,9 +143,9 @@ pub fn toExternal(this: Dependency) External { } pub const Version = struct { - tag: Dependency.Version.Tag = Dependency.Version.Tag.uninitialized, - literal: String = String{}, - value: Value = Value{ .uninitialized = void{} }, + tag: Dependency.Version.Tag = .uninitialized, + literal: String = .{}, + value: Value = .{ .uninitialized = {} }, pub fn deinit(this: *Version) void { switch (this.tag) { @@ -156,25 +156,6 @@ pub const Version = struct { } } - pub const @"0.0.0" = Version{ - .tag = Dependency.Version.Tag.npm, - .literal = String.from("0.0.0"), - .value = Value{ - .npm = Semver.Query.Group{ - .allocator = bun.default_allocator, - .head = .{ - .head = .{ - .range = .{ - .left = .{ - .op = .gte, - }, - }, - }, - }, - }, - }, - }; - pub const zeroed = Version{}; pub fn clone( diff --git a/src/install/install.zig b/src/install/install.zig index 4a46b80d7..076977aa8 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1923,21 +1923,22 @@ pub const PackageManager = struct { while (try iter.next()) |entry| { if (entry.kind != .Directory and entry.kind != .SymLink) continue; const name = entry.name; - var sliced = SlicedString.init(name, name); - var parsed = Semver.Version.parse(sliced, allocator); + const sliced = SlicedString.init(name, name); + const parsed = Semver.Version.parse(sliced, allocator); if (!parsed.valid or parsed.wildcard != .none) continue; // not handling OOM // TODO: wildcard - const total = parsed.version.tag.build.len() + parsed.version.tag.pre.len(); + var version = parsed.version.fill(); + const total = version.tag.build.len() + version.tag.pre.len(); if (total > 0) { tags_buf.ensureUnusedCapacity(total) catch unreachable; var available = tags_buf.items.ptr[tags_buf.items.len..tags_buf.capacity]; - const new_version = parsed.version.cloneInto(name, &available); + const new_version = version.cloneInto(name, &available); tags_buf.items.len += total; - parsed.version = new_version; + version = new_version; } - list.append(parsed.version) catch unreachable; + list.append(version) catch unreachable; } return list; diff --git a/src/install/npm.zig b/src/install/npm.zig index 3da26d856..b98daa690 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -1398,12 +1398,12 @@ pub const PackageManifest = struct { } if (!parsed_version.version.tag.hasPre()) { - release_versions[0] = parsed_version.version; + release_versions[0] = parsed_version.version.fill(); 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; + prerelease_versions[0] = parsed_version.version.fill(); versioned_package_prereleases[0] = package_version; prerelease_versions = prerelease_versions[1..]; versioned_package_prereleases = versioned_package_prereleases[1..]; @@ -1431,7 +1431,7 @@ pub const PackageManifest = struct { 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_versions[dist_tag_i] = Semver.Version.parse(sliced_string, allocator).version.fill(); dist_tag_i += 1; } } diff --git a/src/install/semver.zig b/src/install/semver.zig index 44827b1a8..c92597eff 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -182,7 +182,7 @@ pub const String = extern struct { } else { const a = this.ptr(); const b = that.ptr(); - return strings.eql(this_buf[0..a.len], that_buf[0..b.len]); + return strings.eql(this_buf[a.off..][0..a.len], that_buf[b.off..][0..b.len]); } } @@ -563,7 +563,7 @@ pub const Version = extern struct { major: u32 = 0, minor: u32 = 0, patch: u32 = 0, - tag: Tag = Tag{}, + tag: Tag = .{}, // raw: RawType = RawType{}, /// Assumes that there is only one buffer for all the strings @@ -576,7 +576,7 @@ pub const Version = extern struct { } pub fn cloneInto(this: Version, slice: []const u8, buf: *[]u8) Version { - return Version{ + return .{ .major = this.major, .minor = this.minor, .patch = this.patch, @@ -589,7 +589,7 @@ pub const Version = extern struct { } pub fn fmt(this: Version, input: string) Formatter { - return Formatter{ .version = this, .input = input }; + return .{ .version = this, .input = input }; } pub fn count(this: Version, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) void { @@ -606,10 +606,38 @@ pub const Version = extern struct { return that; } - const HashableVersion = extern struct { major: u32, minor: u32, patch: u32, pre: u64, build: u64 }; + pub const Partial = struct { + major: ?u32 = null, + minor: ?u32 = null, + patch: ?u32 = null, + tag: Tag = .{}, + + pub fn fill(this: Partial) Version { + return .{ + .major = this.major orelse 0, + .minor = this.minor orelse 0, + .patch = this.patch orelse 0, + .tag = this.tag, + }; + } + }; + + const Hashable = extern struct { + major: u32, + minor: u32, + patch: u32, + pre: u64, + build: u64, + }; pub fn hash(this: Version) u64 { - const hashable = HashableVersion{ .major = this.major, .minor = this.minor, .patch = this.patch, .pre = this.tag.pre.hash, .build = this.tag.build.hash }; + const hashable = Hashable{ + .major = this.major, + .minor = this.minor, + .patch = this.patch, + .pre = this.tag.pre.hash, + .build = this.tag.build.hash, + }; const bytes = std.mem.asBytes(&hashable); return std.hash.Wyhash.hash(0, bytes); } @@ -620,15 +648,15 @@ pub const Version = extern struct { pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { const self = formatter.version; - try std.fmt.format(writer, "{d}.{d}.{d}", .{ self.major, self.minor, self.patch }); + try std.fmt.format(writer, "{?d}.{?d}.{?d}", .{ self.major, self.minor, self.patch }); - if (self.tag.pre.len() > 0) { + if (!self.tag.pre.isEmpty()) { const pre = self.tag.pre.slice(formatter.input); try writer.writeAll("-"); try writer.writeAll(pre); } - if (self.tag.build.len() > 0) { + if (!self.tag.build.isEmpty()) { const build = self.tag.build.slice(formatter.input); try writer.writeAll("+"); try writer.writeAll(build); @@ -661,11 +689,11 @@ pub const Version = extern struct { if (lhs.patch < rhs.patch) return .lt; if (lhs.patch > rhs.patch) return .gt; - if (lhs.tag.hasPre() != rhs.tag.hasPre()) - return if (lhs.tag.hasPre()) .lt else .gt; - - if (lhs.tag.hasBuild() != rhs.tag.hasBuild()) - return if (lhs.tag.hasBuild()) .gt else .lt; + if (lhs.tag.hasPre()) { + if (!rhs.tag.hasPre()) return .lt; + } else { + if (rhs.tag.hasPre()) return .gt; + } return .eq; } @@ -715,7 +743,7 @@ pub const Version = extern struct { buf.* = buf.*[build_slice.len..]; } - return Tag{ + return .{ .pre = .{ .value = pre, .hash = this.pre.hash, @@ -736,7 +764,7 @@ pub const Version = extern struct { } pub fn eql(lhs: Tag, rhs: Tag) bool { - return lhs.build.hash == rhs.build.hash and lhs.pre.hash == rhs.pre.hash; + return lhs.pre.hash == rhs.pre.hash; } pub const TagResult = struct { @@ -859,9 +887,9 @@ pub const Version = extern struct { }; pub const ParseResult = struct { - wildcard: Query.Token.Wildcard = Query.Token.Wildcard.none, + wildcard: Query.Token.Wildcard = .none, valid: bool = true, - version: Version = Version{}, + version: Version.Partial = .{}, stopped_at: u32 = 0, }; @@ -1096,8 +1124,8 @@ pub const Range = struct { gte = 6, }; - left: Comparator = Comparator{}, - right: Comparator = Comparator{}, + left: Comparator = .{}, + right: Comparator = .{}, /// * /// >= 0.0.0 @@ -1106,14 +1134,14 @@ pub const Range = struct { /// >= x /// >= 0 pub fn anyRangeSatisfies(this: *const Range) bool { - return this.left.op == .gte and this.left.version.eql(Version{}); + return this.left.op == .gte and this.left.version.eql(.{}); } pub fn initWildcard(version: Version, wildcard: Query.Token.Wildcard) Range { switch (wildcard) { .none => { - return Range{ - .left = Comparator{ + return .{ + .left = .{ .op = Op.eql, .version = version, }, @@ -1121,55 +1149,52 @@ pub const Range = struct { }, .major => { - return Range{ - .left = Comparator{ + return .{ + .left = .{ .op = Op.gte, - .version = Version{ + .version = .{ // .raw = version.raw }, }, }; }, .minor => { - var lhs = Version{ + const lhs = Version{ + .major = version.major +| 1, // .raw = version.raw }; - lhs.major = version.major + 1; - - var rhs = Version{ + const rhs = Version{ + .major = version.major, // .raw = version.raw }; - rhs.major = version.major; - - return Range{ - .left = Comparator{ + return .{ + .left = .{ .op = Op.lt, .version = lhs, }, - .right = Comparator{ + .right = .{ .op = Op.gte, .version = rhs, }, }; }, .patch => { - var lhs = Version{}; - lhs.major = version.major; - lhs.minor = version.minor + 1; - - var rhs = Version{}; - rhs.major = version.major; - rhs.minor = version.minor; - - // rhs.raw = version.raw; - // lhs.raw = version.raw; - + const lhs = Version{ + .major = version.major, + .minor = version.minor +| 1, + // .raw = version.raw; + }; + const rhs = Version{ + .major = version.major, + .minor = version.minor, + // .raw = version.raw; + }; return Range{ - .left = Comparator{ + .left = .{ .op = Op.lt, .version = lhs, }, - .right = Comparator{ + .right = .{ .op = Op.gte, .version = rhs, }, @@ -1193,8 +1218,8 @@ pub const Range = struct { } pub const Comparator = struct { - op: Op = Op.unset, - version: Version = Version{}, + op: Op = .unset, + version: Version = .{}, pub inline fn eql(lhs: Comparator, rhs: Comparator) bool { return lhs.op == rhs.op and lhs.version.eql(rhs.version); @@ -1405,83 +1430,95 @@ pub const Query = struct { } pub fn satisfies(this: *const Query, version: Version) bool { - const left = this.range.satisfies(version); - - return left and (this.next orelse return true).satisfies(version); + return this.range.satisfies(version) and (this.next orelse return true).satisfies(version); } - pub const Token = struct { + const Token = struct { tag: Tag = Tag.none, wildcard: Wildcard = Wildcard.none, - pub fn toRange(this: Token, version: Version) Range { + pub fn toRange(this: Token, version: Version.Partial) Range { switch (this.tag) { // Allows changes that do not modify the left-most non-zero element in the [major, minor, patch] tuple .caret => { - var right_version = version; - // https://github.com/npm/node-semver/blob/cb1ca1d5480a6c07c12ac31ba5f2071ed530c4ed/classes/range.js#L310-L336 - if (right_version.major == 0) { - if (right_version.minor == 0) { - right_version.patch += 1; - } else { - right_version.minor += 1; - right_version.patch = 0; - } - } else { - right_version.major += 1; - right_version.patch = 0; - right_version.minor = 0; - } - - return Range{ - .left = .{ + // https://github.com/npm/node-semver/blob/3a8a4309ae986c1967b3073ba88c9e69433d44cb/classes/range.js#L302-L353 + var range = Range{}; + if (version.major) |major| done: { + range.left = .{ .op = .gte, - .version = version, - }, - .right = .{ + .version = .{ + .major = major, + }, + }; + range.right = .{ .op = .lt, - .version = right_version, - }, - }; + }; + if (version.minor) |minor| { + range.left.version.minor = minor; + if (version.patch) |patch| { + range.left.version.patch = patch; + range.left.version.tag = version.tag; + if (major == 0) { + if (minor == 0) { + range.right.version.patch = patch +| 1; + } else { + range.right.version.minor = minor +| 1; + } + break :done; + } + } else if (major == 0) { + range.right.version.minor = minor +| 1; + break :done; + } + } + range.right.version.major = major +| 1; + } + return range; }, .tilda => { - if (this.wildcard == .minor or this.wildcard == .major) { - return Range.initWildcard(version, .minor); - } - - // This feels like it needs to be tested more. - var right_version = version; - right_version.minor += 1; - right_version.patch = 0; - - return Range{ - .left = .{ + // https://github.com/npm/node-semver/blob/3a8a4309ae986c1967b3073ba88c9e69433d44cb/classes/range.js#L261-L287 + var range = Range{}; + if (version.major) |major| done: { + range.left = .{ .op = .gte, - .version = version, - }, - .right = .{ + .version = .{ + .major = major, + }, + }; + range.right = .{ .op = .lt, - .version = right_version, - }, - }; + }; + if (version.minor) |minor| { + range.left.version.minor = minor; + if (version.patch) |patch| { + range.left.version.patch = patch; + range.left.version.tag = version.tag; + } + range.right.version.major = major; + range.right.version.minor = minor +| 1; + break :done; + } + range.right.version.major = major +| 1; + } + return range; }, .none => unreachable, .version => { if (this.wildcard != Wildcard.none) { - return Range.initWildcard(version, this.wildcard); + return Range.initWildcard(version.fill(), this.wildcard); } - return Range{ .left = .{ .op = .eql, .version = version } }; + return .{ .left = .{ .op = .eql, .version = version.fill() } }; }, else => {}, } return switch (this.wildcard) { - .major => Range{ - .left = .{ .op = .gte, .version = version }, + .major => .{ + .left = .{ .op = .gte, .version = version.fill() }, .right = .{ .op = .lte, - .version = Version{ + .version = .{ .major = std.math.maxInt(u32), .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), @@ -1489,43 +1526,43 @@ pub const Query = struct { }, }, .minor => switch (this.tag) { - .lte => Range{ + .lte => .{ .left = .{ .op = .lte, - .version = Version{ - .major = version.major, + .version = .{ + .major = version.major orelse 0, .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), }, }, }, - .lt => Range{ + .lt => .{ .left = .{ .op = .lt, - .version = Version{ - .major = version.major, + .version = .{ + .major = version.major orelse 0, .minor = 0, .patch = 0, }, }, }, - .gt => Range{ + .gt => .{ .left = .{ .op = .gt, - .version = Version{ - .major = version.major, + .version = .{ + .major = version.major orelse 0, .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), }, }, }, - .gte => Range{ + .gte => .{ .left = .{ .op = .gte, - .version = Version{ - .major = version.major, + .version = .{ + .major = version.major orelse 0, .minor = 0, .patch = 0, }, @@ -1534,51 +1571,51 @@ pub const Query = struct { else => unreachable, }, .patch => switch (this.tag) { - .lte => Range{ + .lte => .{ .left = .{ .op = .lte, - .version = Version{ - .major = version.major, - .minor = version.minor, + .version = .{ + .major = version.major orelse 0, + .minor = version.minor orelse 0, .patch = std.math.maxInt(u32), }, }, }, - .lt => Range{ + .lt => .{ .left = .{ .op = .lt, - .version = Version{ - .major = version.major, - .minor = version.minor, + .version = .{ + .major = version.major orelse 0, + .minor = version.minor orelse 0, .patch = 0, }, }, }, - .gt => Range{ + .gt => .{ .left = .{ .op = .gt, - .version = Version{ - .major = version.major, - .minor = version.minor, + .version = .{ + .major = version.major orelse 0, + .minor = version.minor orelse 0, .patch = std.math.maxInt(u32), }, }, }, - .gte => Range{ + .gte => .{ .left = .{ .op = .gte, - .version = Version{ - .major = version.major, - .minor = version.minor, + .version = .{ + .major = version.major orelse 0, + .minor = version.minor orelse 0, .patch = 0, }, }, }, else => unreachable, }, - .none => Range{ + .none => .{ .left = .{ .op = switch (this.tag) { .gt => .gt, @@ -1587,7 +1624,7 @@ pub const Query = struct { .lte => .lte, else => unreachable, }, - .version = version, + .version = version.fill(), }, }, }; @@ -1706,8 +1743,9 @@ pub const Query = struct { if (!skip_round) { const parse_result = Version.parse(sliced.sub(input[i..]), allocator); - if (parse_result.version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); - if (parse_result.version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); + const version = parse_result.version.fill(); + if (version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); + if (version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); token.wildcard = parse_result.wildcard; @@ -1740,42 +1778,42 @@ pub const Query = struct { i += @as(usize, @boolToInt(!hyphenate)); if (hyphenate) { - var second_version = Version.parse(sliced.sub(input[i..]), allocator); - if (second_version.version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); - if (second_version.version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); - + const second_parsed = Version.parse(sliced.sub(input[i..]), allocator); + var second_version = second_parsed.version.fill(); + if (second_version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); + if (second_version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); const range: Range = brk: { - switch (second_version.wildcard) { + switch (second_parsed.wildcard) { .major => { - second_version.version.major += 1; + second_version.major +|= 1; break :brk Range{ - .left = .{ .op = .gte, .version = parse_result.version }, - .right = .{ .op = .lte, .version = second_version.version }, + .left = .{ .op = .gte, .version = version }, + .right = .{ .op = .lte, .version = second_version }, }; }, .minor => { - second_version.version.major += 1; - second_version.version.minor = 0; - second_version.version.patch = 0; + second_version.major +|= 1; + second_version.minor = 0; + second_version.patch = 0; break :brk Range{ - .left = .{ .op = .gte, .version = parse_result.version }, - .right = .{ .op = .lt, .version = second_version.version }, + .left = .{ .op = .gte, .version = version }, + .right = .{ .op = .lt, .version = second_version }, }; }, .patch => { - second_version.version.minor += 1; - second_version.version.patch = 0; + second_version.minor +|= 1; + second_version.patch = 0; break :brk Range{ - .left = .{ .op = .gte, .version = parse_result.version }, - .right = .{ .op = .lt, .version = second_version.version }, + .left = .{ .op = .gte, .version = version }, + .right = .{ .op = .lt, .version = second_version }, }; }, .none => { break :brk Range{ - .left = .{ .op = .gte, .version = parse_result.version }, - .right = .{ .op = .lte, .version = second_version.version }, + .left = .{ .op = .gte, .version = version }, + .right = .{ .op = .lte, .version = second_version }, }; }, } @@ -1787,11 +1825,11 @@ pub const Query = struct { try list.andRange(range); } - i += second_version.stopped_at + 1; + i += second_parsed.stopped_at + 1; } else if (count == 0 and token.tag == .version) { switch (parse_result.wildcard) { .none => { - try list.orVersion(parse_result.version); + try list.orVersion(version); }, else => { try list.orRange(token.toRange(parse_result.version)); @@ -1825,7 +1863,7 @@ pub const Query = struct { } }; -const expect = struct { +const expect = if (Environment.isTest) struct { pub var counter: usize = 0; pub fn isRangeMatch(input: string, version_str: string) bool { var parsed = Version.parse(SlicedString.init(version_str, version_str), default_allocator); @@ -1838,7 +1876,7 @@ const expect = struct { SlicedString.init(input, input), ) catch |err| Output.panic("Test fail due to error {s}", .{@errorName(err)}); - return list.satisfies(parsed.version); + return list.satisfies(parsed.version.fill()); } pub fn range(input: string, version_str: string, src: std.builtin.SourceLocation) void { @@ -1876,15 +1914,14 @@ const expect = struct { counter = 0; } - pub fn version(input: string, v: [3]u32, src: std.builtin.SourceLocation) void { + pub fn version(input: string, v: [3]?u32, src: std.builtin.SourceLocation) void { Output.initTest(); defer counter += 1; - var result = Version.parse(SlicedString.init(input, input), default_allocator); - var other = Version{ .major = v[0], .minor = v[1], .patch = v[2] }; + const result = Version.parse(SlicedString.init(input, input), default_allocator); std.debug.assert(result.valid); - if (!other.eql(result.version)) { - Output.panic("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{d}.{d}.{d}\" but received <red>\"{d}.{d}.{d}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{ + if (v[0] != result.version.major or v[1] != result.version.minor or v[2] != result.version.patch) { + Output.panic("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{?d}.{?d}.{?d}\" but received <red>\"{?d}.{?d}.{?d}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{ input, v[0], v[1], @@ -1905,11 +1942,15 @@ const expect = struct { defer counter += 1; var result = Version.parse(SlicedString.init(input, input), default_allocator); - if (!v.eql(result.version)) { - Output.panic("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{s}\" but received <red>\"{}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{ + if (!v.eql(result.version.fill())) { + Output.panic("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{?d}.{?d}.{?d}\" but received <red>\"{?d}.{?d}.{?d}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{ input, - v, - result.version, + v.major, + v.minor, + v.patch, + result.version.major, + result.version.minor, + result.version.patch, src.file, src.line, src.column, @@ -1917,10 +1958,11 @@ const expect = struct { }); } } -}; +} else {}; test "Version parsing" { defer expect.done(@src()); + const X: ?u32 = null; expect.version("1.0.0", .{ 1, 0, 0 }, @src()); expect.version("1.1.0", .{ 1, 1, 0 }, @src()); @@ -1930,24 +1972,28 @@ test "Version parsing" { expect.version("0.0.1", .{ 0, 0, 1 }, @src()); expect.version("0.0.0", .{ 0, 0, 0 }, @src()); - expect.version("1.x", .{ 1, 0, 0 }, @src()); - expect.version("2.2.x", .{ 2, 2, 0 }, @src()); - expect.version("2.x.2", .{ 2, 0, 2 }, @src()); + expect.version("*", .{ X, X, X }, @src()); + expect.version("x", .{ X, X, X }, @src()); + expect.version("0", .{ 0, X, X }, @src()); + expect.version("0.0", .{ 0, 0, X }, @src()); + expect.version("0.0.0", .{ 0, 0, 0 }, @src()); - expect.version("1.X", .{ 1, 0, 0 }, @src()); - expect.version("2.2.X", .{ 2, 2, 0 }, @src()); - expect.version("2.X.2", .{ 2, 0, 2 }, @src()); + expect.version("1.x", .{ 1, X, X }, @src()); + expect.version("2.2.x", .{ 2, 2, X }, @src()); + expect.version("2.x.2", .{ 2, X, 2 }, @src()); - expect.version("1.*", .{ 1, 0, 0 }, @src()); - expect.version("2.2.*", .{ 2, 2, 0 }, @src()); - expect.version("2.*.2", .{ 2, 0, 2 }, @src()); - expect.version("3", .{ 3, 0, 0 }, @src()); - expect.version("3.x", .{ 3, 0, 0 }, @src()); - expect.version("3.x.x", .{ 3, 0, 0 }, @src()); - expect.version("3.*.*", .{ 3, 0, 0 }, @src()); - expect.version("3.X.x", .{ 3, 0, 0 }, @src()); + expect.version("1.X", .{ 1, X, X }, @src()); + expect.version("2.2.X", .{ 2, 2, X }, @src()); + expect.version("2.X.2", .{ 2, X, 2 }, @src()); - expect.version("0.0.0", .{ 0, 0, 0 }, @src()); + expect.version("1.*", .{ 1, X, X }, @src()); + expect.version("2.2.*", .{ 2, 2, X }, @src()); + expect.version("2.*.2", .{ 2, X, 2 }, @src()); + expect.version("3", .{ 3, X, X }, @src()); + expect.version("3.x", .{ 3, X, X }, @src()); + expect.version("3.x.x", .{ 3, X, X }, @src()); + expect.version("3.*.*", .{ 3, X, X }, @src()); + expect.version("3.X.x", .{ 3, X, X }, @src()); { var v = Version{ @@ -2007,7 +2053,7 @@ test "Version parsing" { var buf: [1024]u8 = undefined; - var triplet = [3]u32{ 0, 0, 0 }; + var triplet = [3]?u32{ null, null, null }; var x: u32 = 0; var y: u32 = 0; var z: u32 = 0; diff --git a/test/bun.js/install/bun-install.test.ts b/test/bun.js/install/bun-install.test.ts index cf6d3a1ca..d176f5dbe 100644 --- a/test/bun.js/install/bun-install.test.ts +++ b/test/bun.js/install/bun-install.test.ts @@ -82,10 +82,10 @@ it("should handle missing package", async () => { ); expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); + expect(await exited).toBe(1); expect(urls).toEqual([ "http://localhost:54321/foo", ]); - expect(await exited).toBe(1); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -134,11 +134,11 @@ it("should handle @scoped authentication", async () => { expect(err.split(/\r?\n/)).toContain(`GET ${url} - 555`); expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); + expect(await exited).toBe(1); expect(urls).toEqual([ url, ]); expect(seen_token).toBe(true); - expect(await exited).toBe(1); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -150,16 +150,7 @@ it("should handle @scoped authentication", async () => { it("should handle empty string in dependencies", async () => { const urls: string[] = []; - handler = async (request) => { - expect(request.method).toBe("GET"); - expect(request.headers.get("accept")).toBe( - "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", - ); - expect(request.headers.get("npm-auth-type")).toBe(null); - expect(await request.text()).toBe(""); - urls.push(request.url); - return new Response("not to be found", { status: 404 }); - }; + handler = dummyRegistry(urls); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -180,22 +171,31 @@ it("should handle empty string in dependencies", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err).toContain('error: package "bar" not found localhost/bar 404'); - expect(err).toContain("error: bar@ failed to resolve"); + expect(err).toContain("Saved lockfile"); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); - expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([""]); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); expect(urls).toEqual([ "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", ]); - expect(await exited).toBe(1); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect(() => {}).toThrow(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); }); it("should handle workspaces", async () => { @@ -559,7 +559,7 @@ it("should handle life-cycle scripts within workspaces", async () => { await access(join(package_dir, "bun.lockb")); }); -function dummyRegistry(urls) { +function dummyRegistry(urls, version = "0.0.2") { return async (request) => { urls.push(request.url); expect(request.method).toBe("GET"); @@ -573,23 +573,390 @@ function dummyRegistry(urls) { expect(await request.text()).toBe(""); const name = request.url.slice(request.url.lastIndexOf("/") + 1); return new Response(JSON.stringify({ - name: name, + name, versions: { - "0.0.2": { - name: name, - version: "0.0.2", + [version]: { + name, + version, dist: { tarball: `${request.url}.tgz`, }, }, }, "dist-tags": { - latest: "0.0.2", + latest: version, }, })); }; } +it("should handle ^0 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should handle ^1 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: No version matching "^1" found for specifier "bar" (but package exists)'); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(""); + expect(await exited).toBe(1); + expect(urls).toEqual([ + "http://localhost:54321/bar", + ]); + expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } +}); + +it("should handle ^0.0 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should handle ^0.1 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: No version matching "^0.1" found for specifier "bar" (but package exists)'); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(""); + expect(await exited).toBe(1); + expect(urls).toEqual([ + "http://localhost:54321/bar", + ]); + expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } +}); + +it("should handle ^0.0.0 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.0.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain('error: No version matching "^0.0.0" found for specifier "bar" (but package exists)'); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toBe(""); + expect(await exited).toBe(1); + expect(urls).toEqual([ + "http://localhost:54321/bar", + ]); + expect(requested).toBe(1); + try { + await access(join(package_dir, "bun.lockb")); + expect(() => {}).toThrow(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } +}); + +it("should handle ^0.0.2 in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should handle ^0.0.2-rc in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls, "0.0.2-rc"); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.0.2-rc", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2-rc", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { + const urls: string[] = []; + handler = dummyRegistry(urls, "0.0.2-alpha.3"); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "bar": "^0.0.2-alpha.3+b4d", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@0.0.2-alpha.3", "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + "http://localhost:54321/bar", + "http://localhost:54321/bar.tgz", + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".cache", + "bar", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.2", + }); + await access(join(package_dir, "bun.lockb")); +}); + it("should handle dependency aliasing", async () => { const urls = []; handler = dummyRegistry(urls); @@ -637,7 +1004,7 @@ it("should handle dependency aliasing", async () => { expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ name: "baz", version: "0.0.2", - }) + }); await access(join(package_dir, "bun.lockb")); }); @@ -688,7 +1055,7 @@ it("should handle dependency aliasing (versioned)", async () => { expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ name: "baz", version: "0.0.2", - }) + }); await access(join(package_dir, "bun.lockb")); }); @@ -739,7 +1106,7 @@ it("should handle dependency aliasing (dist-tagged)", async () => { expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ name: "baz", version: "0.0.2", - }) + }); await access(join(package_dir, "bun.lockb")); }); @@ -790,7 +1157,7 @@ it("should not reinstall aliased dependencies", async () => { expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ name: "baz", version: "0.0.2", - }) + }); await access(join(package_dir, "bun.lockb")); // Performs `bun install` again, expects no-op urls.length = 0; @@ -824,6 +1191,6 @@ it("should not reinstall aliased dependencies", async () => { expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ name: "baz", version: "0.0.2", - }) + }); await access(join(package_dir, "bun.lockb")); }); |