diff --git a/.changeset/wet-countries-sin.md b/.changeset/wet-countries-sin.md new file mode 100644 index 000000000..cd1137552 --- /dev/null +++ b/.changeset/wet-countries-sin.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": minor +--- + +Refactor FederationRuntimePlugin into two separate plugins for more granular control over the MF2 runtime behaviour (CorePlugin & ResolverPlugin) diff --git a/apps/tester-federation-v2/ios/Podfile.lock b/apps/tester-federation-v2/ios/Podfile.lock index e73868fb4..9ea5abb19 100644 --- a/apps/tester-federation-v2/ios/Podfile.lock +++ b/apps/tester-federation-v2/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - callstack-repack (5.0.0-rc.0): + - callstack-repack (5.0.0-rc.2): - DoubleConversion - glog - hermes-engine @@ -1895,7 +1895,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - callstack-repack: 75464b0e26467fc4a7236373399bc0fc2281f495 + callstack-repack: 3106db24c24f7a76a380230ff7794d225cecb760 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: 7075bb12898bc3998fd60f4b7ca422496cc2cdf7 fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be diff --git a/apps/tester-federation-v2/rspack.config.host-app.mjs b/apps/tester-federation-v2/rspack.config.host-app.mjs index 6adb34a29..03d3069a0 100644 --- a/apps/tester-federation-v2/rspack.config.host-app.mjs +++ b/apps/tester-federation-v2/rspack.config.host-app.mjs @@ -1,11 +1,9 @@ // @ts-check -import { createRequire } from 'node:module'; import path from 'node:path'; import * as Repack from '@callstack/repack'; import rspack from '@rspack/core'; const dirname = Repack.getDirname(import.meta.url); -const { resolve } = createRequire(import.meta.url); /** @type {(env: import('@callstack/repack').EnvOptions) => import('@rspack/core').Configuration} */ export default (env) => { @@ -121,9 +119,6 @@ export default (env) => { MiniApp: `MiniApp@http://localhost:8082/${platform}/mf-manifest.json`, }, dts: false, - runtimePlugins: [ - resolve('@callstack/repack/federation-runtime-plugin'), - ], shared: { react: { singleton: true, diff --git a/apps/tester-federation-v2/webpack.config.host-app.mjs b/apps/tester-federation-v2/webpack.config.host-app.mjs index 770c95e5d..c9a4f4cb9 100644 --- a/apps/tester-federation-v2/webpack.config.host-app.mjs +++ b/apps/tester-federation-v2/webpack.config.host-app.mjs @@ -1,11 +1,9 @@ // @ts-check -import { createRequire } from 'node:module'; import path from 'node:path'; import * as Repack from '@callstack/repack'; import webpack from 'webpack'; const dirname = Repack.getDirname(import.meta.url); -const { resolve } = createRequire(import.meta.url); /** @type {(env: import('@callstack/repack').EnvOptions) => import('webpack').Configuration} */ export default (env) => { @@ -105,9 +103,6 @@ export default (env) => { remotes: { MiniApp: `MiniApp@http://localhost:8082/${platform}/mf-manifest.json`, }, - runtimePlugins: [ - resolve('@callstack/repack/federation-runtime-plugin'), - ], shared: { react: { singleton: true, diff --git a/packages/repack/client.d.ts b/packages/repack/client.d.ts deleted file mode 100644 index 6ca051f49..000000000 --- a/packages/repack/client.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/modules/ScriptManager'; diff --git a/packages/repack/client.js b/packages/repack/client.js deleted file mode 100644 index 6ca051f49..000000000 --- a/packages/repack/client.js +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/modules/ScriptManager'; diff --git a/packages/repack/client/index.d.ts b/packages/repack/client/index.d.ts new file mode 100644 index 000000000..c32d6c64a --- /dev/null +++ b/packages/repack/client/index.d.ts @@ -0,0 +1 @@ +export * from '../dist/modules/ScriptManager'; diff --git a/packages/repack/client/index.js b/packages/repack/client/index.js new file mode 100644 index 000000000..c32d6c64a --- /dev/null +++ b/packages/repack/client/index.js @@ -0,0 +1 @@ +export * from '../dist/modules/ScriptManager'; diff --git a/packages/repack/federation-runtime-plugin.d.ts b/packages/repack/federation-runtime-plugin.d.ts deleted file mode 100644 index 04d05a2c2..000000000 --- a/packages/repack/federation-runtime-plugin.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import FederationRuntimePlugin from './dist/modules/FederationRuntimePlugin'; -export default FederationRuntimePlugin; diff --git a/packages/repack/federation-runtime-plugin.js b/packages/repack/federation-runtime-plugin.js deleted file mode 100644 index 04d05a2c2..000000000 --- a/packages/repack/federation-runtime-plugin.js +++ /dev/null @@ -1,2 +0,0 @@ -import FederationRuntimePlugin from './dist/modules/FederationRuntimePlugin'; -export default FederationRuntimePlugin; diff --git a/packages/repack/mf/core-plugin.d.ts b/packages/repack/mf/core-plugin.d.ts new file mode 100644 index 000000000..c968a3b65 --- /dev/null +++ b/packages/repack/mf/core-plugin.d.ts @@ -0,0 +1 @@ +export { default } from '../dist/modules/FederationRuntimePlugins/CorePlugin'; diff --git a/packages/repack/mf/core-plugin.js b/packages/repack/mf/core-plugin.js new file mode 100644 index 000000000..fb650e84b --- /dev/null +++ b/packages/repack/mf/core-plugin.js @@ -0,0 +1,2 @@ +import RepackCorePlugin from '../dist/modules/FederationRuntimePlugins/CorePlugin'; +export default RepackCorePlugin; diff --git a/packages/repack/mf/resolver-plugin.d.ts b/packages/repack/mf/resolver-plugin.d.ts new file mode 100644 index 000000000..a0afc981b --- /dev/null +++ b/packages/repack/mf/resolver-plugin.d.ts @@ -0,0 +1 @@ +export { default } from '../dist/modules/FederationRuntimePlugins/ResolverPlugin'; diff --git a/packages/repack/mf/resolver-plugin.js b/packages/repack/mf/resolver-plugin.js new file mode 100644 index 000000000..77f215723 --- /dev/null +++ b/packages/repack/mf/resolver-plugin.js @@ -0,0 +1,2 @@ +import RepackResolverPlugin from '../dist/modules/FederationRuntimePlugins/ResolverPlugin'; +export default RepackResolverPlugin; diff --git a/packages/repack/package.json b/packages/repack/package.json index 1568b8362..1753ade54 100644 --- a/packages/repack/package.json +++ b/packages/repack/package.json @@ -4,36 +4,14 @@ "description": "A toolkit to build your React Native application with Rspack or Webpack.", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "react-native": "", "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./client": { - "types": "./dist/modules/ScriptManager/index.d.ts", - "default": "./dist/modules/ScriptManager/index.js" - }, - "./federation-runtime-plugin": { - "types": "./dist/modules/FederationRuntimePlugin.d.ts", - "default": "./dist/modules/FederationRuntimePlugin.js" - }, - "./commands/webpack": { - "types": "./dist/commands/webpack/index.d.ts", - "default": "./dist/commands/webpack/index.js" - }, - "./commands/rspack": { - "types": "./dist/commands/rspack/index.d.ts", - "default": "./dist/commands/rspack/index.js" - }, - "./assets-loader": { - "types": "./dist/loaders/assetsLoader/index.d.ts", - "default": "./dist/loaders/assetsLoader/index.js" - }, - "./flow-loader": { - "types": "./dist/loaders/flowLoader/index.d.ts", - "default": "./dist/loaders/flowLoader/index.js" - }, + ".": "./dist/index.js", + "./client": "./client/index.js", + "./commands/rspack": "./dist/commands/rspack/index.js", + "./commands/webpack": "./dist/commands/webpack/index.js", + "./assets-loader": "./dist/loaders/assetsLoader/index.js", + "./flow-loader": "./dist/loaders/flowLoader/index.js", + "./mf/*": "./mf/*.js", "./package.json": "./package.json" }, "files": [ @@ -42,10 +20,8 @@ "!android/build", "ios", "!ios/build", - "client.js", - "client.d.ts", - "federation-runtime-plugin.js", - "federation-runtime-plugin.d.ts", + "client", + "mf", "callstack-repack.podspec", "src/modules/ScriptManager/NativeScriptManager.ts" ], diff --git a/packages/repack/src/modules/FederationRuntimePlugin.ts b/packages/repack/src/modules/FederationRuntimePlugin.ts deleted file mode 100644 index e118e496d..000000000 --- a/packages/repack/src/modules/FederationRuntimePlugin.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; -import type * as RepackClient from './ScriptManager'; - -const repackFederationRuntimePlugin: () => FederationRuntimePlugin = () => ({ - name: 'repack-federation-runtime-plugin', - afterResolve(args) { - const { ScriptManager } = require('./ScriptManager') as typeof RepackClient; - const { remoteInfo } = args; - - ScriptManager.shared.addResolver( - async (scriptId, caller, referenceUrl) => { - if (scriptId === remoteInfo.entryGlobalName) { - return { url: remoteInfo.entry }; - } - - if (referenceUrl && caller === remoteInfo.entryGlobalName) { - const publicPath = remoteInfo.entry.split('/').slice(0, -1).join('/'); - const bundlePath = scriptId + referenceUrl.split(scriptId)[1]; - return { url: publicPath + '/' + bundlePath }; - } - }, - { key: remoteInfo.entryGlobalName } - ); - - return args; - }, - loadEntry: async ({ remoteInfo }) => { - const client = require('./ScriptManager') as typeof RepackClient; - const { ScriptManager, getWebpackContext } = client; - const { entry, entryGlobalName } = remoteInfo; - - try { - await ScriptManager.shared.loadScript( - entryGlobalName, - undefined, - getWebpackContext(), - entry - ); - - // @ts-ignore - if (!globalThis[entryGlobalName]) { - throw new Error(); - } - - // @ts-ignore - return globalThis[entryGlobalName]; - } catch { - console.error(`Failed to load ${entryGlobalName} entry`); - } - }, -}); - -export default repackFederationRuntimePlugin; diff --git a/packages/repack/src/modules/FederationRuntimePlugins/CorePlugin.ts b/packages/repack/src/modules/FederationRuntimePlugins/CorePlugin.ts new file mode 100644 index 000000000..d4fbd237e --- /dev/null +++ b/packages/repack/src/modules/FederationRuntimePlugins/CorePlugin.ts @@ -0,0 +1,32 @@ +import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +import type * as RepackClient from '../ScriptManager'; + +const RepackCorePlugin: () => FederationRuntimePlugin = () => ({ + name: 'repack-core-plugin', + loadEntry: async ({ remoteInfo }) => { + const client = require('../ScriptManager') as typeof RepackClient; + const { ScriptManager, getWebpackContext } = client; + const { entry, entryGlobalName } = remoteInfo; + + try { + await ScriptManager.shared.loadScript( + entryGlobalName, + undefined, + getWebpackContext(), + entry + ); + + // @ts-ignore + if (!globalThis[entryGlobalName]) { + throw new Error(); + } + + // @ts-ignore + return globalThis[entryGlobalName]; + } catch { + console.error(`Failed to load remote entry: ${entryGlobalName}`); + } + }, +}); + +export default RepackCorePlugin; diff --git a/packages/repack/src/modules/FederationRuntimePlugins/ResolverPlugin.ts b/packages/repack/src/modules/FederationRuntimePlugins/ResolverPlugin.ts new file mode 100644 index 000000000..2560c3772 --- /dev/null +++ b/packages/repack/src/modules/FederationRuntimePlugins/ResolverPlugin.ts @@ -0,0 +1,55 @@ +import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime'; +import type * as RepackClient from '../ScriptManager'; + +export type RepackResolverPluginConfiguration = + | Omit + | ((url: string) => Promise); + +const createScriptLocator = async ( + entryUrl: string, + config?: RepackResolverPluginConfiguration +) => { + if (typeof config === 'function') { + const locator = await config(entryUrl); + return locator; + } + if (typeof config === 'object') { + return { url: entryUrl, ...config }; + } + return { url: entryUrl }; +}; + +const RepackResolverPlugin: ( + config?: RepackResolverPluginConfiguration +) => FederationRuntimePlugin = (config) => ({ + name: 'repack-resolver-plugin', + afterResolve(args) { + const { ScriptManager } = + require('../ScriptManager') as typeof RepackClient; + const { remoteInfo } = args; + + ScriptManager.shared.addResolver( + async (scriptId, caller, referenceUrl) => { + // entry container + if (scriptId === remoteInfo.entryGlobalName) { + const locator = await createScriptLocator(remoteInfo.entry, config); + return locator; + } + // entry chunks + if (referenceUrl && caller === remoteInfo.entryGlobalName) { + const publicPath = remoteInfo.entry.split('/').slice(0, -1).join('/'); + const bundlePath = scriptId + referenceUrl.split(scriptId)[1]; + const url = publicPath + '/' + bundlePath; + + const locator = await createScriptLocator(url, config); + return locator; + } + }, + { key: remoteInfo.entryGlobalName } + ); + + return args; + }, +}); + +export default RepackResolverPlugin; diff --git a/packages/repack/src/plugins/ModuleFederationPluginV2.ts b/packages/repack/src/plugins/ModuleFederationPluginV2.ts index 5a7cb57b7..6fca61c0d 100644 --- a/packages/repack/src/plugins/ModuleFederationPluginV2.ts +++ b/packages/repack/src/plugins/ModuleFederationPluginV2.ts @@ -11,6 +11,15 @@ import { isRspackCompiler } from './utils/isRspackCompiler'; */ export interface ModuleFederationPluginV2Config extends MF.ModuleFederationPluginOptions { + /** + * List of default runtime plugins for Federation Runtime. + * Useful if you want to modify or disable behaviour of runtime plugins. + * + * Defaults to an array containing: + * - '@callstack/repack/mf/core-plugin + * - '@callstack/repack/mf/resolver-plugin + */ + defaultRuntimePlugins?: string[]; /** Enable or disable adding React Native deep imports to shared dependencies. Defaults to true */ reactNativeDeepImports?: boolean; } @@ -83,11 +92,17 @@ export interface ModuleFederationPluginV2Config export class ModuleFederationPluginV2 implements RspackPluginInstance { public config: MF.ModuleFederationPluginOptions; private deepImports: boolean; + private defaultRuntimePlugins: string[]; constructor(pluginConfig: ModuleFederationPluginV2Config) { - const { reactNativeDeepImports, ...config } = pluginConfig; + const { defaultRuntimePlugins, reactNativeDeepImports, ...config } = + pluginConfig; this.config = config; this.deepImports = reactNativeDeepImports ?? true; + this.defaultRuntimePlugins = defaultRuntimePlugins ?? [ + '@callstack/repack/mf/core-plugin', + '@callstack/repack/mf/resolver-plugin', + ]; } private ensureModuleFederationPackageInstalled(context: string) { @@ -105,10 +120,6 @@ export class ModuleFederationPluginV2 implements RspackPluginInstance { context: string, runtimePlugins: string[] | undefined = [] ) { - const repackRuntimePlugin = require.resolve( - '../modules/FederationRuntimePlugin' - ); - const plugins = runtimePlugins .map((pluginPath) => { try { @@ -121,11 +132,14 @@ export class ModuleFederationPluginV2 implements RspackPluginInstance { }) .filter((pluginPath) => !!pluginPath) as string[]; - if (!plugins.includes(repackRuntimePlugin)) { - return [repackRuntimePlugin, ...runtimePlugins]; + for (const plugin of this.defaultRuntimePlugins) { + const pluginPath = require.resolve(plugin); + if (!plugins.includes(pluginPath)) { + plugins.unshift(pluginPath); + } } - return runtimePlugins; + return plugins; } private getModuleFederationPlugin(compiler: Compiler) { diff --git a/packages/repack/src/plugins/__tests__/ModuleFederationPluginV2.test.ts b/packages/repack/src/plugins/__tests__/ModuleFederationPluginV2.test.ts index 815f2fa27..380603b66 100644 --- a/packages/repack/src/plugins/__tests__/ModuleFederationPluginV2.test.ts +++ b/packages/repack/src/plugins/__tests__/ModuleFederationPluginV2.test.ts @@ -16,8 +16,9 @@ const mockPlugin = MFPluginRspack as unknown as jest.Mock< typeof MFPluginRspack >; -const runtimePluginPath = require.resolve( - '../../modules/FederationRuntimePlugin' +const corePluginPath = require.resolve('@callstack/repack/mf/core-plugin'); +const resolverPluginPath = require.resolve( + '@callstack/repack/mf/resolver-plugin' ); describe('ModuleFederationPlugin', () => { @@ -136,22 +137,24 @@ describe('ModuleFederationPlugin', () => { expect(config.shared['@react-native/'].eager).toBe(false); }); - it('should add FederationRuntimePlugin to runtime plugins', () => { + it('should add CorePlugin & ResolverPlugin to runtime plugins by default', () => { new ModuleFederationPluginV2({ name: 'test' }).apply(mockCompiler); const config = mockPlugin.mock.calls[0][0]; - expect(config.runtimePlugins).toContain(runtimePluginPath); + expect(config.runtimePlugins).toContain(corePluginPath); + expect(config.runtimePlugins).toContain(resolverPluginPath); }); - it('should not add FederationRuntimePlugin to runtime plugins when already present', () => { + it('should not add duplicate default runtime plugins when already present', () => { new ModuleFederationPluginV2({ name: 'test', - runtimePlugins: [runtimePluginPath], + runtimePlugins: [corePluginPath, resolverPluginPath], }).apply(mockCompiler); const config = mockPlugin.mock.calls[0][0]; - expect(config.runtimePlugins).toContain(runtimePluginPath); - expect(config.runtimePlugins).toHaveLength(1); + expect(config.runtimePlugins).toContain(corePluginPath); + expect(config.runtimePlugins).toContain(resolverPluginPath); + expect(config.runtimePlugins).toHaveLength(2); }); it('should use loaded-first as default shareStrategy', () => { diff --git a/packages/repack/tsconfig.json b/packages/repack/tsconfig.json index f62f95513..a033b264e 100644 --- a/packages/repack/tsconfig.json +++ b/packages/repack/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { "target": "ES2020", - "module": "commonjs", "lib": ["ES2020", "ES2021"], "declaration": true, "strict": true, - "moduleResolution": "node", + "module": "preserve", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true, @@ -20,10 +20,5 @@ ] } }, - "include": [ - "./client.d.ts", - "./commands.d.ts", - "src/**/*", - "../dev-server/src" - ] + "include": ["./commands.d.ts", "src/**/*", "../dev-server/src"] }