From 94491f628e140062615551eaaf1073a81617a464 Mon Sep 17 00:00:00 2001 From: Hanric Date: Fri, 17 May 2024 10:38:15 +0800 Subject: [PATCH] feat: prevent css flickering (#2502) --- .changeset/long-spies-march.md | 12 ++ .../src/components/Content.tsx | 46 +++--- .../nested-remote/src/components/Content.tsx | 3 +- .../nested-remote/src/modern-app-env.d.ts | 1 + apps/modernjs/modern.config.ts | 13 +- .../dts-plugin/src/core/lib/DTSManager.ts | 9 +- packages/dts-plugin/src/core/lib/DtsWorker.ts | 10 +- packages/dts-plugin/src/core/lib/utils.ts | 13 +- .../dts-plugin/src/dev-worker/DevWorker.ts | 10 +- .../src/lib/container/AsyncBoundaryPlugin.ts | 7 +- .../container/ModuleFederationPlugin.ts | 33 ++++- packages/manifest/src/ManifestManager.ts | 45 +++--- packages/manifest/src/StatsManager.ts | 8 +- packages/modernjs/project.json | 11 +- packages/modernjs/src/cli/index.ts | 133 ++++++++++-------- packages/modernjs/src/cli/manifest.ts | 10 +- packages/modernjs/src/cli/utils.ts | 73 ++++++++++ .../modernjs/src/runtime/MFReactComponent.tsx | 105 ++++++++++++++ packages/modernjs/src/runtime/index.ts | 1 + packages/modernjs/src/types/index.ts | 8 ++ packages/rspack/src/ModuleFederationPlugin.ts | 12 +- packages/runtime/src/index.ts | 4 + .../src/plugins/snapshot/SnapshotHandler.ts | 6 +- packages/runtime/src/remote/index.ts | 4 + .../sdk/src/generateSnapshotFromManifest.ts | 24 +--- .../types/plugins/ModuleFederationPlugin.ts | 7 +- 26 files changed, 430 insertions(+), 178 deletions(-) create mode 100644 .changeset/long-spies-march.md create mode 100644 packages/modernjs/src/runtime/MFReactComponent.tsx diff --git a/.changeset/long-spies-march.md b/.changeset/long-spies-march.md new file mode 100644 index 00000000000..dc3ea7f5025 --- /dev/null +++ b/.changeset/long-spies-march.md @@ -0,0 +1,12 @@ +--- +'@module-federation/webpack-bundler-runtime': patch +'@module-federation/devtools': patch +'@module-federation/enhanced': patch +'@module-federation/manifest': patch +'@module-federation/modern-js': patch +'@module-federation/runtime': patch +'@module-federation/modernjs': patch +'@module-federation/sdk': patch +--- + +feat: add MFComponent diff --git a/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx b/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx index f07a5eb8534..426a537a93e 100644 --- a/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx +++ b/apps/modernjs-ssr/dynamic-nested-remote/src/components/Content.tsx @@ -1,9 +1,12 @@ import React from 'react'; import Button from 'antd/lib/button'; -import { loadRemote, registerRemotes } from '@modern-js/runtime/mf'; +import { + loadRemote, + registerRemotes, + MFReactComponent, +} from '@modern-js/runtime/mf'; import stuff from './stuff.module.css'; -const isServer = typeof window === 'undefined'; registerRemotes([ { name: 'dynamic_remote', @@ -11,23 +14,23 @@ registerRemotes([ }, ]); -const Comp = React.lazy(() => - loadRemote('dynamic_remote/Image').then((m) => { - return { - default: () => ( -
- - 11 - -
- ), - }; - }), -); +// const Comp = React.lazy(() => +// loadRemote('dynamic_remote/Image').then((m) => { +// return { +// default: () => ( +//
+// +// 11 +// +//
+// ), +// }; +// }), +// ); const LazyButton = React.lazy(() => import('./Button').then((m) => { @@ -57,9 +60,10 @@ export default (): JSX.Element => ( Click me to test dynamic nested remote interactive! - + {/* - + */} + diff --git a/apps/modernjs-ssr/nested-remote/src/components/Content.tsx b/apps/modernjs-ssr/nested-remote/src/components/Content.tsx index 8063f721f85..b6c38ac79aa 100644 --- a/apps/modernjs-ssr/nested-remote/src/components/Content.tsx +++ b/apps/modernjs-ssr/nested-remote/src/components/Content.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { MFReactComponent, collectLinks } from '@modern-js/runtime/mf'; import Comp from 'remote/Image'; import Button from 'antd/lib/button'; import stuff from './stuff.module.css'; @@ -14,7 +15,7 @@ export default (): JSX.Element => ( > Click me to test nested remote interactive! - + {collectLinks('remote/Image')} ); diff --git a/apps/modernjs-ssr/nested-remote/src/modern-app-env.d.ts b/apps/modernjs-ssr/nested-remote/src/modern-app-env.d.ts index 3f453508cee..827e6c01d97 100644 --- a/apps/modernjs-ssr/nested-remote/src/modern-app-env.d.ts +++ b/apps/modernjs-ssr/nested-remote/src/modern-app-env.d.ts @@ -1,3 +1,4 @@ /// /// /// +/// diff --git a/apps/modernjs/modern.config.ts b/apps/modernjs/modern.config.ts index 58a90963919..094f6133e95 100644 --- a/apps/modernjs/modern.config.ts +++ b/apps/modernjs/modern.config.ts @@ -1,8 +1,5 @@ import { appTools, defineConfig } from '@modern-js/app-tools'; -import { - ModuleFederationPlugin, - AsyncBoundaryPlugin, -} from '@module-federation/enhanced'; +import { ModuleFederationPlugin } from '@module-federation/enhanced'; // https://modernjs.dev/en/configure/app/usage export default defineConfig({ dev: { @@ -36,12 +33,12 @@ export default defineConfig({ } appendPlugins([ - new AsyncBoundaryPlugin({ - excludeChunk: chunk => chunk.name === 'app1', - eager: module => /\.federation/.test(module?.request || ''), - }), new ModuleFederationPlugin({ name: 'app1', + async: { + excludeChunk: chunk => chunk.name === 'app1', + eager: module => /\.federation/.test(module?.request || ''), + }, exposes: { './thing': './src/test.ts', }, diff --git a/packages/dts-plugin/src/core/lib/DTSManager.ts b/packages/dts-plugin/src/core/lib/DTSManager.ts index 5013fe55204..09115c66560 100644 --- a/packages/dts-plugin/src/core/lib/DTSManager.ts +++ b/packages/dts-plugin/src/core/lib/DTSManager.ts @@ -7,7 +7,6 @@ import { Manifest, inferAutoPublicPath, } from '@module-federation/sdk'; -import cloneDeepWith from 'lodash.clonedeepwith'; import { retrieveRemoteConfig } from '../configurations/remotePlugin'; import { createTypesArchive, downloadTypesArchive } from './archiveHandler'; @@ -27,6 +26,7 @@ import { } from '../constant'; import axios from 'axios'; import { fileLog } from '../../server'; +import { cloneDeepOptions } from './utils'; export const MODULE_DTS_MANAGER_IDENTIFIER = 'MF DTS Manager'; @@ -44,12 +44,7 @@ class DTSManager { extraOptions: Record; constructor(options: DTSManagerOptions) { - this.options = cloneDeepWith(options, (_value, key) => { - // moduleFederationConfig.manifest may have un serialization options - if (key === 'manifest') { - return false; - } - }); + this.options = cloneDeepOptions(options); this.runtimePkgs = [ '@module-federation/runtime', '@module-federation/enhanced/runtime', diff --git a/packages/dts-plugin/src/core/lib/DtsWorker.ts b/packages/dts-plugin/src/core/lib/DtsWorker.ts index d8fdd83e025..ec5d10934e4 100644 --- a/packages/dts-plugin/src/core/lib/DtsWorker.ts +++ b/packages/dts-plugin/src/core/lib/DtsWorker.ts @@ -1,10 +1,10 @@ import path from 'path'; -import cloneDeepWith from 'lodash.clonedeepwith'; import { type RpcWorker, createRpcWorker } from '../rpc/index'; import type { RpcMethod } from '../rpc/types'; import type { DTSManagerOptions } from '../interfaces/DTSManagerOptions'; import type { DTSManager } from './DTSManager'; +import { cloneDeepOptions } from './utils'; export type DtsWorkerOptions = DTSManagerOptions; @@ -14,12 +14,8 @@ export class DtsWorker { private _res: Promise; constructor(options: DtsWorkerOptions) { - this._options = cloneDeepWith(options, (_value, key) => { - // moduleFederationConfig.manifest may have un serialization options - if (key === 'manifest') { - return false; - } - }); + this._options = cloneDeepOptions(options); + this.removeUnSerializationOptions(); this.rpcWorker = createRpcWorker( path.resolve(__dirname, './forkGenerateDts.js'), diff --git a/packages/dts-plugin/src/core/lib/utils.ts b/packages/dts-plugin/src/core/lib/utils.ts index 9461d4e99c2..fb954b9a808 100644 --- a/packages/dts-plugin/src/core/lib/utils.ts +++ b/packages/dts-plugin/src/core/lib/utils.ts @@ -11,7 +11,8 @@ import { } from './typeScriptCompiler'; import { moduleFederationPlugin } from '@module-federation/sdk'; import ansiColors from 'ansi-colors'; -import tsconfigPaths from 'vite-tsconfig-paths'; +import cloneDeepWith from 'lodash.clonedeepwith'; +import { DTSManagerOptions } from '../interfaces/DTSManagerOptions'; export function getDTSManagerConstructor( implementation?: string, @@ -93,3 +94,13 @@ export const isTSProject = ( return false; } }; + +export function cloneDeepOptions(options: DTSManagerOptions) { + const excludeKeys = ['manifest', 'async']; + return cloneDeepWith(options, (_value, key) => { + // moduleFederationConfig.manifest may have un serialization options + if (typeof key === 'string' && excludeKeys.includes(key)) { + return false; + } + }); +} diff --git a/packages/dts-plugin/src/dev-worker/DevWorker.ts b/packages/dts-plugin/src/dev-worker/DevWorker.ts index 92171db2ed6..77aa8bd7397 100644 --- a/packages/dts-plugin/src/dev-worker/DevWorker.ts +++ b/packages/dts-plugin/src/dev-worker/DevWorker.ts @@ -1,6 +1,6 @@ import path from 'path'; -import cloneDeepWith from 'lodash.clonedeepwith'; import { DTSManagerOptions, rpc } from '../core/index'; +import { cloneDeepOptions } from '../core/lib/utils'; export interface DevWorkerOptions extends DTSManagerOptions { name: string; @@ -14,12 +14,8 @@ export class DevWorker { private _res: Promise; constructor(options: DevWorkerOptions) { - this._options = cloneDeepWith(options, (_value, key): void | any => { - // moduleFederationConfig.manifest may have un serialization options - if (key === 'manifest') { - return false; - } - }); + this._options = cloneDeepOptions(options); + this.removeUnSerializationOptions(); this._rpcWorker = rpc.createRpcWorker( path.resolve(__dirname, './forkDevWorker.js'), diff --git a/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts b/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts index f130ae7a758..e14b21e74da 100644 --- a/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts +++ b/packages/enhanced/src/lib/container/AsyncBoundaryPlugin.ts @@ -1,4 +1,5 @@ import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; +import { moduleFederationPlugin } from '@module-federation/sdk'; import type { Compiler, Compilation, @@ -27,11 +28,7 @@ type InferStartupRenderContext = T extends SyncWaterfallHook< type StartupRenderContext = InferStartupRenderContext; -export interface Options { - eager?: RegExp | ((module: Module) => boolean); - excludeChunk?: (chunk: Chunk) => boolean; -} - +export type Options = moduleFederationPlugin.AsyncBoundaryOptions; class AsyncEntryStartupPlugin { private _options: Options; private _runtimeChunks = new Map(); diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts index 5a07bd50658..dbdabb93746 100644 --- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts @@ -671,6 +671,30 @@ export default { }, }, }, + AsyncBoundaryOptions: { + description: 'Make entrypoints startup as async chunks', + type: 'object', + properties: { + eager: { + description: 'whether eager', + anyOf: [ + { + instanceof: 'RegExp', + tsType: 'RegExp', + tsType: '((module: any) => boolean)', + }, + { + type: 'boolean', + }, + ], + }, + excludeChunk: { + description: 'exclude chunk', + instanceof: 'Function', + tsType: '(chunk: any) => boolean', + }, + }, + }, }, title: 'ModuleFederationPluginOptions', type: 'object', @@ -756,7 +780,14 @@ export default { }, async: { description: 'Make entrypoints startup as async chunks', - type: 'boolean', + anyOf: [ + { + $ref: '#/definitions/AsyncBoundaryOptions', + }, + { + type: 'boolean', + }, + ], }, }, }; diff --git a/packages/manifest/src/ManifestManager.ts b/packages/manifest/src/ManifestManager.ts index 9931f5ef8d2..0c79a140d94 100644 --- a/packages/manifest/src/ManifestManager.ts +++ b/packages/manifest/src/ManifestManager.ts @@ -59,47 +59,44 @@ class ManifestManager { ...stats, }; - manifest.exposes = Object.keys(stats.exposes).reduce((sum, cur) => { - const statsExpose = manifest.exposes[cur] as StatsExpose; + manifest.exposes = stats.exposes.reduce((sum, cur) => { const expose: ManifestExpose = { - id: statsExpose.id, - name: statsExpose.name, - assets: statsExpose.assets, - path: statsExpose.path, + id: cur.id, + name: cur.name, + assets: cur.assets, + path: cur.path, }; sum.push(expose); return sum; }, [] as ManifestExpose[]); - manifest.shared = Object.keys(stats.shared).reduce((sum, cur) => { - const statsShared = manifest.shared[cur] as StatsShared; + manifest.shared = stats.shared.reduce((sum, cur) => { const shared: ManifestShared = { - id: statsShared.id, - name: statsShared.name, - version: statsShared.version, - singleton: statsShared.singleton, - requiredVersion: statsShared.requiredVersion, - hash: statsShared.hash, - assets: statsShared.assets, + id: cur.id, + name: cur.name, + version: cur.version, + singleton: cur.singleton, + requiredVersion: cur.requiredVersion, + hash: cur.hash, + assets: cur.assets, }; sum.push(shared); return sum; }, [] as ManifestShared[]); - manifest.remotes = Object.keys(stats.remotes).reduce((sum, cur) => { - const statsRemote = manifest.remotes[cur] as StatsRemote; + manifest.remotes = stats.remotes.reduce((sum, cur) => { // @ts-ignore version/entry will be added as follow const remote: ManifestRemote = { - federationContainerName: statsRemote.federationContainerName, - moduleName: statsRemote.moduleName, - alias: statsRemote.alias, + federationContainerName: cur.federationContainerName, + moduleName: cur.moduleName, + alias: cur.alias, }; - if ('entry' in statsRemote) { + if ('entry' in cur) { // @ts-ignore - remote.entry = statsRemote.entry; - } else if ('version' in statsRemote) { + remote.entry = cur.entry; + } else if ('version' in cur) { // @ts-ignore - remote.entry = statsRemote.version; + remote.entry = cur.version; } sum.push(remote); diff --git a/packages/manifest/src/StatsManager.ts b/packages/manifest/src/StatsManager.ts index d360bddbd99..d2589142500 100644 --- a/packages/manifest/src/StatsManager.ts +++ b/packages/manifest/src/StatsManager.ts @@ -14,6 +14,7 @@ import { } from '@module-federation/sdk'; import { Compilation, Compiler, StatsCompilation, StatsModule } from 'webpack'; import { + isDev, getAssetsByChunk, findChunk, getAssetsByChunkIDs, @@ -400,8 +401,11 @@ class StatsManager { try { const { disableEmit } = extraOptions; const existedStats = compilation.getAsset(this.fileName); - if (existedStats) { - return JSON.parse(existedStats.source.source().toString()); + if (existedStats && !isDev()) { + return { + stats: JSON.parse(existedStats.source.source().toString()), + filename: this.fileName, + }; } const { manifest: manifestOptions = {} } = this._options; let stats = await this._generateStats(compiler, compilation); diff --git a/packages/modernjs/project.json b/packages/modernjs/project.json index 54686743a91..adcbbd6bd7a 100644 --- a/packages/modernjs/project.json +++ b/packages/modernjs/project.json @@ -7,6 +7,7 @@ "build": { "executor": "nx:run-commands", "options": { + "parallel": false, "dependsOn": [ { "target": "build", @@ -14,14 +15,8 @@ } ], "commands": [ - { - "command": "cd packages/modernjs; pnpm run build", - "forwardAllArgs": true - }, - { - "command": "cp packages/modernjs/*.md packages/modernjs/dist", - "forwardAllArgs": true - } + "cd packages/modernjs; pnpm run build", + "cp packages/modernjs/LICENSE packages/modernjs/dist" ] } }, diff --git a/packages/modernjs/src/cli/index.ts b/packages/modernjs/src/cli/index.ts index 1a56f353bee..395869452dc 100644 --- a/packages/modernjs/src/cli/index.ts +++ b/packages/modernjs/src/cli/index.ts @@ -1,13 +1,25 @@ import path from 'path'; import { fs } from '@modern-js/utils'; -import type { CliPlugin, AppTools } from '@modern-js/app-tools'; +import type { + CliPlugin, + AppTools, + webpack, + Rspack, +} from '@modern-js/app-tools'; import { - ModuleFederationPlugin, + ModuleFederationPlugin as WebpackModuleFederationPlugin, AsyncBoundaryPlugin, } from '@module-federation/enhanced'; +import { ModuleFederationPlugin as RspackModuleFederationPlugin } from '@module-federation/enhanced/rspack'; import { StreamingTargetPlugin } from '@module-federation/node'; -import type { PluginOptions } from '../types'; -import { getMFConfig, patchMFConfig } from './utils'; +import type { PluginOptions, BundlerPlugin } from '../types'; +import { + ConfigType, + getMFConfig, + getTargetEnvConfig, + patchMFConfig, + patchWebpackConfig, +} from './utils'; import { updateStatsAndManifest } from './manifest'; import { MODERN_JS_SERVER_DIR } from '../constant'; @@ -15,76 +27,84 @@ export const moduleFederationPlugin = ( userConfig: PluginOptions = {}, ): CliPlugin => ({ name: '@modern-js/plugin-module-federation', - setup: async ({ useConfigContext }) => { + setup: async ({ useConfigContext, useAppContext }) => { const useConfig = useConfigContext(); const enableSSR = Boolean(useConfig?.server?.ssr); - const isStreamSSR = - typeof useConfig?.server?.ssr === 'object' - ? useConfig?.server?.ssr?.mode === 'stream' - : false; const mfConfig = await getMFConfig(userConfig); let outputDir = ''; + const bundlerType = + useAppContext().bundlerType === 'rspack' ? 'rspack' : 'webpack'; + + const WebpackPluginConstructor = + userConfig.webpackPluginImplementation || WebpackModuleFederationPlugin; + const RspackPluginConstructor = + userConfig.webpackPluginImplementation || RspackModuleFederationPlugin; - let browserPlugin: ModuleFederationPlugin; - let nodePlugin: ModuleFederationPlugin; + const MFBundlerPlugin = + bundlerType === 'rspack' + ? RspackPluginConstructor + : WebpackPluginConstructor; + + let browserPlugin: BundlerPlugin; + let nodePlugin: BundlerPlugin; return { config: () => { if (enableSSR) { process.env['MF_DISABLE_EMIT_STATS'] = 'true'; + process.env['MF_SSR_PRJ'] = 'true'; } + + const modifyBundlerConfig = ( + config: ConfigType, + isServer: boolean, + ) => { + const envConfig = getTargetEnvConfig(mfConfig, isServer); + if (isServer) { + nodePlugin = new MFBundlerPlugin(envConfig); + // @ts-ignore + config.plugins?.push(nodePlugin); + // @ts-ignore + config.plugins?.push(new StreamingTargetPlugin(envConfig)); + } else { + outputDir = + config.output?.path || path.resolve(process.cwd(), 'dist'); + browserPlugin = new MFBundlerPlugin(envConfig); + // @ts-ignore + config.plugins?.push(browserPlugin); + } + + patchWebpackConfig({ + config, + isServer, + useConfig, + }); + }; + return { tools: { + rspack(config) { + // not support ssr yet + modifyBundlerConfig(config, false); + }, webpack(config, { isServer }) { - delete config.optimization?.runtimeChunk; - patchMFConfig(mfConfig); - if (isServer) { - nodePlugin = new ModuleFederationPlugin({ - library: { - type: 'commonjs-module', - name: mfConfig.name, - }, - ...mfConfig, - }); - config.plugins?.push(nodePlugin); - config.plugins?.push(new StreamingTargetPlugin(mfConfig)); - } else { - outputDir = - config.output?.path || path.resolve(process.cwd(), 'dist'); - browserPlugin = new ModuleFederationPlugin(mfConfig); - config.plugins?.push(browserPlugin); - - if ( - enableSSR && - isStreamSSR && - typeof config.optimization?.splitChunks === 'object' && - config.optimization.splitChunks.cacheGroups - ) { - config.optimization.splitChunks.chunks = 'async'; - console.warn( - '[Modern.js Module Federation] splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"', - ); - } - } - + modifyBundlerConfig(config, isServer); const enableAsyncEntry = useConfig.source?.enableAsyncEntry; - if (!enableAsyncEntry && mfConfig.async) { - const asyncBoundaryPluginOptions = { - eager: (module) => - module && /\.federation/.test(module?.request || ''), - excludeChunk: (chunk) => chunk.name === mfConfig.name, - }; + if ( + mfConfig.async || + (!enableAsyncEntry && mfConfig.async !== false) + ) { + const asyncBoundaryPluginOptions = + typeof mfConfig.async === 'object' + ? mfConfig.async + : { + eager: (module) => + module && /\.federation/.test(module?.request || ''), + excludeChunk: (chunk) => chunk.name === mfConfig.name, + }; config.plugins?.push( new AsyncBoundaryPlugin(asyncBoundaryPluginOptions), ); } - - if (config.output?.publicPath === 'auto') { - // TODO: only in dev temp - const port = - useConfig.dev?.port || useConfig.server?.port || 8080; - const publicPath = `http://localhost:${port}/`; - config.output.publicPath = publicPath; - } }, devServer: { headers: { @@ -104,7 +124,6 @@ export const moduleFederationPlugin = ( const SERVER_PREFIX = `/${MODERN_JS_SERVER_DIR}`; if ( req.url?.startsWith(SERVER_PREFIX) || - req.url?.includes('remoteEntry.js') || req.url?.includes('.json') ) { const filepath = path.join( diff --git a/packages/modernjs/src/cli/manifest.ts b/packages/modernjs/src/cli/manifest.ts index b6c09cc6a3f..7399e76811a 100644 --- a/packages/modernjs/src/cli/manifest.ts +++ b/packages/modernjs/src/cli/manifest.ts @@ -1,9 +1,9 @@ // TODO: Support Rspack import path from 'path'; -import { ModuleFederationPlugin } from '@module-federation/enhanced'; import { Stats, Manifest } from '@module-federation/sdk'; import { fs } from '@modern-js/utils'; import { MODERN_JS_SERVER_DIR } from '../constant'; +import { BundlerPlugin } from '../types'; function mergeStats(browserStats: Stats, nodeStats: Stats): Stats { const ssrRemoteEntry = nodeStats.metaData.remoteEntry; @@ -25,8 +25,8 @@ function mergeManifest( } function mergeStatsAndManifest( - nodePlugin: ModuleFederationPlugin, - browserPlugin: ModuleFederationPlugin, + nodePlugin: BundlerPlugin, + browserPlugin: BundlerPlugin, ): { mergedStats: Stats; mergedStatsFilePath: string; @@ -63,8 +63,8 @@ function mergeStatsAndManifest( } export function updateStatsAndManifest( - nodePlugin: ModuleFederationPlugin, - browserPlugin: ModuleFederationPlugin, + nodePlugin: BundlerPlugin, + browserPlugin: BundlerPlugin, outputDir: string, ) { const { diff --git a/packages/modernjs/src/cli/utils.ts b/packages/modernjs/src/cli/utils.ts index f6da25888e2..9370383ee0d 100644 --- a/packages/modernjs/src/cli/utils.ts +++ b/packages/modernjs/src/cli/utils.ts @@ -1,3 +1,9 @@ +import type { + webpack, + UserConfig, + AppTools, + Rspack, +} from '@modern-js/app-tools'; import { moduleFederationPlugin } from '@module-federation/sdk'; import path from 'path'; import { bundle } from '@modern-js/node-bundle-require'; @@ -5,6 +11,12 @@ import { PluginOptions } from '../types'; const defaultPath = path.resolve(process.cwd(), 'module-federation.config.ts'); +export type ConfigType = T extends 'webpack' + ? webpack.Configuration + : T extends 'rspack' + ? Rspack.Configuration + : never; + export const getMFConfig = async ( userConfig: PluginOptions, ): Promise => { @@ -37,3 +49,64 @@ export const patchMFConfig = ( mfConfig.async = true; } }; + +export function getTargetEnvConfig( + mfConfig: moduleFederationPlugin.ModuleFederationPluginOptions, + isServer: boolean, +) { + patchMFConfig(mfConfig); + if (isServer) { + return { + library: { + type: 'commonjs-module', + name: mfConfig.name, + }, + ...mfConfig, + }; + } + if (mfConfig.library?.type === 'commonjs-module') { + return { + ...mfConfig, + library: { + ...mfConfig.library, + type: 'global', + }, + }; + } + return mfConfig; +} + +export function patchWebpackConfig(options: { + config: ConfigType; + isServer: boolean; + useConfig: UserConfig; +}) { + const { config, useConfig, isServer } = options; + const enableSSR = Boolean(useConfig?.server?.ssr); + const isStreamSSR = + typeof useConfig?.server?.ssr === 'object' + ? useConfig?.server?.ssr?.mode === 'stream' + : false; + + delete config.optimization?.runtimeChunk; + + if ( + !isServer && + enableSSR && + isStreamSSR && + typeof config.optimization?.splitChunks === 'object' && + config.optimization.splitChunks.cacheGroups + ) { + config.optimization.splitChunks.chunks = 'async'; + console.warn( + '[Modern.js Module Federation] splitChunks.chunks = async is not allowed with stream SSR mode, it will auto changed to "async"', + ); + } + + if (config.output?.publicPath === 'auto') { + // TODO: only in dev temp + const port = useConfig.dev?.port || useConfig.server?.port || 8080; + const publicPath = `http://localhost:${port}/`; + config.output.publicPath = publicPath; + } +} diff --git a/packages/modernjs/src/runtime/MFReactComponent.tsx b/packages/modernjs/src/runtime/MFReactComponent.tsx new file mode 100644 index 00000000000..a83183e5178 --- /dev/null +++ b/packages/modernjs/src/runtime/MFReactComponent.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { + loadRemote, + getInstance, + type FederationHost, +} from '@module-federation/enhanced/runtime'; + +type Comp = React.FC | { default: React.FC }; +interface IProps { + id: string; + loading?: React.ReactNode; +} + +function getLoadedRemoteInfos(instance: FederationHost, id: string) { + const moduleName = instance.remoteHandler.idToModuleNameMap[id]; + if (!moduleName) { + return; + } + const module = instance.moduleCache.get(moduleName); + if (!module) { + return; + } + const { remoteSnapshot } = instance.snapshotHandler.getGlobalRemoteInfo( + module.remoteInfo, + ); + return { + ...module.remoteInfo, + snapshot: remoteSnapshot, + }; +} + +function collectLinks(id: string) { + const links: React.ReactNode[] = []; + const instance = getInstance(); + if (!instance) { + return links; + } + const loadedRemoteInfo = getLoadedRemoteInfos(instance, id); + if (!loadedRemoteInfo) { + return links; + } + const snapshot = loadedRemoteInfo.snapshot; + if (!snapshot) { + return links; + } + const publicPath = + 'publicPath' in snapshot + ? snapshot.publicPath + : 'getPublicPath' in snapshot + ? new Function(snapshot.getPublicPath)() + : ''; + if (!publicPath) { + return links; + } + const modules = 'modules' in snapshot ? snapshot.modules : []; + if (modules) { + modules.forEach((module) => { + [...module.assets.css.sync, ...module.assets.css.async].forEach( + (file, index) => { + // links.push(`${publicPath}${file}`) + links.push( + , + ); + }, + ); + }); + } + return links; +} + +function MFReactComponent(props: IProps) { + const { loading = 'loading...', id } = props; + + const Component = React.lazy(() => + loadRemote(id).then((mod) => { + const links = collectLinks(id); + if (!mod) { + throw new Error('load remote failed'); + } + const Com = + typeof mod === 'object' ? ('default' in mod ? mod.default : mod) : mod; + return { + default: () => ( +
+ {links} + +
+ ), + }; + }), + ); + + return ( + + + + ); +} + +export { MFReactComponent, collectLinks }; diff --git a/packages/modernjs/src/runtime/index.ts b/packages/modernjs/src/runtime/index.ts index 532254ed0fc..55cb683dc63 100644 --- a/packages/modernjs/src/runtime/index.ts +++ b/packages/modernjs/src/runtime/index.ts @@ -1 +1,2 @@ export * from '@module-federation/enhanced/runtime'; +export { MFReactComponent, collectLinks } from './MFReactComponent'; diff --git a/packages/modernjs/src/types/index.ts b/packages/modernjs/src/types/index.ts index fff9479127e..8eb99495d24 100644 --- a/packages/modernjs/src/types/index.ts +++ b/packages/modernjs/src/types/index.ts @@ -1,6 +1,14 @@ import { moduleFederationPlugin } from '@module-federation/sdk'; +import type { ModuleFederationPlugin as WebpackModuleFederationPlugin } from '@module-federation/enhanced'; +import type { ModuleFederationPlugin as RspackModuleFederationPlugin } from '@module-federation/enhanced/rspack'; export interface PluginOptions { config?: moduleFederationPlugin.ModuleFederationPluginOptions; configPath?: string; + webpackPluginImplementation?: typeof WebpackModuleFederationPlugin; + rspackPluginImplementation?: typeof RspackModuleFederationPlugin; } + +export type BundlerPlugin = + | WebpackModuleFederationPlugin + | RspackModuleFederationPlugin; diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index b80554b9237..159951b6d90 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -25,6 +25,7 @@ const RuntimeToolsPath = require.resolve('@module-federation/runtime-tools'); export class ModuleFederationPlugin implements RspackPluginInstance { readonly name = 'RspackModuleFederationPlugin'; private _options: moduleFederationPlugin.ModuleFederationPluginOptions; + private _statsPlugin?: StatsPlugin; constructor(options: moduleFederationPlugin.ModuleFederationPluginOptions) { this._options = options; @@ -102,11 +103,12 @@ export class ModuleFederationPlugin implements RspackPluginInstance { }); if (!disableManifest) { - new StatsPlugin(options, { + this._statsPlugin = new StatsPlugin(options, { pluginVersion: __VERSION__, bundler: 'rspack', - // @ts-ignore - }).apply(compiler); + }); + // @ts-ignore + this._statsPlugin.apply(compiler); } } @@ -184,4 +186,8 @@ export class ModuleFederationPlugin implements RspackPluginInstance { patchChunkSplit(cacheGroups[cacheGroupKey]); }); } + + get statsResourceInfo() { + return this._statsPlugin?.resourceInfo; + } } diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 158d015ec35..0703fc8e102 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -91,5 +91,9 @@ export function registerPlugins( return FederationInstance.registerPlugins.apply(FederationInstance, args); } +export function getInstance() { + return FederationInstance; +} + // Inject for debug setGlobalFederationConstructor(FederationHost); diff --git a/packages/runtime/src/plugins/snapshot/SnapshotHandler.ts b/packages/runtime/src/plugins/snapshot/SnapshotHandler.ts index e4734f09895..3e6dee40ae7 100644 --- a/packages/runtime/src/plugins/snapshot/SnapshotHandler.ts +++ b/packages/runtime/src/plugins/snapshot/SnapshotHandler.ts @@ -153,7 +153,9 @@ export class SnapshotHandler { if (isManifestProvider(globalRemoteSnapshot)) { const remoteEntry = isBrowserEnv() ? globalRemoteSnapshot.remoteEntry - : globalRemoteSnapshot.ssrRemoteEntry || ''; + : globalRemoteSnapshot.ssrRemoteEntry || + globalRemoteSnapshot.remoteEntry || + ''; const moduleSnapshot = await this.getManifestJson( remoteEntry, moduleInfo, @@ -227,7 +229,7 @@ export class SnapshotHandler { } } - private getGlobalRemoteInfo(moduleInfo: Remote): { + getGlobalRemoteInfo(moduleInfo: Remote): { hostGlobalSnapshot: ModuleInfo | undefined; globalSnapshot: ReturnType; remoteSnapshot: GlobalModuleInfo[string] | undefined; diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts index 4a5b8b647c7..5f175f1c0d8 100644 --- a/packages/runtime/src/remote/index.ts +++ b/packages/runtime/src/remote/index.ts @@ -46,6 +46,8 @@ export interface LoadRemoteMatch { export class RemoteHandler { host: FederationHost; + idToModuleNameMap: Record; + hooks = new PluginSystem({ beforeRequest: new AsyncWaterfallHook<{ id: string; @@ -120,6 +122,7 @@ export class RemoteHandler { constructor(host: FederationHost) { this.host = host; + this.idToModuleNameMap = {}; } formatAndRegisterRemote(globalOptions: Options, userOptions: UserOptions) { @@ -166,6 +169,7 @@ export class RemoteHandler { origin: host, }); + this.idToModuleNameMap[id] = remote.name; if (typeof moduleWrapper === 'function') { return moduleWrapper as T; } diff --git a/packages/sdk/src/generateSnapshotFromManifest.ts b/packages/sdk/src/generateSnapshotFromManifest.ts index 21961dd2445..2dd6783a584 100644 --- a/packages/sdk/src/generateSnapshotFromManifest.ts +++ b/packages/sdk/src/generateSnapshotFromManifest.ts @@ -199,24 +199,12 @@ export function generateSnapshotFromManifest( export function isManifestProvider( moduleInfo: ModuleInfo | ManifestProvider, ): moduleInfo is ManifestProvider { - if (isBrowserEnv()) { - if ( - 'remoteEntry' in moduleInfo && - moduleInfo.remoteEntry.includes(MANIFEST_EXT) - ) { - return true; - } else { - return false; - } + if ( + 'remoteEntry' in moduleInfo && + moduleInfo.remoteEntry.includes(MANIFEST_EXT) + ) { + return true; } else { - if ( - 'ssrRemoteEntry' in moduleInfo && - moduleInfo.ssrRemoteEntry && - moduleInfo.ssrRemoteEntry.includes(MANIFEST_EXT) - ) { - return true; - } else { - return false; - } + return false; } } diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index a73e478466f..8d45c8351b1 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -161,6 +161,11 @@ export interface PluginDtsOptions { implementation?: string; } +export type AsyncBoundaryOptions = { + eager?: RegExp | ((module: any) => boolean); + excludeChunk?: (chunk: any) => boolean; +}; + export interface ModuleFederationPluginOptions { /** * Modules that should be exposed by this container. When provided, property name is used as public name, otherwise public name is automatically inferred from request. @@ -211,7 +216,7 @@ export interface ModuleFederationPluginOptions { dev?: boolean | PluginDevOptions; dts?: boolean | PluginDtsOptions; - async?: boolean; + async?: boolean | AsyncBoundaryOptions; } /** * Modules that should be exposed by this container. Property names are used as public paths.