Repository navigation
[FSSDK-13287] Make React Native polyfills optional and isolate optional requires - #1179
Conversation
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>
This comment has been minimized.
This comment has been minimized.
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>
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>
There was a problem hiding this comment.
🟡 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.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
junaed-optimizely
left a comment
There was a problem hiding this comment.
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.

Summary
fast-text-encodingandreact-native-get-random-valuesare loaded if installed. Both keep an existing global, so a nativeTextEncoder(Hermes in RN 0.74+) or an app's own polyfill (for examplereact-native-quick-crypto, imported in the app's entry file before the SDK) is used instead. IfTextEncoderorcrypto.getRandomValuesis still missing,index.react_native.tsthrows at import with an error that names the package to install.lib/import.react_native/. There is one shim per optional package, named after the package:fast-text-encoding.ts,react-native-get-random-values.tsand@react-native-async-storage/async-storage.ts. Each one:requireinsidetry/catchwhile the module loads;isAvailableplus the package's exports that the SDK uses, under the same names, which areundefinedif the package is missing;dist/import.react_native/, and the main React Native bundles load them by relative path. A smallresolveIdplugin keeps the shim imports out of the main bundles. The main bundles no longer contain any optionalrequire, and the node, browser and universal bundles are unchanged.ensureMultiGet, which addsmultiGeton v3) is merged into the async-storage shim. The shim exports the package'sdefault, withmultiGetadded. The "Module not found" check moved toAsyncStorageCache. The CJS bundles read shim default exports through Rollup'sesModuleinterop, applied only to shim imports.require(), of its own package. That catches imports of other packages,tslibhelpers (fromasync/await), and code shared between shims.lib/import.react_native/README.mdexplains 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
requirein that module then loads the wrong module. With all of the SDK's React Native code in one module:fast-text-encodingmissing,require("fast-text-encoding")silently loadedreact-native-get-random-values.getRandomValuescheck then reported "install react-native-get-random-values" even though it was installed.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
requirewhile the module loads for a second reason: on old Metro, a failedrequirecalled later is reported as a fatal error instead of being thrown to thecatch. 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
allowOptionalDependenciesis on, which React Native's and Expo's default Metro configs do.Test plan
murmurhash), usesasync/await(tslib), or shares code with another shim.dist/import.react_native/contains exactly onerequire..default.TextEncoder, Metro 0.80.12), iOS simulator, final build. The SDK was installed from annpm packtarball, results were checked in device logs, andreact-native-get-random-valueswas installed in each case:fast-text-encodingpolyfill, and stored events are read back through the async-storage shim.fast-text-encodingnot installed: the SDK throws "TextEncoder is not available… Install fast-text-encoding" at import.fast-text-encodingnot installed with a custom JSTextEncoderloaded first: works. Before this change, the SDK threw a falsegetRandomValueserror.react-native-quick-crypto(install()in the entry file) with no default polyfills: works, and event UUIDs come from quick-crypto'scrypto.randomUUID.getRandomValuesand no polyfill packages: works. Without any polyfill, the SDK throws thegetRandomValueserror at import.Issues
🤖 Generated with Claude Code