Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1767023508983.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: prerelease
---

The module mocking system has been rewritten to improve compatibility with different versions of React Native. Instead of fully overwriting Metro's module system, the new implementation surgically redirects responsibility for imports to Harness, allowing for better integration with various React Native versions while maintaining the same mocking capabilities. The module mocking API has been slightly modified as part of this rewrite.
27 changes: 7 additions & 20 deletions apps/playground/src/__tests__/mocking/modules.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import {
unmock,
requireActual,
fn,
clearMocks,
resetModules,
} from 'react-native-harness';

describe('Module mocking', () => {
afterEach(() => {
// Clean up mocks after each test
clearMocks();
resetModules();
});

it('should not interfere with modules that are not mocked', () => {
const moduleA = require('react-native');
const moduleB = require('react-native');
expect(moduleA === moduleB).toBe(true);
});

it('should completely mock a module and return mock implementation', () => {
Expand Down Expand Up @@ -133,21 +137,4 @@ describe('Module mocking', () => {
const newNow = require('react-native').now;
expect(newNow).not.toBe(oldNow);
});

it('should unmock all modules when clearMocks is called', () => {
// Mock a module
const mockFactory = () => ({ mockProperty: 'mocked' });
mock('react-native', mockFactory);

// Verify it's mocked
const module = require('react-native');
expect(module.mockProperty).toBe('mocked');

// Unmock all modules
clearMocks();

// Verify it's back to actual
const actualModule = require('react-native');
expect(actualModule).not.toHaveProperty('mockProperty');
});
});
61 changes: 0 additions & 61 deletions packages/metro/src/moduleSystem.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/metro/src/withRnHarness.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { MetroConfig } from 'metro-config';
import { getConfig } from '@react-native-harness/config';
import { patchModuleSystem } from './moduleSystem';
import { getHarnessResolver } from './resolver';
import { getHarnessManifest } from './manifest';
import { getHarnessBabelTransformerPath } from './babel-transformer';
Expand All @@ -25,8 +24,6 @@ export const withRnHarness = <T extends MetroConfig>(
const metroConfig = await config;
const { config: harnessConfig } = await getConfig(process.cwd());

patchModuleSystem();

const harnessResolver = getHarnessResolver(metroConfig, harnessConfig);
const harnessManifest = getHarnessManifest(harnessConfig);
const harnessBabelTransformerPath =
Expand All @@ -40,6 +37,9 @@ export const withRnHarness = <T extends MetroConfig>(
getPolyfills: (...args) => [
...(metroConfig.serializer?.getPolyfills?.(...args) ?? []),
harnessManifest,
require.resolve(
'@react-native-harness/runtime/polyfills/harness-module-system'
),
],
isThirdPartyModule({ path: modulePath }) {
const isThirdPartyByDefault =
Expand Down
73 changes: 73 additions & 0 deletions packages/runtime/assets/harness-module-system.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @ts-nocheck
/* eslint-disable */

// This file is a polyfill that monkey-patches the Metro module system
// to allow capturing nested require calls.

(function (globalObject) {
const myRequire = function (id) {
return globalObject.__r(id);
};

const myImportDefault = function (id) {
return globalObject.__r.importDefault(id);
};

const myImportAll = function (id) {
return globalObject.__r.importAll(id);
};

// Monkey-patch define
const originalDefine = globalObject.__d;
globalObject.__d = function (factory, moduleId, dependencyMap) {
const wrappedFactory = function (...args) {
// Standard Metro with import support (7 arguments)
// args: global, require, importDefault, importAll, module, exports, dependencyMap
const global = args[0];
const moduleObject = args[4];
const exports = args[5];
const depMap = args[6];

return factory(
global,
myRequire,
myImportDefault,
myImportAll,
moduleObject,
exports,
depMap
);
};

// Call the original define with the wrapped factory
return originalDefine.call(this, wrappedFactory, moduleId, dependencyMap);
};

globalObject.__resetModule = function (moduleId) {
const module = globalObject.__r.getModules().get(moduleId);

if (!module) {
return;
}

module.hasError = false;
module.error = undefined;
module.isInitialized = false;
};

globalObject.__resetModules = function () {
const modules = globalObject.__r.getModules();

modules.forEach(function (mod, moduleId) {
globalObject.__resetModule(moduleId);
});
};
})(
typeof globalThis !== 'undefined'
? globalThis
: typeof global !== 'undefined'
? global
: typeof window !== 'undefined'
? window
: this
);
Loading