test(mobile): refuse an unlisted native package by the member a mount reads

Both emitted interop helpers short-circuit on `__esModule`, so answering `true`
hands the refusing trap back unwrapped to `__importDefault` and `__importStar`.
All three import forms now load the importer and throw the named refusal at the
first member read, instead of a namespace import silently yielding `undefined`
and failing later at the call.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-15 17:34:01 -04:00
parent af31269a83
commit f1ea5442bb
2 changed files with 24 additions and 20 deletions
@@ -54,11 +54,11 @@ describe('the mounted module loader', () => {
})
/**
* `__importDefault` reads `__esModule` before any member, so refusing it kills a default import of
* an unlisted package at module load — before the recording can show which member was wanted, and
* for modules that never touch the package at all.
* Both interop helpers short-circuit on `__esModule`, so the trap binds as the module itself in
* all three import forms: the module loads, and the refusal lands on the member a recording
* actually wanted rather than on every module that merely mentions the package.
*/
it('defers an unlisted package refusal from module load to the first member read', () => {
it('lets a default import of an unlisted package load, and refuses the member it uses', () => {
const modules = loaderOver({
'uses-default.ts': `
import Animated from 'react-native-not-substituted'
@@ -66,8 +66,9 @@ describe('the mounted module loader', () => {
`
})
const { read } = modules.load<{ read: () => unknown }>('mobile/src/uses-default.ts')
expect(typeof read).toBe('function')
expect(() => read()).toThrow(
'Unspecified native mounting dependency: react-native-not-substituted'
'Unspecified native mounting dependency: react-native-not-substituted.default'
)
})
@@ -79,16 +80,16 @@ describe('the mounted module loader', () => {
`
})
const { read } = modules.load<{ read: () => unknown }>('mobile/src/uses-named.ts')
expect(() => read()).toThrow('Unspecified native mounting dependency: expo-not-substituted')
expect(() => read()).toThrow(
'Unspecified native mounting dependency: expo-not-substituted.thing'
)
})
/**
* The one shape that loses the named refusal: `__importStar` copies own keys into a fresh object,
* of which the trap has none, so an unlisted member reads back `undefined` and fails at the call.
* Pinned rather than left implicit — it is what lets a screen load a module such as
* `platform/haptics.ts`, which imports a device package it only touches on a press.
* What lets a screen mount a module such as `platform/haptics.ts`, which imports a device package
* it only touches on a press: loading the importer is not itself a use.
*/
it('lets a namespace import of an unlisted package load, with undefined members', () => {
it('lets a namespace import of an unlisted package load, and refuses the member it reads', () => {
const modules = loaderOver({
'uses-namespace.ts': `
import * as Haptics from 'expo-not-substituted'
@@ -96,6 +97,9 @@ describe('the mounted module loader', () => {
`
})
const { read } = modules.load<{ read: () => unknown }>('mobile/src/uses-namespace.ts')
expect(read()).toBeUndefined()
expect(typeof read).toBe('function')
expect(() => read()).toThrow(
'Unspecified native mounting dependency: expo-not-substituted.selectionAsync'
)
})
})
@@ -55,17 +55,17 @@ export function operationModuleLoader(
return new Proxy(
{},
{
// `__esModule` is the module system's interop marker, not a native API. Refusing it kills
// a default import inside `__importDefault`, before any member is read; answering
// `undefined` defers the refusal to the first real member, the same rule the substitute
// traps follow. A namespace import is the one shape that loses it: `__importStar` copies
// own keys into a fresh object, of which there are none, so an unlisted member of an
// `import * as` reads back `undefined` and fails at the call instead of at the read.
// `__esModule` is the module system's interop marker, not a native API. Answering `true`
// makes both emitted interop helpers hand this trap straight back — `__importDefault`
// returns it instead of wrapping it, `__importStar` returns it instead of copying its
// (absent) own keys — so all three import forms load and refuse at the first member read,
// naming the member. Substitute partials answer `undefined` instead, because there a real
// member object must bind as the module's default.
get: (_target, key) => {
if (key === '__esModule') {
return undefined
return true
}
throw new Error(`Unspecified native mounting dependency: ${name}`)
throw new Error(`Unspecified native mounting dependency: ${name}.${String(key)}`)
}
}
)