Skip to content

feat: run on Bun and Deno, and prove it in CI - #352

Merged
unional merged 2 commits into
mainfrom
feat/runtime-agnostic
Sep 3, 2026
Merged

feat: run on Bun and Deno, and prove it in CI#352
unional merged 2 commits into
mainfrom
feat/runtime-agnostic

Conversation

@unional

@unional unional commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

assertron is a leaf dependency of nearly every testing package in this fleet, so its portability ceiling is everyone else's. This raises it and, more importantly, puts a job in CI that executes the claim instead of asserting it in a table.

What actually ships

Audited by packing the tarball and grepping the build output, not by grepping ts/:

Builtin Where Ships?
perf_hooks (bare) ts/assert-order/StateMachine.ts yesesm/assert-order/StateMachine.js and the CJS require("perf_hooks")
node:assert ts/testUtils.ts, ts/**/*.spec.ts no — both excluded by files
node:fs/promises tsdown.config.ts no — build config, untouched

So the shipped coupling was one bare perf_hooks import, and one only.

node:assert is not breaking, because it never shipped

The brief asked whether removing it breaks consumers. It does not, because there is nothing to remove: node:assert appears only in the test helper and the spec files, and files excludes ts/testUtils.ts and every *.spec.*. npm pack --dry-run confirms neither is in the tarball. The library's own AssertionError already comes from iso-error (ts/errors.ts), never from node:assert — consumers catching an assertion failure have always been catching this package's error, with its own shape. No major changeset; this PR is a patch.

The clock

StateMachine reached for perf_hooks and preferred process.hrtime. It now uses performance.now(), a global on Node ≥20 (this package's engines floor), Bun, Deno and the browser. That is the same high-resolution millisecond clock process.hrtime was providing, so AssertOrder#end() and getTimeTaken() return what they always did. There is no Date.now() fallback arm: no supported host lacks performance, and a branch no test can enter is not a safety net — codecov caught it as an uncovered half of the diff, and deleting it was the honest fix. Coverage is now 100% across statements, branches, functions and lines. browser: { "perf_hooks": false } is dropped along with it: it stubbed an import that no longer exists.

The published output now references no Node builtin at all.

The measured result, including the part that undercuts the premise

Deno 2.9 already resolves bare perf_hooks through its Node compatibility layer. I rebuilt the pre-change output and ran the smoke test on Deno to check rather than assume, and it passed. So the old code was not in fact broken on Deno, and this PR should not be read as a bug fix for it.

It still earns its place. Removing the builtin outright is what makes the output loadable where there is no Node compat layer — browsers, workers, edge runtimes — and it clears the last suppression so the lint rule can guard the package with no carve-out.

Smoke results on the built output, ESM and CJS, all green:

Runtime ESM CJS
Node 26.7.0 pass pass
Bun 1.4.0 pass pass
Deno 2.9.6 pass pass

Enforcement, verified rather than assumed

useNodejsImportProtocol is pinned to "error" in biome.json. Inheriting it from the preset would have enforced nothing: the rule ships in the style group at info, where biome prints the finding and still exits 0. Verified by reverting the import to its bare form — biome ci then exits 1 with a real lint/style/useNodejsImportProtocol error — and by a scratch probe on a bare path import. Restored afterwards.

CI

.github/workflows/runtimes.yml is a local reusable workflow running a three-way matrix. All three legs are handed the identical artifact, built once on Node with pnpm, so the matrix tests portability of the output and not of the build.

  • pull-request.yml calls it as a separate runtimes job. The caller job id code is untouched, so the required code / all-checks context still exists.
  • release.yml calls it and adds it to release's needs, so a runtime regression fails the release closed, the way publish-gate already does.

pnpm verify gains a smoke turbo task, so the Node leg also runs locally.

Not done here

ESM-only: the package still ships dual esm/ + cjs/, and both halves are exercised by the smoke test. It could go ESM-only — nothing in the source needs CJS — but that is a breaking change for a package with this many dependents, and it deserves its own PR and its own major. Left alone deliberately.

Checked and found already correct, so unchanged: .husky/commit-msg is 100755 and commitlint ran on this commit; minimumReleaseAge already lives in pnpm-workspace.yaml with minimumReleaseAgeStrict: true, not in .npmrc; every files entry resolves to a real directory. Workflow refs stay cyberuni/.github/... — this repo lives in cyberuni, not unional.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ETjy9oQGyETyFmDBdR9Egz

The shipped output had exactly one platform coupling left: `ts/assert-order/
StateMachine.ts` imported the bare `perf_hooks` specifier to reach a clock. That
was the only Node builtin anywhere in the published tarball. `node:assert` looks
like a second one from a source grep, but it appears only in `ts/testUtils.ts`
and the `.spec.ts` files, both excluded from `files` — it never ships, so there
is nothing to remove and no observable behaviour to break. Consumers already
catch this package's own `AssertionError` from `iso-error`, never Node's.

The clock now uses `performance.now()`, a global on Node >= 16, Bun, Deno and
the browser, with `Date.now()` as the fallback. The `process.hrtime` branch is
gone with it: it bought no precision `performance.now()` does not already give,
and it existed only because the module was written when the platform clock was
not yet universal. `browser: { "perf_hooks": false }` goes too — it stubbed an
import that no longer exists.

Biome's `useNodejsImportProtocol` is pinned to "error" so a bare `fs`/`path`
specifier cannot creep back in. The rule ships in the `style` group at `info`,
where biome reports the finding and still exits 0, so inheriting it from the
preset would have enforced nothing. Verified by reverting the import to its bare
form: `biome ci` then exits 1 with a real `lint/style/useNodejsImportProtocol`
error. The severity is set in `biome.json`, which is parsed as strict JSON — a
`//` comment there makes biome fall back to its defaults silently, so the
reasoning stays in this message.

`scripts/smoke.{mjs,cjs}` exercise the build output rather than `ts/`, because
the build output is all a consumer ever sees and a portability regression
surfaces there and nowhere else. The new `runtimes` workflow runs both against
Node, Bun and Deno on every pull request, all three fed the identical artifact
built once on Node. It is a separate job id from `code`, so the required
`code / all-checks` context is untouched, and `release` gains it as a `needs` so
a runtime regression fails the release closed the way `publish-gate` does.

Measured, not assumed: Deno 2.9 already resolves bare `perf_hooks` through its
Node compatibility layer, so the old code did in fact load there. The fix still
earns its place — it removes the builtin outright, which is what makes the
output loadable on the web and on edge runtimes that have no Node compat layer,
and it lets the lint rule guard the package with no suppression carved out of
it.

ESM-only is left alone deliberately: the package still ships a dual `esm/`+`cjs/`
build, and dropping the CJS half is a breaking change for its many dependents
that deserves its own PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETjy9oQGyETyFmDBdR9Egz
@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 419f686

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
assertron Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4ead2bb) to head (e7a157f).

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #352      +/-   ##
===========================================
+ Coverage   95.18%   100.00%   +4.81%     
===========================================
  Files          21        21              
  Lines         187       179       -8     
  Branches       47        45       -2     
===========================================
+ Hits          178       179       +1     
+ Misses          8         0       -8     
+ Partials        1         0       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

`performance` is a guaranteed global on every host this package supports —
`engines` requires Node >= 20, and Bun, Deno and browsers have had it far
longer. The `Date.now()` arm was therefore dead code that no test could enter,
which codecov correctly reported as an uncovered half of the diff.

Deleting it rather than annotating it away is the honest fix: an untestable
fallback is not a safety net, it is a second code path nobody has ever run.
Coverage is now 100% on statements, branches, functions and lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETjy9oQGyETyFmDBdR9Egz
@unional
unional added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit da5d14e Sep 3, 2026
10 checks passed
@unional
unional deleted the feat/runtime-agnostic branch September 3, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant