diff --git a/.changeset/gentle-melons-deliver.md b/.changeset/gentle-melons-deliver.md new file mode 100644 index 00000000000..588ff3df9fd --- /dev/null +++ b/.changeset/gentle-melons-deliver.md @@ -0,0 +1,8 @@ +--- +'@module-federation/third-party-dts-extractor': patch +'@module-federation/dts-plugin': patch +'@module-federation/runtime': patch +'@module-federation/sdk': patch +--- + +feat: support dynamic remote type hints diff --git a/apps/runtime-demo/3006-runtime-remote/webpack.config.js b/apps/runtime-demo/3006-runtime-remote/webpack.config.js index a96e16d9b12..a1f5927a334 100644 --- a/apps/runtime-demo/3006-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3006-runtime-remote/webpack.config.js @@ -82,7 +82,7 @@ module.exports = composePlugins( // e.g. `config.plugins.push(new MyPlugin())` config.output = { ...config.output, - publicPath: 'http://localhost:3006/', + publicPath: 'http://127.0.0.1:3006/', scriptType: 'text/javascript', }; config.optimization = { diff --git a/apps/runtime-demo/3007-runtime-remote/webpack.config.js b/apps/runtime-demo/3007-runtime-remote/webpack.config.js index 3778291c755..e479a71bec6 100644 --- a/apps/runtime-demo/3007-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3007-runtime-remote/webpack.config.js @@ -72,7 +72,7 @@ module.exports = composePlugins( // e.g. `config.plugins.push(new MyPlugin())` config.output = { ...config.output, - publicPath: 'http://localhost:3007/', + publicPath: 'http://127.0.0.1:3007/', scriptType: 'text/javascript', }; config.optimization = { diff --git a/apps/website-new/docs/en/configure/dev.mdx b/apps/website-new/docs/en/configure/dev.mdx index 942db4f3d03..5f62fb4219f 100644 --- a/apps/website-new/docs/en/configure/dev.mdx +++ b/apps/website-new/docs/en/configure/dev.mdx @@ -11,6 +11,7 @@ The `PluginDevOptions` types are as follows: interface PluginDevOptions { disableLiveReload?: boolean; disableHotTypesReload?: boolean; + disableDynamicRemoteTypeHints?: boolean; } ``` @@ -29,3 +30,11 @@ Whether to disable type hot reloading - Default value: `false` Whether to disable `liveReload`, after setting `true`, any updates from the producer will not trigger the consumer's **page** `liveReload` + +## disableDynamicRemoteTypeHints + +- Type: `boolean` +- Required: No +- Default value: `false` + +Whether to disable type hot reloading diff --git a/apps/website-new/docs/zh/configure/dev.mdx b/apps/website-new/docs/zh/configure/dev.mdx index 4aa0ac887ae..9375c4f9c40 100644 --- a/apps/website-new/docs/zh/configure/dev.mdx +++ b/apps/website-new/docs/zh/configure/dev.mdx @@ -11,6 +11,7 @@ interface PluginDevOptions { disableLiveReload?: boolean; disableHotTypesReload?: boolean; + disableDynamicRemoteTypeHints?: boolean; } ``` @@ -29,3 +30,11 @@ interface PluginDevOptions { - 默认值:`false` 是否禁用 `liveReload`,设置 `true` 后,生产者的任何更新,都不会触发消费者的**页面** `liveReload` + +## disableDynamicRemoteTypeHints + +- 类型:`boolean` +- 是否必填:否 +- 默认值:`false` + +是否禁用动态模块类型提示,设置 `true` 后,消费者不会获取到动态模块的类型。 diff --git a/packages/dts-plugin/package.json b/packages/dts-plugin/package.json index c1496cc4daa..7a45e45e0d6 100644 --- a/packages/dts-plugin/package.json +++ b/packages/dts-plugin/package.json @@ -22,6 +22,11 @@ "import": "./dist/core.js", "require": "./dist/core.js" }, + "./dynamic-remote-type-hints-plugin": { + "types": "./dist/dynamic-remote-type-hints-plugin.d.ts", + "import": "./dist/esm/dynamic-remote-type-hints-plugin.js", + "require": "./dist/dynamic-remote-type-hints-plugin.js" + }, "./*": "./*" }, "typesVersions": { @@ -31,6 +36,9 @@ ], "core": [ "./dist/core.d.ts" + ], + "dynamic-remote-type-hints-plugin": [ + "./dist/dynamic-remote-type-hints-plugin.d.ts" ] } }, @@ -54,7 +62,8 @@ "devDependencies": { "@types/ws": "8.5.10", "@types/koa": "2.11.2", - "@types/node-schedule": "2.1.7" + "@types/node-schedule": "2.1.7", + "@module-federation/runtime": "workspace:*" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0", diff --git a/packages/dts-plugin/src/core/configurations/hostPlugin.ts b/packages/dts-plugin/src/core/configurations/hostPlugin.ts index 4421680e666..6c958e210b2 100644 --- a/packages/dts-plugin/src/core/configurations/hostPlugin.ts +++ b/packages/dts-plugin/src/core/configurations/hostPlugin.ts @@ -33,7 +33,7 @@ const buildApiTypeUrl = (zipUrl?: string) => { return zipUrl.replace('.zip', '.d.ts'); }; -const retrieveRemoteInfo = (options: { +export const retrieveRemoteInfo = (options: { hostOptions: Required; remoteAlias: string; remote: string; diff --git a/packages/dts-plugin/src/core/index.ts b/packages/dts-plugin/src/core/index.ts index cb4427eeb05..b6dd778aeab 100644 --- a/packages/dts-plugin/src/core/index.ts +++ b/packages/dts-plugin/src/core/index.ts @@ -23,7 +23,7 @@ export { UpdateMode, } from './constant'; -export { DTSManagerOptions } from './interfaces/DTSManagerOptions'; -export { HostOptions } from './interfaces/HostOptions'; -export { RemoteOptions } from './interfaces/RemoteOptions'; +export type { DTSManagerOptions } from './interfaces/DTSManagerOptions'; +export type { HostOptions } from './interfaces/HostOptions'; +export type { RemoteOptions } from './interfaces/RemoteOptions'; export * as rpc from './rpc/index'; diff --git a/packages/dts-plugin/src/core/lib/DTSManager.advance.spec.ts b/packages/dts-plugin/src/core/lib/DTSManager.advance.spec.ts index 5b1018bc763..e204c6f506e 100644 --- a/packages/dts-plugin/src/core/lib/DTSManager.advance.spec.ts +++ b/packages/dts-plugin/src/core/lib/DTSManager.advance.spec.ts @@ -21,7 +21,7 @@ describe('DTSManager advance usage', () => { 'react-dom': { singleton: true, eager: true }, }, }, - tsConfigPath: join(__dirname, '../../..', './tsconfig.json'), + tsConfigPath: join(__dirname, '../../..', './tsconfig.spec.json'), typesFolder: typesFolder, compiledTypesFolder: 'compiled-types', deleteTypesFolder: false, diff --git a/packages/dts-plugin/src/core/lib/DTSManager.spec.ts b/packages/dts-plugin/src/core/lib/DTSManager.spec.ts index 7aba38f101f..aa11344e631 100644 --- a/packages/dts-plugin/src/core/lib/DTSManager.spec.ts +++ b/packages/dts-plugin/src/core/lib/DTSManager.spec.ts @@ -22,7 +22,7 @@ describe('DTSManager', () => { 'react-dom': { singleton: true, eager: true }, }, }, - tsConfigPath: join(__dirname, '../../..', './tsconfig.json'), + tsConfigPath: join(__dirname, '../../..', './tsconfig.spec.json'), typesFolder: typesFolder, compiledTypesFolder: 'compiled-types', deleteTypesFolder: false, diff --git a/packages/dts-plugin/src/core/lib/DTSManager.ts b/packages/dts-plugin/src/core/lib/DTSManager.ts index 559d48047bd..78c5ddd6c9c 100644 --- a/packages/dts-plugin/src/core/lib/DTSManager.ts +++ b/packages/dts-plugin/src/core/lib/DTSManager.ts @@ -8,6 +8,7 @@ import { inferAutoPublicPath, } from '@module-federation/sdk'; import cloneDeepWith from 'lodash.clonedeepwith'; +import { ThirdPartyExtractor } from '@module-federation/third-party-dts-extractor'; import { retrieveRemoteConfig } from '../configurations/remotePlugin'; import { createTypesArchive, downloadTypesArchive } from './archiveHandler'; @@ -16,7 +17,10 @@ import { retrieveMfAPITypesPath, retrieveMfTypesPath, } from './typeScriptCompiler'; -import { retrieveHostConfig } from '../configurations/hostPlugin'; +import { + retrieveHostConfig, + retrieveRemoteInfo, +} from '../configurations/hostPlugin'; import { DTSManagerOptions } from '../interfaces/DTSManagerOptions'; import { HostOptions, RemoteInfo } from '../interfaces/HostOptions'; import { @@ -34,14 +38,17 @@ interface UpdateTypesOptions { updateMode: UpdateMode; remoteName?: string; remoteTarPath?: string; + remoteInfo?: RemoteInfo; + once?: boolean; } class DTSManager { options: DTSManagerOptions; runtimePkgs: string[]; remoteAliasMap: Record>; - loadedRemoteAPIAlias: string[]; + loadedRemoteAPIAlias: Set; extraOptions: Record; + updatedRemoteInfos: Record>; constructor(options: DTSManagerOptions) { this.options = cloneDeepWith(options, (_value, key) => { @@ -55,9 +62,10 @@ class DTSManager { '@module-federation/enhanced/runtime', '@module-federation/runtime-tools', ]; - this.loadedRemoteAPIAlias = []; + this.loadedRemoteAPIAlias = new Set(); this.remoteAliasMap = {}; this.extraOptions = options?.extraOptions || {}; + this.updatedRemoteInfos = {}; } generateAPITypes(mapComponentsToExpose: Record) { @@ -249,7 +257,7 @@ class DTSManager { ); const filePath = path.join(destinationPath, REMOTE_API_TYPES_FILE_NAME); fs.writeFileSync(filePath, apiTypeFile); - this.loadedRemoteAPIAlias.push(remoteInfo.alias); + this.loadedRemoteAPIAlias.add(remoteInfo.alias); } catch (err) { fileLog( `Unable to download "${remoteInfo.name}" api types, ${err}`, @@ -260,13 +268,34 @@ class DTSManager { } consumeAPITypes(hostOptions: Required) { - if (!this.loadedRemoteAPIAlias.length) { + const apiTypeFileName = path.join( + hostOptions.context, + hostOptions.typesFolder, + HOST_API_TYPES_FILE_NAME, + ); + try { + const existedFile = fs.readFileSync(apiTypeFileName, 'utf-8'); + const existedImports = new ThirdPartyExtractor('').collectTypeImports( + existedFile, + ); + existedImports.forEach((existedImport) => { + const alias = existedImport + .split('./') + .slice(1) + .join('./') + .replace('/apis.d.ts', ''); + this.loadedRemoteAPIAlias.add(alias); + }); + } catch (err) { + //noop + } + if (!this.loadedRemoteAPIAlias.size) { return; } const packageTypes: string[] = []; const remoteKeys: string[] = []; - const importTypeStr = this.loadedRemoteAPIAlias + const importTypeStr = [...this.loadedRemoteAPIAlias] .sort() .map((alias, index) => { const remoteKey = `RemoteKeys_${index}`; @@ -392,46 +421,97 @@ class DTSManager { } async updateTypes(options: UpdateTypesOptions): Promise { - // can use remoteTarPath directly in the future - const { remoteName, updateMode } = options; - const hostName = this.options?.host?.moduleFederationConfig?.name; - - if (updateMode === UpdateMode.POSITIVE && remoteName === hostName) { - if (!this.options.remote) { - return; - } - this.generateTypes(); - } else { - const { remoteAliasMap } = this; - if (!this.options.host) { - return; - } - const { hostOptions, mapRemotesToDownload } = retrieveHostConfig( - this.options.host, + try { + // can use remoteTarPath directly in the future + const { + remoteName, + updateMode, + remoteInfo: updatedRemoteInfo, + once, + } = options; + const hostName = this.options?.host?.moduleFederationConfig?.name; + fileLog( + `updateTypes options:, ${JSON.stringify(options, null, 2)}`, + 'consumeTypes', + 'info', ); + if (updateMode === UpdateMode.POSITIVE && remoteName === hostName) { + if (!this.options.remote) { + return; + } + this.generateTypes(); + } else { + const { remoteAliasMap } = this; + if (!this.options.host) { + return; + } + const { hostOptions, mapRemotesToDownload } = retrieveHostConfig( + this.options.host, + ); - const loadedRemoteInfo = Object.values(remoteAliasMap).find( - (i) => i.name === remoteName, - ); + const loadedRemoteInfo = Object.values(remoteAliasMap).find( + (i) => i.name === remoteName, + ); - if (!loadedRemoteInfo) { - const remoteInfo = Object.values(mapRemotesToDownload).find((item) => { - return item.name === remoteName; - }); - if (remoteInfo) { - if (!this.remoteAliasMap[remoteInfo.alias]) { - const requiredRemoteInfo = - await this.requestRemoteManifest(remoteInfo); - this.remoteAliasMap[remoteInfo.alias] = requiredRemoteInfo; - } - await this.consumeTargetRemotes( - hostOptions, - this.remoteAliasMap[remoteInfo.alias], + if (!loadedRemoteInfo) { + const remoteInfo = Object.values(mapRemotesToDownload).find( + (item) => { + return item.name === remoteName; + }, ); + if (remoteInfo) { + if (!this.remoteAliasMap[remoteInfo.alias]) { + const requiredRemoteInfo = + await this.requestRemoteManifest(remoteInfo); + this.remoteAliasMap[remoteInfo.alias] = requiredRemoteInfo; + } + await this.consumeTargetRemotes( + hostOptions, + this.remoteAliasMap[remoteInfo.alias], + ); + } else if (updatedRemoteInfo) { + const consumeDynamicRemoteTypes = async () => { + const [_destinationFolder, destinationPath] = + await this.consumeTargetRemotes( + hostOptions, + this.updatedRemoteInfos[updatedRemoteInfo.name], + ); + await this.downloadAPITypes( + this.updatedRemoteInfos[updatedRemoteInfo.name], + destinationPath, + ); + this.consumeAPITypes(hostOptions); + }; + if (!this.updatedRemoteInfos[updatedRemoteInfo.name]) { + const parsedRemoteInfo = retrieveRemoteInfo({ + hostOptions: hostOptions, + remoteAlias: updatedRemoteInfo.alias || updatedRemoteInfo.name, + remote: updatedRemoteInfo.url, + }); + fileLog(`start request manifest`, 'consumeTypes', 'info'); + this.updatedRemoteInfos[updatedRemoteInfo.name] = + await this.requestRemoteManifest(parsedRemoteInfo); + fileLog( + `end request manifest, this.updatedRemoteInfos[updatedRemoteInfo.name]: ${JSON.stringify( + this.updatedRemoteInfos[updatedRemoteInfo.name], + null, + 2, + )}`, + 'consumeTypes', + 'info', + ); + await consumeDynamicRemoteTypes(); + } + if (!once && this.updatedRemoteInfos[updatedRemoteInfo.name]) { + await consumeDynamicRemoteTypes(); + } + } + } else { + await this.consumeTargetRemotes(hostOptions, loadedRemoteInfo); } - } else { - await this.consumeTargetRemotes(hostOptions, loadedRemoteInfo); } + } catch (err) { + fileLog(`updateTypes fail, ${err}`, 'updateTypes', 'error'); } } } diff --git a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts index f84d41feace..8fb7ba9526a 100644 --- a/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts +++ b/packages/dts-plugin/src/core/lib/DtsWorker.spec.ts @@ -18,7 +18,7 @@ describe('generateTypesInChildProcess', () => { 'react-dom': { singleton: true, eager: true }, }, }, - tsConfigPath: join(__dirname, '../../..', './tsconfig.json'), + tsConfigPath: join(__dirname, '../../..', './tsconfig.spec.json'), typesFolder: typesFolder, compiledTypesFolder: 'compiled-types', deleteTypesFolder: false, diff --git a/packages/dts-plugin/src/core/lib/DtsWorker.ts b/packages/dts-plugin/src/core/lib/DtsWorker.ts index d8fdd83e025..73c0f258ef9 100644 --- a/packages/dts-plugin/src/core/lib/DtsWorker.ts +++ b/packages/dts-plugin/src/core/lib/DtsWorker.ts @@ -22,7 +22,7 @@ export class DtsWorker { }); this.removeUnSerializationOptions(); this.rpcWorker = createRpcWorker( - path.resolve(__dirname, './forkGenerateDts.js'), + path.resolve(__dirname, './fork-generate-dts.js'), {}, undefined, true, diff --git a/packages/dts-plugin/src/core/lib/archiveHandler.test.ts b/packages/dts-plugin/src/core/lib/archiveHandler.test.ts index fe6409e78e7..410131ec0f9 100644 --- a/packages/dts-plugin/src/core/lib/archiveHandler.test.ts +++ b/packages/dts-plugin/src/core/lib/archiveHandler.test.ts @@ -27,7 +27,7 @@ describe('archiveHandler', () => { compiledTypesFolder: 'compiledTypesFolder', typesFolder: 'typesRemoteFolder', moduleFederationConfig: {}, - tsConfigPath: './tsconfig.json', + tsConfigPath: './tsconfig.spec.json', deleteTypesFolder: false, } as unknown as Required; diff --git a/packages/dts-plugin/src/core/lib/typeScriptCompiler.test.ts b/packages/dts-plugin/src/core/lib/typeScriptCompiler.test.ts index 1c5f6a194dd..b1670eb5ffe 100644 --- a/packages/dts-plugin/src/core/lib/typeScriptCompiler.test.ts +++ b/packages/dts-plugin/src/core/lib/typeScriptCompiler.test.ts @@ -23,7 +23,7 @@ describe('typeScriptCompiler', () => { compiledTypesFolder: 'compiledTypesFolder', typesFolder: 'typesRemoteFolder', moduleFederationConfig: {}, - tsConfigPath: './tsconfig.json', + tsConfigPath: './tsconfig.spec.json', deleteTypesFolder: false, compilerInstance: 'tsc', compileInChildProcess: false, diff --git a/packages/dts-plugin/src/dev-worker/DevWorker.ts b/packages/dts-plugin/src/dev-worker/DevWorker.ts index 92171db2ed6..848cd4a816f 100644 --- a/packages/dts-plugin/src/dev-worker/DevWorker.ts +++ b/packages/dts-plugin/src/dev-worker/DevWorker.ts @@ -22,7 +22,7 @@ export class DevWorker { }); this.removeUnSerializationOptions(); this._rpcWorker = rpc.createRpcWorker( - path.resolve(__dirname, './forkDevWorker.js'), + path.resolve(__dirname, './fork-dev-worker.js'), {}, undefined, false, diff --git a/packages/dts-plugin/src/dev-worker/forkDevWorker.ts b/packages/dts-plugin/src/dev-worker/forkDevWorker.ts index a9919150ec6..fff3bbb21d1 100644 --- a/packages/dts-plugin/src/dev-worker/forkDevWorker.ts +++ b/packages/dts-plugin/src/dev-worker/forkDevWorker.ts @@ -22,24 +22,12 @@ import { } from '../server'; import { DevWorkerOptions } from './DevWorker'; - -const DEFAULT_LOCAL_IPS = ['localhost', '127.0.0.1']; +import { getIpFromEntry } from './utils'; interface Options extends DevWorkerOptions { name: string; } -function getIpFromEntry(entry: string): string | void { - let ip; - entry.replace(/https?:\/\/([0-9|.]+|localhost):/, (str, matched) => { - ip = matched; - return str; - }); - if (ip) { - return DEFAULT_LOCAL_IPS.includes(ip) ? getIPV4() : ip; - } -} - let typesManager: DTSManager, serverAddress: string, moduleServer: ModuleFederationDevServer, @@ -60,7 +48,7 @@ function getLocalRemoteNames( const name = encodeNameIdentifier ? decodeName(remoteInfo.name, encodeNameIdentifier) : remoteInfo.name; - const ip = getIpFromEntry(remoteInfo.url); + const ip = getIpFromEntry(remoteInfo.url, getIPV4()); if (!ip) { return sum; } @@ -79,10 +67,12 @@ async function updateCallback({ updateMode, name, remoteTypeTarPath, + remoteInfo, + once, }: UpdateCallbackOptions): Promise { const { disableHotTypesReload, disableLiveReload } = cacheOptions || {}; fileLog( - `sync remote module ${name}, types to vmok ${cacheOptions?.name},typesManager.updateTypes run`, + `sync remote module ${name}, types to ${cacheOptions?.name},typesManager.updateTypes run`, 'forkDevWorker', 'info', ); @@ -98,6 +88,8 @@ async function updateCallback({ updateMode, remoteName: name, remoteTarPath: remoteTypeTarPath, + remoteInfo, + once, }); } } diff --git a/packages/dts-plugin/src/dev-worker/index.ts b/packages/dts-plugin/src/dev-worker/index.ts index 24a9d502c8e..45ba2964555 100644 --- a/packages/dts-plugin/src/dev-worker/index.ts +++ b/packages/dts-plugin/src/dev-worker/index.ts @@ -1,2 +1,2 @@ export { createDevWorker } from './createDevWorker'; -export { DevWorker, DevWorkerOptions } from './DevWorker'; +export { DevWorker, type DevWorkerOptions } from './DevWorker'; diff --git a/packages/dts-plugin/src/dev-worker/utils.ts b/packages/dts-plugin/src/dev-worker/utils.ts new file mode 100644 index 00000000000..0cb70707173 --- /dev/null +++ b/packages/dts-plugin/src/dev-worker/utils.ts @@ -0,0 +1,12 @@ +const DEFAULT_LOCAL_IPS = ['localhost', '127.0.0.1']; + +export function getIpFromEntry(entry: string, ipv4?: string): string | void { + let ip; + entry.replace(/https?:\/\/([0-9|.]+|localhost):/, (str, matched) => { + ip = matched; + return str; + }); + if (ip) { + return DEFAULT_LOCAL_IPS.includes(ip) ? ipv4 : ip; + } +} diff --git a/packages/dts-plugin/src/plugins/DevPlugin.ts b/packages/dts-plugin/src/plugins/DevPlugin.ts index 8eb590ae919..e16494d847b 100644 --- a/packages/dts-plugin/src/plugins/DevPlugin.ts +++ b/packages/dts-plugin/src/plugins/DevPlugin.ts @@ -5,7 +5,11 @@ import { moduleFederationPlugin, normalizeOptions, } from '@module-federation/sdk'; -import { WEB_CLIENT_OPTIONS_IDENTIFIER, WebClientOptions } from '../server'; +import { + WEB_CLIENT_OPTIONS_IDENTIFIER, + WebClientOptions, + getIPV4, +} from '../server'; import type { Compiler, WebpackPluginInstance } from 'webpack'; import path from 'path'; import { isDev } from './utils'; @@ -95,13 +99,16 @@ export class DevPlugin implements WebpackPluginInstance { const { _options: { name, dev, dts }, } = this; - + new compiler.webpack.DefinePlugin({ + FEDERATION_IPV4: JSON.stringify(getIPV4()), + }).apply(compiler); const normalizedDev = normalizeOptions( true, { disableLiveReload: true, disableHotTypesReload: false, + disableDynamicRemoteTypeHints: false, }, 'mfOptions.dev', )(dev); @@ -112,7 +119,8 @@ export class DevPlugin implements WebpackPluginInstance { if ( normalizedDev.disableHotTypesReload && - normalizedDev.disableLiveReload + normalizedDev.disableLiveReload && + normalizedDev.disableDynamicRemoteTypeHints ) { return; } @@ -120,6 +128,15 @@ export class DevPlugin implements WebpackPluginInstance { throw new Error('name is required if you want to enable dev server!'); } + if (!normalizedDev.disableDynamicRemoteTypeHints) { + if (!this._options.runtimePlugins) { + this._options.runtimePlugins = []; + } + this._options.runtimePlugins.push( + path.resolve(__dirname, 'dynamic-remote-type-hints-plugin.js'), + ); + } + if (!normalizedDev.disableLiveReload) { const TEMP_DIR = path.join( `${process.cwd()}/node_modules`, diff --git a/packages/dts-plugin/src/runtime-plugins/dynamic-remote-type-hints-plugin.ts b/packages/dts-plugin/src/runtime-plugins/dynamic-remote-type-hints-plugin.ts new file mode 100644 index 00000000000..67f189ddeb2 --- /dev/null +++ b/packages/dts-plugin/src/runtime-plugins/dynamic-remote-type-hints-plugin.ts @@ -0,0 +1,74 @@ +import type { FederationRuntimePlugin } from '@module-federation/runtime/types'; +import { createWebsocket } from '../server/createWebsocket'; +import { + AddDynamicRemoteAction, + FetchTypesAction, +} from '../server/message/Action'; +import { getIpFromEntry } from '../dev-worker/utils'; + +declare const FEDERATION_IPV4: string | undefined; +const PLUGIN_NAME = 'dynamic-remote-type-hints-plugin'; + +function dynamicRemoteTypeHintsPlugin(): FederationRuntimePlugin { + let ws = createWebsocket(); + let isConnected = false; + ws.onopen = () => { + isConnected = true; + }; + + ws.onerror = (err) => { + console.error(`[ ${PLUGIN_NAME} ] err: ${err}`); + }; + + return { + name: 'dynamic-remote-type-hints-plugin', + registerRemote(args) { + const { remote, origin } = args; + try { + if (!isConnected) { + return args; + } + if (!('entry' in remote)) { + return args; + } + const defaultIpV4 = + typeof FEDERATION_IPV4 === 'string' ? FEDERATION_IPV4 : '127.0.0.1'; + const remoteIp = getIpFromEntry(remote.entry, defaultIpV4); + const remoteInfo = { + name: remote.name, + url: remote.entry, + alias: remote.alias || remote.name, + }; + if (remoteIp) { + ws.send( + JSON.stringify( + new AddDynamicRemoteAction({ + remoteIp, + remoteInfo, + name: origin.name, + ip: defaultIpV4, + }), + ), + ); + } + // fetch types + ws.send( + JSON.stringify( + new FetchTypesAction({ + name: origin.name, + ip: defaultIpV4, + remoteInfo, + }), + ), + ); + + return args; + } catch (err) { + console.error(new Error(err as unknown as any)); + return args; + } + }, + }; +} + +export default dynamicRemoteTypeHintsPlugin; diff --git a/packages/dts-plugin/src/runtime-plugins/utils.ts b/packages/dts-plugin/src/runtime-plugins/utils.ts new file mode 100644 index 00000000000..c3f53b1e32f --- /dev/null +++ b/packages/dts-plugin/src/runtime-plugins/utils.ts @@ -0,0 +1,12 @@ +const DEFAULT_LOCAL_IPS = ['localhost', '127.0.0.1']; + +export function getIp(entry: string, ipv4?: string): string | void { + let ip; + entry.replace(/https?:\/\/([0-9|.]+|localhost):/, (str, matched) => { + ip = matched; + return str; + }); + if (ip) { + return DEFAULT_LOCAL_IPS.includes(ip) ? ipv4 : ip; + } +} diff --git a/packages/dts-plugin/src/server/DevServer.ts b/packages/dts-plugin/src/server/DevServer.ts index 4287fccc06a..8a7b061eab9 100644 --- a/packages/dts-plugin/src/server/DevServer.ts +++ b/packages/dts-plugin/src/server/DevServer.ts @@ -13,9 +13,10 @@ import { UpdatePublisherAction, UpdateKind, } from './message/Action'; -import { APIKind, UpdateSubscriberAPI } from './message/API'; +import { APIKind, FetchTypesAPI, UpdateSubscriberAPI } from './message/API'; import { createBroker } from './broker/createBroker'; import { MF_SERVER_IDENTIFIER } from './constant'; +import { RemoteInfo } from '../core/interfaces/HostOptions'; export interface UpdateCallbackOptions { name: string; @@ -23,6 +24,8 @@ export interface UpdateCallbackOptions { updateMode: UpdateMode; remoteTypeTarPath: string; updateSourcePaths?: string[]; + remoteInfo?: RemoteInfo; + once?: boolean; } export interface UpdateSubscriberOptions { @@ -31,6 +34,7 @@ export interface UpdateSubscriberOptions { updateKind: UpdateKind; updateMode: UpdateMode; updateSourcePaths: string[]; + remoteInfo?: RemoteInfo; } export type UpdateCallback = (options: UpdateCallbackOptions) => Promise; @@ -107,7 +111,7 @@ export class ModuleFederationDevServer { this._publishWebSocket?.send(JSON.stringify(startGarfishModule)); this._connectSubscribers(); }); - this._publishWebSocket.on('message', (message) => { + this._publishWebSocket.on('message', async (message) => { try { const parsedMessage: Message = JSON.parse( message.toString(), @@ -122,6 +126,29 @@ export class ModuleFederationDevServer { this._exit(); } } + if (parsedMessage.type === 'API') { + if (parsedMessage.kind === APIKind.FETCH_TYPES) { + const { + payload: { remoteInfo }, + } = parsedMessage as FetchTypesAPI; + + fileLog( + `${ + this._name + } Receive broker FETCH_TYPES, payload as follows: ${JSON.stringify( + remoteInfo, + null, + 2, + )}.`, + MF_SERVER_IDENTIFIER, + 'info', + ); + + await this.fetchDynamicRemoteTypes({ + remoteInfo, + }); + } + } } catch (err) { console.error(err); @@ -308,7 +335,14 @@ export class ModuleFederationDevServer { updateSourcePaths, name, remoteTypeTarPath, + remoteInfo, } = options; + fileLog( + // eslint-disable-next-line max-len + `[_updateSubscriber] run, options: ${JSON.stringify(options, null, 2)}`, + MF_SERVER_IDENTIFIER, + 'warn', + ); if ( updateMode === UpdateMode.PASSIVE && updateSourcePaths.includes(this._name) @@ -343,6 +377,7 @@ export class ModuleFederationDevServer { updateKind, updateSourcePaths, remoteTypeTarPath, + remoteInfo, }); const newUpdateSourcePaths = updateSourcePaths.concat(this._name); const updatePublisher = new UpdatePublisherAction({ @@ -512,4 +547,37 @@ export class ModuleFederationDevServer { }); this._publishWebSocket.send(JSON.stringify(updatePublisher)); } + + async fetchDynamicRemoteTypes(options: { + remoteInfo: RemoteInfo; + once?: boolean; + }) { + const { remoteInfo, once } = options; + const updateMode = UpdateMode.PASSIVE; + const updateKind = UpdateKind.UPDATE_TYPE; + fileLog( + `fetchDynamicRemoteTypes: remoteInfo: ${JSON.stringify(remoteInfo)}`, + MF_SERVER_IDENTIFIER, + 'info', + ); + await this._updateCallback({ + name: this._name, + updateMode, + updateKind, + updateSourcePaths: [], + remoteTypeTarPath: '', + remoteInfo, + once, + }); + + const updatePublisher = new UpdatePublisherAction({ + name: this._name, + ip: this._ip, + updateMode, + updateKind, + updateSourcePaths: [this._name], + remoteTypeTarPath: this._remoteTypeTarPath, + }); + this._publishWebSocket.send(JSON.stringify(updatePublisher)); + } } diff --git a/packages/dts-plugin/src/server/Publisher.ts b/packages/dts-plugin/src/server/Publisher.ts index 903d7df01cc..5651cd2470b 100644 --- a/packages/dts-plugin/src/server/Publisher.ts +++ b/packages/dts-plugin/src/server/Publisher.ts @@ -1,11 +1,19 @@ import WebSocket from 'ws'; -import { UpdateSubscriberAPI, UpdateSubscriberAPIPayload } from './message/API'; +import { + FetchTypesAPI, + FetchTypesAPIPayload, + UpdateSubscriberAPI, + UpdateSubscriberAPIPayload, +} from './message/API'; import { getIdentifier, fileLog } from './utils'; +import { FetchTypesPayload } from './message/Action'; +import { RemoteInfo } from '../core/interfaces/HostOptions'; interface PublisherContext { name: string; ip: string; remoteTypeTarPath: string; + ws: WebSocket; } export class Publisher { @@ -13,12 +21,16 @@ export class Publisher { private _name: string; private _remoteTypeTarPath: string; private _subscribers: Map; + private _ws: WebSocket; + dynamicRemoteMap: Map; constructor(ctx: PublisherContext) { this._name = ctx.name; this._ip = ctx.ip; this._remoteTypeTarPath = ctx.remoteTypeTarPath; this._subscribers = new Map(); + this._ws = ctx.ws; + this.dynamicRemoteMap = new Map(); } get identifier(): string { @@ -89,6 +101,23 @@ export class Publisher { ); } + fetchRemoteTypes(options: FetchTypesAPIPayload) { + fileLog( + `[fetchRemoteTypes] ${ + this.name + } fetchRemoteTypes, options: ${JSON.stringify(options)}, ws: ${Boolean( + this._ws, + )}`, + 'Publisher', + 'info', + ); + if (!this._ws) { + return; + } + const api = new FetchTypesAPI(options); + this._ws.send(JSON.stringify(api)); + } + notifySubscribers(options: UpdateSubscriberAPIPayload): void { const api = new UpdateSubscriberAPI(options); this.broadcast(api); @@ -116,6 +145,7 @@ export class Publisher { } close(): void { + this._ws = undefined; this._subscribers.forEach((_subscriber, identifier) => { fileLog( `[BroadCast] close ${this.name} remove: ${identifier}`, diff --git a/packages/dts-plugin/src/server/WebClient.ts b/packages/dts-plugin/src/server/WebClient.ts index 07c4c0268b1..162e8b63c5e 100644 --- a/packages/dts-plugin/src/server/WebClient.ts +++ b/packages/dts-plugin/src/server/WebClient.ts @@ -6,6 +6,7 @@ import { import { Message } from './message/Message'; import { AddWebClientAction } from './message/Action'; import { APIKind, ReloadWebClientAPI } from './message/API'; +import { createWebsocket } from './createWebsocket'; type WebClientEnv = 'prod' | 'dev'; @@ -31,9 +32,7 @@ export class WebClient { `${this.logPrefix}Trying to connect to {cyan ws://127.0.0.1:${DEFAULT_WEB_SOCKET_PORT}}...}`, ); - this._webSocket = new WebSocket( - `ws://127.0.0.1:${DEFAULT_WEB_SOCKET_PORT}?WEB_SOCKET_CONNECT_MAGIC_ID=${WEB_SOCKET_CONNECT_MAGIC_ID}`, - ); + this._webSocket = createWebsocket(); this._webSocket.onopen = () => { console.log( diff --git a/packages/dts-plugin/src/server/broker/Broker.ts b/packages/dts-plugin/src/server/broker/Broker.ts index 6bbe33af994..1c61bd506ca 100644 --- a/packages/dts-plugin/src/server/broker/Broker.ts +++ b/packages/dts-plugin/src/server/broker/Broker.ts @@ -25,6 +25,8 @@ import { AddWebClientActionPayload, AddPublisherActionPayload, UpdateKind, + FetchTypesPayload, + AddDynamicRemotePayload, } from '../message/Action'; import { DEFAULT_WEB_SOCKET_PORT, @@ -177,6 +179,14 @@ export class Broker { client, ); } + + if (kind === ActionKind.FETCH_TYPES) { + await this._fetchTypes(payload as FetchTypesPayload, client); + } + + if (kind === ActionKind.ADD_DYNAMIC_REMOTE) { + this._addDynamicRemote(payload as AddDynamicRemotePayload); + } } private async _addPublisher( context: AddPublisherActionPayload, @@ -194,7 +204,12 @@ export class Broker { } try { - const publisher = new Publisher({ name, ip, remoteTypeTarPath }); + const publisher = new Publisher({ + name, + ip, + remoteTypeTarPath, + ws: client, + }); this._publisherMap.set(identifier, publisher); fileLog( `[${ActionKind.ADD_PUBLISHER}] ${identifier} Adding Publisher Succeed`, @@ -263,6 +278,28 @@ export class Broker { updateKind, updateSourcePaths: updateSourcePaths || [], }); + + // update dynamic remote as well + this._publisherMap.forEach((p) => { + if (p.name === publisher.name) { + return; + } + const dynamicRemoteInfo = p.dynamicRemoteMap.get(identifier); + if (dynamicRemoteInfo) { + fileLog( + // eslint-disable-next-line max-len + `dynamicRemoteInfo: ${JSON.stringify( + dynamicRemoteInfo, + )}, identifier:${identifier} publish: ${p.name}`, + 'Broker', + 'info', + ); + p.fetchRemoteTypes({ + remoteInfo: dynamicRemoteInfo, + once: false, + }); + } + }); } } catch (err) { const msg = error(err, ActionKind.UPDATE_PUBLISHER, 'Broker'); @@ -270,6 +307,47 @@ export class Broker { client.close(); } } + + private async _fetchTypes(context: FetchTypesPayload, _client: WebSocket) { + const { name, ip, remoteInfo } = context ?? {}; + const identifier = getIdentifier({ name, ip }); + try { + const publisher = this._publisherMap.get(identifier); + fileLog( + `[${ActionKind.FETCH_TYPES}] ${identifier} fetch types`, + 'Broker', + 'info', + ); + if (publisher) { + publisher.fetchRemoteTypes({ + remoteInfo, + once: true, + }); + } + } catch (err) { + fileLog( + `[${ActionKind.FETCH_TYPES}] ${identifier} fetch types fail , error info: ${err}`, + 'Broker', + 'error', + ); + } + } + + private _addDynamicRemote(context: AddDynamicRemotePayload) { + const { name, ip, remoteInfo, remoteIp } = context ?? {}; + const identifier = getIdentifier({ name, ip }); + const publisher = this._publisherMap.get(identifier); + const remoteId = getIdentifier({ name: remoteInfo.name, ip: remoteIp }); + fileLog( + `[${ActionKind.ADD_DYNAMIC_REMOTE}] identifier:${identifier},publisher: ${publisher.name}, remoteId:${remoteId}`, + 'Broker', + 'error', + ); + if (!publisher || publisher.dynamicRemoteMap.has(remoteId)) { + return; + } + publisher.dynamicRemoteMap.set(remoteId, remoteInfo); + } // app1 consumes provider1,provider2. Dependencies at this time: publishers: [provider1, provider2], subscriberName: app1 // provider1 is app1's remote private async _addSubscriber( diff --git a/packages/dts-plugin/src/server/broker/createBroker.ts b/packages/dts-plugin/src/server/broker/createBroker.ts index c43eb0361db..2294e8d4f70 100644 --- a/packages/dts-plugin/src/server/broker/createBroker.ts +++ b/packages/dts-plugin/src/server/broker/createBroker.ts @@ -2,7 +2,7 @@ import { fork, ChildProcess } from 'child_process'; import path from 'path'; export function createBroker(): ChildProcess { - const startBrokerPath = path.resolve(__dirname, './startBroker.js'); + const startBrokerPath = path.resolve(__dirname, './start-broker.js'); const sub = fork(startBrokerPath, [], { detached: true, stdio: 'ignore', diff --git a/packages/dts-plugin/src/server/createWebsocket.ts b/packages/dts-plugin/src/server/createWebsocket.ts new file mode 100644 index 00000000000..f0f1a28e8d9 --- /dev/null +++ b/packages/dts-plugin/src/server/createWebsocket.ts @@ -0,0 +1,11 @@ +import WebSocket from 'isomorphic-ws'; +import { + DEFAULT_WEB_SOCKET_PORT, + WEB_SOCKET_CONNECT_MAGIC_ID, +} from './constant'; + +export function createWebsocket() { + return new WebSocket( + `ws://127.0.0.1:${DEFAULT_WEB_SOCKET_PORT}?WEB_SOCKET_CONNECT_MAGIC_ID=${WEB_SOCKET_CONNECT_MAGIC_ID}`, + ); +} diff --git a/packages/dts-plugin/src/server/message/API/API.ts b/packages/dts-plugin/src/server/message/API/API.ts index 6ee8f6ef8a1..f8895da8d2b 100644 --- a/packages/dts-plugin/src/server/message/API/API.ts +++ b/packages/dts-plugin/src/server/message/API/API.ts @@ -8,6 +8,7 @@ interface APIContent { export const enum APIKind { UPDATE_SUBSCRIBER = 'UPDATE_SUBSCRIBER', RELOAD_WEB_CLIENT = 'RELOAD_WEB_CLIENT', + FETCH_TYPES = 'FETCH_TYPES', } export class API extends Message { diff --git a/packages/dts-plugin/src/server/message/API/FetchTypes.ts b/packages/dts-plugin/src/server/message/API/FetchTypes.ts new file mode 100644 index 00000000000..af42a33195b --- /dev/null +++ b/packages/dts-plugin/src/server/message/API/FetchTypes.ts @@ -0,0 +1,19 @@ +import { RemoteInfo } from '../../../core/interfaces/HostOptions'; +import { API, APIKind } from './API'; + +export interface FetchTypesAPIPayload { + remoteInfo: RemoteInfo; + once?: boolean; +} + +export class FetchTypesAPI extends API { + constructor(payload: FetchTypesAPIPayload) { + super( + { + code: 0, + payload, + }, + APIKind.FETCH_TYPES, + ); + } +} diff --git a/packages/dts-plugin/src/server/message/API/index.ts b/packages/dts-plugin/src/server/message/API/index.ts index e1e9ad2f03b..8e29be93542 100644 --- a/packages/dts-plugin/src/server/message/API/index.ts +++ b/packages/dts-plugin/src/server/message/API/index.ts @@ -1,3 +1,4 @@ export * from './API'; export * from './UpdateSubscriber'; export * from './ReloadWebClient'; +export * from './FetchTypes'; diff --git a/packages/dts-plugin/src/server/message/Action/Action.ts b/packages/dts-plugin/src/server/message/Action/Action.ts index f2ab4a63e39..d698fb81009 100644 --- a/packages/dts-plugin/src/server/message/Action/Action.ts +++ b/packages/dts-plugin/src/server/message/Action/Action.ts @@ -14,6 +14,8 @@ export const enum ActionKind { EXIT_PUBLISHER = 'EXIT_PUBLISHER', ADD_WEB_CLIENT = 'ADD_WEB_CLIENT', NOTIFY_WEB_CLIENT = 'NOTIFY_WEB_CLIENT', + FETCH_TYPES = 'FETCH_TYPES', + ADD_DYNAMIC_REMOTE = 'ADD_DYNAMIC_REMOTE', } export class Action extends Message { diff --git a/packages/dts-plugin/src/server/message/Action/AddDynamicRemote.ts b/packages/dts-plugin/src/server/message/Action/AddDynamicRemote.ts new file mode 100644 index 00000000000..49262705cfe --- /dev/null +++ b/packages/dts-plugin/src/server/message/Action/AddDynamicRemote.ts @@ -0,0 +1,22 @@ +import { UpdateMode } from '../../../core/index'; +import { RemoteInfo } from '../../../core/interfaces/HostOptions'; +import { BaseContext } from '../../types'; +import { Action, ActionKind } from './Action'; +import { UpdateKind } from './Update'; + +// host consumes provider , the action will notify host fetch provider types +export interface AddDynamicRemotePayload extends BaseContext { + remoteInfo: RemoteInfo; + remoteIp: string; +} + +export class AddDynamicRemoteAction extends Action { + constructor(payload: AddDynamicRemotePayload) { + super( + { + payload, + }, + ActionKind.ADD_DYNAMIC_REMOTE, + ); + } +} diff --git a/packages/dts-plugin/src/server/message/Action/AddSubscriber.ts b/packages/dts-plugin/src/server/message/Action/AddSubscriber.ts index 9bbd8fa2ec9..db27cb9c40f 100644 --- a/packages/dts-plugin/src/server/message/Action/AddSubscriber.ts +++ b/packages/dts-plugin/src/server/message/Action/AddSubscriber.ts @@ -1,3 +1,4 @@ +import { RemoteInfo } from '../../../core/interfaces/HostOptions'; import { BaseContext, Publisher } from '../../types'; import { Action, ActionKind } from './Action'; diff --git a/packages/dts-plugin/src/server/message/Action/FetchTypes.ts b/packages/dts-plugin/src/server/message/Action/FetchTypes.ts new file mode 100644 index 00000000000..18234b8e700 --- /dev/null +++ b/packages/dts-plugin/src/server/message/Action/FetchTypes.ts @@ -0,0 +1,19 @@ +import { RemoteInfo } from '../../../core/interfaces/HostOptions'; +import { BaseContext } from '../../types'; +import { Action, ActionKind } from './Action'; + +// host consumes provider , the action will notify host fetch provider types +export interface FetchTypesPayload extends BaseContext { + remoteInfo: RemoteInfo; +} + +export class FetchTypesAction extends Action { + constructor(payload: FetchTypesPayload) { + super( + { + payload, + }, + ActionKind.FETCH_TYPES, + ); + } +} diff --git a/packages/dts-plugin/src/server/message/Action/index.ts b/packages/dts-plugin/src/server/message/Action/index.ts index e5087af55c1..f1e8583999e 100644 --- a/packages/dts-plugin/src/server/message/Action/index.ts +++ b/packages/dts-plugin/src/server/message/Action/index.ts @@ -7,3 +7,6 @@ export * from './ExitSubscriber'; export * from './ExitPublisher'; export * from './NotifyWebClient'; export * from './UpdatePublisher'; +export * from './FetchTypes'; +export * from './AddDynamicRemote'; +export { UpdateMode } from '../../../core/constant'; diff --git a/packages/dts-plugin/tsconfig.json b/packages/dts-plugin/tsconfig.json index f5fc8a97363..ebb9de6e2a5 100644 --- a/packages/dts-plugin/tsconfig.json +++ b/packages/dts-plugin/tsconfig.json @@ -1,22 +1,17 @@ { - "extends": "../../tsconfig.base.json", "compilerOptions": { + "outDir": "dist", + "sourceMap": false, + "target": "es6", "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noFallthroughCasesInSwitch": true + "moduleResolution": "node", + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "resolveJsonModule": true, + "declaration": true, + "allowJs": true, + "lib": ["ES2021", "DOM"] }, - "files": [], - "include": [], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] + "include": ["src", "__tests__"] } diff --git a/packages/dts-plugin/tsconfig.spec.json b/packages/dts-plugin/tsconfig.spec.json index 9b2a121d114..f5fc8a97363 100644 --- a/packages/dts-plugin/tsconfig.spec.json +++ b/packages/dts-plugin/tsconfig.spec.json @@ -1,14 +1,22 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"] + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noFallthroughCasesInSwitch": true }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" + "files": [], + "include": [], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } ] } diff --git a/packages/dts-plugin/tsup.config.ts b/packages/dts-plugin/tsup.config.ts index b6e32b3e30d..8ff1736304a 100644 --- a/packages/dts-plugin/tsup.config.ts +++ b/packages/dts-plugin/tsup.config.ts @@ -9,6 +9,7 @@ function generateConfigurations( return { entry, clean: true, + tsconfig: join(__dirname, 'tsconfig.json'), dts: true, legacyOutput: true, outDir: join('packages', 'dts-plugin', 'dist'), @@ -23,18 +24,35 @@ export const tsup: Options[] = generateConfigurations([ { index: join(__dirname, 'src', 'index.ts'), core: join(__dirname, 'src', 'core', 'index.ts'), - forkDevWorker: join(__dirname, 'src', 'dev-worker', 'forkDevWorker.ts'), - startBroker: join(__dirname, 'src', 'server', 'broker', 'startBroker.ts'), - forkGenerateDts: join( + 'fork-dev-worker': join( + __dirname, + 'src', + 'dev-worker', + 'forkDevWorker.ts', + ), + 'start-broker': join( + __dirname, + 'src', + 'server', + 'broker', + 'startBroker.ts', + ), + 'fork-generate-dts': join( __dirname, 'src', 'core', 'lib', 'forkGenerateDts.ts', ), + 'dynamic-remote-type-hints-plugin': join( + __dirname, + 'src', + 'runtime-plugins', + 'dynamic-remote-type-hints-plugin.ts', + ), }, { - format: ['cjs'], + format: ['cjs', 'esm'], }, ], [ diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts index 2da7b131571..508e336b32f 100644 --- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts @@ -69,6 +69,9 @@ class ModuleFederationPlugin implements WebpackPluginInstance { */ apply(compiler: Compiler): void { const { _options: options } = this; + if (options.dts !== false) { + new DtsPlugin(options).apply(compiler); + } new FederationRuntimePlugin(options).apply(compiler); const library = options.library || { type: 'var', name: options.name }; const remoteType = @@ -102,10 +105,6 @@ class ModuleFederationPlugin implements WebpackPluginInstance { } } - if (options.dts !== false) { - new DtsPlugin(options).apply(compiler); - } - if ( library && !compiler.options.output.enabledLibraryTypes?.includes(library.type) diff --git a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts index 95c711bc3ca..7854259dd82 100644 --- a/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts @@ -669,6 +669,10 @@ export default { description: 'Disable hot types reload.', type: 'boolean', }, + disableDynamicRemoteTypeHints: { + description: 'Disable dynamic remote types hints.', + type: 'boolean', + }, }, }, }, diff --git a/packages/runtime/src/remote/index.ts b/packages/runtime/src/remote/index.ts index 7a839a8f9ea..25c5273931e 100644 --- a/packages/runtime/src/remote/index.ts +++ b/packages/runtime/src/remote/index.ts @@ -21,6 +21,7 @@ import { AsyncHook, AsyncWaterfallHook, SyncHook, + SyncWaterfallHook, } from '../utils/hooks'; import { assert, @@ -47,6 +48,10 @@ export interface LoadRemoteMatch { export class RemoteHandler { host: FederationHost; hooks = new PluginSystem({ + registerRemote: new SyncWaterfallHook<{ + remote: Remote; + origin: FederationHost; + }>('registerRemote'), beforeRequest: new AsyncWaterfallHook<{ id: string; options: Options; @@ -379,6 +384,7 @@ export class RemoteHandler { if (!registeredRemote) { normalizeRemote(); targetRemotes.push(remote); + this.hooks.lifecycle.registerRemote.emit({ remote, origin: host }); } else { const messages = [ `The remote "${remote.name}" is already registered.`, @@ -391,6 +397,7 @@ export class RemoteHandler { this.removeRemote(registeredRemote); normalizeRemote(); targetRemotes.push(remote); + this.hooks.lifecycle.registerRemote.emit({ remote, origin: host }); } warn(messages.join(' ')); } diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index 842b521c572..4f3a27cc6a3 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -128,6 +128,7 @@ export interface PluginManifestOptions { export interface PluginDevOptions { disableLiveReload?: boolean; disableHotTypesReload?: boolean; + disableDynamicRemoteTypeHints?: boolean; } export interface DtsHostOptions { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 18153f7682c..ef4b97bc6b6 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -27,8 +27,14 @@ const parseEntry = ( // Check if the string starts with a type if (strSplit.length >= 2) { - const [name, ...versionOrEntryArr] = strSplit; - const versionOrEntry = devVersionOrUrl || versionOrEntryArr.join(separator); + let [name, ...versionOrEntryArr] = strSplit; + if (str.startsWith(SEPARATOR)) { + versionOrEntryArr = [devVersionOrUrl || strSplit.slice(-1)[0]]; + name = strSplit.slice(0, -1).join(SEPARATOR); + } + + let versionOrEntry = devVersionOrUrl || versionOrEntryArr.join(separator); + if (isEntry(versionOrEntry)) { return { name, diff --git a/packages/third-party-dts-extractor/src/ThirdPartyExtractor.ts b/packages/third-party-dts-extractor/src/ThirdPartyExtractor.ts index fcc6828942d..f3bbc48f9f9 100644 --- a/packages/third-party-dts-extractor/src/ThirdPartyExtractor.ts +++ b/packages/third-party-dts-extractor/src/ThirdPartyExtractor.ts @@ -74,7 +74,7 @@ class ThirdPartyExtractor { } } - collectPkgs(str: string) { + collectTypeImports(str: string): string[] { const { pattern } = this; let match; const imports: Set = new Set(); @@ -82,8 +82,12 @@ class ThirdPartyExtractor { while ((match = pattern.exec(str)) !== null) { imports.add(match[2]); } + return [...imports]; + } - [...imports].forEach((importPath) => { + collectPkgs(str: string) { + const imports = this.collectTypeImports(str); + imports.forEach((importPath) => { this.inferPkgDir(importPath); }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 332efc00b2e..906dd2dee00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1296,6 +1296,9 @@ importers: specifier: 8.17.0 version: 8.17.0 devDependencies: + '@module-federation/runtime': + specifier: workspace:* + version: link:../runtime '@types/koa': specifier: 2.11.2 version: 2.11.2 @@ -2123,6 +2126,20 @@ packages: - supports-color dev: true + /@babel/eslint-parser@7.24.1(@babel/core@7.24.4)(eslint@8.56.0): + resolution: {integrity: sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.56.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + dev: true + /@babel/eslint-parser@7.24.1(@babel/core@7.24.6)(eslint@8.56.0): resolution: {integrity: sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} @@ -2144,7 +2161,7 @@ packages: '@babel/eslint-parser': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 dependencies: - '@babel/eslint-parser': 7.24.1(@babel/core@7.24.6)(eslint@8.56.0) + '@babel/eslint-parser': 7.24.1(@babel/core@7.24.4)(eslint@8.56.0) eslint: 8.56.0 eslint-rule-composer: 0.3.0 dev: true @@ -6553,7 +6570,7 @@ packages: typescript: ^4 || ^5 dependencies: '@babel/core': 7.24.4 - '@babel/eslint-parser': 7.24.1(@babel/core@7.24.6)(eslint@8.56.0) + '@babel/eslint-parser': 7.24.1(@babel/core@7.24.4)(eslint@8.56.0) '@babel/eslint-plugin': 7.23.5(@babel/eslint-parser@7.24.1)(eslint@8.56.0) '@rsbuild/babel-preset': 0.3.4(@rsbuild/core@0.3.11)(@swc/helpers@0.5.3) '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.56.0)(typescript@5.0.4)