Skip to content

Export the ReactNativeFeatureFlags subpath used by virtualized-lists - #57940

Open
giaBaoJS wants to merge 1 commit into
react:mainfrom
giaBaoJS:fix-57933-virtualized-lists-exports
Open

Export the ReactNativeFeatureFlags subpath used by virtualized-lists#57940
giaBaoJS wants to merge 1 commit into
react:mainfrom
giaBaoJS:fix-57933-virtualized-lists-exports

Conversation

@giaBaoJS

Copy link
Copy Markdown

Summary:

Fixes #57933.

A fresh app that renders FlatList makes Metro emit a package-exports violation warning and fall back to file-based resolution.

@react-native/virtualized-lists is a separately published package and a direct dependency of react-native. It imports the feature flags at runtime in two shipped files:

import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';

#57484 removed the "./src/*" mapping on the basis that "these import paths were never used externally", and its caveat covered only rn-tester and Fantom, which are repo-local. virtualized-lists is neither — it ships to npm, so the mapping is load-bearing. This is the same warning, from the same package, that #51699 previously fixed by re-exposing src/*.

This PR re-exports only that one subpath, with "types": null so it stays invisible to TypeScript exactly as #57277 intended. src/private/* as a whole stays unexported.

Alternative approach

The other option is to route feature-flag access through an already-exported entry point (a new curated secondary entry point next to ./asset-registry / ./react-private-interface, or re-exporting the two flags through one of those) and change the virtualized-lists import.

I chose the exports entry because:

  • It is the smaller, lower-risk change: the emitted bundle is byte-identical (verified below), so there is no behaviour change at all.
  • ReactNativeFeatureFlags must stay a singleton — flags are overridden at startup — and not moving the import keeps that guaranteed.
  • A curated entry point would need a readable public name like react-native/private/featureflags, which is more inviting to third parties than a src/private/... path that is already hidden from TypeScript.

If you would rather keep src/ fully unexported and take the entry-point route instead, say so and I will switch this PR over — the guard test below is valuable either way and needs no changes.

Scope note: @react-native/jest-preset

@react-native/jest-preset also deep-imports react-native/src/private/... in four published files (jest/RefreshControlMock.js, jest/mockNativeComponent.js, jest/mocks/requireNativeComponent.js, jest/mocks/RefreshControl.js). All four are import type, so Babel erases them and they never reach a bundler's resolver — they do not produce this warning. I left them alone to keep this change focused; the new test deliberately only checks runtime imports. Happy to follow up separately if you want those tidied.

Preventing recurrence

There was no test guarding the exports map, which is why this regressed silently — Metro only warns and falls back, so nothing failed in CI. The new test in scripts/monorepo-tests resolves every react-native/... subpath imported at runtime by a published package against the exports map, and fails if one is unreachable.

Two things it has to work around, both documented in the test:

  • Jest's resolver (packages/jest-preset/jest/resolver.js) strips exports, and Jest's patched Node module resolution ignores exports too — createRequire(...).resolve() inside Jest resolves unexported paths happily. The test therefore resolves the map directly instead of relying on require.resolve.
  • Files excluded from every package's files field (__tests__, __mocks__, src/private/testing, …) are skipped, since their imports never ship.

Changelog:

[GENERAL] [FIXED] - Fix package-exports warning from Metro when rendering FlatList, caused by @react-native/virtualized-lists importing an unexported react-native subpath

Test Plan:

Reproduced the user-visible symptom using the reporter's reproducer against published react-native@0.87.0 — no simulator needed:

npx react-native bundle --platform ios --dev true --entry-file index.js \
  --bundle-output /tmp/ReproducerApp.jsbundle --assets-dest /tmp/ReproducerApp-assets

Before (stock react-native@0.87.0):

WARN  Attempted to import the module ".../node_modules/react-native/src/private/featureflags/ReactNativeFeatureFlags"
which is not listed in the "exports" of ".../node_modules/react-native" under the requested subpath
"./src/private/featureflags/ReactNativeFeatureFlags". Falling back to file-based resolution.
Consider updating the call site or asking the package maintainer(s) to expose this API.

After applying this exports entry to that app's node_modules/react-native/package.json: 0 warnings, and cmp reports the two bundles are byte-identical — confirming the fix changes only the resolution path, not emitted code.

Counterfactual for the new test — reverting the package.json change makes it fail with:

- Expected  - 0
+ Received  + 2

+ "@react-native/virtualized-lists: Lists/VirtualizeUtils.js imports
+  'react-native/src/private/featureflags/ReactNativeFeatureFlags',
+  which is not listed in react-native's \"exports\"",
+ "@react-native/virtualized-lists: Lists/VirtualizedList.js imports
+  'react-native/src/private/featureflags/ReactNativeFeatureFlags',
+  which is not listed in react-native's \"exports\"",

Restoring it makes it pass again.

I also cross-checked the test's exports resolution against Node's real resolver (outside Jest, where it behaves correctly) over all 72 react-native/... specifiers in the repo plus synthetic wildcard/conditional/missing-path probes: they agree on every one.

Full suite, on main + this change:

yarn test        →  218 suites passed, 5586 passed / 1 skipped, 0 failed
                    (baseline on main: 5585 passed; +1 is the new test)
yarn lint        →  clean (eslint --max-warnings 0)
yarn flow-check  →  Found 0 errors

react#57484 removed the "./src/*" exports mapping from the react-native
package, on the basis that these import paths were never used
externally. @react-native/virtualized-lists is a separately published
package and a direct dependency of react-native, and it imports
'react-native/src/private/featureflags/ReactNativeFeatureFlags' at
runtime from VirtualizedList.js and VirtualizeUtils.js.

As a result, any app rendering FlatList makes Metro emit a
package-exports violation warning and fall back to file-based
resolution.

Re-export that single subpath, keeping "types": null so it stays hidden
from TypeScript as intended by react#57277.

Also add a monorepo test that resolves every react-native subpath
imported at runtime by a published package against the "exports" map,
so a mapping cannot be dropped again without CI failing.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Aug 13, 2026
@huntie

huntie commented Aug 13, 2026

Copy link
Copy Markdown
Member

Great spot! We'll need to solve this slightly differently however, I think by adding this API to react-private-interface.

import {ReactNativeFeatureFlags} from 'react-native/react-private-interface';

Patch here: https://gist.github.com/huntie/0523e2dd114dacf904d28211ad6b67be. Can you apply/retitle this PR to match? :)

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[0.87.0] VirtualizedList triggers Metro package-exports warning

2 participants