You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Release process: add a built-artifact (dist/bundle) check to catch runtime regressions before publish #765
#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.
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.
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.
exports map integrity. Every path referenced by exports / main / module exists in the packed tarball.
Bundle-size delta vs previous latest.npm pack the current latest, compare dist size; flag large jumps (a proxy for accidental inlining).
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.
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.
Activity
feat: add an entry-point load test (#765 item 3)
refactor: move the type-surface check out of the runtime gate
tyler-reitz commentedon Jul 29, 2026
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.tsdiff, 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:
import('reactfire')require('reactfire')requireshim errorrequireshim errorTwo 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-mapcannot 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 deprecatestep 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 emittedtypeof require < "u"guards andrequire.apply(this, arguments), and minification renamed__commonJSto a single letter. A literal implementation of item 1 would have passed 4.2.5 clean. The check matches the barerequireidentifier instead, across every ESM chunk rather than just the entry.feat: diff the published type surface alongside the check that classi…
ci: verify the packed package with publint, attw and size-limit
tyler-reitz commentedon Jul 31, 2026
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:
requirein the ESM distsize-limitscripts/entry-load.mjs, the one bespoke check that remainspublint --strictsize-limit, as an absolute budget rather than a deltaItems 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
publintnorattwcatches #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 arequireinlined 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.
ci: verify the packed package with publint, attw and size-limit