Skip to content

Release process: add a built-artifact (dist/bundle) check to catch runtime regressions before publish #765

Open

Description

@tyler-reitz

Motivation

#759 (the App Router / Vite client crash in 4.2.4 and 4.2.5) shipped because the build-tooling migration silently changed the built ESM output: use-sync-external-store/shim (CJS) started getting bundled into the ESM dist, producing a dynamic require() that throws in any browser bundle (Calling \require` for "react" in an environment that doesn't expose the require function`). The source was unchanged; only the emitted artifact regressed, and nothing in the release flow compared the artifact before publish.

This is the second dist-level regression to ship as a patch: #749 already proposes a published .d.ts diff to catch the type side (the 4.2.4 ObservableStatus break). This issue covers the runtime/bundle side. Together they form a built-artifact diff gate.

Proposed pre-publish checks

A concrete checklist for a pre-publish gate (each item derived from auditing the 4.2.6 release by hand). Items marked would have caught #759.

  1. No dynamic require( / CJS-interop shims in the ESM dist. Grep dist/index.js for \brequire\b, __require, createRequire, __commonJS. (Would have caught App Router crash: reactfire 4.2.4/4.2.5 throw "dynamic usage of require is not supported" on the client #759: require count went 0 at 4.2.3 to 3 at 4.2.4+.)
  2. Externals are not inlined. Only rxfire / rxjs / tslib should be bundled; react, firebase/*, @firebase/*, and use-sync-external-store/shim must stay external import specifiers. Catches accidental bundling that causes duplicate-instance bugs.
  3. Both entry points load. node --input-type=module -e "import('reactfire')" and node -e "require('reactfire')" (in a fixture with react + firebase installed) must resolve without throwing. Catches missing/renamed files and broken imports.
  4. exports map integrity. Every path referenced by exports / main / module exists in the packed tarball.
  5. Bundle-size delta vs previous latest. npm pack the current latest, compare dist size; flag large jumps (a proxy for accidental inlining).
  6. Published .d.ts diff vs previous version. Type-side counterpart, tracked in Add a published .d.ts diff to the release process to catch breaking type changes #749; catches the 4.2.4 ObservableStatus break class.
  7. Runtime smoke render in CI (strongest). A minimal Next App Router (turbopack) and Vite app that renders a data hook against the packed build; fails on the App Router crash: reactfire 4.2.4/4.2.5 throw "dynamic usage of require is not supported" on the client #759 crash. This is the check that catches runtime regressions the static greps miss.

Items 1 to 5 are cheap and scriptable against npm pack output; 7 is the higher-value integration check.

Related

Activity

  1. tyler-reitz commented on Jul 29, 2026

    @tyler-reitz
    ContributorAuthor

    Closing the loop on where each item landed, and on item 7 specifically.

    Items 1, 2, 3, 4 and 5 are implemented in #766, as a Verify built artifact job (no-cjs-in-esm, externals, exports-map, size) plus a Verify package loads job. Both run against the packed tarball the publish job uploads verbatim, so they check the exact bytes that ship.

    Item 6, the published .d.ts diff, is #749 and lands in a follow-up PR alongside a check that classifies a type change as additive or breaking automatically, rather than asking a human to.

    Item 7 will not be built as specified, and here is the reasoning, since it was written as the strongest item on the list.

    Its stated justification was being "the check that catches runtime regressions the static greps miss". That was correct when this issue was written and is no longer true. Item 3, the entry-point load test, catches the #759 class the same way, by observing the failure rather than pattern-matching the artifact. Verified against the published tarballs:

    Version import('reactfire') require('reactfire')
    4.2.4 throws the require shim error loads
    4.2.5 throws the require shim error loads
    4.2.6 loads loads

    Two things worth noting from that. Only the ESM entry breaks, so a check that loaded one entry point would have missed it entirely. And exports-map cannot see this at all: every file it looks for is present in 4.2.5, the package simply does not run.

    That leaves item 7 with a genuine but much narrower remaining value: failures that appear only in a browser or bundler context and do not reproduce on a Node import. Against roughly a day of work, permanent CI minutes on every PR, and additional flake surface, that is not worth building as standalone infrastructure right now.

    It is also not being dropped on the floor. A V5 demo app is already part of the plan for surfacing real-world breaks before we commit to a breaking API, and a smoke render against the packed build belongs there as part of that app's CI rather than as a separate fixture maintained on its own. Recorded to fold in when that work is scoped.

    So: this issue closes when #766 merges. #749 stays open separately, since it also carries the npm deprecate step for 4.2.4 and 4.2.5.

    One note for anyone implementing something similar later. The grep this issue proposes for item 1 does not work. The shipped 4.2.5 bundle never literally writes require(: rolldown emitted typeof require < "u" guards and require.apply(this, arguments), and minification renamed __commonJS to a single letter. A literal implementation of item 1 would have passed 4.2.5 clean. The check matches the bare require identifier instead, across every ESM chunk rather than just the entry.

  2. tyler-reitz commented on Jul 31, 2026

    @tyler-reitz
    ContributorAuthor

    Correcting my earlier comment: #766 has since been rewritten to drop the bespoke gate entirely, so there is no Verify built artifact job and the mapping I gave for items 1 to 5 no longer holds. Where they actually land now:

    Item Covered by
    1. No dynamic require in the ESM dist dropped as a static check; observed by item 3
    2. Externals not inlined dropped; partly observed by item 3, partly by size-limit
    3. Both entry points load scripts/entry-load.mjs, the one bespoke check that remains
    4. exports map integrity publint --strict
    5. Bundle-size delta size-limit, as an absolute budget rather than a delta

    Items 1 and 2 are now served by observing a failure rather than pattern-matching for it. Note that the grep this issue specifies for item 1 would have passed 4.2.5 clean anyway (see the last note on that earlier comment), so little is lost there. What is lost: size-limit's absolute budget will not catch a regression inside the current 3.7 kB of headroom, where a delta check would.

    Item 3 stays a script because neither publint nor attw catches #759. Their output is byte-identical across 4.2.3, 4.2.4, 4.2.5 and 4.2.6: both analyse package structure, and #759 was a require inlined into the body of a structurally valid ESM file.

    The item 7 reasoning in that comment is unchanged, as is the closing plan: this issue closes when #766 merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions