From c939e2e7a717b188399ab235db740c2215f33807 Mon Sep 17 00:00:00 2001 From: Francesco-Voto Date: Mon, 24 Aug 2026 11:35:54 +0200 Subject: [PATCH 1/7] chore: support rn 0.87 --- .../src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts index 433228f33..bb799eff6 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts @@ -47,9 +47,9 @@ export class NativeEntryPlugin { : undefined ); - const getReactNativePolyfills: () => string[] = require( - path.join(reactNativePath, 'rn-get-polyfills.js') - ); + const rnGetPolyfillsPath = path.join(reactNativePath, 'rn-get-polyfills.js'); + const getReactNativePolyfills = require(require('node:fs').existsSync(rnGetPolyfillsPath) ? rnGetPolyfillsPath : '@react-native/js-polyfills'); + const initializeCorePath = this.config?.initializeCoreLocation ?? From 9438ca8c268a718ebcbb9fe02a4ea184d0217acb Mon Sep 17 00:00:00 2001 From: Francesco-Voto Date: Tue, 25 Aug 2026 16:51:30 +0200 Subject: [PATCH 2/7] fix: lint --- .../plugins/NativeEntryPlugin/NativeEntryPlugin.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts index bb799eff6..ddbebc7e2 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts @@ -47,9 +47,15 @@ export class NativeEntryPlugin { : undefined ); - const rnGetPolyfillsPath = path.join(reactNativePath, 'rn-get-polyfills.js'); - const getReactNativePolyfills = require(require('node:fs').existsSync(rnGetPolyfillsPath) ? rnGetPolyfillsPath : '@react-native/js-polyfills'); - + const rnGetPolyfillsPath = path.join( + reactNativePath, + 'rn-get-polyfills.js' + ); + const getReactNativePolyfills = require( + require('node:fs').existsSync(rnGetPolyfillsPath) + ? rnGetPolyfillsPath + : '@react-native/js-polyfills' + ); const initializeCorePath = this.config?.initializeCoreLocation ?? From 527aa348e060f14dccbc9ce2fd2a7b292ba7dd79 Mon Sep 17 00:00:00 2001 From: Francesco-Voto Date: Fri, 11 Sep 2026 10:37:56 +0200 Subject: [PATCH 3/7] chore: add check about what is present in the rn version --- packages/repack/src/loaders/assetsLoader/extractAssets.ts | 7 ++++++- packages/repack/src/modules/IncludeModules.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/repack/src/loaders/assetsLoader/extractAssets.ts b/packages/repack/src/loaders/assetsLoader/extractAssets.ts index f50d91e80..a88d28b0d 100644 --- a/packages/repack/src/loaders/assetsLoader/extractAssets.ts +++ b/packages/repack/src/loaders/assetsLoader/extractAssets.ts @@ -56,7 +56,12 @@ export function extractAssets( ); return dedent` - var AssetRegistry = require('react-native/Libraries/Image/AssetRegistry'); + var AssetRegistry; + try { + AssetRegistry = require('react-native/asset-registry'); + } catch (e) { + AssetRegistry = require('react-native/Libraries/Image/AssetRegistry'); + } module.exports = AssetRegistry.registerAsset({ __packager_asset: true, scales: ${JSON.stringify(scales)}, diff --git a/packages/repack/src/modules/IncludeModules.ts b/packages/repack/src/modules/IncludeModules.ts index 11b0ae9fb..bb9b4745b 100644 --- a/packages/repack/src/modules/IncludeModules.ts +++ b/packages/repack/src/modules/IncludeModules.ts @@ -5,5 +5,10 @@ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -require.resolve('react-native/Libraries/Image/AssetRegistry'); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + require.resolve('react-native/Libraries/Image/AssetRegistry'); +} catch { + require.resolve('react-native/asset-registry'); +} require.resolve('react-native/Libraries/Image/AssetSourceResolver'); From d47bd38d35556ced750dadcd32bb02e531e0a8ee Mon Sep 17 00:00:00 2001 From: Francesco-Voto Date: Fri, 11 Sep 2026 16:00:35 +0200 Subject: [PATCH 4/7] test: update test --- .../rspack/NativeEntryPlugin.test.ts.snap | 35 ++++++++++++++++--- .../webpack/NativeEntryPlugin.test.ts.snap | 35 ++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap index 410b2ec7b..eb452170e 100644 --- a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap @@ -15,7 +15,12 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -767,7 +772,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -967,7 +977,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1167,7 +1182,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1409,7 +1429,12 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), diff --git a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap index 1271284b3..e4855d7c9 100644 --- a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap @@ -17,7 +17,12 @@ exports[`NativeEntryPlugin > with Module Federation v1 > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -228,7 +233,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -552,7 +562,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -881,7 +896,12 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -1246,7 +1266,12 @@ exports[`NativeEntryPlugin > without Module Federation > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. +try { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} catch { + /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +} /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, From b99756ddf240dd595458558e47242fc4349d7386 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sat, 12 Sep 2026 17:35:02 +0100 Subject: [PATCH 5/7] fix(repack): resolve RN asset registry and polyfills across 0.86/0.87 layouts Centralize the React Native layout decision in Node instead of emitting try/catch fallbacks. Emit a single canonical asset-registry request mapped to the real file via a resolve alias, and resolve polyfills from rn-get-polyfills.js when present or @react-native/js-polyfills otherwise. Suggested-changes checkpoint for PR #1443. --- .changeset/rn-087-native-runtime.md | 11 ++ .../src/loaders/assetsLoader/extractAssets.ts | 7 +- packages/repack/src/modules/IncludeModules.ts | 11 +- .../NativeEntryPlugin/NativeEntryPlugin.ts | 29 +++-- .../__tests__/reactNativeRuntime.test.ts | 90 +++++++++++++++ .../NativeEntryPlugin/reactNativeRuntime.ts | 108 ++++++++++++++++++ tests/integration/src/helpers.ts | 5 +- .../rspack/NativeEntryPlugin.test.ts.snap | 55 ++++----- .../webpack/NativeEntryPlugin.test.ts.snap | 55 ++++----- 9 files changed, 290 insertions(+), 81 deletions(-) create mode 100644 .changeset/rn-087-native-runtime.md create mode 100644 packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts create mode 100644 packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts diff --git a/.changeset/rn-087-native-runtime.md b/.changeset/rn-087-native-runtime.md new file mode 100644 index 000000000..21fbc6618 --- /dev/null +++ b/.changeset/rn-087-native-runtime.md @@ -0,0 +1,11 @@ +--- +"@callstack/repack": patch +--- + +Support React Native 0.87's runtime layout. The asset registry request and the +polyfill list are now resolved in Node once per build instead of through emitted +`try`/`catch` fallbacks. A canonical `react-native/asset-registry` request is +aliased to the real file (`Libraries/Image/AssetRegistry` on <= 0.86, +`src/asset-registry.js` on >= 0.87), and polyfills are taken from +`rn-get-polyfills.js` when present or from `@react-native/js-polyfills` otherwise, +with a clear error when neither can be resolved. diff --git a/packages/repack/src/loaders/assetsLoader/extractAssets.ts b/packages/repack/src/loaders/assetsLoader/extractAssets.ts index a88d28b0d..132d3aa69 100644 --- a/packages/repack/src/loaders/assetsLoader/extractAssets.ts +++ b/packages/repack/src/loaders/assetsLoader/extractAssets.ts @@ -56,12 +56,7 @@ export function extractAssets( ); return dedent` - var AssetRegistry; - try { - AssetRegistry = require('react-native/asset-registry'); - } catch (e) { - AssetRegistry = require('react-native/Libraries/Image/AssetRegistry'); - } + var AssetRegistry = require('react-native/asset-registry'); module.exports = AssetRegistry.registerAsset({ __packager_asset: true, scales: ${JSON.stringify(scales)}, diff --git a/packages/repack/src/modules/IncludeModules.ts b/packages/repack/src/modules/IncludeModules.ts index bb9b4745b..ce603e97f 100644 --- a/packages/repack/src/modules/IncludeModules.ts +++ b/packages/repack/src/modules/IncludeModules.ts @@ -5,10 +5,9 @@ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - require.resolve('react-native/Libraries/Image/AssetRegistry'); -} catch { - require.resolve('react-native/asset-registry'); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the `react-native/` prefix +// preserves Module Federation deep-import sharing. +require.resolve('react-native/asset-registry'); require.resolve('react-native/Libraries/Image/AssetSourceResolver'); diff --git a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts index ddbebc7e2..a01abc0e9 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts @@ -3,6 +3,10 @@ import type { ResolveAlias, Compiler as RspackCompiler } from '@rspack/core'; import type { Compiler as WebpackCompiler } from 'webpack'; import { isRspackCompiler, moveElementBefore } from '../../helpers/index.js'; import { makePolyfillsRuntimeModule } from './PolyfillsRuntimeModule.js'; +import { + getReactNativeAssetRegistryAlias, + resolveReactNativePolyfills, +} from './reactNativeRuntime.js'; export interface NativeEntryPluginConfig { /** @@ -47,16 +51,25 @@ export class NativeEntryPlugin { : undefined ); - const rnGetPolyfillsPath = path.join( - reactNativePath, - 'rn-get-polyfills.js' - ); - const getReactNativePolyfills = require( - require('node:fs').existsSync(rnGetPolyfillsPath) - ? rnGetPolyfillsPath - : '@react-native/js-polyfills' + const getReactNativePolyfills = resolveReactNativePolyfills( + compiler.context, + reactNativePath ); + // Map the canonical `react-native/asset-registry` request (emitted by the + // assets loader and IncludeModules) to the registry file that exists for + // the installed React Native layout. Done here because Repack's default + // resolver ignores `package.json` exports and the two layouts are not + // reachable by a single request across both resolver modes. + const assetRegistryAlias = + getReactNativeAssetRegistryAlias(reactNativePath); + if (assetRegistryAlias) { + compiler.options.resolve.alias = { + ...compiler.options.resolve.alias, + ...assetRegistryAlias, + }; + } + const initializeCorePath = this.config?.initializeCoreLocation ?? path.join(reactNativePath, 'Libraries/Core/InitializeCore.js'); diff --git a/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts b/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts new file mode 100644 index 000000000..de3df9947 --- /dev/null +++ b/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts @@ -0,0 +1,90 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { + ASSET_REGISTRY_REQUEST, + getReactNativeAssetRegistryAlias, + resolveReactNativePolyfills, +} from '../reactNativeRuntime.js'; + +function makeTmpRn(files: Record) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-layout-')); + for (const [rel, contents] of Object.entries(files)) { + const target = path.join(dir, rel); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, contents); + } + return dir; +} + +describe('resolveReactNativePolyfills', () => { + it('uses rn-get-polyfills.js when present (React Native <= 0.86)', () => { + const rn = makeTmpRn({ + 'rn-get-polyfills.js': + "module.exports = () => [require.resolve('./console.js')];", + 'console.js': '// polyfill', + }); + const getPolyfills = resolveReactNativePolyfills(rn, rn); + expect(typeof getPolyfills).toBe('function'); + const paths = getPolyfills(); + expect(Array.isArray(paths)).toBe(true); + expect(paths).toHaveLength(1); + expect(path.basename(paths[0])).toBe('console.js'); + }); + + it('falls back to @react-native/js-polyfills when rn-get-polyfills.js is absent', () => { + // React Native >= 0.87 layout: no rn-get-polyfills.js. Resolution of + // `@react-native/js-polyfills` is delegated to require.resolve, so under the + // test harness it resolves from the installed tree. We only assert the + // documented contract: a function returning an array of absolute paths. + const rn = makeTmpRn({ 'index.js': '' }); + const getPolyfills = resolveReactNativePolyfills(rn, rn); + expect(typeof getPolyfills).toBe('function'); + const paths = getPolyfills(); + expect(Array.isArray(paths)).toBe(true); + expect(paths.every((p) => path.isAbsolute(p))).toBe(true); + }); +}); + +describe('getReactNativeAssetRegistryAlias', () => { + it('maps to src/asset-registry on React Native >= 0.87 layout', () => { + const rn = makeTmpRn({ + 'src/asset-registry.js': 'module.exports = {};', + }); + const alias = getReactNativeAssetRegistryAlias(rn); + expect(alias).toEqual({ + [`${ASSET_REGISTRY_REQUEST}$`]: path.join(rn, 'src', 'asset-registry'), + }); + }); + + it('maps to Libraries/Image/AssetRegistry on React Native <= 0.86 layout', () => { + const rn = makeTmpRn({ + 'Libraries/Image/AssetRegistry.js': 'module.exports = {};', + }); + const alias = getReactNativeAssetRegistryAlias(rn); + expect(alias).toEqual({ + [`${ASSET_REGISTRY_REQUEST}$`]: path.join( + rn, + 'Libraries', + 'Image', + 'AssetRegistry' + ), + }); + }); + + it('prefers the modern layout when both are present', () => { + const rn = makeTmpRn({ + 'src/asset-registry.js': 'module.exports = {};', + 'Libraries/Image/AssetRegistry.js': 'module.exports = {};', + }); + const alias = getReactNativeAssetRegistryAlias(rn); + expect(alias?.[`${ASSET_REGISTRY_REQUEST}$`]).toBe( + path.join(rn, 'src', 'asset-registry') + ); + }); + + it('returns null when no registry file exists (test fixtures)', () => { + const rn = makeTmpRn({ 'index.js': '' }); + expect(getReactNativeAssetRegistryAlias(rn)).toBeNull(); + }); +}); diff --git a/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts b/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts new file mode 100644 index 000000000..b436efc9c --- /dev/null +++ b/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts @@ -0,0 +1,108 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * Canonical request used to reach React Native's asset registry. + * + * React Native <= 0.86 exposed the registry at + * `react-native/Libraries/Image/AssetRegistry`; 0.87 moved it to + * `src/asset-registry.js`, reachable only through the package `exports` map. + * There is no single request string that resolves across both layouts under + * both of Repack's resolver modes (the default resolver ignores `exports`, + * while `enablePackageExports` honours it and 0.87 dropped the `./*` wildcard). + * + * We therefore always emit this canonical request and map it to the real file + * with a resolve alias (see {@link getReactNativeAssetRegistryAlias}). Keeping + * the `react-native/` prefix is required so Module Federation's deep-import + * sharing (`shared['react-native/']`) continues to treat it as a singleton. + */ +export const ASSET_REGISTRY_REQUEST = 'react-native/asset-registry'; + +/** + * Resolves React Native's polyfill list, returning the same + * `() => string[]` contract as the historic `rn-get-polyfills.js`. + * + * React Native <= 0.86 shipped `rn-get-polyfills.js` at the package root, which + * re-exported `@react-native/js-polyfills` (a direct dependency of react-native). + * 0.87 removed that file and dropped the dependency entirely, so the polyfills + * are now only reachable through packages that still pull them in (e.g. + * `@react-native/metro-config`, itself an optional peer of the CLI plugin). + * + * The polyfills are inlined into the emitted bundle, so they must be resolvable + * for production bundles too - they cannot be treated as dev-only. We resolve + * from every plausible location and fail with an actionable message rather than + * a cryptic `MODULE_NOT_FOUND` from Repack's own directory. + */ +export function resolveReactNativePolyfills( + projectRoot: string, + reactNativePath: string +): () => string[] { + const rnGetPolyfillsPath = path.join(reactNativePath, 'rn-get-polyfills.js'); + if (fs.existsSync(rnGetPolyfillsPath)) { + return require(rnGetPolyfillsPath) as () => string[]; + } + + // React Native >= 0.87: locate `@react-native/js-polyfills` from locations + // that own it, following the same "resolve the owner, then chain" pattern + // used for the hermes parser. + const lookupPaths = [reactNativePath, projectRoot]; + try { + lookupPaths.push( + require.resolve('@react-native/metro-config', { paths: [projectRoot] }) + ); + } catch { + // metro-config is an optional peer; a missing entry is handled below. + } + + try { + const jsPolyfillsPath = require.resolve('@react-native/js-polyfills', { + paths: lookupPaths, + }); + return require(jsPolyfillsPath) as () => string[]; + } catch { + throw new Error( + '[RepackNativeEntryPlugin] Unable to locate React Native polyfills. ' + + "React Native >= 0.87 no longer depends on '@react-native/js-polyfills', " + + 'so Repack cannot resolve the polyfills that must be present in the ' + + 'bundle. Add a version-matched `@react-native/js-polyfills` (or ' + + '`@react-native/metro-config`) to your project so it is available while bundling.' + ); + } +} + +/** + * Builds the `resolve.alias` entry that maps the canonical + * {@link ASSET_REGISTRY_REQUEST} to the real registry file for the installed + * React Native layout. + * + * The alias target is extensionless so platform extensions (`.native.js`, + * `.ios.js`, ...) still apply. Returns `null` when no registry file is found + * (for example in test fixtures), in which case no alias is injected and the + * canonical request is left to fail resolution like any missing module. + */ +export function getReactNativeAssetRegistryAlias( + reactNativePath: string +): Record | null { + const modern = path.join(reactNativePath, 'src', 'asset-registry'); + const legacy = path.join( + reactNativePath, + 'Libraries', + 'Image', + 'AssetRegistry' + ); + + let target: string | undefined; + if (fs.existsSync(`${modern}.js`)) { + target = modern; + } else if (fs.existsSync(`${legacy}.js`)) { + target = legacy; + } + + if (!target) { + return null; + } + + // Exact-match alias (`$`) so only the canonical request is remapped, while + // the request keeps its `react-native/` prefix for Module Federation sharing. + return { [`${ASSET_REGISTRY_REQUEST}$`]: target }; +} diff --git a/tests/integration/src/helpers.ts b/tests/integration/src/helpers.ts index ea231fe62..a42a287bb 100644 --- a/tests/integration/src/helpers.ts +++ b/tests/integration/src/helpers.ts @@ -88,7 +88,10 @@ export function getReactNativeVirtualModules( 'node_modules/react-native/package.json': '{ "name": "react-native", "main": "./index.js" }', 'node_modules/react-native/index.js': `module.exports = { PixelRatio: { get: () => ${pixelRatio} } };`, - 'node_modules/react-native/Libraries/Image/AssetRegistry.js': + // Canonical asset registry request emitted by the assets loader. On a real + // app this is aliased to the version-appropriate file by NativeEntryPlugin; + // in tests the virtual React Native exposes it directly at this path. + 'node_modules/react-native/asset-registry.js': 'module.exports = { registerAsset: (spec) => spec };', 'node_modules/react-native/Libraries/Image/AssetSourceResolver.js': ` module.exports = class AssetSourceResolver { diff --git a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap index eb452170e..2e031dcdf 100644 --- a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap @@ -15,12 +15,11 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -772,12 +771,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -977,12 +975,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1182,12 +1179,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1429,12 +1425,11 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), diff --git a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap index e4855d7c9..7d5efcdd9 100644 --- a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap @@ -17,12 +17,11 @@ exports[`NativeEntryPlugin > with Module Federation v1 > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -233,12 +232,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -562,12 +560,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -896,12 +893,11 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -1266,12 +1262,11 @@ exports[`NativeEntryPlugin > without Module Federation > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// react-native/Libraries/Image/AssetRegistry was replaced by react-native/asset-registry in newer versions. -try { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} catch { - /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); -} +// Canonical asset registry request. NativeEntryPlugin aliases this to the real +// file for the installed React Native version (Libraries/Image/AssetRegistry on +// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix +// preserves Module Federation deep-import sharing. +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, From 7b31d9210ee609745d2eeda1e0676cb7eb1238ce Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sat, 12 Sep 2026 17:41:23 +0100 Subject: [PATCH 6/7] refactor(repack): keep legacy asset-registry request, alias only on 0.87 Emit react-native/Libraries/Image/AssetRegistry unchanged so the Module Federation share key and all <=0.86 behavior are untouched. Inject an exact-match alias to src/asset-registry.js only when the 0.87 layout is detected, prepended ahead of user aliases (enhanced-resolve and Rspack match aliases in insertion order). Make polyfill resolution injectable with hermetic unit tests for the shim / project-root / metro-config / error paths, and add a 0.87 src-layout integration test asserting alias ordering and resolution. Restore the legacy snapshots and virtual fixture. --- .changeset/rn-087-native-runtime.md | 13 +- .../src/loaders/assetsLoader/extractAssets.ts | 2 +- packages/repack/src/modules/IncludeModules.ts | 6 +- .../NativeEntryPlugin/NativeEntryPlugin.ts | 14 +- .../__tests__/reactNativeRuntime.test.ts | 146 +++++++++++++----- .../NativeEntryPlugin/reactNativeRuntime.ts | 118 ++++++++------ tests/integration/src/helpers.ts | 5 +- .../NativeEntryPlugin.srcLayout.test.ts | 105 +++++++++++++ .../Libraries/Core/InitializeCore.js | 1 + .../Libraries/Image/AssetSourceResolver.js | 1 + .../react-native-src-layout/index.js | 1 + .../react-native-src-layout/package.json | 1 + .../src/asset-registry.js | 1 + .../rspack/NativeEntryPlugin.test.ts.snap | 30 +--- .../webpack/NativeEntryPlugin.test.ts.snap | 30 +--- 15 files changed, 308 insertions(+), 166 deletions(-) create mode 100644 tests/integration/src/plugins/NativeEntryPlugin.srcLayout.test.ts create mode 100644 tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Core/InitializeCore.js create mode 100644 tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Image/AssetSourceResolver.js create mode 100644 tests/integration/src/plugins/__fixtures__/react-native-src-layout/index.js create mode 100644 tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json create mode 100644 tests/integration/src/plugins/__fixtures__/react-native-src-layout/src/asset-registry.js diff --git a/.changeset/rn-087-native-runtime.md b/.changeset/rn-087-native-runtime.md index 21fbc6618..7d8baf2fe 100644 --- a/.changeset/rn-087-native-runtime.md +++ b/.changeset/rn-087-native-runtime.md @@ -2,10 +2,9 @@ "@callstack/repack": patch --- -Support React Native 0.87's runtime layout. The asset registry request and the -polyfill list are now resolved in Node once per build instead of through emitted -`try`/`catch` fallbacks. A canonical `react-native/asset-registry` request is -aliased to the real file (`Libraries/Image/AssetRegistry` on <= 0.86, -`src/asset-registry.js` on >= 0.87), and polyfills are taken from -`rn-get-polyfills.js` when present or from `@react-native/js-polyfills` otherwise, -with a clear error when neither can be resolved. +Support React Native 0.87. Polyfills are read from `rn-get-polyfills.js` when +present, otherwise from `@react-native/js-polyfills` (resolved from the project, +falling back through `@react-native/metro-config`), with an actionable error when +neither can be found. The asset registry request is aliased to +`src/asset-registry.js` on the 0.87 layout; on 0.86 and earlier behaviour is +unchanged. diff --git a/packages/repack/src/loaders/assetsLoader/extractAssets.ts b/packages/repack/src/loaders/assetsLoader/extractAssets.ts index 132d3aa69..f50d91e80 100644 --- a/packages/repack/src/loaders/assetsLoader/extractAssets.ts +++ b/packages/repack/src/loaders/assetsLoader/extractAssets.ts @@ -56,7 +56,7 @@ export function extractAssets( ); return dedent` - var AssetRegistry = require('react-native/asset-registry'); + var AssetRegistry = require('react-native/Libraries/Image/AssetRegistry'); module.exports = AssetRegistry.registerAsset({ __packager_asset: true, scales: ${JSON.stringify(scales)}, diff --git a/packages/repack/src/modules/IncludeModules.ts b/packages/repack/src/modules/IncludeModules.ts index ce603e97f..11b0ae9fb 100644 --- a/packages/repack/src/modules/IncludeModules.ts +++ b/packages/repack/src/modules/IncludeModules.ts @@ -5,9 +5,5 @@ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the `react-native/` prefix -// preserves Module Federation deep-import sharing. -require.resolve('react-native/asset-registry'); +require.resolve('react-native/Libraries/Image/AssetRegistry'); require.resolve('react-native/Libraries/Image/AssetSourceResolver'); diff --git a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts index a01abc0e9..91f3ea520 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts @@ -56,17 +56,19 @@ export class NativeEntryPlugin { reactNativePath ); - // Map the canonical `react-native/asset-registry` request (emitted by the - // assets loader and IncludeModules) to the registry file that exists for - // the installed React Native layout. Done here because Repack's default - // resolver ignores `package.json` exports and the two layouts are not - // reachable by a single request across both resolver modes. + // Map `react-native/Libraries/Image/AssetRegistry` to the relocated + // `src/asset-registry.js` on the React Native >= 0.87 layout (no-op on <= 0.86). + // Done here because Repack's default resolver ignores `package.json` exports. + // The exact-match alias must be prepended: enhanced-resolve and Rspack match + // aliases in insertion order, so a user's generic `react-native` alias would + // otherwise win and rewrite the request to a non-existent path before the + // specific key is consulted. const assetRegistryAlias = getReactNativeAssetRegistryAlias(reactNativePath); if (assetRegistryAlias) { compiler.options.resolve.alias = { - ...compiler.options.resolve.alias, ...assetRegistryAlias, + ...compiler.options.resolve.alias, }; } diff --git a/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts b/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts index de3df9947..274eb619d 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/__tests__/reactNativeRuntime.test.ts @@ -7,8 +7,13 @@ import { resolveReactNativePolyfills, } from '../reactNativeRuntime.js'; -function makeTmpRn(files: Record) { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-layout-')); +const tmpDirs: string[] = []; + +function makeTmp(files: Record) { + const dir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'rn-layout-')) + ); + tmpDirs.push(dir); for (const [rel, contents] of Object.entries(files)) { const target = path.join(dir, rel); fs.mkdirSync(path.dirname(target), { recursive: true }); @@ -17,74 +22,131 @@ function makeTmpRn(files: Record) { return dir; } +afterEach(() => { + while (tmpDirs.length) { + const dir = tmpDirs.pop(); + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + } +}); + describe('resolveReactNativePolyfills', () => { it('uses rn-get-polyfills.js when present (React Native <= 0.86)', () => { - const rn = makeTmpRn({ + const rn = makeTmp({ 'rn-get-polyfills.js': "module.exports = () => [require.resolve('./console.js')];", 'console.js': '// polyfill', }); - const getPolyfills = resolveReactNativePolyfills(rn, rn); - expect(typeof getPolyfills).toBe('function'); + // Resolver would fail if consulted; reaching the result proves the shim was used. + const getPolyfills = resolveReactNativePolyfills(rn, rn, () => { + throw new Error('resolver should not be called when the shim exists'); + }); const paths = getPolyfills(); - expect(Array.isArray(paths)).toBe(true); expect(paths).toHaveLength(1); expect(path.basename(paths[0])).toBe('console.js'); }); - it('falls back to @react-native/js-polyfills when rn-get-polyfills.js is absent', () => { - // React Native >= 0.87 layout: no rn-get-polyfills.js. Resolution of - // `@react-native/js-polyfills` is delegated to require.resolve, so under the - // test harness it resolves from the installed tree. We only assert the - // documented contract: a function returning an array of absolute paths. - const rn = makeTmpRn({ 'index.js': '' }); - const getPolyfills = resolveReactNativePolyfills(rn, rn); - expect(typeof getPolyfills).toBe('function'); - const paths = getPolyfills(); - expect(Array.isArray(paths)).toBe(true); - expect(paths.every((p) => path.isAbsolute(p))).toBe(true); + it('resolves @react-native/js-polyfills from the project root', () => { + const rn = makeTmp({ 'index.js': '' }); + const projectRoot = makeTmp({ 'package.json': '{}' }); + const polyfillsModule = path.join(projectRoot, 'polyfills.js'); + fs.writeFileSync( + polyfillsModule, + "module.exports = () => ['/abs/console.js'];" + ); + + const getPolyfills = resolveReactNativePolyfills( + projectRoot, + rn, + (req, paths) => { + if (req.includes('metro-config')) throw new Error('no metro-config'); + if (req.includes('js-polyfills') && paths.includes(projectRoot)) { + return polyfillsModule; + } + throw new Error('unexpected ' + req); + } + ); + + expect(getPolyfills()).toEqual(['/abs/console.js']); + }); + + it('falls back through @react-native/metro-config when the project root has no polyfills', () => { + const rn = makeTmp({ 'index.js': '' }); + const projectRoot = makeTmp({ 'package.json': '{}' }); + + const metroPkg = path.join( + projectRoot, + 'node_modules', + '@react-native', + 'metro-config', + 'package.json' + ); + fs.mkdirSync(path.dirname(metroPkg), { recursive: true }); + fs.writeFileSync(metroPkg, '{"name":"@react-native/metro-config"}'); + const metroDir = path.dirname(metroPkg); + const polyfillsModule = path.join(projectRoot, 'polyfills.js'); + fs.writeFileSync( + polyfillsModule, + "module.exports = () => ['/abs/error-guard.js'];" + ); + + let consultedMetro = false; + const getPolyfills = resolveReactNativePolyfills( + projectRoot, + rn, + (req, paths) => { + if (req.includes('metro-config')) { + consultedMetro = true; + return metroPkg; + } + if (req.includes('js-polyfills')) { + // Resolvable only from metro-config's directory, not the project root. + if (paths.includes(metroDir)) return polyfillsModule; + throw new Error('not resolvable from ' + paths.join(',')); + } + throw new Error('unexpected ' + req); + } + ); + + expect(consultedMetro).toBe(true); + expect(getPolyfills()).toEqual(['/abs/error-guard.js']); + }); + + it('throws a descriptive error when nothing resolves', () => { + const rn = makeTmp({ 'index.js': '' }); + const projectRoot = makeTmp({ 'package.json': '{}' }); + expect(() => + resolveReactNativePolyfills(rn, projectRoot, () => { + throw new Error('cannot resolve'); + }) + ).toThrow(/Unable to locate React Native polyfills/); }); }); describe('getReactNativeAssetRegistryAlias', () => { - it('maps to src/asset-registry on React Native >= 0.87 layout', () => { - const rn = makeTmpRn({ - 'src/asset-registry.js': 'module.exports = {};', - }); - const alias = getReactNativeAssetRegistryAlias(rn); - expect(alias).toEqual({ + it('maps to src/asset-registry on the React Native >= 0.87 layout', () => { + const rn = makeTmp({ 'src/asset-registry.js': 'module.exports = {};' }); + expect(getReactNativeAssetRegistryAlias(rn)).toEqual({ [`${ASSET_REGISTRY_REQUEST}$`]: path.join(rn, 'src', 'asset-registry'), }); }); - it('maps to Libraries/Image/AssetRegistry on React Native <= 0.86 layout', () => { - const rn = makeTmpRn({ + it('returns null on the React Native <= 0.86 layout (legacy file present)', () => { + const rn = makeTmp({ 'Libraries/Image/AssetRegistry.js': 'module.exports = {};', }); - const alias = getReactNativeAssetRegistryAlias(rn); - expect(alias).toEqual({ - [`${ASSET_REGISTRY_REQUEST}$`]: path.join( - rn, - 'Libraries', - 'Image', - 'AssetRegistry' - ), - }); + expect(getReactNativeAssetRegistryAlias(rn)).toBeNull(); }); - it('prefers the modern layout when both are present', () => { - const rn = makeTmpRn({ + it('returns null when the legacy file also exists (no remap)', () => { + const rn = makeTmp({ 'src/asset-registry.js': 'module.exports = {};', 'Libraries/Image/AssetRegistry.js': 'module.exports = {};', }); - const alias = getReactNativeAssetRegistryAlias(rn); - expect(alias?.[`${ASSET_REGISTRY_REQUEST}$`]).toBe( - path.join(rn, 'src', 'asset-registry') - ); + expect(getReactNativeAssetRegistryAlias(rn)).toBeNull(); }); - it('returns null when no registry file exists (test fixtures)', () => { - const rn = makeTmpRn({ 'index.js': '' }); + it('returns null when no registry file exists', () => { + const rn = makeTmp({ 'index.js': '' }); expect(getReactNativeAssetRegistryAlias(rn)).toBeNull(); }); }); diff --git a/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts b/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts index b436efc9c..34608f773 100644 --- a/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts +++ b/packages/repack/src/plugins/NativeEntryPlugin/reactNativeRuntime.ts @@ -2,21 +2,25 @@ import fs from 'node:fs'; import path from 'node:path'; /** - * Canonical request used to reach React Native's asset registry. + * Request the assets loader and IncludeModules emit for the asset registry. * - * React Native <= 0.86 exposed the registry at - * `react-native/Libraries/Image/AssetRegistry`; 0.87 moved it to - * `src/asset-registry.js`, reachable only through the package `exports` map. - * There is no single request string that resolves across both layouts under - * both of Repack's resolver modes (the default resolver ignores `exports`, - * while `enablePackageExports` honours it and 0.87 dropped the `./*` wildcard). + * React Native <= 0.86 ships this file and, on 0.86, resolves it directly (its + * `exports` map has a `./Libraries/*` wildcard, so it works with package exports + * on or off). React Native 0.87 removes the file and drops the `./*` wildcard, so + * the request is remapped to `src/asset-registry.js` with a resolve alias (see + * {@link getReactNativeAssetRegistryAlias}). * - * We therefore always emit this canonical request and map it to the real file - * with a resolve alias (see {@link getReactNativeAssetRegistryAlias}). Keeping - * the `react-native/` prefix is required so Module Federation's deep-import - * sharing (`shared['react-native/']`) continues to treat it as a singleton. + * Keeping this request unchanged across versions preserves the Module Federation + * deep-import share key (`shared['react-native/']`) so hosts and remotes built + * with different Re.Pack versions still share a single registry instance. */ -export const ASSET_REGISTRY_REQUEST = 'react-native/asset-registry'; +export const ASSET_REGISTRY_REQUEST = + 'react-native/Libraries/Image/AssetRegistry'; + +type Resolver = (request: string, paths: string[]) => string; + +const defaultResolver: Resolver = (request, paths) => + require.resolve(request, { paths }); /** * Resolves React Native's polyfill list, returning the same @@ -25,41 +29,51 @@ export const ASSET_REGISTRY_REQUEST = 'react-native/asset-registry'; * React Native <= 0.86 shipped `rn-get-polyfills.js` at the package root, which * re-exported `@react-native/js-polyfills` (a direct dependency of react-native). * 0.87 removed that file and dropped the dependency entirely, so the polyfills - * are now only reachable through packages that still pull them in (e.g. - * `@react-native/metro-config`, itself an optional peer of the CLI plugin). + * are now only reachable through packages that still pull them in - in practice + * `@react-native/metro-config`, itself an optional peer of the CLI plugin and a + * template devDependency. * * The polyfills are inlined into the emitted bundle, so they must be resolvable - * for production bundles too - they cannot be treated as dev-only. We resolve - * from every plausible location and fail with an actionable message rather than - * a cryptic `MODULE_NOT_FOUND` from Repack's own directory. + * for production bundles too - they cannot be treated as dev-only. `resolveFrom` + * is injectable so the lookup chain can be exercised hermetically (the real + * resolver leaks the surrounding install layout, e.g. pnpm's virtual store). */ export function resolveReactNativePolyfills( projectRoot: string, - reactNativePath: string + reactNativePath: string, + resolveFrom: Resolver = defaultResolver ): () => string[] { const rnGetPolyfillsPath = path.join(reactNativePath, 'rn-get-polyfills.js'); if (fs.existsSync(rnGetPolyfillsPath)) { return require(rnGetPolyfillsPath) as () => string[]; } - // React Native >= 0.87: locate `@react-native/js-polyfills` from locations - // that own it, following the same "resolve the owner, then chain" pattern - // used for the hermes parser. - const lookupPaths = [reactNativePath, projectRoot]; + // React Native >= 0.87: resolve the polyfills from the project, then chain + // through `@react-native/metro-config`, which owns the dependency. Each + // location is tried in turn (same "resolve the owner, then chain" pattern used + // for the hermes parser). + const lookupDirs: string[] = [projectRoot]; try { - lookupPaths.push( - require.resolve('@react-native/metro-config', { paths: [projectRoot] }) + const metroConfigPackageJson = resolveFrom( + '@react-native/metro-config/package.json', + [projectRoot] ); + lookupDirs.push(path.dirname(metroConfigPackageJson)); } catch { // metro-config is an optional peer; a missing entry is handled below. } - try { - const jsPolyfillsPath = require.resolve('@react-native/js-polyfills', { - paths: lookupPaths, - }); - return require(jsPolyfillsPath) as () => string[]; - } catch { + let jsPolyfillsPath: string | undefined; + for (const dir of lookupDirs) { + try { + jsPolyfillsPath = resolveFrom('@react-native/js-polyfills', [dir]); + break; + } catch { + // try the next location + } + } + + if (!jsPolyfillsPath) { throw new Error( '[RepackNativeEntryPlugin] Unable to locate React Native polyfills. ' + "React Native >= 0.87 no longer depends on '@react-native/js-polyfills', " + @@ -68,41 +82,43 @@ export function resolveReactNativePolyfills( '`@react-native/metro-config`) to your project so it is available while bundling.' ); } + + return require(jsPolyfillsPath) as () => string[]; } /** - * Builds the `resolve.alias` entry that maps the canonical - * {@link ASSET_REGISTRY_REQUEST} to the real registry file for the installed - * React Native layout. + * Builds the `resolve.alias` entry that maps {@link ASSET_REGISTRY_REQUEST} to + * the relocated registry file on the React Native >= 0.87 layout. * - * The alias target is extensionless so platform extensions (`.native.js`, - * `.ios.js`, ...) still apply. Returns `null` when no registry file is found - * (for example in test fixtures), in which case no alias is injected and the - * canonical request is left to fail resolution like any missing module. + * Returns `null` (no alias) whenever the legacy file already exists, or no + * registry can be found at all. On <= 0.86 the legacy request resolves natively, + * so injecting an alias there would mutate resolution for no benefit - and would + * break the 0.86 `exports` wildcard path. The alias target is extensionless so + * platform extensions (`.native.js`, `.ios.js`, ...) still apply. */ export function getReactNativeAssetRegistryAlias( reactNativePath: string ): Record | null { - const modern = path.join(reactNativePath, 'src', 'asset-registry'); - const legacy = path.join( + const legacyFile = path.join( reactNativePath, 'Libraries', 'Image', - 'AssetRegistry' + 'AssetRegistry.js' ); + const modernFile = path.join(reactNativePath, 'src', 'asset-registry.js'); - let target: string | undefined; - if (fs.existsSync(`${modern}.js`)) { - target = modern; - } else if (fs.existsSync(`${legacy}.js`)) { - target = legacy; - } - - if (!target) { + // Only remap on the new layout: legacy file gone, relocated file present. + if (fs.existsSync(legacyFile) || !fs.existsSync(modernFile)) { return null; } - // Exact-match alias (`$`) so only the canonical request is remapped, while - // the request keeps its `react-native/` prefix for Module Federation sharing. - return { [`${ASSET_REGISTRY_REQUEST}$`]: target }; + // Exact-match alias (`$`) so only the exact request is remapped, and the + // request keeps its `react-native/` prefix for Module Federation sharing. + return { + [`${ASSET_REGISTRY_REQUEST}$`]: path.join( + reactNativePath, + 'src', + 'asset-registry' + ), + }; } diff --git a/tests/integration/src/helpers.ts b/tests/integration/src/helpers.ts index a42a287bb..ea231fe62 100644 --- a/tests/integration/src/helpers.ts +++ b/tests/integration/src/helpers.ts @@ -88,10 +88,7 @@ export function getReactNativeVirtualModules( 'node_modules/react-native/package.json': '{ "name": "react-native", "main": "./index.js" }', 'node_modules/react-native/index.js': `module.exports = { PixelRatio: { get: () => ${pixelRatio} } };`, - // Canonical asset registry request emitted by the assets loader. On a real - // app this is aliased to the version-appropriate file by NativeEntryPlugin; - // in tests the virtual React Native exposes it directly at this path. - 'node_modules/react-native/asset-registry.js': + 'node_modules/react-native/Libraries/Image/AssetRegistry.js': 'module.exports = { registerAsset: (spec) => spec };', 'node_modules/react-native/Libraries/Image/AssetSourceResolver.js': ` module.exports = class AssetSourceResolver { diff --git a/tests/integration/src/plugins/NativeEntryPlugin.srcLayout.test.ts b/tests/integration/src/plugins/NativeEntryPlugin.srcLayout.test.ts new file mode 100644 index 000000000..16877c64a --- /dev/null +++ b/tests/integration/src/plugins/NativeEntryPlugin.srcLayout.test.ts @@ -0,0 +1,105 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { plugins } from '@callstack/repack'; +import { createFsFromVolume, Volume } from 'memfs'; +import { afterEach, describe, expect, it } from 'vitest'; +import { createCompiler, createVirtualModulePlugin } from '../helpers.js'; + +const _dirname = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURE = path.join(_dirname, '__fixtures__', 'react-native-src-layout'); +const ASSET_REGISTRY_ALIAS_KEY = 'react-native/Libraries/Image/AssetRegistry$'; + +let projectRoot: string | undefined; + +/** + * Creates a temporary project root that carries a real `@react-native/js-polyfills` + * package, mirroring how RN >= 0.87 exposes polyfills only through a package that + * still depends on js-polyfills (rather than through react-native itself). + */ +function makeProjectRoot() { + const dir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'rn87-project-')) + ); + const pkg = path.join(dir, 'node_modules', '@react-native', 'js-polyfills'); + fs.mkdirSync(pkg, { recursive: true }); + fs.writeFileSync( + path.join(pkg, 'package.json'), + JSON.stringify({ name: '@react-native/js-polyfills', main: 'index.js' }) + ); + fs.writeFileSync( + path.join(pkg, 'index.js'), + "module.exports = () => [require.resolve('./error-guard.js')];" + ); + fs.writeFileSync( + path.join(pkg, 'error-guard.js'), + 'globalThis.__SRC_LAYOUT_POLYFILL__ = true;' + ); + return dir; +} + +afterEach(() => { + if (projectRoot) { + fs.rmSync(projectRoot, { recursive: true, force: true }); + projectRoot = undefined; + } +}); + +describe('NativeEntryPlugin - React Native 0.87 src layout', () => { + it('aliases the legacy asset registry request, ahead of a user react-native alias', async () => { + projectRoot = makeProjectRoot(); + const virtualPlugin = await createVirtualModulePlugin({ + './index.js': + "var A = require('react-native/Libraries/Image/AssetRegistry');" + + "globalThis.__APP_REGISTERED__ = A.registerAsset({ name: 'logo' });", + }); + + const compiler = await createCompiler({ + context: projectRoot, + mode: 'development', + devtool: false, + entry: './index.js', + resolve: { + alias: { 'react-native': FIXTURE }, + }, + output: { path: '/out' }, + plugins: [new plugins.NativeEntryPlugin({}), virtualPlugin], + }); + + // The specific alias must be injected and ordered before the generic key, + // otherwise a user `react-native` alias rewrites the request to a path that + // does not exist on the 0.87 layout before the specific key is consulted. + const alias = compiler.options.resolve.alias as Record; + const aliasKeys = Object.keys(alias); + expect(alias[ASSET_REGISTRY_ALIAS_KEY]).toBe( + path.join(FIXTURE, 'src', 'asset-registry') + ); + expect(aliasKeys.indexOf(ASSET_REGISTRY_ALIAS_KEY)).toBeLessThan( + aliasKeys.indexOf('react-native') + ); + + // Run manually: the harness configures no JS loaders, so repack's own runtime + // entries (InitializeScriptManager/ScriptManager) emit unrelated ESM parse + // errors, exactly as in NativeEntryPlugin.test.ts. We assert only that nothing + // related to the asset registry / IncludeModules / polyfills failed to resolve. + const volume = new Volume(); + // @ts-expect-error memfs is compatible enough with the output filesystem + compiler.outputFileSystem = createFsFromVolume(volume); + const stats = await new Promise((resolve, reject) => { + compiler.run((error, s) => (error ? reject(error) : resolve(s))); + }); + + const messages: string[] = ( + stats.toJson({ errors: true }).errors ?? [] + ).map((e: any) => `${e.message ?? ''}\n${e.details ?? ''}`); + const offenders = messages.filter((m) => + /AssetRegistry|asset-registry|IncludeModules|polyfill/i.test(m) + ); + expect(offenders).toEqual([]); + + const code = volume.readFileSync('/out/main.js', 'utf-8') as string; + expect(code).toContain('__SRC_LAYOUT_POLYFILL__'); + expect(code).toContain('__SRC_LAYOUT_INITIALIZE_CORE__'); + }); +}); diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Core/InitializeCore.js b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Core/InitializeCore.js new file mode 100644 index 000000000..b2223fe27 --- /dev/null +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Core/InitializeCore.js @@ -0,0 +1 @@ +globalThis.__SRC_LAYOUT_INITIALIZE_CORE__ = true; diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Image/AssetSourceResolver.js b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Image/AssetSourceResolver.js new file mode 100644 index 000000000..9f12ec1f8 --- /dev/null +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/Libraries/Image/AssetSourceResolver.js @@ -0,0 +1 @@ +module.exports = class AssetSourceResolver {}; diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/index.js b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/index.js new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json new file mode 100644 index 000000000..4faa61ae0 --- /dev/null +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json @@ -0,0 +1 @@ +{ "type": "commonjs", "name": "react-native", "version": "0.87.1", "main": "index.js" } diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/src/asset-registry.js b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/src/asset-registry.js new file mode 100644 index 000000000..819ff02bf --- /dev/null +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/src/asset-registry.js @@ -0,0 +1 @@ +module.exports = { registerAsset: (spec) => spec }; diff --git a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap index 2e031dcdf..410b2ec7b 100644 --- a/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap @@ -15,11 +15,7 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -771,11 +767,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -975,11 +967,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1179,11 +1167,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), @@ -1425,11 +1409,7 @@ var __webpack_modules__ = ({ * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); }), diff --git a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap index 7d5efcdd9..1271284b3 100644 --- a/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap +++ b/tests/integration/src/plugins/__snapshots__/webpack/NativeEntryPlugin.test.ts.snap @@ -17,11 +17,7 @@ exports[`NativeEntryPlugin > with Module Federation v1 > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -232,11 +228,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -560,11 +552,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -893,11 +881,7 @@ module.exports = globalThis.__MF_EXTERNAL__; * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, @@ -1262,11 +1246,7 @@ exports[`NativeEntryPlugin > without Module Federation > should execute polyfill * These modules are required by assetsLoader and should be shared as deep imports when using ModuleFederation. */ -// Canonical asset registry request. NativeEntryPlugin aliases this to the real -// file for the installed React Native version (Libraries/Image/AssetRegistry on -// <= 0.86, src/asset-registry.js on >= 0.87). Keeping the \`react-native/\` prefix -// preserves Module Federation deep-import sharing. -/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/asset-registry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); +/*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetRegistry'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /*require.resolve*/(Object(function webpackMissingModule() { var e = new Error("Cannot find module 'react-native/Libraries/Image/AssetSourceResolver'"); e.code = 'MODULE_NOT_FOUND'; throw e; }())); /***/ }, From 1d60c45e3e51a58c26b84358650f0fa16ac8fd23 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Sat, 12 Sep 2026 17:41:36 +0100 Subject: [PATCH 7/7] style: apply biome formatting to 0.87 fixture package.json --- .../__fixtures__/react-native-src-layout/package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json index 4faa61ae0..58629e999 100644 --- a/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json +++ b/tests/integration/src/plugins/__fixtures__/react-native-src-layout/package.json @@ -1 +1,6 @@ -{ "type": "commonjs", "name": "react-native", "version": "0.87.1", "main": "index.js" } +{ + "type": "commonjs", + "name": "react-native", + "version": "0.87.1", + "main": "index.js" +}