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 .changeset/perfect-dots-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": minor
---

Support Module Federation `preloadRemote` through `PrefetchPlugin`.
5 changes: 5 additions & 0 deletions apps/tester-federation-v2/configs/rspack.host-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export default (env) => {
new rspack.IgnorePlugin({
resourceRegExp: /^@react-native-masked-view/,
}),
new rspack.DefinePlugin({
__WITH_PRELOAD__:
process.env.WITH_PRELOAD === 'true' ||
process.env.WITH_PRELOAD === '1',
}),
],
};
};
28 changes: 28 additions & 0 deletions apps/tester-federation-v2/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,30 @@ PODS:
- ReactTestApp-DevSupport (4.3.3):
- React-Core
- React-jsi
- RNCAsyncStorage (1.24.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (4.10.0):
- DoubleConversion
- glog
Expand Down Expand Up @@ -1918,6 +1942,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "ReactNativeHost (from `../../../node_modules/.pnpm/react-native-test-app@4.3.3_react-native@0.79.1_@babel+core@7.25.2_@react-native-community+cl_kcnkti3wkp73kvg3lvpmdgswpy/node_modules/@rnx-kit/react-native-host`)"
- ReactTestApp-DevSupport (from `../node_modules/react-native-test-app`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- RNScreens (from `../node_modules/react-native-screens`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

Expand Down Expand Up @@ -2075,6 +2100,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/.pnpm/react-native-test-app@4.3.3_react-native@0.79.1_@babel+core@7.25.2_@react-native-community+cl_kcnkti3wkp73kvg3lvpmdgswpy/node_modules/@rnx-kit/react-native-host"
ReactTestApp-DevSupport:
:path: "../node_modules/react-native-test-app"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNScreens:
:path: "../node_modules/react-native-screens"
Yoga:
Expand Down Expand Up @@ -2155,6 +2182,7 @@ SPEC CHECKSUMS:
ReactCommon: 9f975582dc535de1de110bdb46d4553140a77541
ReactNativeHost: e66210ecf04fd35d3d47970b7b33363d4933d0e4
ReactTestApp-DevSupport: ba564d9c3503d107cd6508a22681a19f8af34e81
RNCAsyncStorage: aa2fec76310ebe0c7fe159a26755e099170116bb
RNScreens: c5c07a86e4088ce92f0d3854082250dfa9c61f75
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
Expand Down
2 changes: 2 additions & 0 deletions apps/tester-federation-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"dependencies": {
"@callstack/repack": "workspace:*",
"@module-federation/enhanced": "0.12.0",
"@module-federation/runtime": "0.12.0",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.10.1",
"react": "catalog:",
Expand Down
33 changes: 33 additions & 0 deletions apps/tester-federation-v2/src/host/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
import { ScriptManager } from '@callstack/repack/client';
import { preloadRemote } from '@module-federation/runtime';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppRegistry } from 'react-native';

import { components } from '../../app.json';
import App from './App';

if (__WITH_PRELOAD__) {
// enable caching of scripts in the AsyncStorage
ScriptManager.shared.setStorage(AsyncStorage);

// preload eagerly on startup
// you can kill the dev server before going to the mini app screen
// and it will still work because of the assets being present in cache
ScriptManager.shared
// invalidate cache to make sure we fetch the latest assets
.invalidateScripts()
// preload the MiniApp remote entry and all its assets
.then(() => {
return preloadRemote([
{ nameOrAlias: 'MiniApp', resourceCategory: 'sync', depsRemote: false },
]);
})
.then(() => {
console.log('preloaded MiniApp assets');
})
.catch((e) => {
// preloadRemote will fail if the remote entry is not a manifest
console.error('error preloading MiniApp assets');
console.error(e);
});

ScriptManager.shared.on('prefetching', (script) => {
console.debug('prefetching', script.locator.uniqueId);
});
}

AppRegistry.registerComponent(components[0].appKey, () => App);
1 change: 1 addition & 0 deletions packages/repack/mf/prefetch-plugin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../dist/modules/FederationRuntimePlugins/PrefetchPlugin.js';
1 change: 1 addition & 0 deletions packages/repack/mf/prefetch-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../dist/modules/FederationRuntimePlugins/PrefetchPlugin.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import type * as RepackClient from '../ScriptManager/index.js';

interface PrefetchAsset {
name: string;
remoteName: string;
url: string;
}

function getAssetName(asset: string): string {
// remove the extension from the asset name
return asset.split('.')[0];
}

function getAssetUrl(asset: string) {
// create placeholder reference url for the asset
return 'prefetch:///' + asset;
}

function prefetchAsset(asset: PrefetchAsset) {
const client = require('../ScriptManager/index.js') as typeof RepackClient;
const { ScriptManager, getWebpackContext } = client;

// caller should be undefined when fetching/loading the remote entry container
const caller = asset.name === asset.remoteName ? undefined : asset.remoteName;

return ScriptManager.shared.prefetchScript(
asset.name,
caller,
getWebpackContext(),
asset.url
);
}

const RepackPrefetchPlugin: () => FederationRuntimePlugin = () => ({
name: 'repack-prefetch-plugin',
generatePreloadAssets: async (args) => {
const preloadConfig = args.preloadOptions.preloadConfig;
const remoteName = preloadConfig.nameOrAlias;
const remoteSnapshot = args.remoteSnapshot;

if (preloadConfig.depsRemote !== false) {
console.warn(
'[RepackPrefetchPlugin] ' +
'The depsRemote configuration option is not implemented yet. ' +
'This setting will be ignored and will have no effect. ' +
'You can hide this warning by setting depsRemote explicitly to false.'
);
}

function handleAssets(assets: string[]): PrefetchAsset[] {
return assets.map((asset) => ({
name: getAssetName(asset),
remoteName,
url: getAssetUrl(asset),
}));
}

let assets: PrefetchAsset[] = [];

if ('modules' in remoteSnapshot) {
for (const exposedModule of remoteSnapshot.modules) {
if (preloadConfig.exposes) {
if (!preloadConfig.exposes.includes(exposedModule.moduleName)) {
continue;
}
}

if (preloadConfig.resourceCategory === 'all') {
assets.push(...handleAssets(exposedModule.assets.js.async));
assets.push(...handleAssets(exposedModule.assets.js.sync));
} else if (preloadConfig.resourceCategory === 'sync') {
assets.push(...handleAssets(exposedModule.assets.js.sync));
}
}

if (preloadConfig.filter) {
assets = assets.filter((asset) => preloadConfig.filter!(asset.name));
}

assets.unshift({
name: remoteSnapshot.globalName,
remoteName: remoteSnapshot.globalName,
url: getAssetUrl(remoteSnapshot.remoteEntry),
});
}

await Promise.all(assets.map(prefetchAsset));

// noop for compatibility
return Promise.resolve({
cssAssets: [],
jsAssetsWithoutEntry: [],
entryAssets: [],
});
},
});

export default RepackPrefetchPlugin;
1 change: 1 addition & 0 deletions packages/repack/src/plugins/ModuleFederationPluginV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class ModuleFederationPluginV2 implements RspackPluginInstance {
this.defaultRuntimePlugins = defaultRuntimePlugins ?? [
'@callstack/repack/mf/core-plugin',
'@callstack/repack/mf/resolver-plugin',
'@callstack/repack/mf/prefetch-plugin',
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const corePluginPath = require.resolve('@callstack/repack/mf/core-plugin');
const resolverPluginPath = require.resolve(
'@callstack/repack/mf/resolver-plugin'
);
const prefetchPluginPath = require.resolve(
'@callstack/repack/mf/prefetch-plugin'
);

describe('ModuleFederationPlugin', () => {
afterEach(() => {
Expand Down Expand Up @@ -164,7 +167,8 @@ describe('ModuleFederationPlugin', () => {
const config = mockPlugin.mock.calls[0][0];
expect(config.runtimePlugins).toContain(corePluginPath);
expect(config.runtimePlugins).toContain(resolverPluginPath);
expect(config.runtimePlugins).toHaveLength(2);
expect(config.runtimePlugins).toContain(prefetchPluginPath);
expect(config.runtimePlugins).toHaveLength(3);
});

it('should use loaded-first as default shareStrategy', () => {
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.