diff --git a/package.json b/package.json index 213efbb5..3d6ed710 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,20 @@ "workspaces": [ "packages/**" ], + "files": [ + "packages/lib/dist/*", + "packages/types/*" + ], + "main": "./packages/lib/dist/index.js", + "module": "./packages/lib/dist/index.mjs", + "types": "./packages/lib/types/index.d.ts", + "exports": { + ".": { + "types": "./packages/lib/types/index.d.ts", + "import": "./packages/lib/dist/index.mjs", + "require": "./packages/lib/dist/index.js" + } + }, "engines": { "node": "^14.18.0 || >=16.0.0", "pnpm": ">=8.0.1" @@ -12,7 +26,7 @@ "license": "MulanPSL-2.0", "scripts": { "preinstall": "npx only-allow pnpm", - "prepare": "husky install", + "prepare": "pnpm run build", "postinstall": "npx playwright install", "lint-staged": "lint-staged", "format": "prettier -w packages/lib/**/*.ts", @@ -52,5 +66,6 @@ "eslint --cache --fix", "eslint" ] - } + }, + "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903" } diff --git a/packages/examples/react-vite/package.json b/packages/examples/react-vite/package.json index b2fa7473..2e6f9b27 100644 --- a/packages/examples/react-vite/package.json +++ b/packages/examples/react-vite/package.json @@ -8,6 +8,7 @@ "build:remotes": "pnpm --parallel --filter \"./remote\" build", "serve:remotes": "pnpm --parallel --filter \"./remote\" serve", "dev:hosts": "pnpm --filter \"./host\" dev", + "dev:remotes": "pnpm --filter \"./remote\" dev", "stop": "kill-port --port 5000,5001" }, "devDependencies": { diff --git a/packages/examples/vue3-advanced-demo/package.json b/packages/examples/vue3-advanced-demo/package.json index e08cfe8f..bc991d2f 100644 --- a/packages/examples/vue3-advanced-demo/package.json +++ b/packages/examples/vue3-advanced-demo/package.json @@ -15,6 +15,7 @@ "build:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" build", "serve:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" serve", "dev:hosts": "pnpm --filter \"./team-red\" dev", + "dev:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" dev", "stop": "kill-port --port 5000,5001,5002", "clean": "pnpm run clean" }, diff --git a/packages/lib/package.json b/packages/lib/package.json index ccef202e..dcaf098b 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -37,13 +37,14 @@ }, "homepage": "https://github.com/originjs/vite-plugin-federation#readme", "scripts": { - "build": "vite build", + "build": "vite build && tsc", "dev": "vite build --watch", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path ." }, "dependencies": { "estree-walker": "^3.0.2", - "magic-string": "^0.27.0" + "magic-string": "^0.27.0", + "rollup": "^3.9.1" }, "devDependencies": { "@rollup/plugin-virtual": "^3.0.1", diff --git a/packages/lib/src/dev/expose-development.ts b/packages/lib/src/dev/expose-development.ts index 35d94ca0..06305ab4 100644 --- a/packages/lib/src/dev/expose-development.ts +++ b/packages/lib/src/dev/expose-development.ts @@ -13,17 +13,91 @@ // SPDX-License-Identifier: MulanPSL-2.0 // ***************************************************************************** -import { parseExposeOptions } from '../utils' -import { parsedOptions } from '../public' +import { resolve } from 'path' +import { getModuleMarker, normalizePath, parseExposeOptions } from '../utils' +import { EXTERNALS, SHARED, builderInfo, parsedOptions } from '../public' import type { VitePluginFederationOptions } from 'types' import type { PluginHooks } from '../../types/pluginHooks' +import { UserConfig, ViteDevServer } from 'vite' +import { importShared } from './import-shared' export function devExposePlugin( options: VitePluginFederationOptions ): PluginHooks { parsedOptions.devExpose = parseExposeOptions(options) + let moduleMap = '' + let remoteFile: string | null = null + + const exposeModules = (baseDir) => { + for (const item of parsedOptions.devExpose) { + const moduleName = getModuleMarker(`\${${item[0]}}`, SHARED) + EXTERNALS.push(moduleName) + const importPath = normalizePath(item[1].import) + const exposeFilepath = normalizePath(resolve(item[1].import)) + moduleMap += `\n"${item[0]}":() => { + return __federation_import('/${importPath}', '${baseDir}@fs/${exposeFilepath}').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},` + } + } + + const buildRemoteFile = (baseDir:string) => { + return `(${importShared})(); + import RefreshRuntime from "${baseDir}@react-refresh" + RefreshRuntime.injectIntoGlobalHook(window) + window.$RefreshReg$ = () => {} + window.$RefreshSig$ = () => (type) => type + window.__vite_plugin_react_preamble_installed__ = true + const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']); + let moduleMap = { + ${moduleMap} + }; + const __federation_import = async (urlImportPath, fsImportPath) => { + let importedModule; + try { + return await import(fsImportPath); + } catch(ex) { + return await import(urlImportPath); + } + }; + export const get =(module) => { + if(!moduleMap[module]) throw new Error('Can not find remote module ' + module) + return moduleMap[module](); + }; + export const init =(shareScope) => { + globalThis.__federation_shared__= globalThis.__federation_shared__|| {}; + Object.entries(shareScope).forEach(([key, value]) => { + const versionKey = Object.keys(value)[0]; + const versionValue = Object.values(value)[0]; + const scope = versionValue.scope || 'default' + globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {}; + const shared= globalThis.__federation_shared__[scope]; + (shared[key] = shared[key]||{})[versionKey] = versionValue; + }); + } + ` + } return { - name: 'originjs:expose-development' + name: 'originjs:expose-development', + config: (config: UserConfig) => { + if (config.base) { + exposeModules(config.base) + remoteFile = buildRemoteFile(config.base) + } + }, + configureServer: (server: ViteDevServer) => { + const remoteFilePath = `${builderInfo.assetsDir}/${options.filename}` + server.middlewares.use((req, res, next) => { + if (req.url && req.url.includes(remoteFilePath)) { + res.writeHead(200, 'OK', { + 'Content-Type': 'text/javascript', + 'Access-Control-Allow-Origin': '*' + }) + res.write(remoteFile) + res.end() + } else { + next() + } + }) + } } } diff --git a/packages/lib/src/dev/import-shared.js b/packages/lib/src/dev/import-shared.js new file mode 100644 index 00000000..b1d3441b --- /dev/null +++ b/packages/lib/src/dev/import-shared.js @@ -0,0 +1,40 @@ +export const importShared = function () { + if (!globalThis.importShared) { + const moduleCache = Object.create(null) + const getSharedFromRuntime = async (name, shareScope) => { + let module = null + if (globalThis?.__federation_shared__?.[shareScope]?.[name]) { + const versionObj = globalThis.__federation_shared__[shareScope][name] + const versionValue = Object.values(versionObj)[0] + module = await (await versionValue.get())() + } + if (module) { + return flattenModule(module, name) + } + } + const flattenModule = (module, name) => { + // use a shared module which export default a function will getting error 'TypeError: xxx is not a function' + if (typeof module.default === 'function') { + Object.keys(module).forEach((key) => { + if (key !== 'default') { + module.default[key] = module[key] + } + }) + moduleCache[name] = module.default + return module.default + } + if (module.default) module = Object.assign({}, module.default, module) + moduleCache[name] = module + return module + } + globalThis.importShared = async (name, shareScope = 'default') => { + try { + return moduleCache[name] + ? new Promise((r) => r(moduleCache[name])) + : (await getSharedFromRuntime(name, shareScope)) || null + } catch (ex) { + console.log(ex) + } + } + } +} diff --git a/packages/lib/src/dev/remote-development.ts b/packages/lib/src/dev/remote-development.ts index a107556a..8c6974b9 100644 --- a/packages/lib/src/dev/remote-development.ts +++ b/packages/lib/src/dev/remote-development.ts @@ -14,7 +14,11 @@ // ***************************************************************************** import type { UserConfig } from 'vite' -import type { ConfigTypeSet, VitePluginFederationOptions } from 'types' +import type { + ConfigTypeSet, + ExposesConfig, + VitePluginFederationOptions +} from 'types' import { walk } from 'estree-walker' import MagicString from 'magic-string' import { readFileSync } from 'fs' @@ -30,11 +34,16 @@ import { } from '../utils' import { builderInfo, parsedOptions, devRemotes } from '../public' import type { PluginHooks } from '../../types/pluginHooks' +import { Literal } from 'estree' +import { importShared } from './import-shared' + +const exposedItems: string[] = [] export function devRemotePlugin( options: VitePluginFederationOptions ): PluginHooks { parsedOptions.devRemote = parseRemoteOptions(options) + const shareScope = options.shareScope || 'default' // const remotes: { id: string; regexp: RegExp; config: RemotesConfig }[] = [] for (const item of parsedOptions.devRemote) { devRemotes.push({ @@ -81,48 +90,62 @@ function get(name, ${REMOTE_FROM_PARAMETER}){ return module }) } -const wrapShareScope = ${REMOTE_FROM_PARAMETER} => { - return { - ${getModuleMarker('shareScope')} +function merge(obj1, obj2) { + const mergedObj = Object.assign(obj1, obj2); + for (const key of Object.keys(mergedObj)) { + if (typeof mergedObj[key] === 'object' && typeof obj2[key] === 'object') { + mergedObj[key] = merge(mergedObj[key], obj2[key]); + } } + return mergedObj; +} +const wrapShareModule = ${REMOTE_FROM_PARAMETER} => { + return merge({ + ${getModuleMarker('shareScope')} + }, (globalThis.__federation_shared__ || {})['${shareScope}'] || {}); } const initMap = Object.create(null); -async function __federation_method_ensure(remoteId) { + +async function __federation_method_ensure(remoteId, retryCount) { const remote = remotesMap[remoteId]; - if (!remote.inited) { - if ('var' === remote.format) { - // loading js with script tag - return new Promise(resolve => { - const callback = () => { - if (!remote.inited) { - remote.lib = window[remoteId]; - remote.lib.init(wrapShareScope(remote.from)) - remote.inited = true; - } - resolve(remote.lib); - } - return loadJS(remote.url, callback); - }); - } else if (['esm', 'systemjs'].includes(remote.format)) { - // loading js with import(...) - return new Promise((resolve, reject) => { - const getUrl = typeof remote.url === 'function' ? remote.url : () => Promise.resolve(remote.url); - getUrl().then(url => { - import(/* @vite-ignore */ url).then(lib => { - if (!remote.inited) { - const shareScope = wrapShareScope(remote.from) - lib.init(shareScope); - remote.lib = lib; - remote.lib.init(shareScope); - remote.inited = true; - } - resolve(remote.lib); - }).catch(reject) - }) - }) - } + if (!remote.inited || retryCount > 0) { + if ('var' === remote.format) { + // loading js with script tag + return new Promise(resolve => { + const callback = () => { + if (!remote.inited) { + remote.lib = window[remoteId]; + remote.lib.init(wrapShareModule(remote.from)); + remote.inited = true; + } + resolve(remote.lib); + }; + return loadJS(remote.url, callback); + }); + } else if (['esm', 'systemjs'].includes(remote.format)) { + // loading js with import(...) + return new Promise((resolve, reject) => { + const getUrl = typeof remote.url === 'function' ? remote.url : () => { + const url = new URL(remote.url, window.location.origin); + url.searchParams.append("retryCount", retryCount); + return Promise.resolve(url.toString()); + } + getUrl().then(url => { + import(/* @vite-ignore */ url).then(lib => { + if (!remote.inited || retryCount > 0) { + const shareScope = wrapShareModule(remote.from); + lib.init(shareScope); + remote.lib = lib; + remote.lib.init(shareScope); + remote.inited = true; + } + resolve(remote.lib); + }).catch(reject); + }); + }) + } } else { - return remote.lib; + return remote.lib; } } @@ -140,8 +163,37 @@ function __federation_method_wrapDefault(module ,need){ return module; } -function __federation_method_getRemote(remoteName, componentName){ - return __federation_method_ensure(remoteName).then((remote) => remote.get(componentName).then(factory => factory())); +async function __federation_method_getRemote(remoteName, componentName) { + const remoteConfig = remotesMap[remoteName]; + let retryCount = 0; + const getRemote = async () => { + try { + const remoteModule = await __federation_method_ensure(remoteName, retryCount); + const factory = await remoteModule.get(componentName); + return factory(); + } catch (err) { + retryCount++; + if (retryCount > remoteConfig.importRetryCount) { + if(remoteConfig.onImportFail){ + return remoteConfig.onImportFail(remoteName, componentName, err); + } else { + throw err; + } + } else { + const retryBackoff = 10 ** retryCount; + const retry = () => { + return new Promise((resolve) => { + setTimeout(() => { + const retryResult = getRemote(); + resolve(retryResult); + }, retryBackoff) + }) + } + return await retry(); + } + } + }; + return getRemote(); } function __federation_method_setRemote(remoteName, remoteConfig) { @@ -206,6 +258,8 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation return } + code += `(${importShared})();\n` + let ast: AcornNode | null = null try { ast = this.parse(code) @@ -215,7 +269,6 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation if (!ast) { return null } - const magicString = new MagicString(code) const hasStaticImported = new Map() @@ -223,12 +276,87 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation let manualRequired: any = null // set static import if exists walk(ast, { enter(node: any) { + if ( + node.type === 'MemberExpression' && + node.object.type === 'MemberExpression' && + node.object.object.type === 'MetaProperty' && + node.object.object.meta.name === 'import' && + node.object.property.type === 'Identifier' && + node.object.property.name === 'env' && + node.property.name === 'BASE_URL' + ) { + const serverPort = viteDevServer.config.inlineConfig.server?.port + const baseUrlFromConfig = + viteDevServer.config.env.BASE_URL && + viteDevServer.config.env.BASE_URL !== '/' + ? viteDevServer.config.env.BASE_URL + : '' + // This assumes that the dev server will always be running on localhost. That's probably not a good assumption, but I don't know how to work around it right now. + const baseUrl = `"//localhost:${serverPort}${baseUrlFromConfig}"` + magicString.overwrite(node.start, node.end, baseUrl) + node = { type: 'Literal', value: baseUrl } as Literal + } if ( node.type === 'ImportDeclaration' && node.source?.value === 'virtual:__federation__' ) { manualRequired = node } + if ( + isExposed(id, parsedOptions.devExpose) && + node.type === 'ImportDeclaration' && + node.source?.value + ) { + const moduleName = node.source.value + if ( + parsedOptions.devShared.some( + (sharedInfo) => sharedInfo[0] === moduleName + ) + ) { + const namedImportDeclaration: (string | never)[] = [] + let defaultImportDeclaration: string | null = null + if (!node.specifiers?.length) { + // invalid import , like import './__federation_shared_lib.js' , and remove it + magicString.remove(node.start, node.end) + } else { + node.specifiers.forEach((specify) => { + if (specify.imported?.name) { + namedImportDeclaration.push( + `${ + specify.imported.name === specify.local.name + ? specify.imported.name + : `${specify.imported.name}:${specify.local.name}` + }` + ) + } else { + defaultImportDeclaration = specify.local.name + } + }) + + if (defaultImportDeclaration && namedImportDeclaration.length) { + // import a, {b} from 'c' -> const a = await importShared('c'); const {b} = a; + const imports = namedImportDeclaration.join(',') + const line = `const ${defaultImportDeclaration} = await importShared('${moduleName}') || await import('${moduleName}');\nconst {${imports}} = ${defaultImportDeclaration};\n` + + magicString.overwrite(node.start, node.end, line) + } else if (defaultImportDeclaration) { + magicString.overwrite( + node.start, + node.end, + `const ${defaultImportDeclaration} = await importShared('${moduleName}') || await import('${moduleName}');\n` + ) + } else if (namedImportDeclaration.length) { + magicString.overwrite( + node.start, + node.end, + `const {${namedImportDeclaration.join( + ',' + )}} = await importShared('${moduleName}') || await import('${moduleName}');\n` + ) + } + } + } + } if ( (node.type === 'ImportExpression' || @@ -411,4 +539,27 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation } return res } + function isExposed(id: string, options: (string | ConfigTypeSet)[]) { + if (exposedItems.includes(id)) { + return true + } + if (options.length >= 2 && (options[1] as ExposesConfig).import) { + if (normalizePath((options[1] as ExposesConfig).import)) { + return true + } + } + for (let i = 0, length = options.length; i < length; i++) { + const item = options[i] + if ( + Array.isArray(item) && + item.length >= 2 && + (item[1] as ExposesConfig).import + ) { + if (normalizePath((item[1] as ExposesConfig).import)) { + return true + } + } + } + return false + } } diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index bdd992b7..6b84c437 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -25,8 +25,13 @@ import { dirname } from 'path' import { prodRemotePlugin } from './prod/remote-production' import type { VitePluginFederationOptions } from '../types' import { builderInfo, DEFAULT_ENTRY_FILENAME, parsedOptions } from './public' -import type { PluginHooks } from '../types/pluginHooks' -import type { ModuleInfo } from 'rollup' +import type { PluginHookBuildStart, PluginHookConfig, PluginHookConfigResolved, PluginHookConfigureServer, PluginHookGenerateBundle, PluginHookModuleParsed, PluginHookOutputOptions, PluginHookRenderChunk, PluginHooks, PluginHookTransform } from '../types/pluginHooks' +import { + InputOptions, + NullValue, + type MinimalPluginContext, + type ModuleInfo +} from 'rollup' import { prodSharedPlugin } from './prod/shared-production' import { prodExposePlugin } from './prod/expose-production' import { devSharedPlugin } from './dev/shared-development' @@ -57,7 +62,7 @@ export default function federation( devExposePlugin(options), devRemotePlugin(options) ] - } else { + } else { pluginList = [] } builderInfo.isHost = !!( @@ -79,7 +84,7 @@ export default function federation( virtualMod = virtual(virtualFiles) } - return { + const plugin: Plugin = { name: 'originjs:federation', // for scenario vite.config.js build.cssCodeSplit: false // vite:css-post plugin will summarize all the styles in the style.xxxxxx.css file @@ -99,8 +104,12 @@ export default function federation( if (!Array.isArray(_options.external)) { _options.external = [_options.external as string] } - for (const pluginHook of pluginList) { - pluginHook.options?.call(this, _options) + for (const pluginHook of pluginList) {( + pluginHook.options as (( + this: MinimalPluginContext, + options: InputOptions + ) => InputOptions | NullValue) | undefined + )?.call(this, _options) } return _options }, @@ -109,7 +118,7 @@ export default function federation( registerPlugins(options.mode, env.command) registerCount++ for (const pluginHook of pluginList) { - pluginHook.config?.call(this, config, env) + (pluginHook.config as PluginHookConfig)?.call(this, config, env); } // only run when builder is vite,rollup doesnt has hook named `config` @@ -118,17 +127,17 @@ export default function federation( }, configureServer(server: ViteDevServer) { for (const pluginHook of pluginList) { - pluginHook.configureServer?.call(this, server) + (pluginHook.configureServer as PluginHookConfigureServer)?.call(this, server); } }, configResolved(config: ResolvedConfig) { for (const pluginHook of pluginList) { - pluginHook.configResolved?.call(this, config) + (pluginHook.configResolved as PluginHookConfigResolved)?.call(this, config); } }, buildStart(inputOptions) { for (const pluginHook of pluginList) { - pluginHook.buildStart?.call(this, inputOptions) + (pluginHook.buildStart as PluginHookBuildStart)?.call(this, inputOptions); } }, @@ -168,36 +177,33 @@ export default function federation( transform(code: string, id: string) { for (const pluginHook of pluginList) { - const result = pluginHook.transform?.call(this, code, id) + const result = (pluginHook.transform as PluginHookTransform)?.call(this, code, id); if (result) { - return result + return result; } } return code }, moduleParsed(moduleInfo: ModuleInfo): void { for (const pluginHook of pluginList) { - pluginHook.moduleParsed?.call(this, moduleInfo) + (pluginHook.moduleParsed as PluginHookModuleParsed)?.call(this, moduleInfo); } }, outputOptions(outputOptions) { for (const pluginHook of pluginList) { - pluginHook.outputOptions?.call(this, outputOptions) + (pluginHook.outputOptions as PluginHookOutputOptions)?.call(this, outputOptions) } return outputOptions }, renderChunk(code, chunkInfo, _options) { for (const pluginHook of pluginList) { - const result = pluginHook.renderChunk?.call( - this, - code, - chunkInfo, - _options - ) - if (result) { - return result + if (pluginHook.renderChunk) { + const result = (pluginHook.renderChunk as PluginHookRenderChunk)?.call(this, code, chunkInfo, _options, { chunks: {} }) + if (result) { + return result + } } } return null @@ -205,8 +211,9 @@ export default function federation( generateBundle: function (_options, bundle, isWrite) { for (const pluginHook of pluginList) { - pluginHook.generateBundle?.call(this, _options, bundle, isWrite) + (pluginHook.generateBundle as PluginHookGenerateBundle)?.call(this, _options, bundle, isWrite); } } } + return plugin } diff --git a/packages/lib/src/prod/expose-production.ts b/packages/lib/src/prod/expose-production.ts index ad448c68..19dbe5d6 100644 --- a/packages/lib/src/prod/expose-production.ts +++ b/packages/lib/src/prod/expose-production.ts @@ -309,4 +309,4 @@ export function prodExposePlugin( } } } -} +} \ No newline at end of file diff --git a/packages/lib/src/prod/remote-production.ts b/packages/lib/src/prod/remote-production.ts index 3476286b..4592b3b9 100644 --- a/packages/lib/src/prod/remote-production.ts +++ b/packages/lib/src/prod/remote-production.ts @@ -159,44 +159,49 @@ export function prodRemotePlugin( const initMap = Object.create(null); - async function __federation_method_ensure(remoteId) { - const remote = remotesMap[remoteId]; - if (!remote.inited) { - if ('var' === remote.format) { - // loading js with script tag - return new Promise(resolve => { - const callback = () => { - if (!remote.inited) { - remote.lib = window[remoteId]; - remote.lib.init(wrapShareModule(remote.from)) - remote.inited = true; - } - resolve(remote.lib); - } - return loadJS(remote.url, callback); - }); - } else if (['esm', 'systemjs'].includes(remote.format)) { - // loading js with import(...) - return new Promise((resolve, reject) => { - const getUrl = typeof remote.url === 'function' ? remote.url : () => Promise.resolve(remote.url); - getUrl().then(url => { - import(/* @vite-ignore */ url).then(lib => { - if (!remote.inited) { - const shareScope = wrapShareModule(remote.from); - lib.init(shareScope); - remote.lib = lib; - remote.lib.init(shareScope); - remote.inited = true; - } - resolve(remote.lib); - }).catch(reject) - }) - }) - } - } else { - return remote.lib; - } - } + + async function __federation_method_ensure(remoteId, retryCount) { + const remote = remotesMap[remoteId]; + if (!remote.inited || retryCount > 0) { + if ('var' === remote.format) { + // loading js with script tag + return new Promise(resolve => { + const callback = () => { + if (!remote.inited) { + remote.lib = window[remoteId]; + remote.lib.init(wrapShareModule(remote.from)); + remote.inited = true; + } + resolve(remote.lib); + }; + return loadJS(remote.url, callback); + }); + } else if (['esm', 'systemjs'].includes(remote.format)) { + // loading js with import(...) + return new Promise((resolve, reject) => { + const getUrl = typeof remote.url === 'function' ? remote.url : () => { + const url = new URL(remote.url, window.location.origin); + url.searchParams.append("retryCount", retryCount); + return Promise.resolve(url.toString()); + } + getUrl().then(url => { + import(/* @vite-ignore */ url).then(lib => { + if (!remote.inited || retryCount > 0) { + const shareScope = wrapShareModule(remote.from); + lib.init(shareScope); + remote.lib = lib; + remote.lib.init(shareScope); + remote.inited = true; + } + resolve(remote.lib); + }).catch(reject); + }); + }) + } + } else { + return remote.lib; + } + } function __federation_method_unwrapDefault(module) { return (module?.__esModule || module?.[Symbol.toStringTag] === 'Module') ? module.default : module @@ -212,9 +217,38 @@ export function prodRemotePlugin( return module; } - function __federation_method_getRemote(remoteName, componentName) { - return __federation_method_ensure(remoteName).then((remote) => remote.get(componentName).then(factory => factory())); - } + async function __federation_method_getRemote(remoteName, componentName) { + const remoteConfig = remotesMap[remoteName]; + let retryCount = 0; + const getRemote = async () => { + try { + const remoteModule = await __federation_method_ensure(remoteName, retryCount); + const factory = await remoteModule.get(componentName); + return factory(); + } catch (err) { + retryCount++; + if (retryCount > remoteConfig.importRetryCount) { + if(remoteConfig.onImportFail){ + return remoteConfig.onImportFail(remoteName, componentName, err); + } else { + throw err; + } + } else { + const retryBackoff = 10 ** retryCount; + const retry = () => { + return new Promise((resolve) => { + setTimeout(() => { + const retryResult = getRemote(); + resolve(retryResult); + }, retryBackoff) + }) + } + return await retry(); + } + } + }; + return getRemote(); + } function __federation_method_setRemote(remoteName, remoteConfig) { remotesMap[remoteName] = remoteConfig; diff --git a/packages/lib/src/utils/index.ts b/packages/lib/src/utils/index.ts index 54f902fd..e3f230e8 100644 --- a/packages/lib/src/utils/index.ts +++ b/packages/lib/src/utils/index.ts @@ -115,14 +115,18 @@ export function parseRemoteOptions( shareScope: options.shareScope || 'default', format: 'esm', from: 'vite', - externalType: 'url' + externalType: 'url', + importRetryCount: 0, + onImportFail: undefined }), (item) => ({ external: Array.isArray(item.external) ? item.external : [item.external], shareScope: item.shareScope || options.shareScope || 'default', format: item.format || 'esm', from: item.from ?? 'vite', - externalType: item.externalType || 'url' + externalType: item.externalType || 'url', + importRetryCount: item.format === "var" ? undefined : item.importRetryCount || 0, + onImportFail: item.onImportFail ? item.onImportFail : undefined }) ) } @@ -232,9 +236,9 @@ export function createRemotesMap(remotes: Remote[]): string { ${remotes .map( (remote) => - `'${remote.id}':{url:${createUrl(remote)},format:'${ + `'${remote.id}':{url:${createUrl(remote)} ,format:'${ remote.config.format - }',from:'${remote.config.from}'}` + }', from:'${remote.config.from}', importRetryCount: ${remote.config.importRetryCount ?? 0}, onImportFail: ${remote.config.onImportFail? remote.config.onImportFail.toString(): "undefined"}}` ) .join(',\n ')} };` diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 6b2d5b00..9742eaef 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,7 +1,9 @@ { - "include": ["src"], + "include": ["src", "types/*.d.ts"], "exclude": ["dist", "node_modules", "test"], "compilerOptions": { + "composite": false, + "declaration": false, "outDir": "dist", "target": "es2019", "module": "commonjs", @@ -12,6 +14,7 @@ "noUnusedLocals": true, "esModuleInterop": true, "baseUrl": ".", - "noImplicitAny": false + "noImplicitAny": false, + "skipLibCheck": true } } diff --git a/packages/lib/types/import-shared.d.ts b/packages/lib/types/import-shared.d.ts new file mode 100644 index 00000000..e0088380 --- /dev/null +++ b/packages/lib/types/import-shared.d.ts @@ -0,0 +1 @@ +declare module "*.js?raw"; \ No newline at end of file diff --git a/packages/lib/types/index.d.ts b/packages/lib/types/index.d.ts index 8f52937a..32ba016c 100644 --- a/packages/lib/types/index.d.ts +++ b/packages/lib/types/index.d.ts @@ -220,7 +220,7 @@ declare interface RemotesObject { /** * Advanced configuration for container locations from which modules should be resolved and loaded at runtime. */ -declare interface RemotesConfig { +declare type RemotesConfig = { /** * Container locations from which modules should be resolved and loaded at runtime. */ @@ -245,6 +245,18 @@ declare interface RemotesConfig { * from */ from?: 'vite' | 'webpack' + + /** + * The number of times to retry imports before failure. + * The first retry will happen after a 10ms delay. Subsequent retries will happen + * after a (10 ** n)ms delay. + */ + importRetryCount?: number + + /** + * Method called when import fails (after `importRetryCount` has been exhausted) + */ + onImportFail?: (remoteName: string, componentName: string, errorConfig: RemotesConfig, error: Error) => void; } /** diff --git a/packages/lib/types/pluginHooks.d.ts b/packages/lib/types/pluginHooks.d.ts index d97f7052..412c5008 100644 --- a/packages/lib/types/pluginHooks.d.ts +++ b/packages/lib/types/pluginHooks.d.ts @@ -2,3 +2,66 @@ import { Plugin as VitePlugin } from 'vite' export interface PluginHooks extends VitePlugin { virtualFile?: Record } +export type PluginHookConfig = + | (( + this: void, + config: UserConfig, + env: ConfigEnv + ) => UserConfig | null | void | Promise) + | undefined + +export type PluginHookConfigureServer = + | (( + this: void, + server: ViteDevServer + ) => (() => void) | void | Promise<(() => void) | void>) + | undefined + +export type PluginHookConfigResolved = + | ((this: void, config: ResolvedConfig) => void | Promise) + | undefined + +export type PluginHookBuildStart = + | ((this: PluginContext, options: NormalizedInputOptions) => void) + | undefined + +export type PluginHookTransform = + | (( + this: TransformPluginContext, + code: string, + id: string, + options?: { + ssr?: boolean + } + ) => + | Promise> + | (string | NullValue | Partial)) + | undefined + +export type PluginHookModuleParsed = ( + this: PluginContext, + info: ModuleInfo +) => void | undefined + +export type PluginHookOutputOptions = + | ((this: PluginContext, options: OutputOptions) => OutputOptions | NullValue) + | undefined + +export type PluginHookRenderChunk = + | (( + this: MinimalPluginContext, + code: string, + chunk: RenderedChunk, + options: NormalizedOutputOptions, + meta: { chunks: Record } + ) => { code: string; map?: SourceMapInput } | string | NullValue) + | undefined + +export type PluginHookGenerateBundle = + | (( + this: PluginContext, + options: NormalizedOutputOptions, + bundle: OutputBundle, + isWrite: boolean + ) => void) + | undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50b356f9..770e4f80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,7 +143,7 @@ importers: devDependencies: '@originjs/vite-plugin-federation': specifier: ^1.2.3 - version: 1.3.6 + version: 1.3.7 kill-port: specifier: ^2.0.1 version: 2.0.1 @@ -1148,6 +1148,9 @@ importers: magic-string: specifier: ^0.27.0 version: 0.27.0 + rollup: + specifier: ^3.9.1 + version: 3.23.1 devDependencies: '@rollup/plugin-virtual': specifier: ^3.0.1 @@ -1330,105 +1333,90 @@ packages: '@babel/plugin-proposal-async-generator-functions@7.18.6': resolution: {integrity: sha512-WAz4R9bvozx4qwf74M+sfqPMKfSqwM0phxPTR6iJIi8robgzXwkEgmeJG1gEKhm6sDqT/U9aV3lfcqybIpev8w==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-class-properties@7.18.6': resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-class-static-block@7.18.6': resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 '@babel/plugin-proposal-dynamic-import@7.18.6': resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-export-namespace-from@7.18.6': resolution: {integrity: sha512-zr/QcUlUo7GPo6+X1wC98NJADqmy5QTFWWhqeQWiki4XHafJtLl/YMGkmRB2szDD2IYJCCdBTd4ElwhId9T7Xw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-json-strings@7.18.6': resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-logical-assignment-operators@7.18.6': resolution: {integrity: sha512-zMo66azZth/0tVd7gmkxOkOjs2rpHyhpcFo565PUP37hSp6hSd9uUKIfTDFMz58BwqgQKhJ9YxtM5XddjXVn+Q==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-numeric-separator@7.18.6': resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-object-rest-spread@7.18.6': resolution: {integrity: sha512-9yuM6wr4rIsKa1wlUAbZEazkCrgw2sMPEXCr4Rnwetu7cEW1NydkCWytLuYletbf8vFxdJxFhwEZqMpOx2eZyw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-optional-catch-binding@7.18.6': resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-optional-chaining@7.18.6': resolution: {integrity: sha512-PatI6elL5eMzoypFAiYDpYQyMtXTn+iMhuxxQt5mAXD4fEmKorpSI3PHd+i3JXBJN3xyA6MvJv7at23HffFHwA==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-methods@7.18.6': resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-property-in-object@7.18.6': resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-unicode-property-regex@7.18.6': resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 @@ -2126,8 +2114,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@originjs/vite-plugin-federation@1.3.6': - resolution: {integrity: sha512-tHLMjdMJFPFMSJrUuJJiv8l7OFRvM19E9O1B9dhbk+04i3RnYwE9A6oNtSUM1dnvkalzCLwZIuMpti28/tnh8g==} + '@originjs/vite-plugin-federation@1.3.7': + resolution: {integrity: sha512-k73iJPON4R8dPi9y6fcNgTQDLbdeoHU5OWUesgF5/E7dnqbFxOW964JbvrRK2Ktm4beaEMSDAki3pPKKRIuAPw==} engines: {node: '>=14.0.0', pnpm: '>=7.0.1'} '@rollup/plugin-babel@6.0.3': @@ -2456,7 +2444,6 @@ packages: '@vitejs/plugin-react-refresh@1.3.6': resolution: {integrity: sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==} engines: {node: '>=12.0.0'} - deprecated: This package has been deprecated in favor of @vitejs/plugin-react '@vitejs/plugin-react@3.0.0': resolution: {integrity: sha512-1mvyPc0xYW5G8CHQvJIJXLoMjl5Ct3q2g5Y2s6Ccfgwm45y48LBvsla7az+GkkAtYikWQ4Lxqcsq5RHLcZgtNQ==} @@ -3010,7 +2997,7 @@ packages: engines: {node: '>= 0.8.0'} concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} @@ -6621,7 +6608,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.13.0 - '@originjs/vite-plugin-federation@1.3.6': + '@originjs/vite-plugin-federation@1.3.7': dependencies: estree-walker: 3.0.2 magic-string: 0.27.0