Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/content/blog/introducing-nub.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Bun and Deno proved there's enormous appetite for an all-in-one toolkit that rep

## Introducing Nub

Nub embraces it instead. It's a single Rust binary that transpiles your code and executes it on the stock `node` your project pins — so the runtime underneath *is* Node. On Node 26.7's own test suite, scored the way Deno's compatibility corpus scores it, it clears 98.5% of what real Node passes, versus 74.2% for Deno 2.9 and 68.1% for Bun 1.4. It combines:
Nub embraces it instead. It's a single Rust binary that transpiles your code and executes it on the stock `node` your project pins — so the runtime underneath *is* Node. On Node 26.7's own test suite, scored the way Deno's compatibility corpus scores it, it clears 98.5% of what real Node passes, versus 74.2% for Deno 2.9 and 70.1% for Bun 1.4. It combines:

```console
$ nub index.ts # file runner — full TypeScript, on stock Node
Expand Down
2 changes: 1 addition & 1 deletion site/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ const COMPAT = [
{ name: 'Node 26.7', rate: 100, tests: '5,046 / 5,046', us: false, dim: false },
{ name: 'Nub', rate: 98.5, tests: '4,968 / 5,046', us: true, dim: false },
{ name: 'Deno 2.9', rate: 74.2, tests: '3,742 / 5,046', us: false, dim: true },
{ name: 'Bun 1.4', rate: 68.1, tests: '3,438 / 5,046', us: false, dim: true },
{ name: 'Bun 1.4', rate: 70.1, tests: '3,535 / 5,046', us: false, dim: true },
];

function Compatibility() {
Expand Down
18 changes: 9 additions & 9 deletions tests/cross-runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cross-runtime Node-compatibility benchmark

This harness runs Node's own test suite — the whole `test/` tree of a Node release, the same corpus Deno vendors as [`denoland/node_test`](https://github.com/denoland/node_test) — identically against `node`, `nub`, `bun`, and `deno`, and reports pass rates per runtime under several explicit scoring lenses. Nobody curates the file list: every runtime runs the same files, with the same flags, under the same pass criterion, and every runtime's failures are published by name in `results.json`.
This harness runs Node's own test suite — the whole `test/` tree of a Node release, the same corpus Deno vendors as [`denoland/node_test`](https://github.com/denoland/node_test) — identically against `node`, `nub`, `bun`, and `deno`, and reports pass rates per runtime under several explicit scoring lenses. Nobody curates the file list: every runtime runs the same files, with the same flags, under the same pass criterion, and every runtime's failures are published by name in `results.json`. One accommodation is symmetric by design: a `node:test`-based file runs under each runtime's own test mode where it needs one (`deno test`, `bun test`) — Node runs those files as plain scripts, Deno and Bun register their tests only inside their runners, and both runtimes' own compat suites make the same switch.

## What's pinned (so it reproduces forever)

Expand All @@ -23,7 +23,7 @@ This harness runs Node's own test suite — the whole `test/` tree of a Node rel

The nub binary is a release build of `main` at `e78a6701dd` plus the `NODE_OPTIONS` coverage-exclude change committed beside this results file. This table and the results table below are generated from `results.json` by [`readme-table.mjs`](./readme-table.mjs).

macOS arm64, 2026-08-22. The retry pass flipped 1 node, 1 nub, 4 bun, 2 deno and 0 node25 verdicts, which bounds the load effect.
macOS arm64, 2026-08-22. The retry pass flipped 1 node, 1 nub, 1 bun, 2 deno and 0 node25 verdicts, which bounds the load effect. Bun's verdicts were re-measured 2026-08-30 with the `bun test` accommodation and `BUN_TEST_DRAIN_EVENT_LOOP=1` (see `buildPlainCommand` in `run.mjs`), one full bun pass over the same v26.7.0 checkout: 99 files flipped to pass and none flipped to fail.
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated

## Reproduce it yourself

Expand Down Expand Up @@ -80,12 +80,12 @@ Node-relative pass rate (raw in parentheses). The rows are generated from `resul
<!-- results-table -->
| Lens | files / node passes | nub | deno 2.9.5 | bun 1.4.0 | node 25.9.0 |
|------|---------------------|-----|------------|-----------|-------------|
| `denoExclusions` | 5,078 / 5,046 | **98.45%** (97.87) | 74.16% (73.89) | 68.13% (67.84) | 90.15% (89.62) |
| `bunUniverse` | 4,760 / 4,736 | **98.16%** (97.67) | 71.75% (71.49) | 69.83% (69.60) | 89.55% (89.12) |
| `fullCorpus` | 5,664 / 5,616 | **97.40%** (96.61) | 68.07% (67.67) | 63.82% (63.47) | 89.96% (89.23) |
| `fullCorpusNoEngine` | 4,946 / 4,904 | **97.37%** (96.58) | 71.66% (71.25) | 69.51% (69.15) | 90.03% (89.30) |
| `bunUniverseNoEngine` | 4,111 / 4,091 | **98.22%** (97.74) | 76.04% (75.80) | 76.83% (76.60) | 89.54% (89.13) |
| `engineSpecificOnly` | 718 / 712 | **97.61%** (96.80) | 43.40% (43.04) | 24.58% (24.37) | 89.47% (88.72) |
| `denoExclusions` | 5,078 / 5,046 | **98.45%** (97.87) | 74.16% (73.89) | 70.06% (69.75) | 90.15% (89.62) |
| `bunUniverse` | 4,760 / 4,736 | **98.16%** (97.67) | 71.75% (71.49) | 71.79% (71.55) | 89.55% (89.12) |
| `fullCorpus` | 5,664 / 5,616 | **97.40%** (96.61) | 68.07% (67.67) | 65.58% (65.22) | 89.96% (89.23) |
| `fullCorpusNoEngine` | 4,946 / 4,904 | **97.37%** (96.58) | 71.66% (71.25) | 71.41% (71.03) | 90.03% (89.30) |
| `bunUniverseNoEngine` | 4,111 / 4,091 | **98.22%** (97.74) | 76.04% (75.80) | 78.98% (78.74) | 89.54% (89.13) |
| `engineSpecificOnly` | 718 / 712 | **97.61%** (96.80) | 43.40% (43.04) | 25.42% (25.21) | 89.47% (88.72) |
<!-- /results-table -->

Per directory, the three that only run properly with the full checkout, the pty and the compiled fixture (node-relative passes / Node's passes): `pseudo-tty/` nub 28 / 31, deno 15, bun 12; `wpt/` nub 24 / 25, bun 6, deno 0 (see the caveat above); `ffi/` nub 11 / 13, bun 13, deno 13 (both skip every `ffi` test — `common.skip()` exits 0 — which counts as a pass under Node's own convention).
Expand Down Expand Up @@ -123,7 +123,7 @@ A regenerated `results.json` is only half the update. The published figures are

Not every failure is a defect a user would feel. A runtime can throw the right error at the right moment and word it differently — JavaScriptCore and V8 phrase the same brand check differently, so an `assert.throws({ message })` fails on text alone. That is worth measuring rather than asserting, so we did: re-run the corpus with failure output retained (one change to `run.mjs` — keep `raw.out` in `judge()`; the pass criterion is untouched), then bucket each failure by which keys differ inside `node:assert`'s `Comparison { }` block.

Forgiving every failure whose *only* difference is the message text moves bun 1.4.0 from 66.8% to 68.1% and deno 2.9.5 from 78.4% to 78.8%. nub does not move at all: it executes on the stock Node binary, so its error messages *are* Node's, and the count is zero rather than small. Also forgiving a differing or absent `code`/`name` reaches 69.1% and 80.3%.
Forgiving every failure whose *only* difference is the message text moves bun 1.4.0 from 66.8% to 68.1% and deno 2.9.5 from 78.4% to 78.8% (measured on the 2026-08-29 verdicts, before the `bun test` accommodation shifted bun's baseline; the classification of individual failures is unaffected). nub does not move at all: it executes on the stock Node binary, so its error messages *are* Node's, and the count is zero rather than small. Also forgiving a differing or absent `code`/`name` reaches 69.1% and 80.3%.

**The published figures forgive neither, and the second is the reason.** Message text is cosmetic; error identity is not. bun frequently throws a raw JavaScriptCore `TypeError` carrying no `code` property at all, which breaks any program branching on `err.code === 'ERR_INVALID_ARG_TYPE'` — a real incompatibility that a text-only classifier reports as a wording nit.

Expand Down
Loading
Loading