From 9bb0ee6bb19b76f8437525b1e6b116c19ed90948 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 23 Jun 2025 04:19:15 +0000 Subject: [PATCH] Checkpoint before follow-up message --- packages/bun-types/bun.d.ts | 60 +++++++-- src/semver/SemverObject.zig | 224 +++++++++++++++++++++++++++++++- src/semver/Version.zig | 150 ++++++++++++++++++++- test/cli/install/semver.test.ts | 165 ++++++++++++++++++++++- 4 files changed, 583 insertions(+), 16 deletions(-) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 9bc2e89162..7b24f41da9 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -783,7 +783,6 @@ declare module "bun" { path?: string | undefined; syscall?: string | undefined; } - /** * Concatenate an array of typed arrays into a single `ArrayBuffer`. This is a fast path. * @@ -1408,7 +1407,6 @@ declare module "bun" { * @param sql Function to execute SQL queries within the savepoint */ type SQLSavepointContextCallback = (sql: SavepointSQL) => Promise | Array; - /** * Main SQL client interface providing connection and transaction management */ @@ -1961,8 +1959,7 @@ declare module "bun" { * ... on User { * id * } - * } - * }`; + * }`; * ``` * * Will be replaced with: @@ -2118,7 +2115,6 @@ declare module "bun" { path: string; kind: ImportKind; } - /** * @see [Bun.build API docs](https://bun.sh/docs/bundler#api) */ @@ -2842,7 +2838,6 @@ declare module "bun" { * @link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState */ type WebSocketReadyState = 0 | 1 | 2 | 3; - /** * A fast WebSocket designed for servers. * @@ -3643,7 +3638,6 @@ declare module "bun" { errno?: number; syscall?: string; } - /** * Options for TLS connections */ @@ -4426,7 +4420,6 @@ declare module "bun" { * This can be 3.5x faster than `new Uint8Array(size)`, but if you send uninitialized memory to your users (even unintentionally), it can potentially leak anything recently in memory. */ function allocUnsafe(size: number): Uint8Array; - /** * Options for `Bun.inspect` */ @@ -4852,6 +4845,51 @@ declare module "bun" { * Throws an error if either version is invalid. */ function order(v1: StringLike, v2: StringLike): -1 | 0 | 1; + + /** + * Returns the major version number, or null if the version is invalid. + */ + function major(version: StringLike): number | null; + + /** + * Returns the minor version number, or null if the version is invalid. + */ + function minor(version: StringLike): number | null; + + /** + * Returns the patch version number, or null if the version is invalid. + */ + function patch(version: StringLike): number | null; + + /** + * Returns an array of prerelease components, or null if the version doesn't have a prerelease or is invalid. + * Numeric components are parsed as numbers. + */ + function prerelease(version: StringLike): (string | number)[] | null; + + /** + * Parses a version string into an object with its components. + * Returns null if the version is invalid. + */ + function parse(version: StringLike): { + major: number; + minor: number; + patch: number; + prerelease: (string | number)[] | null; + build: (string | number)[] | null; + version: string; + raw: string; + } | null; + + /** + * Increments the version by the release type. + * Returns the new version string, or null if the version is invalid. + * + * @param version The version to increment + * @param releaseType The type of release: "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease" | "release" + * @param identifier Optional identifier for pre-releases (e.g., "alpha", "beta") + */ + function bump(version: StringLike, releaseType: "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease" | "release", identifier?: string): string | null; } namespace unsafe { @@ -5166,7 +5204,6 @@ declare module "bun" { */ static readonly algorithms: SupportedCryptoAlgorithms[]; } - /** * Resolve a `Promise` after milliseconds. This is like * {@link setTimeout} except it returns a `Promise`. @@ -5211,7 +5248,6 @@ declare module "bun" { * Internally, it calls [nanosleep(2)](https://man7.org/linux/man-pages/man2/nanosleep.2.html) */ function sleepSync(ms: number): void; - /** * Hash `input` using [SHA-2 512/256](https://en.wikipedia.org/wiki/SHA-2#Comparison_of_SHA_functions) * @@ -5872,7 +5908,6 @@ declare module "bun" { interface HTMLBundle { index: string; } - /** * Represents a TCP or TLS socket connection used for network communication. * This interface provides methods for reading, writing, managing the connection state, @@ -6614,7 +6649,6 @@ declare module "bun" { * @category HTTP & Networking */ function listen(options: UnixSocketOptions): UnixSocketListener; - /** * @category HTTP & Networking */ @@ -8031,4 +8065,4 @@ declare module "bun" { */ [Symbol.iterator](): IterableIterator<[string, string]>; } -} +} \ No newline at end of file diff --git a/src/semver/SemverObject.zig b/src/semver/SemverObject.zig index 0afd7fb8ea..1fcefb3e93 100644 --- a/src/semver/SemverObject.zig +++ b/src/semver/SemverObject.zig @@ -1,5 +1,5 @@ pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const object = JSC.JSValue.createEmptyObject(globalThis, 2); + const object = JSC.JSValue.createEmptyObject(globalThis, 8); object.put( globalThis, @@ -25,6 +25,78 @@ pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { ), ); + object.put( + globalThis, + JSC.ZigString.static("major"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("major"), + 1, + SemverObject.major, + false, + ), + ); + + object.put( + globalThis, + JSC.ZigString.static("minor"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("minor"), + 1, + SemverObject.minor, + false, + ), + ); + + object.put( + globalThis, + JSC.ZigString.static("patch"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("patch"), + 1, + SemverObject.patch, + false, + ), + ); + + object.put( + globalThis, + JSC.ZigString.static("parse"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("parse"), + 1, + SemverObject.parse, + false, + ), + ); + + object.put( + globalThis, + JSC.ZigString.static("prerelease"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("prerelease"), + 1, + SemverObject.prerelease, + false, + ), + ); + + object.put( + globalThis, + JSC.ZigString.static("bump"), + JSC.host_fn.NewFunction( + globalThis, + JSC.ZigString.static("bump"), + 3, + SemverObject.bump, + false, + ), + ); + return object; } @@ -125,6 +197,156 @@ pub fn satisfies(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun return JSC.jsBoolean(right_group.satisfies(left_version, right.slice(), left.slice())); } +// Add a helper to reduce boilerplate for major, minor, patch +fn getVersionComponent( + globalThis: *JSC.JSGlobalObject, + callFrame: *JSC.CallFrame, + comptime component: enum { major, minor, patch }, +) bun.JSError!JSC.JSValue { + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(512, arena.allocator()); + const allocator = stack_fallback.get(); + + const arguments = callFrame.arguments_old(1).slice(); + if (arguments.len < 1) { + return JSC.JSValue.null; + } + + const arg_string = try arguments[0].toJSString(globalThis); + const version_slice = arg_string.toSlice(globalThis, allocator); + defer version_slice.deinit(); + + if (!strings.isAllASCII(version_slice.slice())) return JSC.JSValue.null; + + const parse_result = Version.parse(SlicedString.init(version_slice.slice(), version_slice.slice())); + if (!parse_result.valid) { + return JSC.JSValue.null; + } + + const version = parse_result.version; + return switch (component) { + .major => JSC.jsNumber(version.major orelse 0), + .minor => JSC.jsNumber(version.minor orelse 0), + .patch => JSC.jsNumber(version.patch orelse 0), + }; +} + +pub fn major(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return getVersionComponent(globalThis, callFrame, .major); +} + +pub fn minor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return getVersionComponent(globalThis, callFrame, .minor); +} + +pub fn patch(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return getVersionComponent(globalThis, callFrame, .patch); +} + +pub fn prerelease(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(1024, arena.allocator()); + const allocator = stack_fallback.get(); + + const arguments = callFrame.arguments_old(1).slice(); + if (arguments.len < 1) return JSC.JSValue.null; + + const arg_string = try arguments[0].toJSString(globalThis); + const version_slice = arg_string.toSlice(globalThis, allocator); + defer version_slice.deinit(); + + if (!strings.isAllASCII(version_slice.slice())) return JSC.JSValue.null; + + const parse_result = Version.parse(SlicedString.init(version_slice.slice(), version_slice.slice())); + if (!parse_result.valid or !parse_result.version.min().tag.hasPre()) { + return JSC.JSValue.null; + } + + return parse_result.version.min().tag.toComponentsArray(true, globalThis, allocator, version_slice.slice()); +} + +pub fn parse(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(1024, arena.allocator()); + const allocator = stack_fallback.get(); + + const arguments = callFrame.arguments_old(1).slice(); + if (arguments.len < 1) return JSC.JSValue.null; + + const arg_string = try arguments[0].toJSString(globalThis); + const version_slice = arg_string.toSlice(globalThis, allocator); + defer version_slice.deinit(); + + if (!strings.isAllASCII(version_slice.slice())) return JSC.JSValue.null; + + const sliced_string = SlicedString.init(version_slice.slice(), version_slice.slice()); + const parse_result = Version.parse(sliced_string); + if (!parse_result.valid) { + return JSC.JSValue.null; + } + + const version = parse_result.version.min(); + const obj = JSC.JSValue.createEmptyObject(globalThis, 6); + + obj.put(globalThis, JSC.ZigString.static("major"), JSC.jsNumber(version.major)); + obj.put(globalThis, JSC.ZigString.static("minor"), JSC.jsNumber(version.minor)); + obj.put(globalThis, JSC.ZigString.static("patch"), JSC.jsNumber(version.patch)); + + const pre_array = try version.tag.toComponentsArray(true, globalThis, allocator, version_slice.slice()); + obj.put(globalThis, JSC.ZigString.static("prerelease"), pre_array); + + const build_array = try version.tag.toComponentsArray(false, globalThis, allocator, version_slice.slice()); + obj.put(globalThis, JSC.ZigString.static("build"), build_array); + + const raw_version_string = try std.fmt.allocPrint(allocator, "{s}", .{version.fmt(version_slice.slice())}); + obj.put(globalThis, JSC.ZigString.static("version"), bun.String.createUTF8ForJS(globalThis, raw_version_string)); + + obj.put(globalThis, JSC.ZigString.static("raw"), arguments[0]); + + return obj; +} + +pub fn bump(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(1024, arena.allocator()); + const allocator = stack_fallback.get(); + + const arguments = callFrame.arguments_old(3).slice(); // v, releaseType, identifier + if (arguments.len < 2) return JSC.JSValue.null; + + // Arg 1: Version + const version_str = try arguments[0].toJSString(globalThis); + const version_slice = version_str.toSlice(globalThis, allocator); + defer version_slice.deinit(); + if (!strings.isAllASCII(version_slice.slice())) return JSC.JSValue.null; + const parse_result = Version.parse(SlicedString.init(version_slice.slice(), version_slice.slice())); + if (!parse_result.valid) return JSC.JSValue.null; + + // Arg 2: Release Type + const release_type_str = try arguments[1].toJSString(globalThis); + const release_type_slice = release_type_str.toSlice(globalThis, allocator); + defer release_type_slice.deinit(); + const release_type = Version.ReleaseType.fromString(release_type_slice.slice()) orelse return JSC.JSValue.null; + + // Arg 3: Identifier (optional) + var identifier: ?[]const u8 = null; + if (arguments.len > 2 and arguments[2].isString()) { + const id_str = try arguments[2].toJSString(globalThis); + const id_slice = id_str.toSlice(globalThis, allocator); + defer id_slice.deinit(); + identifier = id_slice.slice(); + } + + const new_version_str = (parse_result.version.min().bump(allocator, release_type, identifier) catch return JSC.JSValue.null); + defer allocator.free(new_version_str); + + return bun.String.createUTF8ForJS(globalThis, new_version_str); +} + const std = @import("std"); const bun = @import("bun"); const strings = bun.strings; diff --git a/src/semver/Version.zig b/src/semver/Version.zig index 72ed02a07e..87f7cb0573 100644 --- a/src/semver/Version.zig +++ b/src/semver/Version.zig @@ -277,6 +277,126 @@ pub const Version = extern struct { } }; + pub const ReleaseType = enum { + major, + premajor, + minor, + preminor, + patch, + prepatch, + prerelease, + release, + + pub fn fromString(s: []const u8) ?ReleaseType { + return std.meta.stringToEnum(ReleaseType, s); + } + }; + + pub fn bump( + self: Version, + allocator: std.mem.Allocator, + release_type: ReleaseType, + identifier: ?[]const u8, + original_buf: []const u8, + ) ![]const u8 { + var new_version = self; + new_version.tag.build = .{}; // Build metadata is always removed + + // We'll need to allocate new strings for prerelease tags + var pre_strings = std.ArrayList(u8).init(allocator); + defer pre_strings.deinit(); + + switch (release_type) { + .major => { + new_version.major +|= 1; + new_version.minor = 0; + new_version.patch = 0; + new_version.tag.pre = .{}; + }, + .minor => { + new_version.minor +|= 1; + new_version.patch = 0; + new_version.tag.pre = .{}; + }, + .patch => { + new_version.patch +|= 1; + new_version.tag.pre = .{}; + }, + .premajor => { + new_version.major +|= 1; + new_version.minor = 0; + new_version.patch = 0; + try pre_strings.writer().print("{s}.0", .{identifier orelse "0"}); + new_version.tag.pre = ExternalString.from(pre_strings.items); + }, + .preminor => { + new_version.minor +|= 1; + new_version.patch = 0; + try pre_strings.writer().print("{s}.0", .{identifier orelse "0"}); + new_version.tag.pre = ExternalString.from(pre_strings.items); + }, + .prepatch => { + new_version.patch +|= 1; + try pre_strings.writer().print("{s}.0", .{identifier orelse "0"}); + new_version.tag.pre = ExternalString.from(pre_strings.items); + }, + .release => { + new_version.tag.pre = .{}; + }, + .prerelease => { + if (!new_version.tag.hasPre()) { + // Same as prepatch + new_version.patch +|= 1; + try pre_strings.writer().print("{s}.0", .{identifier orelse "0"}); + new_version.tag.pre = ExternalString.from(pre_strings.items); + } else { + // Increment existing prerelease + const existing_pre = self.tag.pre.slice(original_buf); + + // Find last numeric component + var last_dot: ?usize = null; + var i: usize = existing_pre.len; + while (i > 0) : (i -= 1) { + if (existing_pre[i - 1] == '.') { + last_dot = i - 1; + break; + } + } + + if (last_dot) |dot_pos| { + const last_part = existing_pre[dot_pos + 1 ..]; + if (std.fmt.parseUnsigned(u64, last_part, 10)) |num| { + try pre_strings.writer().print("{s}.{d}", .{ existing_pre[0..dot_pos], num + 1 }); + } else { + try pre_strings.writer().print("{s}.0", .{existing_pre}); + } + } else { + // No dots, check if the whole thing is numeric + if (std.fmt.parseUnsigned(u64, existing_pre, 10)) |num| { + try pre_strings.writer().print("{d}", .{num + 1}); + } else { + try pre_strings.writer().print("{s}.0", .{existing_pre}); + } + } + new_version.tag.pre = ExternalString.from(pre_strings.items); + } + }, + } + + // Build the final version string + var output = std.ArrayList(u8).init(allocator); + errdefer output.deinit(); + + try output.writer().print("{d}.{d}.{d}", .{ new_version.major, new_version.minor, new_version.patch }); + + if (new_version.tag.hasPre()) { + try output.append('-'); + try output.appendSlice(new_version.tag.pre.slice(pre_strings.items)); + } + + return output.toOwnedSlice(); + } + pub const PinnedVersion = enum { major, // ^ minor, // ~ @@ -551,7 +671,7 @@ pub const Version = extern struct { return lhs.orderPre(rhs, lhs_buf, rhs_buf); } - const pre_order = lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf); + const pre_order = lhs.orderPre(rhs, lhs_buf, rhs_buf); if (pre_order != .eq) return pre_order; return lhs.build.order(&rhs.build, lhs_buf, rhs_buf); @@ -612,6 +732,33 @@ pub const Version = extern struct { return !this.build.isEmpty(); } + pub fn toComponentsArray( + self: Tag, + is_pre: bool, + globalThis: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + buf: []const u8, + ) bun.JSError!JSC.JSValue { + const tag_str = if (is_pre) self.pre.slice(buf) else self.build.slice(buf); + if (tag_str.len == 0) { + return JSC.JSValue.null; + } + + var list = std.ArrayList(JSC.JSValue).init(allocator); + defer list.deinit(); + + var it = strings.split(tag_str, "."); + while (it.next()) |part| { + if (std.fmt.parseUnsigned(u64, part, 10) catch null) |num| { + try list.append(JSC.jsNumber(@floatFromInt(num))); + } else { + try list.append(bun.String.createUTF8ForJS(globalThis, part)); + } + } + + return JSC.JSValue.createArrayFromElements(globalThis, list.items); + } + pub fn eql(lhs: Tag, rhs: Tag) bool { return lhs.pre.hash == rhs.pre.hash; } @@ -998,3 +1145,4 @@ const String = bun.Semver.String; const Query = bun.Semver.Query; const assert = bun.assert; +const JSC = bun.JSC; diff --git a/test/cli/install/semver.test.ts b/test/cli/install/semver.test.ts index 80d3ae62c2..4ba25ec7f2 100644 --- a/test/cli/install/semver.test.ts +++ b/test/cli/install/semver.test.ts @@ -490,7 +490,6 @@ describe("Bun.semver.satisfies()", () => { ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3-pre.2"], ["1.2.3-pre+asdf - 2.4.3-pre+asdf", "2.4.3-alpha"], ["1.2.3+asdf - 2.4.3+asdf", "1.2.3"], - ["1.0.0", "1.0.0"], [">=*", "0.2.4"], ["", "1.0.0"], ["*", "1.2.3"], @@ -738,3 +737,167 @@ describe("Bun.semver.satisfies()", () => { expect(unsortedPrereleases.sort(Bun.semver.order)).toMatchSnapshot(); }); }); + +describe("Bun.semver.major()", () => { + test("should return major version", () => { + expect(Bun.semver.major("1.2.3")).toBe(1); + expect(Bun.semver.major("v2.0.0-alpha.1")).toBe(2); + expect(Bun.semver.major("0.0.1")).toBe(0); + expect(Bun.semver.major("999.888.777")).toBe(999); + }); + + test("should return null for invalid versions", () => { + expect(Bun.semver.major("not-a-version")).toBe(null); + expect(Bun.semver.major("")).toBe(null); + expect(Bun.semver.major("v")).toBe(null); + }); +}); + +describe("Bun.semver.minor()", () => { + test("should return minor version", () => { + expect(Bun.semver.minor("1.2.3")).toBe(2); + expect(Bun.semver.minor("v2.0.0-alpha.1")).toBe(0); + expect(Bun.semver.minor("0.999.1")).toBe(999); + }); + + test("should return null for invalid versions", () => { + expect(Bun.semver.minor("not-a-version")).toBe(null); + expect(Bun.semver.minor("")).toBe(null); + }); +}); + +describe("Bun.semver.patch()", () => { + test("should return patch version", () => { + expect(Bun.semver.patch("1.2.3")).toBe(3); + expect(Bun.semver.patch("v2.0.0-alpha.1")).toBe(0); + expect(Bun.semver.patch("0.1.999")).toBe(999); + }); + + test("should return null for invalid versions", () => { + expect(Bun.semver.patch("not-a-version")).toBe(null); + expect(Bun.semver.patch("")).toBe(null); + }); +}); + +describe("Bun.semver.prerelease()", () => { + test("should return prerelease components", () => { + expect(Bun.semver.prerelease("1.2.3-alpha.1")).toEqual(["alpha", 1]); + expect(Bun.semver.prerelease("1.0.0-rc.2.beta")).toEqual(["rc", 2, "beta"]); + expect(Bun.semver.prerelease("1.0.0-0")).toEqual([0]); + expect(Bun.semver.prerelease("1.0.0-x.7.z.92")).toEqual(["x", 7, "z", 92]); + }); + + test("should return null for non-prerelease versions", () => { + expect(Bun.semver.prerelease("1.2.3")).toBe(null); + expect(Bun.semver.prerelease("1.2.3+build")).toBe(null); + expect(Bun.semver.prerelease("invalid")).toBe(null); + }); +}); + +describe("Bun.semver.parse()", () => { + test("should parse a version into an object", () => { + const v = "1.2.3-alpha.1+build.123"; + const parsed = Bun.semver.parse(v); + expect(parsed).not.toBe(null); + expect(parsed.major).toBe(1); + expect(parsed.minor).toBe(2); + expect(parsed.patch).toBe(3); + expect(parsed.prerelease).toEqual(["alpha", 1]); + expect(parsed.build).toEqual(["build", 123]); + expect(parsed.version).toBe("1.2.3-alpha.1+build.123"); + expect(parsed.raw).toBe(v); + }); + + test("should parse simple versions", () => { + const parsed = Bun.semver.parse("1.2.3"); + expect(parsed).not.toBe(null); + expect(parsed.major).toBe(1); + expect(parsed.minor).toBe(2); + expect(parsed.patch).toBe(3); + expect(parsed.prerelease).toBe(null); + expect(parsed.build).toBe(null); + expect(parsed.version).toBe("1.2.3"); + }); + + test("should return null for invalid versions", () => { + expect(Bun.semver.parse("not-a-version")).toBe(null); + expect(Bun.semver.parse("")).toBe(null); + expect(Bun.semver.parse("v")).toBe(null); + }); +}); + +describe("Bun.semver.bump()", () => { + describe("major", () => { + test("increments major version", () => { + expect(Bun.semver.bump("1.2.3", "major")).toBe("2.0.0"); + expect(Bun.semver.bump("0.0.1", "major")).toBe("1.0.0"); + expect(Bun.semver.bump("1.2.3-alpha", "major")).toBe("2.0.0"); + expect(Bun.semver.bump("1.2.3+build", "major")).toBe("2.0.0"); + }); + }); + + describe("minor", () => { + test("increments minor version", () => { + expect(Bun.semver.bump("1.2.3", "minor")).toBe("1.3.0"); + expect(Bun.semver.bump("0.0.1", "minor")).toBe("0.1.0"); + expect(Bun.semver.bump("1.2.3-alpha", "minor")).toBe("1.3.0"); + }); + }); + + describe("patch", () => { + test("increments patch version", () => { + expect(Bun.semver.bump("1.2.3", "patch")).toBe("1.2.4"); + expect(Bun.semver.bump("0.0.1", "patch")).toBe("0.0.2"); + expect(Bun.semver.bump("1.2.3-alpha", "patch")).toBe("1.2.4"); + }); + }); + + describe("premajor", () => { + test("increments major and adds prerelease", () => { + expect(Bun.semver.bump("1.2.3", "premajor")).toBe("2.0.0-0.0"); + expect(Bun.semver.bump("1.2.3", "premajor", "alpha")).toBe("2.0.0-alpha.0"); + expect(Bun.semver.bump("1.2.3", "premajor", "beta")).toBe("2.0.0-beta.0"); + }); + }); + + describe("preminor", () => { + test("increments minor and adds prerelease", () => { + expect(Bun.semver.bump("1.2.3", "preminor")).toBe("1.3.0-0.0"); + expect(Bun.semver.bump("1.2.3", "preminor", "alpha")).toBe("1.3.0-alpha.0"); + }); + }); + + describe("prepatch", () => { + test("increments patch and adds prerelease", () => { + expect(Bun.semver.bump("1.2.3", "prepatch")).toBe("1.2.4-0.0"); + expect(Bun.semver.bump("1.2.3", "prepatch", "alpha")).toBe("1.2.4-alpha.0"); + }); + }); + + describe("release", () => { + test("removes prerelease", () => { + expect(Bun.semver.bump("1.2.3-alpha.1", "release")).toBe("1.2.3"); + expect(Bun.semver.bump("1.2.3-0", "release")).toBe("1.2.3"); + expect(Bun.semver.bump("1.2.3", "release")).toBe("1.2.3"); + }); + }); + + describe("prerelease", () => { + test("increments prerelease version", () => { + expect(Bun.semver.bump("1.2.3-alpha.1", "prerelease")).toBe("1.2.3-alpha.2"); + expect(Bun.semver.bump("1.2.3-0", "prerelease")).toBe("1.2.3-1"); + expect(Bun.semver.bump("1.2.3-alpha", "prerelease")).toBe("1.2.3-alpha.0"); + }); + + test("adds prerelease if none exists", () => { + expect(Bun.semver.bump("1.2.3", "prerelease")).toBe("1.2.4-0.0"); + expect(Bun.semver.bump("1.2.3", "prerelease", "alpha")).toBe("1.2.4-alpha.0"); + }); + }); + + test("returns null for invalid versions", () => { + expect(Bun.semver.bump("not-a-version", "major")).toBe(null); + expect(Bun.semver.bump("", "major")).toBe(null); + expect(Bun.semver.bump("1.2.3", "invalid" as any)).toBe(null); + }); +});