Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions packages/metro-runtime/src/polyfills/__tests__/require-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,216 @@ describe('require', () => {
moduleSystem.__r(0);
});

test('mode=1 returns exports directly for ES6 modules (namespace-shaped return)', () => {
createModuleSystem(moduleSystem, false, '');

createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
const ns = importDefault(1, 1);
expect(ns.default).toEqual({bar: 'bar'});
// For ESM, the helper returns exports itself (which has .default).
expect(ns).toBe(require(1));
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
exports.__esModule = true;
exports.default = {bar: 'bar'};
exports.other = 'other';
},
);

expect.assertions(2);
moduleSystem.__r(0);
});

test('mode=1 wraps CJS exports as {default: exports} (namespace-shaped return)', () => {
createModuleSystem(moduleSystem, false, '');

createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
const ns = importDefault(1, 1);
expect(ns.default).toEqual({bar: 'bar'});
// Wrapper's .default IS the module's exports (for CJS).
expect(ns.default).toBe(require(1));
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
module.exports = {bar: 'bar'};
},
);

expect.assertions(2);
moduleSystem.__r(0);
});

test('mode=1 preserves CJS liveness for module.exports=X post-init reassignment', () => {
createModuleSystem(moduleSystem, false, '');

let saved;
createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
// First read - captures the initial exports.
const first = importDefault(1, 1).default;
expect(first).toEqual({tag: 'initial'});

// Reassign source module.exports via saved reference (mimics a
// captured `module` in a lazy handler).
saved.exports = {tag: 'REASSIGNED'};

// Second read - must see the fresh exports, not the cached wrapper.
// The helper is non-memoising: each call re-invokes metroRequire
// (which returns publicModule.exports fresh) and rewraps.
const second = importDefault(1, 1).default;
expect(second).toEqual({tag: 'REASSIGNED'});
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
module.exports = {tag: 'initial'};
saved = module;
},
);

expect.assertions(2);
moduleSystem.__r(0);
});

test('mode=0/undefined keeps memoising, unchanged by the mode argument', () => {
// The legacy 1-arg path is deliberately left as it was: the first
// resolution is cached on the module definition and reused. This is the
// contrast to the mode=1 test above, and pins the fact that adding the
// mode argument did not alter behaviour for existing callers.
createModuleSystem(moduleSystem, false, '');

let saved;
createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
const first = importDefault(1);
expect(first).toEqual({tag: 'initial'});

saved.exports = {tag: 'REASSIGNED'};

// Memoised: still the value captured on the first call.
const second = importDefault(1);
expect(second).toEqual({tag: 'initial'});
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
module.exports = {tag: 'initial'};
saved = module;
},
);

expect.assertions(2);
moduleSystem.__r(0);
});

test('mode=1 for ESM preserves live default reassignment', () => {
createModuleSystem(moduleSystem, false, '');

let sourceExports;
createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
const first = importDefault(1, 1).default;
expect(first).toBe('a');

sourceExports.default = 'b';

// ESM path: helper returns exports directly. `.default` on that is
// a live property read.
const second = importDefault(1, 1).default;
expect(second).toBe('b');
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
exports.__esModule = true;
exports.default = 'a';
sourceExports = exports;
},
);

expect.assertions(2);
moduleSystem.__r(0);
});

test('mode=0/undefined preserves the legacy value-shaped return', () => {
createModuleSystem(moduleSystem, false, '');

createModule(
moduleSystem,
0,
'foo.js',
(global, require, importDefault, importAll, module, exports) => {
// No mode arg = legacy behaviour = returns the value directly.
expect(importDefault(1)).toEqual({bar: 'bar'});
expect(importDefault(1, 0)).toEqual({bar: 'bar'});
expect(importDefault(2)).toBe(null);
expect(importDefault(2, 0)).toBe(null);
},
);

createModule(
moduleSystem,
1,
'bar.js',
(global, require, importDefault, importAll, module, exports) => {
exports.__esModule = true;
exports.default = {bar: 'bar'};
},
);

createModule(
moduleSystem,
2,
'nullcjs.js',
(global, require, importDefault, importAll, module, exports) => {
module.exports = null;
},
);

expect.assertions(4);
moduleSystem.__r(0);
});

test('supports named imports', () => {
createModuleSystem(moduleSystem, false, '');

Expand Down
18 changes: 18 additions & 0 deletions packages/metro-runtime/src/polyfills/require.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ function shouldPrintRequireCycle(modules: ReadonlyArray<?string>): boolean {

function metroImportDefault(
moduleId: ModuleID | VerboseModuleNameForDev,
experimentalMode?: number,
): any | Exports {
if (__DEV__ && typeof moduleId === 'string') {
const verboseName = moduleId;
Expand All @@ -248,6 +249,23 @@ function metroImportDefault(
//$FlowFixMe[incompatible-type]: at this point we know that moduleId is a number
const moduleIdReallyIsNumber: number = moduleId;

if (experimentalMode === 1) {
// Mode 1: namespace-shaped return. Consumers do `ns.default` at each read
// site (the default-import emission under `unstable_liveBindings`). For
// ESM the exports object already has `.default`; for CJS we wrap it as
// `{default: exports}` so the accessor resolves to the module's exports,
// which for CJS is the default.
//
// Deliberately not memoised, unlike the value-shaped path below. The CJS
// wrapper is allocated per call, and caching it would pin `.default` to
// whichever exports object was current on the first call - hiding a later
// `module.exports = X`. Re-resolving keeps the read live. Under the
// serialiser rewrite for proven-classified deps this helper is bypassed
// entirely, so only the unclassified-fallback slice reaches here.
const exports: Exports = metroRequire(moduleIdReallyIsNumber);
return exports && exports.__esModule ? exports : {default: exports};
}

const maybeInitializedModule = modules.get(moduleIdReallyIsNumber);

if (
Expand Down
Loading
Loading