Skip to content

v0.14.0 — Deno and Bun, 91% smaller Linux prebuilds, release scaffolding

Choose a tag to compare

@codetalcott codetalcott released this 13 Sep 20:14
c5e30ab

Distribution. A napi-mojo addon now runs on Deno and Bun as well as Node,
the Linux prebuilds are 91% smaller, every platform package declares what it
actually contains, and napi-mojo release --scaffold gives an addon author the
same publishing setup this repo uses. Three new CI gates hold the claims: a
cross-runtime job, an artifact-portability parser, and a glibc-floor check.

No Mojo signature changed, so downstream addons compile against 0.14.0
unmodified — but recompiling does change one runtime behaviour, module export
attributes, described first below.

See docs/plan-distribution.md for the
measurements behind all of it.

Behaviour changes

  • Module exports are now enumerable, writable and configurable — ordinary
    exports.foo = fn semantics instead of napi_default. They were none of the
    three, so Object.keys(addon) returned only the classes, {...addon} lost
    every function, and assigning over an export silently did nothing. Class
    prototype members are unchanged: a JS class method is non-enumerable, and
    matching that is correct. If your code worked around the old behaviour with
    Object.getOwnPropertyNames, it still works.
  • addAsyncCleanupHook() returns its handle (an External) rather than
    true, and removeAsyncCleanupHook(handle) takes it. The old pair could
    not work: add dropped the handle, so remove registered a second hook with
    an identical (function, data) pair just to have one to remove, and the
    caller's hook stayed registered. The duplicate also aborted Bun and corrupted
    Deno's heap. The handle is type-tagged and remembers its removal, so a second
    removeAsyncCleanupHook(handle) throws handle already removed and an
    External from anywhere else throws a TypeError — both were reachable from
    plain JavaScript and crashed the process, because
    napi_remove_async_cleanup_hook frees the handle it is given.

Added

  • Deno and Bun are supported and gated. Every surface this framework
    exposes behaves identically on Node, Deno and Bun — async work,
    ThreadsafeFunction, type tagging, primitives in a napi_ref included. The
    runtimes CI job keeps that true. Two upstream defects are recorded with
    reproducers; neither is napi-mojo's, and both reproduce from a plain C addon.
    Do not gate on napi_get_version: Bun answers 9 and implements the v10
    behaviour anyway.
  • napi-mojo release --scaffold writes an addon author's prebuild and
    publish setup: optionalDependencies per platform, a loader, platform
    manifests, and a release workflow whose consume job has no checkout and no
    toolchain. It is safe to run in an existing project: package.json and the
    platform manifests are patched (dependencies merged, an existing main and
    an absent files left alone, author fields kept), and an existing index.js
    or release.yml is kept unless you pass --force. Platform manifests
    declare a licence covering what they carry, with the texts, and inherit a
    repository (from the git remote if needed) so provenance publishing
    accepts them. napi-mojo init's pixi.toml declares every prebuild
    platform, and the workflow generates bindings before building and installs
    exactly the version it published.
  • Load failures explain themselves. When a prebuilt binary is installed
    but cannot load — a container image without the C++ runtime
    (oven/bun:*-distroless, denoland/deno:alpine), glibc older than 2.35, or
    musl — napi-mojo/demo and every scaffolded addon loader now throw an error
    with a code (ERR_NATIVE_NO_CXX_RUNTIME, ERR_NATIVE_GLIBC_TOO_OLD,
    ERR_NATIVE_LIBSTDCXX_TOO_OLD, ERR_NATIVE_MUSL), the fix, and a link to
    the new docs/TROUBLESHOOTING.md; the loader's
    message is kept as cause. demo.js used to report every such failure as
    "No prebuilt demo binary available". The doc carries a support matrix, a
    Dockerfile build-time check, and a two-line fix for Bun distroless — all
    built and loaded on both Linux architectures on every release, with the
    recipe extracted from the doc itself.
  • napi-mojo release --sync sets every platform manifest and
    optionalDependencies entry to package.json's version; the scaffold wires
    it to npm version, and the release workflow refuses to publish when they
    disagree.
  • napi-mojo release --bootstrap does the one-time first publish npm's
    trusted publishing requires, as a 0.0.0-bootstrap.0 placeholder under the
    bootstrap dist-tag. Publishing npm/<platform> directly — what the
    scaffold's first version printed — pushes a package with no binary at the
    real version, which can never be republished.
  • scripts/check-portable.mjs, a release gate that reads an artifact's
    Mach-O/ELF load commands and refuses one that depends on the machine that
    built it — the property a load test on the build machine cannot establish.
    It models where the loader actually looks: a library beside the artifact
    counts only if a self-relative search path (@loader_path, $ORIGIN) sends
    the loader there, a @loader_path/ dependency must exist, @executable_path
    is node's directory rather than the addon's, and a library the bundler's
    manifest names but that is not present fails the check.
  • publish.yml preflights its publish targets before spending three
    platform builds. The 0.13.0 release failed at the last step on a package npm
    had never seen, leaving two platform packages published at the new version
    and the root package on the old one. That is now a 20-second stop before
    anything is pushed.

Changed

  • The Linux packages are 91% smaller — 26.1 MB → 2.4 MB unpacked, a
    9.4 MB → 0.9 MB tarball. GCC's libstdc++.so.6 and libgcc_s.so.1 are no
    longer bundled: the Mojo runtime needs at most GLIBCXX_3.4.30, and its
    GLIBC_2.35 requirement — which cannot be bundled, glibc being the loader —
    already implies a host that provides it, and Node itself links both.

    If you install on a host with glibc ≥ 2.35 but an unusually old
    libstdc++
    , require() now fails with a loader version error instead of
    working. That combination is constructible but not something any mainstream
    distribution produces. Two CI gates hold the premise: check-glibc-floor.mjs
    on every PR, and a no-toolchain container consume job on every release.

    Under Bun or Deno, the container image must provide libstdc++.
    Neither runtime links it, so the "Node links it" argument does not apply.
    oven/bun:1.3.11-distroless and denoland/deno:alpine-2.9.6 contain none
    and fail at require() with libstdc++.so.6: cannot open shared object file, where 0.13.0 loaded. The Debian- and Ubuntu-based Bun and Deno images,
    and denoland/deno:distroless, work. (oven/bun:*-alpine is musl and never
    could load a glibc addon.)

  • The Linux packages no longer declare GPL-3.0-or-later WITH GCC-exception-3.1, because they no longer contain GCC's runtime. All
    three platforms are now MIT AND Apache-2.0 WITH LLVM-exception. Tarballs
    published at 0.13.0 and earlier do contain those libraries; the exception is
    what permitted it, and licenses/NOTICE.bundle.txt says so for anyone
    auditing one.

  • The 19 handle-only raw_* wrappers no longer take AnyOrigin — phase 1
    of the deferred signature flip. Every argument was already a V8 handle
    aliased MutUntrackedOrigin, so nothing formed a Pointer(to=<local>) and
    there was no lifetime for AnyOrigin to extend: the widening calls were
    ceremony. Semantically a no-op. The remaining 128 wrappers take a raw pointer
    and stay deferred; see
    docs/plan-origin-migration.md.

Fixed

  • The prebuilt platform packages declared "license": "MIT" while shipping
    the Mojo runtime and, on Linux, GCC's libstdc++ and libgcc_s. Each now
    declares what it actually contains and carries the licence texts.