From eb75b029b3227005aa1e01f351973610bb541162 Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Fri, 12 Dec 2025 13:40:52 -0500 Subject: [PATCH 1/2] feat: Officially support bun and add it to CI --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++- .gitignore | 1 + README.md | 4 ++-- test/electron/package.json | 2 +- test/napi/lib/functions.js | 42 +++++++++++++++++++++++++----------- test/napi/lib/threads.js | 11 ++++++---- test/napi/lib/typedarrays.js | 25 +++++++++++++++++++++ test/napi/lib/workers.js | 15 ++++++++----- test/napi/package.json | 5 +++-- 9 files changed, 109 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d92df27b6..97294ada0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: echo "rust_toolchain=$PARTIAL_RUST_TOOLCHAINS" >> $GITHUB_OUTPUT fi - build: + node: needs: matrix runs-on: ${{ matrix.os }} strategy: @@ -120,3 +120,33 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} slug: neon-bindings/neon files: target/codecov.json + + bun: + needs: matrix + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust-toolchain: ${{fromJson(needs.matrix.outputs.rust_toolchain)}} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Use Rust ${{ matrix.rust-toolchain }} + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: convert lockfile + run: bun install --lockfile-only + + - name: bun install + run: bun ci + + - name: test + working-directory: ./test/napi + run: bun run test:bun diff --git a/.gitignore b/.gitignore index 43948c23d..612cc3618 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ wip # Node **/node_modules npm-debug.log +bun.lock # JS build **/build diff --git a/README.md b/README.md index f03344733..a2c76cfd6 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ using a different version of Node and believe it should be supported, let us kno Older Node version support (minimum v10) may require lower Node-API versions. See the Node [version support matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix) for more details. -### Bun (experimental) +### Bun -[Bun](https://bun.sh/) is an alternate JavaScript runtime that targets Node compatibility. In many cases Neon modules will work in bun; however, at the time of this writing, some Node-API functions are [not implemented](https://github.com/oven-sh/bun/issues/158). +[Bun](https://bun.sh/) is an alternate JavaScript runtime that targets Node compatibility. Bun is supported and tested in CI. For details on compatibility see the [Bun support tracking issue](https://github.com/neon-bindings/neon/issues/1128). ### Rust diff --git a/test/electron/package.json b/test/electron/package.json index b10d3c7d2..d2fd8e943 100644 --- a/test/electron/package.json +++ b/test/electron/package.json @@ -6,7 +6,7 @@ "author": "The Neon Community", "license": "MIT", "scripts": { - "install": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "install": "cargo-cp-artifact -ac electron-tests index.node -- cargo build --message-format=json-render-diagnostics", "start": "electron .", "test": "playwright test" }, diff --git a/test/napi/lib/functions.js b/test/napi/lib/functions.js index 5012b74fb..2a0d213fc 100644 --- a/test/napi/lib/functions.js +++ b/test/napi/lib/functions.js @@ -104,6 +104,11 @@ describe("JsFunction", function () { }); it("bind a strict JsFunction to a number", function () { + // https://github.com/neon-bindings/neon/issues/1128#usestrict + if (process.versions.bun) { + return this.skip(); + } + assert.isTrue(isStrict(STRICT)); // strict mode functions are allowed to have a primitive this binding @@ -154,6 +159,11 @@ describe("JsFunction", function () { }); it("call a JsFunction with the default this", function () { + // https://github.com/neon-bindings/neon/issues/1128#usestrict + if (process.versions.bun) { + return this.skip(); + } + addon.call_js_function_with_implicit_this(function () { "use strict"; // ensure the undefined this isn't replaced with the global object assert.strictEqual(this, undefined); @@ -161,6 +171,11 @@ describe("JsFunction", function () { }); it("exec a JsFunction with the default this", function () { + // https://github.com/neon-bindings/neon/issues/1128#usestrict + if (process.versions.bun) { + return this.skip(); + } + addon.exec_js_function_with_implicit_this(function () { "use strict"; // ensure the undefined this isn't replaced with the global object assert.strictEqual(this, undefined); @@ -349,19 +364,20 @@ describe("JsFunction", function () { assert.strictEqual(addon.count_called() + 1, addon.count_called()); }); - (global.gc ? it : it.skip)( - "should drop function when going out of scope", - function (cb) { - // Run from an `IIFE` to ensure that `f` is out of scope and eligible for garbage - // collection when `global.gc()` is executed. - (() => { - const msg = "Hello, World!"; - const f = addon.caller_with_drop_callback(() => msg, cb); + it("should drop function when going out of scope", function (cb) { + if (!global.gc) { + return this.skip(); + } - assert.strictEqual(f(), msg); - })(); + // Run from an `IIFE` to ensure that `f` is out of scope and eligible for garbage + // collection when `global.gc()` is executed. + (() => { + const msg = "Hello, World!"; + const f = addon.caller_with_drop_callback(() => msg, cb); - global.gc(); - } - ); + assert.strictEqual(f(), msg); + })(); + + global.gc(); + }); }); diff --git a/test/napi/lib/threads.js b/test/napi/lib/threads.js index b575fa8b4..5c9177fb5 100644 --- a/test/napi/lib/threads.js +++ b/test/napi/lib/threads.js @@ -1,10 +1,13 @@ const addon = require(".."); const assert = require("chai").assert; -(function () { - // These tests require GC exposed to shutdown properly; skip if it is not - return typeof global.gc === "function" ? describe : describe.skip; -})()("sync", function () { +describe("sync", function () { + before(function () { + if (!global.gc) { + this.skip(); + } + }); + let unhandledRejectionListeners = []; beforeEach(() => { diff --git a/test/napi/lib/typedarrays.js b/test/napi/lib/typedarrays.js index ca38f0985..fc722cadd 100644 --- a/test/napi/lib/typedarrays.js +++ b/test/napi/lib/typedarrays.js @@ -314,6 +314,11 @@ describe("Typed arrays", function () { }); it("correctly constructs a view over a slice of a buffer", function () { + // https://github.com/neon-bindings/neon/issues/1128#byteoffset + if (process.versions.bun) { + return this.skip(); + } + var buf = new ArrayBuffer(128); var a = addon.return_uint32array_from_arraybuffer_region(buf, 16, 4); @@ -457,6 +462,11 @@ describe("Typed arrays", function () { } it("provides correct metadata when detaching a typed array's buffer", function () { + // https://github.com/neon-bindings/neon/issues/1128#detach + if (process.versions.bun) { + return this.skip(); + } + var buf = new ArrayBuffer(16); var arr = new Uint32Array(buf, 4, 2); var buf = arr.buffer; @@ -492,16 +502,31 @@ describe("Typed arrays", function () { }); it("provides correct metadata when detaching an escaped typed array's buffer", function () { + // https://github.com/neon-bindings/neon/issues/1128#detach + if (process.versions.bun) { + return this.skip(); + } + var buf = new ArrayBuffer(16); testDetach(new Uint32Array(buf, 4, 2), addon.detach_and_escape, 8, 2, 4); }); it("provides correct metadata when detaching a casted typed array's buffer", function () { + // https://github.com/neon-bindings/neon/issues/1128#detach + if (process.versions.bun) { + return this.skip(); + } + var buf = new ArrayBuffer(16); testDetach(new Uint32Array(buf, 4, 2), addon.detach_and_cast, 8, 2, 4); }); it("provides correct metadata when detaching an un-rooted typed array's buffer", function () { + // https://github.com/neon-bindings/neon/issues/1128#detach + if (process.versions.bun) { + return this.skip(); + } + var buf = new ArrayBuffer(16); testDetach(new Uint32Array(buf, 4, 2), addon.detach_and_unroot, 8, 2, 4); }); diff --git a/test/napi/lib/workers.js b/test/napi/lib/workers.js index 8da128f9d..14bf37e0a 100644 --- a/test/napi/lib/workers.js +++ b/test/napi/lib/workers.js @@ -105,7 +105,7 @@ describe("Worker / Root Tagging Tests", () => { describe("Multi-Threaded", () => { it("should fail to use `get_and_replace`", (cb) => { const worker = new Worker(__filename); - after(() => worker.terminate()); + worker.unref(); worker.once("message", (message) => { assert.ok(/wrong module/.test(message)); @@ -117,7 +117,7 @@ describe("Worker / Root Tagging Tests", () => { it("should fail to use `get_or_init`", (cb) => { const worker = new Worker(__filename); - after(() => worker.terminate()); + worker.unref(); worker.once("message", (message) => { assert.ok(/wrong module/.test(message)); @@ -129,7 +129,7 @@ describe("Worker / Root Tagging Tests", () => { it("should fail to use `get_or_init`", (cb) => { const worker = new Worker(__filename); - after(() => worker.terminate()); + worker.unref(); worker.once("message", (message) => { assert.ok(/wrong module/.test(message)); @@ -192,7 +192,7 @@ describe("Instance-local storage", () => { assert(!Number.isNaN(mainThreadId)); const worker = new Worker(__filename); - after(() => worker.terminate()); + worker.unref(); worker.once("message", (message) => { assert.strictEqual(typeof message, "number"); @@ -206,7 +206,12 @@ describe("Instance-local storage", () => { worker.postMessage("get_thread_id"); }); - it("should be able to exit a worker without a crash", (cb) => { + it("should be able to exit a worker without a crash", function (cb) { + // https://github.com/neon-bindings/neon/issues/1128#terminate + if (process.versions.bun) { + return this.skip(); + } + const worker = new Worker(__filename, { workerData: "notify_when_startup_complete", }); diff --git a/test/napi/package.json b/test/napi/package.json index af42ecb82..6a7958cf8 100644 --- a/test/napi/package.json +++ b/test/napi/package.json @@ -5,9 +5,10 @@ "author": "The Neon Community", "license": "MIT", "scripts": { - "install": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "install": "cargo-cp-artifact -ac napi-tests index.node -- cargo build --message-format=json-render-diagnostics", "mocha": "mocha", - "test": "mocha --v8-expose-gc --timeout 5000 --recursive lib" + "test": "mocha --expose-gc --timeout 5000 --recursive lib", + "test:bun": "bun ../../node_modules/mocha/bin/mocha.js --v8-expose-gc --timeout 5000 --recursive lib" }, "devDependencies": { "cargo-cp-artifact": "^0.1.9", From b6bfdcb9ee3ef49e4c03a46d56f6a7f82e9f2074 Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Mon, 9 Mar 2026 12:21:17 -0400 Subject: [PATCH 2/2] Fix class tests on bun --- test/napi/lib/class.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/napi/lib/class.js b/test/napi/lib/class.js index ecbfda8dc..64865fab5 100644 --- a/test/napi/lib/class.js +++ b/test/napi/lib/class.js @@ -50,6 +50,8 @@ describe("classes", function () { const message = new Message("test"); const StringBuffer = addon.StringBuffer; const buffer = new StringBuffer(); + const normalizeFn = (f) => + f.toString().replace(/\{\s+(\[native code\])\s+\}/, "{ $1 }"); assert.strictEqual(message.read.name, "read"); assert.strictEqual(message.append.name, "append"); @@ -66,15 +68,15 @@ describe("classes", function () { assert.strictEqual(util.inspect(buffer.trimEnd), "[Function: trimEnd]"); assert.strictEqual( - message.read.toString(), + normalizeFn(message.read), "function read() { [native code] }" ); assert.strictEqual( - message.append.toString(), + normalizeFn(message.append), "function append() { [native code] }" ); assert.strictEqual( - message.concat.toString(), + normalizeFn(message.concat), "function concat() { [native code] }" ); @@ -92,15 +94,15 @@ describe("classes", function () { ); assert.strictEqual( - buffer.includes.toString(), + normalizeFn(buffer.includes), "function includes() { [native code] }" ); assert.strictEqual( - buffer.trimStart.toString(), + normalizeFn(buffer.trimStart), "function trimStart() { [native code] }" ); assert.strictEqual( - buffer.trimEnd.toString(), + normalizeFn(buffer.trimEnd), "function trimEnd() { [native code] }" ); });