From 906b287e31c732909b3fe250239fb52f0cbeee48 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 4 Jul 2025 15:17:24 -0700 Subject: [PATCH 1/8] fix BUN-KHE (#20820) Co-authored-by: Claude --- src/string/StringBuilder.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/string/StringBuilder.zig b/src/string/StringBuilder.zig index 803fdcd4ef..25af0cbf96 100644 --- a/src/string/StringBuilder.zig +++ b/src/string/StringBuilder.zig @@ -66,7 +66,7 @@ pub fn append16(this: *StringBuilder, slice: []const u16, fallback_allocator: st var list = std.ArrayList(u8).init(fallback_allocator); var out = bun.strings.toUTF8ListWithTypeBun(&list, []const u16, slice, false) catch return null; out.append(0) catch return null; - return list.items[0 .. list.items.len - 1 :0]; + return out.items[0 .. out.items.len - 1 :0]; } } From 7ba4b1d01e5fb0bffb9a09f8fbc1e011fc72a4b1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 5 Jul 2025 08:58:42 +0100 Subject: [PATCH 2/8] Fix: deprecated goo.gl links in snapshots raised in issue #20086 (#20424) --- docs/guides/test/snapshot.md | 2 +- .../bun/__snapshots__/runner.test.ts.snap | 2 +- .../test/__snapshots__/index.test.ts.snap | 2 +- src/bun.js/test/snapshot.zig | 2 +- .../__snapshots__/bun-build-api.test.ts.snap | 2 +- .../esbuild/__snapshots__/css.test.ts.snap | 2 +- .../__snapshots__/transpiler.test.js.snap | 2 +- .../__snapshots__/create-jsx.test.ts.snap | 2 +- .../__snapshots__/inspect.test.ts.snap | 2 +- .../__snapshots__/bun-audit.test.ts.snap | 2 +- .../bun-install-dep.test.ts.snap | 2 +- .../bun-install-registry.test.ts.snap | 2 +- .../__snapshots__/bun-install.test.ts.snap | 2 +- .../__snapshots__/bun-lock.test.ts.snap | 2 +- .../install/__snapshots__/bun-pm.test.ts.snap | 2 +- .../__snapshots__/bun-workspaces.test.ts.snap | 2 +- .../__snapshots__/catalogs.test.ts.snap | 2 +- .../install/__snapshots__/semver.test.ts.snap | 2 +- .../test/__snapshots__/coverage.test.ts.snap | 2 +- .../dev-server-ssr-100.test.ts.snap | 2 +- .../__snapshots__/dev-server.test.ts.snap | 2 +- .../__snapshots__/next-build.test.ts.snap | 2 +- .../sass/__snapshots__/sass.test.ts.snap | 2 +- .../__snapshots__/server-side.test.ts.snap | 2 +- .../bun-inspect-table.test.ts.snap | 2 +- .../__snapshots__/console-table.test.ts.snap | 2 +- test/js/bun/ffi/__snapshots__/cc.test.ts.snap | 2 +- .../bun/glob/__snapshots__/scan.test.ts.snap | 2 +- .../test/__snapshots__/test-interop.js.snap | 2 +- .../test/__snapshots__/test-test.test.ts.snap | 2 +- .../__snapshots__/bun-snapshots.test.ts.snap | 2 +- .../existing-snapshots.test.ts.snap | 2 +- .../snapshots/__snapshots__/more.test.ts.snap | 2 +- .../__snapshots__/moremore.test.ts.snap | 2 +- .../__snapshots__/snapshot.test.ts.snap | 52 +++++++++---------- .../different-directory.test.ts.snap | 2 +- .../snapshot-tests/snapshots/snapshot.test.ts | 2 +- .../__snapshots__/inspect-error.test.js.snap | 2 +- .../__snapshots__/reportError.test.ts.snap | 2 +- .../__snapshots__/vm-sourceUrl.test.ts.snap | 2 +- .../__snapshots__/rollup-v4.test.ts.snap | 2 +- .../__snapshots__/console-log.test.ts.snap | 2 +- .../__snapshots__/error-event.test.ts.snap | 2 +- .../issue/__snapshots__/03830.test.ts.snap | 2 +- 44 files changed, 69 insertions(+), 69 deletions(-) diff --git a/docs/guides/test/snapshot.md b/docs/guides/test/snapshot.md index 12d1fa1a00..3c62c16abf 100644 --- a/docs/guides/test/snapshot.md +++ b/docs/guides/test/snapshot.md @@ -46,7 +46,7 @@ test The `snap.test.ts.snap` file is a JavaScript file that exports a serialized version of the value passed into `expect()`. The `{foo: "bar"}` object has been serialized to JSON. ```js -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`snapshot 1`] = ` { diff --git a/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap b/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap index e438223dcb..3b11d92fe0 100644 --- a/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap +++ b/packages/bun-internal-test/runners/bun/__snapshots__/runner.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`runTests() can run all tests 1`] = ` { diff --git a/packages/bun-plugin-svelte/test/__snapshots__/index.test.ts.snap b/packages/bun-plugin-svelte/test/__snapshots__/index.test.ts.snap index 505eba0291..f317370527 100644 --- a/packages/bun-plugin-svelte/test/__snapshots__/index.test.ts.snap +++ b/packages/bun-plugin-svelte/test/__snapshots__/index.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`Bun.plugin using { forceSide: 'server' } allows for imported components to be SSR'd: foo.svelte - head 1`] = `""`; diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig index 897db792bd..bdb21bf784 100644 --- a/src/bun.js/test/snapshot.zig +++ b/src/bun.js/test/snapshot.zig @@ -13,7 +13,7 @@ const VirtualMachine = JSC.VirtualMachine; const Expect = @import("./expect.zig").Expect; pub const Snapshots = struct { - const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; + const file_header = "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n"; const snapshots_dir_name = "__snapshots__" ++ [_]u8{std.fs.path.sep}; pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); diff --git a/test/bundler/__snapshots__/bun-build-api.test.ts.snap b/test/bundler/__snapshots__/bun-build-api.test.ts.snap index 3f625ec089..e485dbc149 100644 --- a/test/bundler/__snapshots__/bun-build-api.test.ts.snap +++ b/test/bundler/__snapshots__/bun-build-api.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`Bun.build Bun.write(BuildArtifact) 1`] = ` "var __defProp = Object.defineProperty; diff --git a/test/bundler/esbuild/__snapshots__/css.test.ts.snap b/test/bundler/esbuild/__snapshots__/css.test.ts.snap index bebc1bf66c..31fedbf0e0 100644 --- a/test/bundler/esbuild/__snapshots__/css.test.ts.snap +++ b/test/bundler/esbuild/__snapshots__/css.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`esbuild-bundler css/CSSAtImportConditionsFromExternalRepo: /out/001/default/style.css 1`] = ` "/* 001/default/a.css */ diff --git a/test/bundler/transpiler/__snapshots__/transpiler.test.js.snap b/test/bundler/transpiler/__snapshots__/transpiler.test.js.snap index 8e21dd38d1..a78b2167dd 100644 --- a/test/bundler/transpiler/__snapshots__/transpiler.test.js.snap +++ b/test/bundler/transpiler/__snapshots__/transpiler.test.js.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`Bun.Transpiler using statements work right 1`] = ` "let __bun_temp_ref_1$ = []; diff --git a/test/cli/create/__snapshots__/create-jsx.test.ts.snap b/test/cli/create/__snapshots__/create-jsx.test.ts.snap index 2d13b2dc65..793466f6ca 100644 --- a/test/cli/create/__snapshots__/create-jsx.test.ts.snap +++ b/test/cli/create/__snapshots__/create-jsx.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`development: true react spa (no tailwind) dev server 1`] = ` " diff --git a/test/cli/inspect/__snapshots__/inspect.test.ts.snap b/test/cli/inspect/__snapshots__/inspect.test.ts.snap index 1b8aaf67ee..af9ed4cfee 100644 --- a/test/cli/inspect/__snapshots__/inspect.test.ts.snap +++ b/test/cli/inspect/__snapshots__/inspect.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`junit reporter 1`] = ` " diff --git a/test/cli/install/__snapshots__/bun-audit.test.ts.snap b/test/cli/install/__snapshots__/bun-audit.test.ts.snap index d7b514d5f5..35cf46d7bd 100644 --- a/test/cli/install/__snapshots__/bun-audit.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-audit.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`\`bun audit\` should exit code 1 when there are vulnerabilities: bun-audit-expect-vulnerabilities-found 1`] = ` "minimist <0.2.4 diff --git a/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap b/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap index 94cd72502d..96abf0eb3e 100644 --- a/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-dep.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`npa @scoped/package 1`] = ` { diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 0c7bcd1b7c..5f05a34fcd 100644 --- a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`auto-install symlinks (and junctions) are created correctly in the install cache 1`] = ` "{ diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index f82188350b..eb0f27c652 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`should report error on invalid format for package.json 1`] = ` "1 | foo diff --git a/test/cli/install/__snapshots__/bun-lock.test.ts.snap b/test/cli/install/__snapshots__/bun-lock.test.ts.snap index 61cfd88fdd..879aa0517c 100644 --- a/test/cli/install/__snapshots__/bun-lock.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-lock.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`should write plaintext lockfiles 1`] = ` "{ diff --git a/test/cli/install/__snapshots__/bun-pm.test.ts.snap b/test/cli/install/__snapshots__/bun-pm.test.ts.snap index e548c4ef52..4298b81516 100644 --- a/test/cli/install/__snapshots__/bun-pm.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-pm.test.ts.snap @@ -1,3 +1,3 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`bun pm migrate 1`] = `"E7F4C15F76D43059-37ed01456afdc149-B17A9541F8322712-04892ad4e094e703"`; diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index 922c072bd8..6f2caec2ca 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`dependency on workspace without version in package.json: version: * 1`] = ` "{ diff --git a/test/cli/install/__snapshots__/catalogs.test.ts.snap b/test/cli/install/__snapshots__/catalogs.test.ts.snap index e4af894193..815c94e560 100644 --- a/test/cli/install/__snapshots__/catalogs.test.ts.snap +++ b/test/cli/install/__snapshots__/catalogs.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`basic detect changes (bun.lock) 1`] = ` "{ diff --git a/test/cli/install/__snapshots__/semver.test.ts.snap b/test/cli/install/__snapshots__/semver.test.ts.snap index 85897d6404..59c2bd81a2 100644 --- a/test/cli/install/__snapshots__/semver.test.ts.snap +++ b/test/cli/install/__snapshots__/semver.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`Bun.semver.satisfies() pre-release snapshot 1`] = ` [ diff --git a/test/cli/test/__snapshots__/coverage.test.ts.snap b/test/cli/test/__snapshots__/coverage.test.ts.snap index 12122efc4d..fa77d0dba3 100644 --- a/test/cli/test/__snapshots__/coverage.test.ts.snap +++ b/test/cli/test/__snapshots__/coverage.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`lcov coverage reporter 1`] = ` "TN: diff --git a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap index 0f2f17408e..e88fa04fe8 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server-ssr-100.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`ssr works for 100-ish requests 1`] = ` { diff --git a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap index fb0bfc5e8c..46f3ea6329 100644 --- a/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/dev-server.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`hot reloading works on the client (+ tailwind hmr) 1`] = ` { diff --git a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap index 1e3b9f6cba..01c4b4859a 100644 --- a/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap +++ b/test/integration/next-pages/test/__snapshots__/next-build.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`next build works: bun 1`] = ` { diff --git a/test/integration/sass/__snapshots__/sass.test.ts.snap b/test/integration/sass/__snapshots__/sass.test.ts.snap index 5160965305..56d639fe8d 100644 --- a/test/integration/sass/__snapshots__/sass.test.ts.snap +++ b/test/integration/sass/__snapshots__/sass.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`sass source maps 1`] = ` { diff --git a/test/integration/svelte/__snapshots__/server-side.test.ts.snap b/test/integration/svelte/__snapshots__/server-side.test.ts.snap index 79744a952b..9f29e19be9 100644 --- a/test/integration/svelte/__snapshots__/server-side.test.ts.snap +++ b/test/integration/svelte/__snapshots__/server-side.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`When bun-plugin-svelte is enabled via Bun.plugin() can be render()-ed 1`] = ` { diff --git a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap index 0dbd06b2d1..f166e92f89 100644 --- a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap +++ b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`inspect.table { a: 1, b: 2 } 1`] = ` "┌───┬────────┐ diff --git a/test/js/bun/console/__snapshots__/console-table.test.ts.snap b/test/js/bun/console/__snapshots__/console-table.test.ts.snap index 28d4755f7e..256621eb70 100644 --- a/test/js/bun/console/__snapshots__/console-table.test.ts.snap +++ b/test/js/bun/console/__snapshots__/console-table.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`console.table expected output for: not object (number) 1`] = ` "42 diff --git a/test/js/bun/ffi/__snapshots__/cc.test.ts.snap b/test/js/bun/ffi/__snapshots__/cc.test.ts.snap index 7f08a1de50..619f2abb61 100644 --- a/test/js/bun/ffi/__snapshots__/cc.test.ts.snap +++ b/test/js/bun/ffi/__snapshots__/cc.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`can run a .c file: cc-fixture-stderr 1`] = `"Hello, World!"`; diff --git a/test/js/bun/glob/__snapshots__/scan.test.ts.snap b/test/js/bun/glob/__snapshots__/scan.test.ts.snap index 91b9cce320..b2ae59fed8 100644 --- a/test/js/bun/glob/__snapshots__/scan.test.ts.snap +++ b/test/js/bun/glob/__snapshots__/scan.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`fast-glob e2e tests patterns regular fixtures/*: fixtures/* 1`] = ` [ diff --git a/test/js/bun/test/__snapshots__/test-interop.js.snap b/test/js/bun/test/__snapshots__/test-interop.js.snap index 39cfad68b8..9ab001e8fb 100644 --- a/test/js/bun/test/__snapshots__/test-interop.js.snap +++ b/test/js/bun/test/__snapshots__/test-interop.js.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`expect() toMatchSnapshot to return undefined 1`] = `"abc"`; diff --git a/test/js/bun/test/__snapshots__/test-test.test.ts.snap b/test/js/bun/test/__snapshots__/test-test.test.ts.snap index d07cc41e5a..4749566917 100644 --- a/test/js/bun/test/__snapshots__/test-test.test.ts.snap +++ b/test/js/bun/test/__snapshots__/test-test.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`unhandled errors between tests are reported in beforeAll 1`] = ` " diff --git a/test/js/bun/test/snapshot-tests/__snapshots__/bun-snapshots.test.ts.snap b/test/js/bun/test/snapshot-tests/__snapshots__/bun-snapshots.test.ts.snap index 449a99d727..5b7b1e57cc 100644 --- a/test/js/bun/test/snapshot-tests/__snapshots__/bun-snapshots.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/__snapshots__/bun-snapshots.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`toMatchSnapshot errors should throw if arguments are in the wrong order: right spot 1`] = ` { diff --git a/test/js/bun/test/snapshot-tests/__snapshots__/existing-snapshots.test.ts.snap b/test/js/bun/test/snapshot-tests/__snapshots__/existing-snapshots.test.ts.snap index 7c0d55c60f..86f185de3b 100644 --- a/test/js/bun/test/snapshot-tests/__snapshots__/existing-snapshots.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/__snapshots__/existing-snapshots.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`it will work with an existing snapshot file made with bun 1`] = ` { diff --git a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/more.test.ts.snap b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/more.test.ts.snap index bee96183b3..f4907cae49 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/more.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/more.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://bun.sh/docs/test/snapshots exports[`d0 d1 t1 1`] = `"hello\`snapshot\\"`; diff --git a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/moremore.test.ts.snap b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/moremore.test.ts.snap index ab8c168fae..a47ab54cec 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/moremore.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/moremore.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://bun.sh/docs/test/snapshots exports[`test snapshots with Boolean and Number 1`] = `1`; diff --git a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap index 34d5ce7c32..d649371c77 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/snapshots/__snapshots__/snapshot.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://bun.sh/docs/test/snapshots exports[`most types 1`] = `3`; @@ -370,35 +370,35 @@ exports[`most types: testing 7 3`] = `8`; exports[`most types: undefined 1`] = `undefined`; exports[`snapshots dollars 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"$"\`; " `; exports[`snapshots backslash 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"\\\\"\`; " `; exports[`snapshots dollars curly 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"\\\${}"\`; " `; exports[`snapshots dollars curly 2 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"\\\${"\`; " `; exports[`snapshots stuff 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \` "æ™ @@ -409,7 +409,7 @@ exports[\`abc 1\`] = \` `; exports[`snapshots stuff 2 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \` "æ™ @@ -420,28 +420,28 @@ exports[\`abc 1\`] = \` `; exports[`snapshots regexp 1 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`/\\\${1..}/\`; " `; exports[`snapshots regexp 2 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`/\\\${2..}/\`; " `; exports[`snapshots string 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"abc"\`; " `; exports[`snapshots string with newline 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \` "qwerty @@ -451,35 +451,35 @@ ioup" `; exports[`snapshots null byte 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"1 \\x00"\`; " `; exports[`snapshots null byte 2 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"2 \\x00"\`; " `; exports[`snapshots backticks 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"This is \\\`wrong\\\`"\`; " `; exports[`snapshots unicode 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"😊abc\\\`\\\${def} �, � "\`; " `; exports[`snapshots jest newline oddity 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \` " @@ -489,14 +489,14 @@ exports[\`abc 1\`] = \` `; exports[`snapshots grow file for new snapshot 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"hello"\`; " `; exports[`snapshots grow file for new snapshot 2`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"hello"\`; @@ -505,7 +505,7 @@ exports[\`def 1\`] = \`"hello"\`; `; exports[`snapshots grow file for new snapshot 3`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"goodbye"\`; @@ -514,35 +514,35 @@ exports[\`def 1\`] = \`"hello"\`; `; exports[`snapshots backtick in test name 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`\\\` 1\`] = \`"abc"\`; " `; exports[`snapshots dollars curly in test name 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`\\\${} 1\`] = \`"abc"\`; " `; exports[`snapshots #15283 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`Should work 1\`] = \`"This is \\\`wrong\\\`"\`; " `; exports[`snapshots #15283 unicode 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`Should work 1\`] = \`"😊This is \\\`wrong\\\`"\`; " `; exports[`snapshots replaces file that fails to parse when update flag is used 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`t1 1\`] = \`"abc def ghi jkl"\`; @@ -553,7 +553,7 @@ exports[\`t3 1\`] = \`"abc def ghi"\`; `; exports[`snapshots property matchers 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \` { @@ -582,7 +582,7 @@ exports[`inline snapshots #15283 1`] = ` `; exports[`snapshots unicode surrogate halves 1`] = ` -"// Bun Snapshot v1, https://goo.gl/fbAQLP +"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[\`abc 1\`] = \`"😊abc\\\`\\\${def} �, � "\`; " diff --git a/test/js/bun/test/snapshot-tests/snapshots/more-snapshots/__snapshots__/different-directory.test.ts.snap b/test/js/bun/test/snapshot-tests/snapshots/more-snapshots/__snapshots__/different-directory.test.ts.snap index f45127b386..6b43eec53e 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/more-snapshots/__snapshots__/different-directory.test.ts.snap +++ b/test/js/bun/test/snapshot-tests/snapshots/more-snapshots/__snapshots__/different-directory.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://bun.sh/docs/test/snapshots exports[`snapshots in different directory 1`] = ` "12 diff --git a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts index debc737ef4..ae2276109c 100644 --- a/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts +++ b/test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts @@ -311,7 +311,7 @@ for (const inlineSnapshot of [false, true]) { { forceUpdate: true }, ); expect(await t.getSnapshotContents()).toBe( - '// Bun Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`t1 1`] = `"abc def ghi jkl"`;\n\nexports[`t2 1`] = `"abc\\`def"`;\n\nexports[`t3 1`] = `"abc def ghi"`;\n', + '// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`t1 1`] = `"abc def ghi jkl"`;\n\nexports[`t2 1`] = `"abc\\`def"`;\n\nexports[`t3 1`] = `"abc def ghi"`;\n', ); }); diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index eff7103964..c60a55e2b9 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`error.cause 1`] = ` "1 | import { expect, test } from "bun:test"; diff --git a/test/js/bun/util/__snapshots__/reportError.test.ts.snap b/test/js/bun/util/__snapshots__/reportError.test.ts.snap index bc70b514db..661ab1af60 100644 --- a/test/js/bun/util/__snapshots__/reportError.test.ts.snap +++ b/test/js/bun/util/__snapshots__/reportError.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`reportError 1`] = ` "1 | reportError(new Error("reportError Test!")); diff --git a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap index 82c5b1b753..d176c37e73 100644 --- a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap +++ b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`can get sourceURL from eval inside node:vm 1`] = ` "evalmachine.:2 diff --git a/test/js/third_party/rollup-v4/__snapshots__/rollup-v4.test.ts.snap b/test/js/third_party/rollup-v4/__snapshots__/rollup-v4.test.ts.snap index ef4df2b8df..390f4c3fa1 100644 --- a/test/js/third_party/rollup-v4/__snapshots__/rollup-v4.test.ts.snap +++ b/test/js/third_party/rollup-v4/__snapshots__/rollup-v4.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`it works 1`] = ` { diff --git a/test/js/web/console/__snapshots__/console-log.test.ts.snap b/test/js/web/console/__snapshots__/console-log.test.ts.snap index 45f884b607..a29a10db51 100644 --- a/test/js/web/console/__snapshots__/console-log.test.ts.snap +++ b/test/js/web/console/__snapshots__/console-log.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`console.group: console-group-error 1`] = ` "Warning log diff --git a/test/js/web/websocket/__snapshots__/error-event.test.ts.snap b/test/js/web/websocket/__snapshots__/error-event.test.ts.snap index 62c8d485e2..a8a8127ee1 100644 --- a/test/js/web/websocket/__snapshots__/error-event.test.ts.snap +++ b/test/js/web/websocket/__snapshots__/error-event.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`WebSocket error event snapshot: Snapshot snapshot 1`] = `ErrorEvent { type: "error", diff --git a/test/regression/issue/__snapshots__/03830.test.ts.snap b/test/regression/issue/__snapshots__/03830.test.ts.snap index da75c83eb3..7ecf1cfe71 100644 --- a/test/regression/issue/__snapshots__/03830.test.ts.snap +++ b/test/regression/issue/__snapshots__/03830.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`macros should not lead to seg faults under any given input 1`] = ` "2 | fn(\`©\${Number(0)}\`); From d306e65d0e6ca9fc38ab030d3987cdb43604a02f Mon Sep 17 00:00:00 2001 From: CountBleck Date: Sat, 5 Jul 2025 17:25:52 -0700 Subject: [PATCH 3/8] Remove duplicate toBeInstanceOf type declaration for Expect (#20844) --- packages/bun-types/test.d.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index 7692df3f77..c02e72f1d7 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -1184,14 +1184,6 @@ declare module "bun:test" { * expect(null).toBeInstanceOf(Array); // fail */ toBeInstanceOf(value: unknown): void; - /** - * Asserts that the expected value is an instance of value - * - * @example - * expect([]).toBeInstanceOf(Array); - * expect(null).toBeInstanceOf(Array); // fail - */ - toBeInstanceOf(value: unknown): void; /** * Asserts that a value is `undefined`. * From 9feaab47f52c764b17fc44380845132dd2be48bb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 5 Jul 2025 22:19:39 -0700 Subject: [PATCH 4/8] Update environment.json --- .cursor/environment.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor/environment.json b/.cursor/environment.json index e876769e2a..92e8fb10c7 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,5 +1,5 @@ { - "snapshot": "snapshot-20250630-a406baa4-6bb1-41d9-a125-85176851e05b", + "snapshot": "snapshot-20250706-71021aff-cc0d-4a7f-a468-d443b16c4bf1", "install": "bun install", "terminals": [ { From e1957228f318103ce363986b7cb35d5a204d284e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Jul 2025 21:07:38 -0700 Subject: [PATCH 5/8] [internal] Add error when .classes.ts files are invalid --- src/codegen/generate-classes.ts | 38 +++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 19fb13650a..1399a5cd46 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2415,19 +2415,35 @@ pub const WriteBytesFn = *const fn(*anyopaque, ptr: [*]const u8, len: u32) callc `; const classes: ClassDefinition[] = []; -for (const file of files) { - const result = require(path.resolve(file)); - if (!(result?.default?.length ?? 0)) continue; - console.log("Found", result.default.length, "classes from", file); - for (let { name, proto = {}, klass = {} } of result.default) { - let protoProps = Object.keys(proto).length ? `${Object.keys(proto).length} fields` : ""; - let klassProps = Object.keys(klass).length ? `${Object.keys(klass).length} class fields` : ""; - let props = [protoProps, klassProps].filter(Boolean).join(", "); - if (props.length) props = ` (${props})`; - console.log(` - ${name}` + props); +{ + let errors = []; + for (const file of files) { + const filepath = path.resolve(file); + const result = require(filepath); + if (!(result?.default?.length ?? 0)) { + errors.push( + new TypeError( + `Missing classes in "${path.relative(process.cwd(), filepath)}". Expected \`export default [ define(...) ] satisfies Array\` but got ${Bun.inspect(result).slice(0, 100) + "..."} `, + ), + ); + continue; + } + + console.log("Found", result.default.length, "classes from", file); + for (let { name, proto = {}, klass = {} } of result.default) { + let protoProps = Object.keys(proto).length ? `${Object.keys(proto).length} fields` : ""; + let klassProps = Object.keys(klass).length ? `${Object.keys(klass).length} class fields` : ""; + let props = [protoProps, klassProps].filter(Boolean).join(", "); + if (props.length) props = ` (${props})`; + console.log(` - ${name}` + props); + } + + classes.push(...result.default); } - classes.push(...result.default); + if (errors.length) { + throw new AggregateError(errors, "Failed to generate classes"); + } } classes.sort((a, b) => (a.name < b.name ? -1 : 1)); From c370645afc9121b3b9c8d8d32293b54b80cfe15c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Jul 2025 21:08:26 -0700 Subject: [PATCH 6/8] Update zig-javascriptcore-classes.mdc --- .cursor/rules/zig-javascriptcore-classes.mdc | 77 +++++++++++--------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/.cursor/rules/zig-javascriptcore-classes.mdc b/.cursor/rules/zig-javascriptcore-classes.mdc index 965932a988..88636c9752 100644 --- a/.cursor/rules/zig-javascriptcore-classes.mdc +++ b/.cursor/rules/zig-javascriptcore-classes.mdc @@ -1,8 +1,9 @@ --- description: How Zig works with JavaScriptCore bindings generator -globs: +globs: alwaysApply: false --- + # Bun's JavaScriptCore Class Bindings Generator This document explains how Bun's class bindings generator works to bridge Zig and JavaScript code through JavaScriptCore (JSC). @@ -24,7 +25,7 @@ The `.classes.ts` files define the JavaScript API using a declarative approach: ```typescript // Example: encoding.classes.ts define({ - name: "TextDecoder", + name: "TextDecoder", constructor: true, JSType: "object", finalize: true, @@ -40,17 +41,18 @@ define({ }, fatal: { // Read-only property - getter: true, + getter: true, }, ignoreBOM: { // Read-only property getter: true, - } - } + }, + }, }); ``` Each class definition specifies: + - The class name - Whether it has a constructor - JavaScript type (object, function, etc.) @@ -87,7 +89,7 @@ pub const TextDecoder = struct { // Fields }); } - + // Prototype methods - note return type includes JSError pub fn decode( this: *TextDecoder, @@ -96,23 +98,23 @@ pub const TextDecoder = struct { ) bun.JSError!JSC.JSValue { // Implementation } - + // Getters pub fn getEncoding(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.createStringFromUTF8(globalObject, this.encoding); } - + pub fn getFatal(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.jsBoolean(this.fatal); } - + // Cleanup - note standard pattern of using deinit/deref fn deinit(this: *TextDecoder) void { // Release any retained resources // Free the pointer at the end. bun.destroy(this); } - + // Finalize - called by JS garbage collector. This should call deinit, or deref if reference counted. pub fn finalize(this: *TextDecoder) void { this.deinit(); @@ -121,6 +123,7 @@ pub const TextDecoder = struct { ``` Key components in the Zig file: + - The struct containing native state - `pub const js = JSC.Codegen.JS` to include generated code - Constructor and methods using `bun.JSError!JSValue` return type for proper error handling @@ -128,6 +131,7 @@ Key components in the Zig file: - Methods matching the JavaScript interface - Getters/setters for properties - Proper resource cleanup pattern with `deinit()` and `finalize()` +- Update `src/bun.js/bindings/generated_classes_list.zig` to include the new class ## Code Generation System @@ -140,6 +144,7 @@ The binding generator produces C++ code that connects JavaScript and Zig: 5. **Property Caching**: Implements the caching system for properties The generated C++ code includes: + - A JSC wrapper class (`JSTextDecoder`) - A prototype class (`JSTextDecoderPrototype`) - A constructor function (`JSTextDecoderConstructor`) @@ -152,28 +157,29 @@ The `CallFrame` object provides access to JavaScript execution context: ```zig pub fn decode( - this: *TextDecoder, + this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame ) bun.JSError!JSC.JSValue { // Get arguments const input = callFrame.argument(0); const options = callFrame.argument(1); - + // Get this value const thisValue = callFrame.thisValue(); - + // Implementation with error handling if (input.isUndefinedOrNull()) { return globalObject.throw("Input cannot be null or undefined", .{}); } - + // Return value or throw error return JSC.JSValue.jsString(globalObject, "result"); } ``` CallFrame methods include: + - `argument(i)`: Get the i-th argument - `argumentCount()`: Get the number of arguments - `thisValue()`: Get the `this` value @@ -201,17 +207,17 @@ JSC_DEFINE_CUSTOM_GETTER(TextDecoderPrototype__encodingGetterWrap, (...)) { auto throwScope = DECLARE_THROW_SCOPE(vm); JSTextDecoder* thisObject = jsCast(JSValue::decode(encodedThisValue)); JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); - + // Check for cached value and return if present if (JSValue cachedValue = thisObject->m_encoding.get()) return JSValue::encode(cachedValue); - + // Get value from Zig implementation JSC::JSValue result = JSC::JSValue::decode( TextDecoderPrototype__getEncoding(thisObject->wrapped(), globalObject) ); RETURN_IF_EXCEPTION(throwScope, {}); - + // Store in cache for future access thisObject->m_encoding.set(vm, thisObject, result); RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); @@ -253,7 +259,7 @@ This system provides several key benefits: 1. **Automatic Memory Management**: The JavaScriptCore GC tracks and manages these values 2. **Proper Garbage Collection**: The WriteBarrier ensures values are properly visited during GC 3. **Consistent Access**: Zig code can easily get/set these cached JS values -4. **Performance**: Cached values avoid repeated computation or serialization +4. **Performance**: Cached values avoid repeated computation or serialization ### Use Cases @@ -281,7 +287,7 @@ Bun uses a consistent pattern for resource cleanup: pub fn deinit(this: *TextDecoder) void { // Release resources like strings this._encoding.deref(); // String deref pattern - + // Free any buffers if (this.buffer) |buffer| { bun.default_allocator.free(buffer); @@ -312,7 +318,7 @@ Bun uses `bun.JSError!JSValue` return type for proper error handling: ```zig pub fn decode( - this: *TextDecoder, + this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame ) bun.JSError!JSC.JSValue { @@ -320,13 +326,14 @@ pub fn decode( if (callFrame.argumentCount() < 1) { return globalObject.throw("Missing required argument", .{}); } - + // Or returning a success value return JSC.JSValue.jsString(globalObject, "Success!"); } ``` This pattern allows Zig functions to: + 1. Return JavaScript values on success 2. Throw JavaScript exceptions on error 3. Propagate errors automatically through the call stack @@ -339,7 +346,7 @@ The binding system includes robust error handling: // Example of type checking in generated code JSTextDecoder* thisObject = jsDynamicCast(callFrame->thisValue()); if (UNLIKELY(!thisObject)) { - scope.throwException(lexicalGlobalObject, + scope.throwException(lexicalGlobalObject, Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "TextDecoder"_s)); return {}; } @@ -351,7 +358,7 @@ The binding system creates proper JavaScript prototype chains: 1. **Constructor**: JSTextDecoderConstructor with standard .prototype property 2. **Prototype**: JSTextDecoderPrototype with methods and properties -3. **Instances**: Each JSTextDecoder instance with __proto__ pointing to prototype +3. **Instances**: Each JSTextDecoder instance with **proto** pointing to prototype This ensures JavaScript inheritance works as expected: @@ -360,7 +367,7 @@ This ensures JavaScript inheritance works as expected: void JSTextDecoderConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSTextDecoderPrototype* prototype) { Base::finishCreation(vm, 0, "TextDecoder"_s, PropertyAdditionMode::WithoutStructureTransition); - + // Set up the prototype chain putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); ASSERT(inherits(info())); @@ -372,7 +379,7 @@ void JSTextDecoderConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globa The binding system is optimized for performance: 1. **Direct Pointer Access**: JavaScript objects maintain a direct pointer to Zig objects -2. **Property Caching**: WriteBarrier caching avoids repeated native calls for stable properties +2. **Property Caching**: WriteBarrier caching avoids repeated native calls for stable properties 3. **Memory Management**: JSC garbage collection integrated with Zig memory management 4. **Type Conversion**: Fast paths for common JavaScript/Zig type conversions @@ -381,6 +388,7 @@ The binding system is optimized for performance: To create a new class binding in Bun: 1. **Define the class interface** in a `.classes.ts` file: + ```typescript define({ name: "MyClass", @@ -393,12 +401,13 @@ To create a new class binding in Bun: myProperty: { getter: true, cache: true, - } - } + }, + }, }); ``` 2. **Implement the native functionality** in a `.zig` file: + ```zig pub const MyClass = struct { // Generated bindings @@ -409,9 +418,9 @@ To create a new class binding in Bun: // State value: []const u8, - + pub const new = bun.TrivialNew(@This()); - + // Constructor pub fn constructor( globalObject: *JSGlobalObject, @@ -420,7 +429,7 @@ To create a new class binding in Bun: const arg = callFrame.argument(0); // Implementation } - + // Method pub fn myMethod( this: *MyClass, @@ -429,17 +438,17 @@ To create a new class binding in Bun: ) bun.JSError!JSC.JSValue { // Implementation } - + // Getter pub fn getMyProperty(this: *MyClass, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.jsString(globalObject, this.value); } - + // Resource cleanup pub fn deinit(this: *MyClass) void { // Clean up resources } - + pub fn finalize(this: *MyClass) void { this.deinit(); bun.destroy(this); @@ -474,11 +483,13 @@ For each Zig class, the system generates: ### 3. Zig Bindings - **External Function Declarations**: + ```zig extern fn TextDecoderPrototype__decode(*TextDecoder, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSC.EncodedJSValue; ``` - **Cached Value Accessors**: + ```zig pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { ... } pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { ... } From dacb75dc1f69b0377cb1ebe131f0a9f029ccd3b0 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 7 Jul 2025 01:07:03 -0700 Subject: [PATCH 7/8] Fix crash in bundler related to onLoad plugins that return file loader for HTML imports (#20849) --- src/bundler/ParseTask.zig | 4 -- src/bundler/bundle_v2.zig | 6 +-- test/bundler/bundler_plugin.test.ts | 76 +++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/bundler/ParseTask.zig b/src/bundler/ParseTask.zig index c42df528f3..b12b1c8ef4 100644 --- a/src/bundler/ParseTask.zig +++ b/src/bundler/ParseTask.zig @@ -34,10 +34,6 @@ emit_decorator_metadata: bool = false, ctx: *BundleV2, package_version: string = "", is_entry_point: bool = false, -/// This is set when the file is an entrypoint, and it has an onLoad plugin. -/// In this case we want to defer adding this to additional_files until after -/// the onLoad plugin has finished. -defer_copy_for_bundling: bool = false, const ParseTaskStage = union(enum) { needs_source_code: void, diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index fb4bf0ad22..dd4d7fd766 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1964,7 +1964,8 @@ pub const BundleV2 = struct { this.decrementScanCounter(); }, .success => |code| { - const should_copy_for_bundling = load.parse_task.defer_copy_for_bundling and code.loader.shouldCopyForBundling(); + // When a plugin returns a file loader, we always need to populate additional_files + const should_copy_for_bundling = code.loader.shouldCopyForBundling(); if (should_copy_for_bundling) { const source_index = load.source_index; var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()]; @@ -2619,9 +2620,6 @@ pub const BundleV2 = struct { pub fn enqueueOnLoadPluginIfNeededImpl(this: *BundleV2, parse: *ParseTask) bool { if (this.plugins) |plugins| { if (plugins.hasAnyMatches(&parse.path, true)) { - if (parse.is_entry_point and parse.loader != null and parse.loader.?.shouldCopyForBundling()) { - parse.defer_copy_for_bundling = true; - } // This is where onLoad plugins are enqueued debug("enqueue onLoad: {s}:{s}", .{ parse.path.namespace, diff --git a/test/bundler/bundler_plugin.test.ts b/test/bundler/bundler_plugin.test.ts index e2cdcbffa9..44188998ed 100644 --- a/test/bundler/bundler_plugin.test.ts +++ b/test/bundler/bundler_plugin.test.ts @@ -820,4 +820,80 @@ describe("bundler", () => { }, }; }); + + itBundled("plugin/FileLoaderWithCustomContents", { + files: { + "index.html": /* html */ ` + + + + Test + + + + + + + `, + "script.js": /* js */ ` + console.log("Script loaded"); + `, + "image.jpeg": "actual image data would be here", + }, + entryPoints: ["./index.html"], + outdir: "/out", + plugins(build) { + // This plugin intercepts .jpeg files and returns them with custom contents + // This previously caused a crash because additional_files wasn't populated + build.onLoad({ filter: /\.jpe?g$/ }, async args => { + return { + loader: "file", + contents: "custom image contents", + }; + }); + }, + onAfterBundle(api) { + // Verify the build succeeded and files were created + api.assertFileExists("index.html"); + // The image should be copied with a hashed name + const html = api.readFile("index.html"); + expect(html).toContain('src="'); + expect(html).toContain('.jpeg"'); + }, + }); + + itBundled("plugin/FileLoaderMultipleAssets", { + files: { + "index.js": /* js */ ` + import imgUrl from "./image.png"; + import wasmUrl from "./module.wasm"; + console.log(imgUrl, wasmUrl); + `, + "image.png": "png data", + "module.wasm": "wasm data", + }, + entryPoints: ["./index.js"], + outdir: "/out", + plugins(build) { + // Test multiple file types with custom contents + build.onLoad({ filter: /\.(png|wasm)$/ }, async args => { + const ext = args.path.split(".").pop(); + return { + loader: "file", + contents: `custom ${ext} contents`, + }; + }); + }, + run: { + stdout: /\.(png|wasm)/, + }, + onAfterBundle(api) { + // Verify the build succeeded and files were created + api.assertFileExists("index.js"); + const js = api.readFile("index.js"); + // Should contain references to the copied files + expect(js).toContain('.png"'); + expect(js).toContain('.wasm"'); + }, + }); }); From 0399ae0ee96e885daeee7bb120dc36aa969d645a Mon Sep 17 00:00:00 2001 From: Michael H Date: Tue, 8 Jul 2025 04:21:36 +1000 Subject: [PATCH 8/8] followup for `bun pm version` (#20799) --- docs/cli/pm.md | 7 +- src/cli/pm_version_command.zig | 289 +++-- test/cli/install/bun-pm-version.test.ts | 1392 ++++++++++++----------- 3 files changed, 888 insertions(+), 800 deletions(-) diff --git a/docs/cli/pm.md b/docs/cli/pm.md index 0739de7ae9..577ab383cd 100644 --- a/docs/cli/pm.md +++ b/docs/cli/pm.md @@ -175,13 +175,14 @@ Increment: Options: --no-git-tag-version Skip git operations --allow-same-version Prevents throwing error if version is the same - --message=, -m Custom commit message - --preid= Prerelease identifier + --message=, -m Custom commit message, use %s for version substitution + --preid= Prerelease identifier (i.e beta → 1.0.1-beta.0) + --force, -f Bypass dirty git history check Examples: $ bun pm version patch $ bun pm version 1.2.3 --no-git-tag-version - $ bun pm version prerelease --preid beta + $ bun pm version prerelease --preid beta --message "Release beta: %s" ``` To bump the version in `package.json`: diff --git a/src/cli/pm_version_command.zig b/src/cli/pm_version_command.zig index 2dc75823b6..5bc8511b9b 100644 --- a/src/cli/pm_version_command.zig +++ b/src/cli/pm_version_command.zig @@ -11,6 +11,7 @@ const logger = bun.logger; const JSON = bun.JSON; const RunCommand = bun.RunCommand; const Environment = bun.Environment; +const JSPrinter = bun.js_printer; pub const PmVersionCommand = struct { const VersionType = enum { @@ -59,30 +60,45 @@ pub const PmVersionCommand = struct { defer ctx.allocator.free(package_json_contents); const package_json_source = logger.Source.initPathString(package_json_path, package_json_contents); - const json = JSON.parsePackageJSONUTF8(&package_json_source, ctx.log, ctx.allocator) catch |err| { + const json_result = JSON.parsePackageJSONUTF8WithOpts( + &package_json_source, + ctx.log, + ctx.allocator, + .{ + .is_json = true, + .allow_comments = true, + .allow_trailing_commas = true, + .guess_indentation = true, + }, + ) catch |err| { Output.errGeneric("Failed to parse package.json: {s}", .{@errorName(err)}); Global.exit(1); }; - const scripts = json.asProperty("scripts"); + var json = json_result.root; + + if (json.data != .e_object) { + Output.errGeneric("Failed to parse package.json: root must be an object", .{}); + Global.exit(1); + } + + const scripts = if (pm.options.do.run_scripts) json.asProperty("scripts") else null; const scripts_obj = if (scripts) |s| if (s.expr.data == .e_object) s.expr else null else null; - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("preversion")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "preversion", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("preversion")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "preversion", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -96,49 +112,63 @@ pub const PmVersionCommand = struct { else => {}, } } - Output.errGeneric("No version field found in package.json", .{}); - Global.exit(1); + break :brk_version null; }; - const new_version_str = try calculateNewVersion(ctx.allocator, current_version, version_type, new_version, pm.options.preid, package_json_dir); + const new_version_str = try calculateNewVersion(ctx.allocator, current_version orelse "0.0.0", version_type, new_version, pm.options.preid, package_json_dir); defer ctx.allocator.free(new_version_str); - if (!pm.options.allow_same_version and strings.eql(current_version, new_version_str)) { - Output.errGeneric("Version not changed", .{}); - Global.exit(1); + if (current_version) |version| { + if (!pm.options.allow_same_version and strings.eql(version, new_version_str)) { + Output.errGeneric("Version not changed", .{}); + Global.exit(1); + } } { - const updated_contents = try updateVersionString(ctx.allocator, package_json_contents, current_version, new_version_str); - defer ctx.allocator.free(updated_contents); + try json.data.e_object.putString(ctx.allocator, "version", new_version_str); - const file = std.fs.cwd().openFile(package_json_path, .{ .mode = .write_only }) catch |err| { - Output.errGeneric("Failed to open package.json for writing: {s}", .{@errorName(err)}); + var buffer_writer = JSPrinter.BufferWriter.init(ctx.allocator); + buffer_writer.append_newline = package_json_contents.len > 0 and package_json_contents[package_json_contents.len - 1] == '\n'; + var package_json_writer = JSPrinter.BufferPrinter.init(buffer_writer); + + _ = JSPrinter.printJSON( + @TypeOf(&package_json_writer), + &package_json_writer, + json, + &package_json_source, + .{ + .indent = json_result.indentation, + .mangled_props = null, + }, + ) catch |err| { + Output.errGeneric("Failed to save package.json: {s}", .{@errorName(err)}); Global.exit(1); }; - defer file.close(); - try file.seekTo(0); - try file.setEndPos(0); - try file.writeAll(updated_contents); + std.fs.cwd().writeFile(.{ + .sub_path = package_json_path, + .data = package_json_writer.ctx.writtenWithoutTrailingZero(), + }) catch |err| { + Output.errGeneric("Failed to write package.json: {s}", .{@errorName(err)}); + Global.exit(1); + }; } - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("version")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "version", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("version")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "version", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -147,22 +177,20 @@ pub const PmVersionCommand = struct { try gitCommitAndTag(ctx.allocator, new_version_str, pm.options.message, package_json_dir); } - if (pm.options.do.run_scripts) { - if (scripts_obj) |s| { - if (s.get("postversion")) |script| { - if (script.asString(ctx.allocator)) |script_command| { - try RunCommand.runPackageScriptForeground( - ctx, - ctx.allocator, - script_command, - "postversion", - package_json_dir, - pm.env, - &.{}, - pm.options.log_level == .silent, - ctx.debug.use_system_shell, - ); - } + if (scripts_obj) |s| { + if (s.get("postversion")) |script| { + if (script.asString(ctx.allocator)) |script_command| { + try RunCommand.runPackageScriptForeground( + ctx, + ctx.allocator, + script_command, + "postversion", + package_json_dir, + pm.env, + &.{}, + pm.options.log_level == .silent, + ctx.debug.use_system_shell, + ); } } } @@ -201,7 +229,7 @@ pub const PmVersionCommand = struct { return; } - if (!try isGitClean(cwd) and !pm.options.force) { + if (!pm.options.force and !try isGitClean(cwd)) { Output.errGeneric("Git working directory not clean.", .{}); Global.exit(1); } @@ -256,6 +284,15 @@ pub const PmVersionCommand = struct { Output.prettyln("Current package version: v{s}", .{version}); } + const patch_version = try calculateNewVersion(ctx.allocator, current_version, .patch, null, pm.options.preid, cwd); + const minor_version = try calculateNewVersion(ctx.allocator, current_version, .minor, null, pm.options.preid, cwd); + const major_version = try calculateNewVersion(ctx.allocator, current_version, .major, null, pm.options.preid, cwd); + const prerelease_version = try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, pm.options.preid, cwd); + defer ctx.allocator.free(patch_version); + defer ctx.allocator.free(minor_version); + defer ctx.allocator.free(major_version); + defer ctx.allocator.free(prerelease_version); + const increment_help_text = \\ \\Increment: @@ -266,13 +303,20 @@ pub const PmVersionCommand = struct { \\ ; Output.pretty(increment_help_text, .{ - current_version, try calculateNewVersion(ctx.allocator, current_version, .patch, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .minor, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .major, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, pm.options.preid, cwd), + current_version, patch_version, + current_version, minor_version, + current_version, major_version, + current_version, prerelease_version, }); if (strings.indexOfChar(current_version, '-') != null or pm.options.preid.len > 0) { + const prepatch_version = try calculateNewVersion(ctx.allocator, current_version, .prepatch, null, pm.options.preid, cwd); + const preminor_version = try calculateNewVersion(ctx.allocator, current_version, .preminor, null, pm.options.preid, cwd); + const premajor_version = try calculateNewVersion(ctx.allocator, current_version, .premajor, null, pm.options.preid, cwd); + defer ctx.allocator.free(prepatch_version); + defer ctx.allocator.free(preminor_version); + defer ctx.allocator.free(premajor_version); + const prerelease_help_text = \\ prepatch {s} → {s} \\ preminor {s} → {s} @@ -280,12 +324,15 @@ pub const PmVersionCommand = struct { \\ ; Output.pretty(prerelease_help_text, .{ - current_version, try calculateNewVersion(ctx.allocator, current_version, .prepatch, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .preminor, null, pm.options.preid, cwd), - current_version, try calculateNewVersion(ctx.allocator, current_version, .premajor, null, pm.options.preid, cwd), + current_version, prepatch_version, + current_version, preminor_version, + current_version, premajor_version, }); } + const beta_prerelease_version = try calculateNewVersion(ctx.allocator, current_version, .prerelease, null, "beta", cwd); + defer ctx.allocator.free(beta_prerelease_version); + const set_specific_version_help_text = \\ from-git Use version from latest git tag \\ 1.2.3 Set specific version @@ -293,78 +340,22 @@ pub const PmVersionCommand = struct { \\Options: \\ --no-git-tag-version Skip git operations \\ --allow-same-version Prevents throwing error if version is the same - \\ --message=\, -m Custom commit message - \\ --preid=\ Prerelease identifier + \\ --message=\, -m Custom commit message, use %s for version substitution + \\ --preid=\ Prerelease identifier (i.e beta → {s}) + \\ --force, -f Bypass dirty git history check \\ \\Examples: \\ $ bun pm version patch \\ $ bun pm version 1.2.3 --no-git-tag-version - \\ $ bun pm version prerelease --preid beta + \\ $ bun pm version prerelease --preid beta --message "Release beta: %s" \\ \\More info: https://bun.sh/docs/cli/pm#version \\ ; - Output.pretty(set_specific_version_help_text, .{}); + Output.pretty(set_specific_version_help_text, .{beta_prerelease_version}); Output.flush(); } - fn updateVersionString(allocator: std.mem.Allocator, contents: []const u8, old_version: []const u8, new_version: []const u8) ![]const u8 { - const version_key = "\"version\""; - - var search_start: usize = 0; - while (std.mem.indexOfPos(u8, contents, search_start, version_key)) |key_pos| { - var colon_pos = key_pos + version_key.len; - while (colon_pos < contents.len and (contents[colon_pos] == ' ' or contents[colon_pos] == '\t')) { - colon_pos += 1; - } - - if (colon_pos >= contents.len or contents[colon_pos] != ':') { - search_start = key_pos + 1; - continue; - } - - colon_pos += 1; - while (colon_pos < contents.len and (contents[colon_pos] == ' ' or contents[colon_pos] == '\t')) { - colon_pos += 1; - } - - if (colon_pos >= contents.len or contents[colon_pos] != '"') { - search_start = key_pos + 1; - continue; - } - - const value_start = colon_pos + 1; - - var value_end = value_start; - while (value_end < contents.len and contents[value_end] != '"') { - if (contents[value_end] == '\\' and value_end + 1 < contents.len) { - value_end += 2; - } else { - value_end += 1; - } - } - - if (value_end >= contents.len) { - search_start = key_pos + 1; - continue; - } - - const current_value = contents[value_start..value_end]; - if (strings.eql(current_value, old_version)) { - var result = std.ArrayList(u8).init(allocator); - try result.appendSlice(contents[0..value_start]); - try result.appendSlice(new_version); - try result.appendSlice(contents[value_end..]); - return result.toOwnedSlice(); - } - - search_start = value_end + 1; - } - - Output.errGeneric("Version not found in package.json", .{}); - Global.exit(1); - } - fn calculateNewVersion(allocator: std.mem.Allocator, current_str: []const u8, version_type: VersionType, specific_version: ?[]const u8, preid: []const u8, cwd: []const u8) bun.OOM![]const u8 { if (version_type == .specific) { return try allocator.dupe(u8, specific_version.?); @@ -487,12 +478,15 @@ pub const PmVersionCommand = struct { .windows = if (Environment.isWindows) .{ .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), }, - }) catch return false; + }) catch |err| { + Output.errGeneric("Failed to spawn git process: {s}", .{@errorName(err)}); + Global.exit(1); + }; switch (proc) { .err => |err| { Output.err(err, "Failed to spawn git process", .{}); - return false; + Global.exit(1); }, .result => |result| { return result.isOK() and result.stdout.items.len == 0; @@ -566,24 +560,27 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git add failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (stage_proc) { .err => |err| { Output.err(err, "Git add failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git add failed with exit code {d}", .{result.status.exited.code}); - return; + Global.exit(1); } }, } - const commit_message = custom_message orelse try std.fmt.allocPrint(allocator, "v{s}", .{version}); - defer if (custom_message == null) allocator.free(commit_message); + const commit_message = if (custom_message) |msg| + try std.mem.replaceOwned(u8, allocator, msg, "%s", version) + else + try std.fmt.allocPrint(allocator, "v{s}", .{version}); + defer allocator.free(commit_message); const commit_proc = bun.spawnSync(&.{ .argv = &.{ git_path, "commit", "-m", commit_message }, @@ -597,18 +594,18 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git commit failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (commit_proc) { .err => |err| { Output.err(err, "Git commit failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git commit failed", .{}); - return; + Global.exit(1); } }, } @@ -628,18 +625,18 @@ pub const PmVersionCommand = struct { }, }) catch |err| { Output.errGeneric("Git tag failed: {s}", .{@errorName(err)}); - return; + Global.exit(1); }; switch (tag_proc) { .err => |err| { Output.err(err, "Git tag failed unexpectedly", .{}); - return; + Global.exit(1); }, .result => |result| { if (!result.isOK()) { Output.errGeneric("Git tag failed", .{}); - return; + Global.exit(1); } }, } diff --git a/test/cli/install/bun-pm-version.test.ts b/test/cli/install/bun-pm-version.test.ts index 1734e8a0c3..6f37ad2a9e 100644 --- a/test/cli/install/bun-pm-version.test.ts +++ b/test/cli/install/bun-pm-version.test.ts @@ -107,681 +107,771 @@ describe("bun pm version", () => { return { output, error, code }; } - it("should show help when no arguments provided", async () => { - const testDir = setupTest(); + describe("help and version previews", () => { + it("should show help when no arguments provided", async () => { + const testDir = setupTest(); - const { output, code } = await runCommand([bunExe(), "pm", "version"], testDir); + const { output, code } = await runCommand([bunExe(), "pm", "version"], testDir); - expect(code).toBe(0); - expect(output).toContain("bun pm version"); - expect(output).toContain("Current package version: v1.0.0"); - expect(output).toContain("patch"); - expect(output).toContain("minor"); - expect(output).toContain("major"); + expect(code).toBe(0); + expect(output).toContain("bun pm version"); + expect(output).toContain("Current package version: v1.0.0"); + expect(output).toContain("patch"); + expect(output).toContain("minor"); + expect(output).toContain("major"); + }); + + it("shows help with version previews", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "2.5.3" }, null, 2), + }); + + const { output: output1, code: code1 } = await runCommand([bunExe(), "pm", "version"], testDir1); + + expect(code1).toBe(0); + expect(output1).toContain("Current package version: v2.5.3"); + expect(output1).toContain("patch 2.5.3 → 2.5.4"); + expect(output1).toContain("minor 2.5.3 → 2.6.0"); + expect(output1).toContain("major 2.5.3 → 3.0.0"); + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha.0" }, null, 2), + }); + + const { output: output2, code: code2 } = await runCommand([bunExe(), "pm", "version"], testDir2); + + expect(code2).toBe(0); + expect(output2).toContain("prepatch"); + expect(output2).toContain("preminor"); + expect(output2).toContain("premajor"); + expect(output2).toContain("1.0.1-alpha.0"); + expect(output2).toContain("1.1.0-alpha.0"); + expect(output2).toContain("2.0.0-alpha.0"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "--preid", "beta"], + testDir3, + ); + + expect(code3).toBe(0); + expect(output3).toContain("prepatch"); + expect(output3).toContain("preminor"); + expect(output3).toContain("premajor"); + expect(output3).toContain("1.0.1-beta.0"); + expect(output3).toContain("1.1.0-beta.0"); + expect(output3).toContain("2.0.0-beta.0"); + + const testDir4 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test" }, null, 2), + }); + + const { output: output4 } = await runCommand([bunExe(), "pm", "version"], testDir4); + + expect(output4).not.toContain("Current package version:"); + expect(output4).toContain("patch 1.0.0 → 1.0.1"); + }); }); - it("should increment versions correctly", async () => { - const testDir = setupTest(); + describe("basic version incrementing", () => { + it("should increment versions correctly", async () => { + const testDir = setupTest(); - const { output: patchOutput, code: patchCode } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir, - ); - expect(patchCode).toBe(0); - expect(patchOutput.trim()).toBe("v1.0.1"); + const { output: patchOutput, code: patchCode } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + ); + expect(patchCode).toBe(0); + expect(patchOutput.trim()).toBe("v1.0.1"); - const { output: minorOutput, code: minorCode } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - testDir, - ); - expect(minorCode).toBe(0); - expect(minorOutput.trim()).toBe("v1.1.0"); + const { output: minorOutput, code: minorCode } = await runCommand( + [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], + testDir, + ); + expect(minorCode).toBe(0); + expect(minorOutput.trim()).toBe("v1.1.0"); - const { output: majorOutput, code: majorCode } = await runCommand( - [bunExe(), "pm", "version", "major", "--no-git-tag-version"], - testDir, - ); - expect(majorCode).toBe(0); - expect(majorOutput.trim()).toBe("v2.0.0"); + const { output: majorOutput, code: majorCode } = await runCommand( + [bunExe(), "pm", "version", "major", "--no-git-tag-version"], + testDir, + ); + expect(majorCode).toBe(0); + expect(majorOutput.trim()).toBe("v2.0.0"); - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("2.0.0"); - }); - - it("should set specific version", async () => { - const testDir = setupTest(); - - const { output, code } = await runCommand([bunExe(), "pm", "version", "3.2.1", "--no-git-tag-version"], testDir); - - expect(code).toBe(0); - expect(output.trim()).toBe("v3.2.1"); - - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("3.2.1"); - }); - - it("handles various error conditions", async () => { - const testDir1 = setupTest(); - await Bun.write(`${testDir1}/package.json`, ""); - - const { error: error1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir1, - false, - ); - expect(code1).toBe(1); - expect(error1).toContain("No version field found in package.json"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "invalid-version" }, null, 2), + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("2.0.0"); }); - const { error: error2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir2, - false, - ); - expect(code2).toBe(1); - expect(error2).toContain("is not a valid semver"); + it("should set specific version", async () => { + const testDir = setupTest(); - const testDir3 = setupTest(); + const { output, code } = await runCommand([bunExe(), "pm", "version", "3.2.1", "--no-git-tag-version"], testDir); - const { error: error3, code: code3 } = await runCommand( - [bunExe(), "pm", "version", "invalid-arg", "--no-git-tag-version"], - testDir3, - false, - ); - expect(code3).toBe(1); - expect(error3).toContain("Invalid version argument"); + expect(code).toBe(0); + expect(output.trim()).toBe("v3.2.1"); - const testDir4 = setupTest(); - - const { error: error4, code: code4 } = await runCommand( - [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version"], - testDir4, - false, - ); - expect(code4).toBe(1); - expect(error4).toContain("Version not changed"); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version", "--allow-same-version"], - testDir4, - ); - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.0"); - }); - - it("handles git operations correctly", async () => { - const testDir1 = setupGitTest(); - - const { - output: output1, - code: code1, - error: stderr1, - } = await runCommand([bunExe(), "pm", "version", "patch"], testDir1); - - expect(stderr1.trim()).toBe(""); - expect(output1.trim()).toBe("v1.0.1"); - expect(code1).toBe(0); - - const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir1); - expect(tagOutput).toContain("v1.0.1"); - - const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir1); - expect(logOutput).toContain("v1.0.1"); - - const testDir2 = setupGitTest(); - - const { - output: output2, - error: error2, - code: code2, - } = await runCommand([bunExe(), "pm", "version", "patch", "--message", "Custom release message"], testDir2); - expect(error2).toBe(""); - - const { output: gitLogOutput } = await runCommand(["git", "log", "--oneline"], testDir2); - expect(gitLogOutput).toContain("Custom release message"); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v1.0.1"); - - const testDir3 = setupGitTest(); - - await Bun.write(join(testDir3, "untracked.txt"), "untracked content"); - - const { error: error3, code: code3 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir3, false); - - expect(code3).toBe(1); - expect(error3).toContain("Git working directory not clean"); - - const testDir4 = setupTest(); - - const { output: output4, code: code4 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir4); - - expect(code4).toBe(0); - expect(output4.trim()).toBe("v1.0.1"); - - const packageJson = await Bun.file(`${testDir4}/package.json`).json(); - expect(packageJson.version).toBe("1.0.1"); - - const testDir5 = setupGitTest(); - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.1"); - - const packageJson5 = await Bun.file(`${testDir5}/package.json`).json(); - expect(packageJson5.version).toBe("1.0.1"); - - const { output: tagOutput5 } = await runCommand(["git", "tag", "-l"], testDir5); - expect(tagOutput5.trim()).toBe(""); - - const { output: logOutput5 } = await runCommand(["git", "log", "--oneline"], testDir5); - expect(logOutput5).toContain("Initial commit"); - expect(logOutput5).not.toContain("v1.0.1"); - - const testDir6 = setupGitTest(); - const { output: output6, code: code6 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--git-tag-version=false"], - testDir6, - ); - - expect(code6).toBe(0); - expect(output6.trim()).toBe("v1.0.1"); - - const packageJson6 = await Bun.file(`${testDir6}/package.json`).json(); - expect(packageJson6.version).toBe("1.0.1"); - - const { output: tagOutput6 } = await runCommand(["git", "tag", "-l"], testDir6); - expect(tagOutput6.trim()).toBe(""); - - const { output: logOutput6 } = await runCommand(["git", "log", "--oneline"], testDir6); - expect(logOutput6).toContain("Initial commit"); - expect(logOutput6).not.toContain("v1.0.1"); - - const testDir7 = setupGitTest(); - const { output: output7, code: code7 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--git-tag-version=true"], - testDir7, - ); - - expect(code7).toBe(0); - expect(output7.trim()).toBe("v1.0.1"); - - const packageJson7 = await Bun.file(`${testDir7}/package.json`).json(); - expect(packageJson7.version).toBe("1.0.1"); - - const { output: tagOutput7 } = await runCommand(["git", "tag", "-l"], testDir7); - expect(tagOutput7).toContain("v1.0.1"); - - const { output: logOutput7 } = await runCommand(["git", "log", "--oneline"], testDir7); - expect(logOutput7).toContain("v1.0.1"); - }); - - it("preserves JSON formatting correctly", async () => { - const originalJson1 = `{ - "name": "test", - "version": "1.0.0", - "scripts": { - "test": "echo test" - }, - "dependencies": { - "lodash": "^4.17.21" - } -}`; - - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": originalJson1, + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("3.2.1"); }); - const { output: output1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - testDir1, - ); - - expect(code1).toBe(0); - expect(output1.trim()).toBe("v1.0.1"); - - const updatedJson1 = await Bun.file(`${testDir1}/package.json`).text(); - - expect(updatedJson1).toContain('"version": "1.0.1"'); - expect(updatedJson1).toContain('"name": "test"'); - expect(updatedJson1).toContain(' "test": "echo test"'); - - const originalJson2 = `{ - "name": "test-package", - "version" : "2.5.0" , - "description": "A test package with weird formatting", - "main": "index.js", - "scripts":{ - "test":"npm test", - "build" : "webpack" - }, -"keywords":["test","package"], - "author": "Test Author" -}`; - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": originalJson2, - }); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - testDir2, - ); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v2.6.0"); - - const updatedJson2 = await Bun.file(`${testDir2}/package.json`).text(); - - expect(updatedJson2).toContain('"version" : "2.6.0" ,'); - expect(updatedJson2).toContain('"name": "test-package"'); - expect(updatedJson2).toContain('"main": "index.js"'); - expect(updatedJson2).toContain('"scripts":{'); - expect(updatedJson2).toContain('"build" : "webpack"'); - - const originalLines1 = originalJson1.split("\n"); - const updatedLines1 = updatedJson1.split("\n"); - expect(updatedLines1.length).toBe(originalLines1.length); - - const originalLines2 = originalJson2.split("\n"); - const updatedLines2 = updatedJson2.split("\n"); - expect(updatedLines2.length).toBe(originalLines2.length); - }); - - it("shows help with version previews", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "2.5.3" }, null, 2), - }); - - const { output: output1, code: code1 } = await runCommand([bunExe(), "pm", "version"], testDir1); - - expect(code1).toBe(0); - expect(output1).toContain("Current package version: v2.5.3"); - expect(output1).toContain("patch 2.5.3 → 2.5.4"); - expect(output1).toContain("minor 2.5.3 → 2.6.0"); - expect(output1).toContain("major 2.5.3 → 3.0.0"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha.0" }, null, 2), - }); - - const { output: output2, code: code2 } = await runCommand([bunExe(), "pm", "version"], testDir2); - - expect(code2).toBe(0); - expect(output2).toContain("prepatch"); - expect(output2).toContain("preminor"); - expect(output2).toContain("premajor"); - expect(output2).toContain("1.0.1-alpha.0"); - expect(output2).toContain("1.1.0-alpha.0"); - expect(output2).toContain("2.0.0-alpha.0"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output3, code: code3 } = await runCommand([bunExe(), "pm", "version", "--preid", "beta"], testDir3); - - expect(code3).toBe(0); - expect(output3).toContain("prepatch"); - expect(output3).toContain("preminor"); - expect(output3).toContain("premajor"); - expect(output3).toContain("1.0.1-beta.0"); - expect(output3).toContain("1.1.0-beta.0"); - expect(output3).toContain("2.0.0-beta.0"); - - const testDir4 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test" }, null, 2), - }); - - const { output: output4 } = await runCommand([bunExe(), "pm", "version"], testDir4); - - expect(output4).not.toContain("Current package version:"); - expect(output4).toContain("patch 1.0.0 → 1.0.1"); - }); - - it("handles custom preid and prerelease scenarios", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output1, code: code1 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--preid", "beta", "--no-git-tag-version"], - testDir1, - ); - - expect(code1).toBe(0); - expect(output1.trim()).toBe("v1.0.1-beta.0"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - }); - - const { output: output3, code: code3 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir3, - ); - - expect(code3).toBe(0); - expect(output3.trim()).toBe("v1.0.1-0"); - - const testDir5 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha" }, null, 2), - }); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.0-alpha.1"); - - const testDir6 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0-3" }, null, 2), - }); - - const { output: output6, code: code6 } = await runCommand( - [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], - testDir6, - ); - - expect(code6).toBe(0); - expect(output6.trim()).toBe("v1.0.0-4"); - }); - - it("runs lifecycle scripts in correct order and handles failures", async () => { - const testDir1 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo 'step1' >> lifecycle.log", - version: "echo 'step2' >> lifecycle.log", - postversion: "echo 'step3' >> lifecycle.log", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir1, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(await Bun.file(join(testDir1, "lifecycle.log")).exists()).toBe(true); - const logContent = await Bun.file(join(testDir1, "lifecycle.log")).text(); - expect(logContent.trim()).toBe("step1\nstep2\nstep3"); - - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo $npm_lifecycle_event > event.log && echo $npm_lifecycle_script > script.log", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir2, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); - - const eventContent = await Bun.file(join(testDir2, "event.log")).text(); - const scriptContent = await Bun.file(join(testDir2, "script.log")).text(); - - expect(eventContent.trim()).toBe("preversion"); - expect(scriptContent.trim()).toContain("echo $npm_lifecycle_event"); - - const testDir3 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "exit 1", - }, - }, - null, - 2, - ), - }); - - const proc = Bun.spawn([bunExe(), "pm", "version", "minor", "--no-git-tag-version"], { - cwd: testDir3, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }); - - await proc.exited; - expect(proc.exitCode).toBe(1); - - const packageJson = await Bun.file(join(testDir3, "package.json")).json(); - expect(packageJson.version).toBe("1.0.0"); - - const testDir4 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "mkdir -p build && echo 'built' > build/output.txt", - version: "cp build/output.txt version-output.txt", - postversion: "rm -rf build", - }, - }, - null, - 2, - ), - }); - - await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { - cwd: testDir4, - env: bunEnv, - stderr: "ignore", - stdout: "ignore", - }).exited; - - expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); - expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); - - const content = await Bun.file(join(testDir4, "version-output.txt")).text(); - expect(content.trim()).toBe("built"); - - const testDir5 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify( - { - name: "test", - version: "1.0.0", - scripts: { - preversion: "echo 'should not run' >> ignored.log", - version: "echo 'should not run' >> ignored.log", - postversion: "echo 'should not run' >> ignored.log", - }, - }, - null, - 2, - ), - }); - - const { output: output5, code: code5 } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version", "--ignore-scripts"], - testDir5, - ); - - expect(code5).toBe(0); - expect(output5.trim()).toBe("v1.0.1"); - - const packageJson5 = await Bun.file(join(testDir5, "package.json")).json(); - expect(packageJson5.version).toBe("1.0.1"); - - expect(await Bun.file(join(testDir5, "ignored.log")).exists()).toBe(false); - }); - - it("should version workspace packages individually", async () => { - const testDir = setupMonorepoTest(); - - const { output: outputA, code: codeA } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - join(testDir, "packages", "pkg-a"), - ); - - expect(codeA).toBe(0); - expect(outputA.trim()).toBe("v2.0.1"); - - const rootPackageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(rootPackageJson.version).toBe("1.0.0"); - - const pkgAJson = await Bun.file(`${testDir}/packages/pkg-a/package.json`).json(); - const pkgBJson = await Bun.file(`${testDir}/packages/pkg-b/package.json`).json(); - - expect(pkgAJson.version).toBe("2.0.1"); - expect(pkgBJson.version).toBe("3.0.0"); - }); - - it("should work from subdirectories", async () => { - const testDir = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), - "src/index.js": "console.log('hello');", - }); - - const { output, code } = await runCommand( - [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], - join(testDir, "src"), - ); - - expect(code).toBe(0); - expect(output.trim()).toBe("v1.0.1"); - - const packageJson = await Bun.file(`${testDir}/package.json`).json(); - expect(packageJson.version).toBe("1.0.1"); - - const monorepoDir = setupMonorepoTest(); - - await Bun.write(join(monorepoDir, "packages", "pkg-a", "lib", "index.js"), ""); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], - join(monorepoDir, "packages", "pkg-a", "lib"), - ); - - expect(code2).toBe(0); - expect(output2.trim()).toBe("v2.1.0"); - - const rootJson = await Bun.file(`${monorepoDir}/package.json`).json(); - const pkgAJson = await Bun.file(`${monorepoDir}/packages/pkg-a/package.json`).json(); - const pkgBJson = await Bun.file(`${monorepoDir}/packages/pkg-b/package.json`).json(); - - expect(rootJson.version).toBe("1.0.0"); - expect(pkgAJson.version).toBe("2.1.0"); - expect(pkgBJson.version).toBe("3.0.0"); - }); - - it("should preserve prerelease identifiers correctly", async () => { - const scenarios = [ - { - version: "1.0.3-alpha.1", - preid: "beta", - expected: { - patch: "1.0.3-alpha.1 → 1.0.4", - minor: "1.0.3-alpha.1 → 1.1.0", - major: "1.0.3-alpha.1 → 2.0.0", - prerelease: "1.0.3-alpha.1 → 1.0.3-beta.2", - prepatch: "1.0.3-alpha.1 → 1.0.4-beta.0", - preminor: "1.0.3-alpha.1 → 1.1.0-beta.0", - premajor: "1.0.3-alpha.1 → 2.0.0-beta.0", - }, - }, - { - version: "1.0.3-1", - preid: "abcd", - expected: { - patch: "1.0.3-1 → 1.0.4", - minor: "1.0.3-1 → 1.1.0", - major: "1.0.3-1 → 2.0.0", - prerelease: "1.0.3-1 → 1.0.3-abcd.2", - prepatch: "1.0.3-1 → 1.0.4-abcd.0", - preminor: "1.0.3-1 → 1.1.0-abcd.0", - premajor: "1.0.3-1 → 2.0.0-abcd.0", - }, - }, - { - version: "2.5.0-rc.3", - preid: "next", - expected: { - patch: "2.5.0-rc.3 → 2.5.1", - minor: "2.5.0-rc.3 → 2.6.0", - major: "2.5.0-rc.3 → 3.0.0", - prerelease: "2.5.0-rc.3 → 2.5.0-next.4", - prepatch: "2.5.0-rc.3 → 2.5.1-next.0", - preminor: "2.5.0-rc.3 → 2.6.0-next.0", - premajor: "2.5.0-rc.3 → 3.0.0-next.0", - }, - }, - { - version: "1.0.0-a", - preid: "b", - expected: { - patch: "1.0.0-a → 1.0.1", - minor: "1.0.0-a → 1.1.0", - major: "1.0.0-a → 2.0.0", - prerelease: "1.0.0-a → 1.0.0-b.1", - prepatch: "1.0.0-a → 1.0.1-b.0", - preminor: "1.0.0-a → 1.1.0-b.0", - premajor: "1.0.0-a → 2.0.0-b.0", - }, - }, - ]; - - for (const scenario of scenarios) { + it("handles empty package.json", async () => { const testDir = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: scenario.version }, null, 2), + "package.json": "{}", + }); + + const { output, code } = await runCommand([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], testDir); + + expect(code).toBe(0); + expect(output.trim()).toBe("v0.0.1"); + + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("0.0.1"); + }); + }); + + describe("error handling", () => { + it("handles various error conditions", async () => { + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "invalid-version" }, null, 2), + }); + + const { error: error2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir2, + false, + ); + expect(error2).toContain("is not a valid semver"); + expect(code2).toBe(1); + + const testDir3 = setupTest(); + + const { error: error3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "invalid-arg", "--no-git-tag-version"], + testDir3, + false, + ); + expect(error3).toContain("Invalid version argument"); + expect(code3).toBe(1); + + const testDir4 = setupTest(); + + const { error: error4, code: code4 } = await runCommand( + [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version"], + testDir4, + false, + ); + expect(error4).toContain("Version not changed"); + expect(code4).toBe(1); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "1.0.0", "--no-git-tag-version", "--allow-same-version"], + testDir4, + ); + expect(output5.trim()).toBe("v1.0.0"); + expect(code5).toBe(0); + }); + + it("handles missing package.json like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "README.md": "# Test project", + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("package.json"); + expect(code).toBe(1); + // its an ealier check that "bun pm *" commands do so not "bun pm version" specific + // expect(error.includes("ENOENT") || error.includes("no such file")).toBe(true); + }); + + it("handles empty string package.json like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": '""', + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("Failed to parse package.json"); + expect(code).toBe(1); + }); + + it("handles malformed JSON like npm", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": '{ "name": "test", invalid json }', + }); + + const { error, code } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir, + false, + ); + expect(error).toContain("Failed to parse package.json"); + expect(code).toBe(1); + }); + }); + + describe("git integration", () => { + it("creates git commits and tags by default", async () => { + const testDir1 = setupGitTest(); + + const { + output: output1, + code: code1, + error: stderr1, + } = await runCommand([bunExe(), "pm", "version", "patch"], testDir1); + + expect(stderr1.trim()).toBe(""); + expect(output1.trim()).toBe("v1.0.1"); + expect(code1).toBe(0); + + const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir1); + expect(tagOutput).toContain("v1.0.1"); + + const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir1); + expect(logOutput).toContain("v1.0.1"); + }); + + it("supports custom commit messages", async () => { + const testDir2 = setupGitTest(); + + const { + output: output2, + error: error2, + code: code2, + } = await runCommand([bunExe(), "pm", "version", "patch", "--message", "Custom release message"], testDir2); + expect(error2).toBe(""); + + const { output: gitLogOutput } = await runCommand(["git", "log", "--oneline"], testDir2); + expect(gitLogOutput).toContain("Custom release message"); + + expect(code2).toBe(0); + expect(output2.trim()).toBe("v1.0.1"); + }); + + it("fails when git working directory is not clean", async () => { + const testDir3 = setupGitTest(); + + await Bun.write(join(testDir3, "untracked.txt"), "untracked content"); + + const { error: error3, code: code3 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir3, false); + + expect(error3).toContain("Git working directory not clean"); + expect(code3).toBe(1); + }); + + it("allows dirty working directory with --force flag", async () => { + const testDir = setupGitTest(); + + await Bun.write(join(testDir, "untracked.txt"), "untracked content"); + + const { output, code, error } = await runCommand([bunExe(), "pm", "version", "patch", "--force"], testDir); + + expect(code).toBe(0); + expect(error.trim()).toBe(""); + expect(output.trim()).toBe("v1.0.1"); + + const { output: tagOutput } = await runCommand(["git", "tag", "-l"], testDir); + expect(tagOutput).toContain("v1.0.1"); + + const { output: logOutput } = await runCommand(["git", "log", "--oneline"], testDir); + expect(logOutput).toContain("v1.0.1"); + }); + + it("works without git when no repo is present", async () => { + const testDir4 = setupTest(); + + const { output: output4, code: code4 } = await runCommand([bunExe(), "pm", "version", "patch"], testDir4); + + expect(code4).toBe(0); + expect(output4.trim()).toBe("v1.0.1"); + + const packageJson = await Bun.file(`${testDir4}/package.json`).json(); + expect(packageJson.version).toBe("1.0.1"); + }); + + it("respects --no-git-tag-version flag", async () => { + const testDir5 = setupGitTest(); + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.1"); + + const packageJson5 = await Bun.file(`${testDir5}/package.json`).json(); + expect(packageJson5.version).toBe("1.0.1"); + + const { output: tagOutput5 } = await runCommand(["git", "tag", "-l"], testDir5); + expect(tagOutput5.trim()).toBe(""); + + const { output: logOutput5 } = await runCommand(["git", "log", "--oneline"], testDir5); + expect(logOutput5).toContain("Initial commit"); + expect(logOutput5).not.toContain("v1.0.1"); + }); + + it("respects --git-tag-version=false flag", async () => { + const testDir6 = setupGitTest(); + const { output: output6, code: code6 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--git-tag-version=false"], + testDir6, + ); + + expect(code6).toBe(0); + expect(output6.trim()).toBe("v1.0.1"); + + const packageJson6 = await Bun.file(`${testDir6}/package.json`).json(); + expect(packageJson6.version).toBe("1.0.1"); + + const { output: tagOutput6 } = await runCommand(["git", "tag", "-l"], testDir6); + expect(tagOutput6.trim()).toBe(""); + + const { output: logOutput6 } = await runCommand(["git", "log", "--oneline"], testDir6); + expect(logOutput6).toContain("Initial commit"); + expect(logOutput6).not.toContain("v1.0.1"); + }); + + it("respects --git-tag-version=true flag", async () => { + const testDir7 = setupGitTest(); + const { output: output7, code: code7 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--git-tag-version=true"], + testDir7, + ); + + expect(code7).toBe(0); + expect(output7.trim()).toBe("v1.0.1"); + + const packageJson7 = await Bun.file(`${testDir7}/package.json`).json(); + expect(packageJson7.version).toBe("1.0.1"); + + const { output: tagOutput7 } = await runCommand(["git", "tag", "-l"], testDir7); + expect(tagOutput7).toContain("v1.0.1"); + + const { output: logOutput7 } = await runCommand(["git", "log", "--oneline"], testDir7); + expect(logOutput7).toContain("v1.0.1"); + }); + + it("supports %s substitution in commit messages", async () => { + const testDir8 = setupGitTest(); + const { output: output8, code: code8 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--message", "Bump version to %s"], + testDir8, + ); + + expect(code8).toBe(0); + expect(output8.trim()).toBe("v1.0.1"); + + const { output: logOutput8 } = await runCommand(["git", "log", "--oneline", "-1"], testDir8); + expect(logOutput8).toContain("Bump version to 1.0.1"); + + const testDir9 = setupGitTest(); + const { output: output9, code: code9 } = await runCommand( + [bunExe(), "pm", "version", "2.5.0", "-m", "Release %s with fixes"], + testDir9, + ); + + expect(code9).toBe(0); + expect(output9.trim()).toBe("v2.5.0"); + + const { output: logOutput9 } = await runCommand(["git", "log", "--oneline", "-1"], testDir9); + expect(logOutput9).toContain("Release 2.5.0 with fixes"); + }); + }); + + describe("JSON formatting preservation", () => { + it("preserves JSON formatting correctly", async () => { + const originalJson1 = `{ + "name": "test", + "version": "1.0.0", + "scripts": { + "test": "echo test" + }, + "dependencies": { + "lodash": "^4.17.21" + } + }`; + + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": originalJson1, + }); + + const { output: output1, code: code1 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + testDir1, + ); + + expect(code1).toBe(0); + expect(output1.trim()).toBe("v1.0.1"); + + const updatedJson1 = await Bun.file(`${testDir1}/package.json`).text(); + + expect(updatedJson1).toContain(' "version": "1.0.1"'); + expect(updatedJson1).toContain('"name": "test"'); + expect(updatedJson1).toContain(' "test": "echo test"'); + + expect(JSON.parse(updatedJson1)).toMatchObject({ + name: "test", + version: "1.0.1", + scripts: { + test: "echo test", + }, + }); + }); + }); + + describe("prerelease handling", () => { + it("handles custom preid and prerelease scenarios", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output1, code: code1 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--preid", "beta", "--no-git-tag-version"], + testDir1, + ); + + expect(code1).toBe(0); + expect(output1.trim()).toBe("v1.0.1-beta.0"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + }); + + const { output: output3, code: code3 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir3, + ); + + expect(code3).toBe(0); + expect(output3.trim()).toBe("v1.0.1-0"); + + const testDir5 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-alpha" }, null, 2), + }); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.0-alpha.1"); + + const testDir6 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0-3" }, null, 2), + }); + + const { output: output6, code: code6 } = await runCommand( + [bunExe(), "pm", "version", "prerelease", "--no-git-tag-version"], + testDir6, + ); + + expect(code6).toBe(0); + expect(output6.trim()).toBe("v1.0.0-4"); + }); + + it("should preserve prerelease identifiers correctly", async () => { + const scenarios = [ + { + version: "1.0.3-alpha.1", + preid: "beta", + expected: { + patch: "1.0.3-alpha.1 → 1.0.4", + minor: "1.0.3-alpha.1 → 1.1.0", + major: "1.0.3-alpha.1 → 2.0.0", + prerelease: "1.0.3-alpha.1 → 1.0.3-beta.2", + prepatch: "1.0.3-alpha.1 → 1.0.4-beta.0", + preminor: "1.0.3-alpha.1 → 1.1.0-beta.0", + premajor: "1.0.3-alpha.1 → 2.0.0-beta.0", + }, + }, + { + version: "1.0.3-1", + preid: "abcd", + expected: { + patch: "1.0.3-1 → 1.0.4", + minor: "1.0.3-1 → 1.1.0", + major: "1.0.3-1 → 2.0.0", + prerelease: "1.0.3-1 → 1.0.3-abcd.2", + prepatch: "1.0.3-1 → 1.0.4-abcd.0", + preminor: "1.0.3-1 → 1.1.0-abcd.0", + premajor: "1.0.3-1 → 2.0.0-abcd.0", + }, + }, + { + version: "2.5.0-rc.3", + preid: "next", + expected: { + patch: "2.5.0-rc.3 → 2.5.1", + minor: "2.5.0-rc.3 → 2.6.0", + major: "2.5.0-rc.3 → 3.0.0", + prerelease: "2.5.0-rc.3 → 2.5.0-next.4", + prepatch: "2.5.0-rc.3 → 2.5.1-next.0", + preminor: "2.5.0-rc.3 → 2.6.0-next.0", + premajor: "2.5.0-rc.3 → 3.0.0-next.0", + }, + }, + { + version: "1.0.0-a", + preid: "b", + expected: { + patch: "1.0.0-a → 1.0.1", + minor: "1.0.0-a → 1.1.0", + major: "1.0.0-a → 2.0.0", + prerelease: "1.0.0-a → 1.0.0-b.1", + prepatch: "1.0.0-a → 1.0.1-b.0", + preminor: "1.0.0-a → 1.1.0-b.0", + premajor: "1.0.0-a → 2.0.0-b.0", + }, + }, + ]; + + for (const scenario of scenarios) { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: scenario.version }, null, 2), + }); + + const { output, code } = await runCommand( + [bunExe(), "pm", "version", "--no-git-tag-version", `--preid=${scenario.preid}`], + testDir, + ); + + expect(code).toBe(0); + expect(output).toContain(`Current package version: v${scenario.version}`); + + for (const [incrementType, expectedTransformation] of Object.entries(scenario.expected)) { + expect(output).toContain(`${incrementType.padEnd(10)} ${expectedTransformation}`); + } + } + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.3-alpha.1" }, null, 2), + }); + + const { output: output2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "--no-git-tag-version"], + testDir2, + ); + + expect(code2).toBe(0); + expect(output2).toContain("prerelease 1.0.3-alpha.1 → 1.0.3-alpha.2"); + }); + }); + + describe("lifecycle scripts", () => { + it("runs lifecycle scripts in correct order and handles failures", async () => { + const testDir1 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo 'step1' >> lifecycle.log", + version: "echo 'step2' >> lifecycle.log", + postversion: "echo 'step3' >> lifecycle.log", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir1, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(await Bun.file(join(testDir1, "lifecycle.log")).exists()).toBe(true); + const logContent = await Bun.file(join(testDir1, "lifecycle.log")).text(); + expect(logContent.trim()).toBe("step1\nstep2\nstep3"); + + const testDir2 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo $npm_lifecycle_event > event.log && echo $npm_lifecycle_script > script.log", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir2, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(Bun.file(join(testDir2, "event.log")).exists()).resolves.toBe(true); + expect(Bun.file(join(testDir2, "script.log")).exists()).resolves.toBe(true); + + const eventContent = await Bun.file(join(testDir2, "event.log")).text(); + const scriptContent = await Bun.file(join(testDir2, "script.log")).text(); + + expect(eventContent.trim()).toBe("preversion"); + expect(scriptContent.trim()).toContain("echo $npm_lifecycle_event"); + + const testDir3 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "exit 1", + }, + }, + null, + 2, + ), + }); + + const proc = Bun.spawn([bunExe(), "pm", "version", "minor", "--no-git-tag-version"], { + cwd: testDir3, + env: bunEnv, + stderr: "pipe", + stdout: "ignore", + }); + + await proc.exited; + expect(proc.exitCode).toBe(1); + expect(await proc.stderr.text()).toContain('script "preversion" exited with code 1'); + + const packageJson = await Bun.file(join(testDir3, "package.json")).json(); + expect(packageJson.version).toBe("1.0.0"); + + const testDir4 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "mkdir -p build && echo 'built' > build/output.txt", + version: "cp build/output.txt version-output.txt", + postversion: "rm -rf build", + }, + }, + null, + 2, + ), + }); + + await Bun.spawn([bunExe(), "pm", "version", "patch", "--no-git-tag-version"], { + cwd: testDir4, + env: bunEnv, + stderr: "ignore", + stdout: "ignore", + }).exited; + + expect(Bun.file(join(testDir4, "version-output.txt")).exists()).resolves.toBe(true); + expect(Bun.file(join(testDir4, "build")).exists()).resolves.toBe(false); + + const content = await Bun.file(join(testDir4, "version-output.txt")).text(); + expect(content.trim()).toBe("built"); + + const testDir5 = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify( + { + name: "test", + version: "1.0.0", + scripts: { + preversion: "echo 'should not run' >> ignored.log", + version: "echo 'should not run' >> ignored.log", + postversion: "echo 'should not run' >> ignored.log", + }, + }, + null, + 2, + ), + }); + + const { output: output5, code: code5 } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version", "--ignore-scripts"], + testDir5, + ); + + expect(code5).toBe(0); + expect(output5.trim()).toBe("v1.0.1"); + + const packageJson5 = await Bun.file(join(testDir5, "package.json")).json(); + expect(packageJson5.version).toBe("1.0.1"); + + expect(await Bun.file(join(testDir5, "ignored.log")).exists()).toBe(false); + }); + }); + + describe("workspace and directory handling", () => { + it("should version workspace packages individually", async () => { + const testDir = setupMonorepoTest(); + + const { output: outputA, code: codeA } = await runCommand( + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + join(testDir, "packages", "pkg-a"), + ); + + expect(codeA).toBe(0); + expect(outputA.trim()).toBe("v2.0.1"); + + const rootPackageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(rootPackageJson.version).toBe("1.0.0"); + + const pkgAJson = await Bun.file(`${testDir}/packages/pkg-a/package.json`).json(); + const pkgBJson = await Bun.file(`${testDir}/packages/pkg-b/package.json`).json(); + + expect(pkgAJson.version).toBe("2.0.1"); + expect(pkgBJson.version).toBe("3.0.0"); + }); + + it("should work from subdirectories", async () => { + const testDir = tempDirWithFiles(`version-${i++}`, { + "package.json": JSON.stringify({ name: "test", version: "1.0.0" }, null, 2), + "src/index.js": "console.log('hello');", }); const { output, code } = await runCommand( - [bunExe(), "pm", "version", "--no-git-tag-version", `--preid=${scenario.preid}`], - testDir, + [bunExe(), "pm", "version", "patch", "--no-git-tag-version"], + join(testDir, "src"), ); expect(code).toBe(0); - expect(output).toContain(`Current package version: v${scenario.version}`); + expect(output.trim()).toBe("v1.0.1"); - for (const [incrementType, expectedTransformation] of Object.entries(scenario.expected)) { - expect(output).toContain(`${incrementType.padEnd(10)} ${expectedTransformation}`); - } - } + const packageJson = await Bun.file(`${testDir}/package.json`).json(); + expect(packageJson.version).toBe("1.0.1"); - const testDir2 = tempDirWithFiles(`version-${i++}`, { - "package.json": JSON.stringify({ name: "test", version: "1.0.3-alpha.1" }, null, 2), + const monorepoDir = setupMonorepoTest(); + + await Bun.write(join(monorepoDir, "packages", "pkg-a", "lib", "index.js"), ""); + + const { output: output2, code: code2 } = await runCommand( + [bunExe(), "pm", "version", "minor", "--no-git-tag-version"], + join(monorepoDir, "packages", "pkg-a", "lib"), + ); + + expect(code2).toBe(0); + expect(output2.trim()).toBe("v2.1.0"); + + const rootJson = await Bun.file(`${monorepoDir}/package.json`).json(); + const pkgAJson = await Bun.file(`${monorepoDir}/packages/pkg-a/package.json`).json(); + const pkgBJson = await Bun.file(`${monorepoDir}/packages/pkg-b/package.json`).json(); + + expect(rootJson.version).toBe("1.0.0"); + expect(pkgAJson.version).toBe("2.1.0"); + expect(pkgBJson.version).toBe("3.0.0"); }); - - const { output: output2, code: code2 } = await runCommand( - [bunExe(), "pm", "version", "--no-git-tag-version"], - testDir2, - ); - - expect(code2).toBe(0); - expect(output2).toContain("prerelease 1.0.3-alpha.1 → 1.0.3-alpha.2"); }); });