aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alex Lam S.L <alexlamsl@gmail.com> 2023-01-21 14:16:26 +0200
committerGravatar GitHub <noreply@github.com> 2023-01-21 04:16:26 -0800
commitf0fa760479ea14f6ebdd5ed724e267c538910a2e (patch)
treee65337e8f523f88c5e877f401bfd788fef179ace
parent24e8aa105f9d5d55d560884eaa38dc2e51d403aa (diff)
downloadbun-f0fa760479ea14f6ebdd5ed724e267c538910a2e.tar.gz
bun-f0fa760479ea14f6ebdd5ed724e267c538910a2e.tar.zst
bun-f0fa760479ea14f6ebdd5ed724e267c538910a2e.zip
[semver] parse `^` & `~` expressions correctly (#1854)
* [semver] parse `^` & `~` expressions correctly * handle semver ranges correctly against build tags
-rw-r--r--src/analytics/analytics_thread.zig6
-rw-r--r--src/install/dependency.zig25
-rw-r--r--src/install/install.zig13
-rw-r--r--src/install/npm.zig6
-rw-r--r--src/install/semver.zig408
-rw-r--r--test/bun.js/install/bun-install.test.ts435
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"));
});