Skip to content

fix(deps): require use-sync-external-store ^1.5.0 so the ESM build loads in Node - #846

Merged
jhuleatt merged 1 commit into
mainfrom
fix/use-sync-external-store-esm
Oct 29, 2026
Merged

jhuleatt merged 1 commit into
mainfrom
fix/use-sync-external-store-esm

Conversation

@jhuleatt

@jhuleatt jhuleatt commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

dist/index.js imports use-sync-external-store/shim. The import stays external on purpose: inlining the shim broke Next.js, see vite.config.ts. Versions of use-sync-external-store before 1.5.0 have no exports map, so Node's ESM resolver treats /shim as a directory and won't load it:

Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '.../node_modules/use-sync-external-store/shim' is not supported resolving ES modules imported from .../node_modules/reactfire/dist/index.js

Bundlers resolve directory imports, so browser builds are fine. It breaks anything that loads reactfire's ESM build with Node's own resolver, for example:

  • Vitest tests in apps that use reactfire. Vitest loads node_modules packages with Node directly, so the whole test file fails to load.
  • SSR setups that leave dependencies external, which is Vite SSR's default.
  • Plain Node ESM scripts.

The CJS build (require('reactfire')) isn't affected.

reactfire declares ^1.2.0, so fresh installs usually get a newer version. But any project that ends up with an older copy (from an existing lockfile, or another package that pins it) is broken. This repo's own lockfile still had 1.2.0.

Changes

  • Raise use-sync-external-store to ^1.5.0, the first version with an exports map (./shim → ./shim/index.js). The lockfile now resolves 1.7.0. 1.5.0+ also lists React 19 in its peer range; 1.2.0 stopped at React 18.

Testing

A consumer project that pins use-sync-external-store, importing the packed tarball:

reactfire use-sync-external-store installed Node ESM import Vitest test importing reactfire
main (^1.2.0) 1.2.0 (deduped) ❌ ERR_UNSUPPORTED_DIR_IMPORT ❌ test file fails to load
this PR (^1.5.0) consumer keeps 1.2.0, npm nests 1.7.0 for reactfire ✅ 74 exports ✅
this PR (^1.5.0) 1.5.0 (deduped) ✅ 74 exports ✅

Also:

  • In this repo, after a build, import('reactfire') fails on main. With this change it loads all 74 exports on both Node 22 and 24.
  • tsc (src and tests) and the full emulator test suite pass. So does the React 19 type-check setup.

…ads in Node

dist/index.js imports `use-sync-external-store/shim`, which is kept
external. Before 1.5.0 the package had no `exports` map, so Node's ESM
resolver treats `/shim` as a directory and throws
ERR_UNSUPPORTED_DIR_IMPORT. That breaks anything that loads the ESM build
without a bundler: Vitest tests in apps that use reactfire, SSR that keeps
dependencies external, and plain Node scripts. The CJS build is fine.

Raise the floor to ^1.5.0 (lockfile: 1.2.0 -> 1.7.0).
@jhuleatt
jhuleatt force-pushed the fix/use-sync-external-store-esm branch from 4c3270c to 4463c3f Compare October 28, 2026 14:47
@jhuleatt
jhuleatt requested a review from tyler-reitz October 28, 2026 21:00

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Your table reproduces with packed tarballs in a "type": "module" app (React 18, firebase 12, Node 22):

reactfire use-sync-external-store import('reactfire')
main 1.2.0 pinned ERR_UNSUPPORTED_DIR_IMPORT
main unpinned (1.7.0) 74 exports
this PR 1.2.0 pinned (npm nests 1.7.0) 74 exports
this PR 1.4.0 pinned (npm nests 1.7.0) 74 exports
this PR 1.5.0 pinned (deduped) 74 exports

A Vitest test importing reactfire fails to load on main + 1.2.0 and passes on this PR.

React 16 and 17 still work. With 16.14.0 and 17.0.2 (no native useSyncExternalStore), the 1.7.0 shim falls back and useObservable both server-renders and updates on the client under jsdom. Breaking the shim's fallback makes that test throw, so it really goes through the shim.

src only uses the three-argument useSyncExternalStore from /shim, so nothing depends on 1.2.x behavior. fix: is right too: the floor moves, but the supported React range doesn't change.

This branch predates #766 and #794, so CI never ran the package gate or the React 19 legs on it. Merged onto current main, publint --strict, attw and size-limit pass, and the useObservable tests pass on React 18 (21/21) and 19.3.1 (20, plus the #793 skip).

Non-blocking:

  • A consumer that pins an older copy now ends up with a second, nested copy of the shim. It's small and stateless, so that should be harmless (inference).
  • Importing use-sync-external-store/shim/index.js instead would also fix it without moving the floor: under Node ESM it loads on both 1.2.0 and 1.7.0. I haven't checked it against the Next.js case that made the shim external, so the floor bump seems like the safer fix.

@jhuleatt
jhuleatt merged commit c0b6301 into main Oct 29, 2026
17 checks passed
@jhuleatt
jhuleatt deleted the fix/use-sync-external-store-esm branch October 29, 2026 19:35
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.

2 participants