Skip to content

[FSSDK-13287] Make React Native polyfills optional and isolate optional requires - #1179

Merged
raju-opti merged 6 commits into
masterfrom
raju/optional-polyfill
Oct 15, 2026
Merged

raju-opti merged 6 commits into
masterfrom
raju/optional-polyfill

Conversation

@raju-opti

@raju-opti raju-opti commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Polyfill packages are optional. fast-text-encoding and react-native-get-random-values are loaded if installed. Both keep an existing global, so a native TextEncoder (Hermes in RN 0.74+) or an app's own polyfill (for example react-native-quick-crypto, imported in the app's entry file before the SDK) is used instead. If TextEncoder or crypto.getRandomValues is still missing, index.react_native.ts throws at import with an error that names the package to install.
  • Optional import shims in lib/import.react_native/. There is one shim per optional package, named after the package: fast-text-encoding.ts, react-native-get-random-values.ts and @react-native-async-storage/async-storage.ts. Each one:
    • does its require inside try/catch while the module loads;
    • exports isAvailable plus the package's exports that the SDK uses, under the same names, which are undefined if the package is missing;
    • never throws. The importer decides what a missing package means.
  • Shims are built as separate files. Rollup builds each shim to the same path under dist/import.react_native/, and the main React Native bundles load them by relative path. A small resolveId plugin keeps the shim imports out of the main bundles. The main bundles no longer contain any optional require, and the node, browser and universal bundles are unchanged.
  • AsyncStorage: the compat helper (ensureMultiGet, which adds multiGet on v3) is merged into the async-storage shim. The shim exports the package's default, with multiGet added. The "Module not found" check moved to AsyncStorageCache. The CJS bundles read shim default exports through Rollup's esModule interop, applied only to shim imports.
  • Build check: a Rollup plugin fails the build unless every built shim contains exactly one require(), of its own package. That catches imports of other packages, tslib helpers (from async/await), and code shared between shims.
  • README: lib/import.react_native/README.md explains the Metro bug behind this setup, how the shims avoid it, and the rules for writing a shim.

Why the shims are separate files

Metro before 0.82.5 (React Native 0.78 and older) drops an optional dependency that isn't installed from the module's dependency map, without adjusting the indices in the module's code. Every later require in that module then loads the wrong module. With all of the SDK's React Native code in one module:

  • With fast-text-encoding missing, require("fast-text-encoding") silently loaded react-native-get-random-values.
  • The getRandomValues check then reported "install react-native-get-random-values" even though it was installed.
  • A missing async-storage would have shifted netinfo and both polyfill requires.

A shim's only dependency is its own package, so a missing package can't shift anything else, and the main bundle's shim requires always resolve.

The shim does its require while the module loads for a second reason: on old Metro, a failed require called later is reported as a fatal error instead of being thrown to the catch. Metro 0.82.5 fixed the bug.

Compatibility

Existing apps already install both polyfill packages, because the old code imported them unconditionally. With everything installed, nothing changes. A missing optional package is only tolerated at build time when Metro's allowOptionalDependencies is on, which React Native's and Expo's default Metro configs do.

Test plan

  • Automated:
    • vitest: 86 files, 1103 tests. This includes shim tests that load the async-storage shim with a missing package, with a package that has no default export, and with v1/v2 and v3 storage.
    • mocha: 815 tests.
    • Lint and platform-isolation validation pass.
  • Build check: the build fails as expected for a shim that imports another package (murmurhash), uses async/await (tslib), or shares code with another shim.
  • Build output:
    • Each file in dist/import.react_native/ contains exactly one require.
    • The main React Native bundles (CJS and ESM) load the shims by relative path.
    • The CJS bundle reads the async-storage shim's .default.
  • Manual, RN 0.73.11 (Hermes without native TextEncoder, Metro 0.80.12), iOS simulator, final build. The SDK was installed from an npm pack tarball, results were checked in device logs, and react-native-get-random-values was installed in each case:
    • Everything installed: decisions and events work, murmurhash uses the fast-text-encoding polyfill, and stored events are read back through the async-storage shim.
    • fast-text-encoding not installed: the SDK throws "TextEncoder is not available… Install fast-text-encoding" at import.
    • async-storage not installed: the SDK imports fine, and the batch event processor throws "Module not found: @react-native-async-storage/async-storage". No fatal "Requiring unknown module" error is reported.
  • Manual, earlier builds (same separate-module setup, before the shims were renamed and restructured):
    • RN 0.73, fast-text-encoding not installed with a custom JS TextEncoder loaded first: works. Before this change, the SDK threw a false getRandomValues error.
    • RN 0.87.1, react-native-quick-crypto (install() in the entry file) with no default polyfills: works, and event UUIDs come from quick-crypto's crypto.randomUUID.
    • RN 0.87.1, custom JS getRandomValues and no polyfill packages: works. Without any polyfill, the SDK throws the getRandomValues error at import.

Issues

  • FSSDK-13287

🤖 Generated with Claude Code

raju-opti and others added 2 commits October 14, 2026 06:40
fast-text-encoding and react-native-get-random-values are now only required
when TextEncoder / crypto.getRandomValues are missing, so apps can drop them
or load their own polyfill before the SDK.

These requires and the async-storage require are moved into their own modules
(imported as react_native_optional/*, shipped in dist/react_native_optional)
instead of being bundled. Metro before 0.82.5 drops a missing optional
dependency from a module's dependency map, which shifts every later require in
that module to the wrong module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loaders now live in lib/react_native_optional and are built to the same
path in dist, so the main bundles can require them by the same relative path
as in the source. This replaces the react_native_optional/* path alias and the
rollup output.paths rewrite with a small resolveId plugin, and builds all
loaders in one multi-input rollup build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@datadog-optimizely-experimentation

This comment has been minimized.

@coveralls

coveralls commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 77.624%. remained the same — raju/optional-polyfill into master

Shims now live in lib/import.react_native, are named after the package they
require, and export isAvailable plus the package's exports used by the SDK.
They never throw; importers handle a missing package:
- index.react_native.ts throws if TextEncoder or crypto.getRandomValues is
  still missing after the polyfill shims run
- AsyncStorageCache throws "Module not found" when AsyncStorage is missing

The async-storage compat helper is merged into its shim, which applies
ensureMultiGet right after the require and exports the result as default.
The CJS bundles read shim default exports with Rollup's esModule interop.

A rollup check fails the build unless every built shim contains exactly one
require(), of its own package. A README in the shim directory explains the
Metro bug behind this setup and the rules for writing a shim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@raju-opti raju-opti changed the title Load React Native polyfills only when missing [FSSDK-13287] Make React Native polyfills optional and isolate optional requires Oct 14, 2026
@raju-opti
raju-opti requested a balanced review from Copilot October 15, 2026 11:48
The shim specs are named after their packages, so the *.react_native.spec.ts
exclude doesn't cover them, and they use Node's module loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Windows path separators prevent shim externalization, reintroducing the Metro resolution bug in Windows-built bundles.

1 open finding
What changed in this PR

Makes React Native polyfills optional and isolates optional dependencies to avoid older Metro resolution bugs.

Changes:

  • Builds optional dependency shims separately, with a single-require check.
  • Adds missing-polyfill errors and moves AsyncStorage compatibility into its shim.
  • Updates tests and documents shim requirements.
File Description
rollup.config.mjs Builds, externalizes, and validates shims.
lib/​utils/​cache/​async_storage_cache.react_native.ts Checks AsyncStorage availability.
lib/​utils/​cache/​async_storage_cache.react_native.spec.ts Uses the shim export.
lib/​project_config/​config_manager_factory.react_native.spec.ts Mocks shim availability.
lib/​index.react_native.ts Loads optional polyfills and validates globals.
lib/​import.react_native/​README.md Explains Metro behavior and shim rules.
lib/​import.react_native/​react-native-get-random-values.ts Adds optional random-values loading.
lib/​import.react_native/​fast-text-encoding.ts Adds optional text-encoding loading.
lib/​import.react_native/​@react-native-async-storage/​async-storage.ts Combines optional loading and storage compatibility.
lib/​import.react_native/​@react-native-async-storage/​async-storage.spec.ts Tests availability and compatibility.
lib/​event_processor/​event_processor_factory.react_native.spec.ts Updates missing-storage mocks.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rollup.config.mjs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@junaed-optimizely junaed-optimizely 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.

LGTM!

Non-blocking follow-up: the React Native install docs still say fast-text-encoding and react-native-get-random-values are required. After this PR they're only needed when the environment doesn't already provide TextEncoder or crypto.getRandomValues, or the app can load its own polyfill before importing the SDK. Worth updating the docs when this ships.

@raju-opti
raju-opti merged commit f308ecb into master Oct 15, 2026
22 of 26 checks passed
@raju-opti
raju-opti deleted the raju/optional-polyfill branch October 15, 2026 20:49
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.

4 participants