fix(resolution): leaf-aware subpath imports, real package.json, preset subpath shadowing#68
Merged
Merged
Conversation
…t subpath shadowing
Three resolution-fidelity gaps around deep (subpath) imports, all silent
failures:
- react-native subpath default exports (mock engine): import Platform from
'react-native/Libraries/Utilities/Platform' received the whole mock object,
so Platform.OS was undefined with no error. Virtual subpath modules and the
CJS bridge now derive the intended export from the subpath basename and
serve it as the default; CJS requires get Babel-interop shape via a live
wrapper so both direct-property and _interopRequireDefault consumers work.
Unknown leaves keep the whole-mock fallback.
- Package manifests: require('react-native/package.json').version (and preset
equivalents) read the mock instead of the real file, breaking version
gates. Both interception layers now exempt manifests, falling back to the
mock only when the package is not installed.
- Preset subpath shadowing (both engines): deep imports of preset packages
(react-native-gesture-handler/Swipeable) bypassed the preset mock and
loaded real native-runtime code, or failed resolution on versions that no
longer ship the deep file. All three redirect layers (Vite plugin, ESM
loader hook, CJS require hook) now match subpaths leaf-aware. Exempt:
JSON, asset extensions, and Node-safe utility entries (jest-utils,
jestSetup, mock, plugin), which pass through to the real file. Unknown
leaves warn under diagnostics.
CJS interop wrappers are memoized per specifier keyed by the per-package
mock object — not the container, which the hot runtime reuses across files
while rebuilding the per-package values (a container-keyed memo served
file 1's mocks to every later file in a hot worker; caught in review with a
two-file repro, now guarded by a cross-file identity test).
The end-to-end assertion required RNGH 3.0's real jest-utils module graph to load under Node — its ESM flavor's extensionless imports fail to resolve on some environments, which is the package's own Node compatibility, not this project's contract (and matches the pre-redirect status quo for these paths). The behavioral guarantee — utility subpaths are never shadowed — is now asserted at the loader and plugin resolution layers, with both packages registered as presets so the pass-through is attributable to the utility-leaf exemption.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three resolution-fidelity gaps around deep (subpath) imports, all failing silently:
import Platform from 'react-native/Libraries/Utilities/Platform'— the dominant deep-import pattern in ecosystem libraries and migrated Jest suites — received the entire mock object asPlatform, soPlatform.OSwas silentlyundefined. Same on the CJS side.require('react-native/package.json').version(a common version gate) read the mock and gotundefined, sending version-gated libraries down wrong code paths.import Swipeable from 'react-native-gesture-handler/Swipeable'bypassed the preset mock entirely and loaded the package's real native-runtime code — or failed resolution outright on package versions that no longer ship the deep file (RNGH 3.x dropped/Swipeable).Change
{ __esModule, default }via a live Proxy wrapper) so bothrequire('.../Platform').OSand_interopRequireDefaultconsumers work. Unknown leaves keep the previous whole-mock fallback.package.jsonsubpaths, resolving the real manifest; the mock fallback is kept when the package is not installed.resolveId/load, ESM loader hook, CJS require hook) now match subpaths of preset packages, leaf-aware. Exempt: JSON subpaths, asset-extension subpaths (fonts/images keep stubbing from their real files), and Node-safe utility entries (jest-utils,jestSetup,mock,plugin), which pass through to the real file. Unknown leaves fall back to the root mock and warn underdiagnostics.__vitest_native_preset_mockscontainer, which the hot runtime reuses across files while rebuilding the per-package values. (An earlier container-keyed draft served file 1's mocks to every later file in a hot worker; caught in pre-merge review via a two-file repro.)Tests
tests/regression.test.ts: leaf default exports (ESM + CJS), interop shape, identity stability, real-manifest reads (ESM + CJS).tests/native-unit.test.ts: loader subpath redirect + JSON/asset passthrough, specifier helpers, plugin-level resolveId/load for preset subpaths,react-native/package.jsoncarve-out, leaf-aware virtual code generation.tests-native/preset-subpath.test.tsx(runs under stock and hot configs): deep ESM import renders via the preset mock, root-vs-subpath identity, utility passthrough (jest-utils), real-manifest read.tests-native/preset-subpath-cross-file.test.tsx: cross-file twin — catches stale subpath caches in a shared hot worker as an identity mismatch. Verified in a single hot worker with--maxWorkers=1 --no-file-parallelism.Full gate: mock suite 1291 passed, native suites (stock/hot/hot-isolation/android/navigation) green, crosscheck 75/75, packed consumer fixtures green, lint/format/typecheck clean.