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
7 changes: 7 additions & 0 deletions .changeset/module-federation-2-runtime-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@callstack/repack": patch
---

Keep the options of Module Federation 2.x runtime plugins passed to `ModuleFederationPluginV2` as `[path, options]` in `runtimePlugins`. Such entries were silently dropped before.

Fix the typings of the `@callstack/repack/mf/*` runtime plugins with `@module-federation/enhanced` 0.17 and newer, where `FederationRuntimePlugin` and `FederationHost` were renamed to `ModuleFederationRuntimePlugin` and `ModuleFederation`. The types are now derived from the installed runtime, so they work with every supported version.
4 changes: 2 additions & 2 deletions packages/repack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@
"@babel/core": "^7.29.7",
"@babel/plugin-transform-export-namespace-from": "^7.29.7",
"@babel/plugin-transform-modules-commonjs": "^7.29.7",
"@module-federation/enhanced": "0.8.9",
"@module-federation/sdk": "0.6.10",
"@module-federation/enhanced": "2.9.2",
"@module-federation/sdk": "2.9.2",
"@rspack/core": "catalog:",
"@swc/helpers": "catalog:",
"@types/babel__core": "^7.20.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import type * as RepackClient from '../ScriptManager/index.js';
import type { MFRuntimePlugin } from './types.js';

const RepackCorePlugin: () => FederationRuntimePlugin = () => ({
const RepackCorePlugin: () => MFRuntimePlugin = () => ({
name: 'repack-core-plugin',
loadEntry: async ({ remoteInfo }) => {
const client = require('../ScriptManager/index.js') as typeof RepackClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import type * as RepackClient from '../ScriptManager/index.js';
import type { MFRuntimePlugin } from './types.js';

interface PrefetchAsset {
name: string;
Expand Down Expand Up @@ -32,7 +32,7 @@ function prefetchAsset(asset: PrefetchAsset) {
);
}

const RepackPrefetchPlugin: () => FederationRuntimePlugin = () => ({
const RepackPrefetchPlugin: () => MFRuntimePlugin = () => ({
name: 'repack-prefetch-plugin',
generatePreloadAssets: async (args) => {
const preloadConfig = args.preloadOptions.preloadConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type {
FederationHost,
FederationRuntimePlugin,
} from '@module-federation/enhanced/runtime';
import type { ScriptLocator } from '../ScriptManager/index.js';
import type { MFInstance, MFRuntimePlugin } from './types.js';

type MFRemote = Parameters<FederationHost['registerRemotes']>[0][0];
type MFRemote = Parameters<MFInstance['registerRemotes']>[0][0];

export type RepackResolverPluginConfiguration =
| Omit<ScriptLocator, 'url'>
Expand Down Expand Up @@ -87,7 +84,7 @@ const registerResolver = async (

const RepackResolverPlugin: (
config?: RepackResolverPluginConfiguration
) => FederationRuntimePlugin = (config) => ({
) => MFRuntimePlugin = (config) => ({
name: 'repack-resolver-plugin',
registerRemote: (args) => {
registerResolver(args.remote, config);
Expand Down
10 changes: 10 additions & 0 deletions packages/repack/src/modules/FederationRuntimePlugins/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { init } from '@module-federation/enhanced/runtime';

// Module Federation 0.17 renamed FederationHost and FederationRuntimePlugin to
// ModuleFederation and ModuleFederationRuntimePlugin. Deriving both from `init`
// keeps the published typings working with every supported version.
export type MFInstance = ReturnType<typeof init>;

export type MFRuntimePlugin = NonNullable<
Parameters<typeof init>[0]['plugins']
>[number];
23 changes: 18 additions & 5 deletions packages/repack/src/plugins/ModuleFederationPluginV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface ModuleFederationPluginV2Config
reactNativeDeepImports?: boolean;
}

// a runtime plugin path, or [path, options] (Module Federation 2.x)
type RuntimePlugin = NonNullable<
MF.ModuleFederationPluginOptions['runtimePlugins']
>[number];

const runtimePluginPath = (plugin: RuntimePlugin) =>
typeof plugin === 'string' ? plugin : plugin[0];

/**
* Webpack plugin to configure Module Federation with platform differences
* handled under the hood.
Expand Down Expand Up @@ -140,23 +148,28 @@ export class ModuleFederationPluginV2 {

private adaptRuntimePlugins(
context: string,
runtimePlugins: string[] | undefined = []
runtimePlugins: RuntimePlugin[] | undefined = []
) {
const plugins = runtimePlugins
.map((pluginPath) => {
.map((plugin) => {
try {
// resolve the paths to compare against absolute paths
return require.resolve(pluginPath, { paths: [context] });
const pluginPath = require.resolve(runtimePluginPath(plugin), {
paths: [context],
});
if (typeof plugin === 'string') return pluginPath;
const entry: RuntimePlugin = [pluginPath, plugin[1]];
return entry;
} catch {
// ignore invalid paths
return undefined;
}
})
.filter((pluginPath) => !!pluginPath) as string[];
.filter((plugin): plugin is RuntimePlugin => !!plugin);

for (const plugin of this.defaultRuntimePlugins) {
const pluginPath = require.resolve(plugin);
if (!plugins.includes(pluginPath)) {
if (!plugins.some((entry) => runtimePluginPath(entry) === pluginPath)) {
plugins.unshift(pluginPath);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ type CompilerWarning = Error & {
};
};

jest.mock('@module-federation/enhanced/rspack');
jest.mock('@module-federation/enhanced/rspack', () => ({
// 2.x's export shape can't be automocked into a constructor
ModuleFederationPlugin: jest.fn(() => ({ apply: jest.fn() })),
}));

const mockCompiler = {
context: __dirname,
Expand Down Expand Up @@ -212,6 +215,22 @@ describe('ModuleFederationPlugin', () => {
expect(config.runtimePlugins).toHaveLength(3);
});

it('should keep options of runtime plugins passed as [path, options]', () => {
new ModuleFederationPluginV2({
name: 'test',
runtimePlugins: [['@callstack/repack/mf/resolver-plugin', { retry: 2 }]],
}).apply(mockCompiler);

const config = mockPlugin.mock.calls[0][0];
expect(config.runtimePlugins).toContainEqual([
resolverPluginPath,
{ retry: 2 },
]);
expect(config.runtimePlugins).toContain(corePluginPath);
expect(config.runtimePlugins).toContain(prefetchPluginPath);
expect(config.runtimePlugins).toHaveLength(3);
});

it('should use loaded-first as default shareStrategy', () => {
new ModuleFederationPluginV2({ name: 'test' }).apply(mockCompiler);

Expand Down
Loading
Loading