From 49b6caebbef899abf18cd8d62f64261aac6a18cd Mon Sep 17 00:00:00 2001 From: Jiannan Zhang Date: Wed, 8 Feb 2023 14:40:40 +0800 Subject: [PATCH] fix: shared lib load failed (#338) * fix: shared lib load failed * test: remove deleted features and parameters --- .../team-green/vite.config.ts | 2 +- .../host/src/App.vue | 8 +- .../remote/vite.config.ts | 14 +- ....spec.ts => vue3-demo-esm-shared-store.ts} | 0 .../vue3-demo-esm/home/vite.config.ts | 6 +- .../vue3-demo-esm/layout/vite.config.ts | 1 - packages/lib/src/index.ts | 6 + packages/lib/src/prod/remote-production.ts | 236 +++++++++------ packages/lib/src/prod/shared-production.ts | 279 +----------------- 9 files changed, 177 insertions(+), 375 deletions(-) rename packages/examples/vue3-demo-esm-shared-store/__tests__/{vue3-demo-esm-shared-store.dev&serve.spec.ts => vue3-demo-esm-shared-store.ts} (100%) diff --git a/packages/examples/vue3-advanced-demo/team-green/vite.config.ts b/packages/examples/vue3-advanced-demo/team-green/vite.config.ts index d6c8358a..54edd211 100644 --- a/packages/examples/vue3-advanced-demo/team-green/vite.config.ts +++ b/packages/examples/vue3-advanced-demo/team-green/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ shared: ['vue', 'pinia'] }) ], - base: 'http://localhost:5003', + base: 'http://localhost:5001', build: { minify: false, target: ["chrome89", "edge89", "firefox89", "safari15"] diff --git a/packages/examples/vue3-demo-esm-expose-store/host/src/App.vue b/packages/examples/vue3-demo-esm-expose-store/host/src/App.vue index 57b26dfa..8e62529f 100644 --- a/packages/examples/vue3-demo-esm-expose-store/host/src/App.vue +++ b/packages/examples/vue3-demo-esm-expose-store/host/src/App.vue @@ -11,10 +11,10 @@ import {counterState} from 'remote-store/remoteStore' export default { - setup() { - let counter = counterState(); - return {counter}; - }, + data(){ + let counter = counterState() + return {counter} + } }; diff --git a/packages/examples/vue3-demo-esm-expose-store/remote/vite.config.ts b/packages/examples/vue3-demo-esm-expose-store/remote/vite.config.ts index 47907a94..db2dce84 100644 --- a/packages/examples/vue3-demo-esm-expose-store/remote/vite.config.ts +++ b/packages/examples/vue3-demo-esm-expose-store/remote/vite.config.ts @@ -1,7 +1,6 @@ import {defineConfig} from 'vite' import vue from '@vitejs/plugin-vue' import federation from '@originjs/vite-plugin-federation' -import topLevelAwait from 'vite-plugin-top-level-await' // https://vitejs.dev/config/ export default defineConfig({ @@ -19,14 +18,15 @@ export default defineConfig({ pinia: {} } }), - topLevelAwait({ - // The export name of top-level await promise for each chunk module - promiseExportName: "__tla", - // The function to generate import names of top-level await promise in each chunk module - promiseImportName: i => `__tla_${i}` - }) + // topLevelAwait({ + // // The export name of top-level await promise for each chunk module + // promiseExportName: "__tla", + // // The function to generate import names of top-level await promise in each chunk module + // promiseImportName: i => `__tla_${i}` + // }) ], build: { + target:'esnext', assetsInlineLimit: 40960, minify: false, cssCodeSplit: false, diff --git a/packages/examples/vue3-demo-esm-shared-store/__tests__/vue3-demo-esm-shared-store.dev&serve.spec.ts b/packages/examples/vue3-demo-esm-shared-store/__tests__/vue3-demo-esm-shared-store.ts similarity index 100% rename from packages/examples/vue3-demo-esm-shared-store/__tests__/vue3-demo-esm-shared-store.dev&serve.spec.ts rename to packages/examples/vue3-demo-esm-shared-store/__tests__/vue3-demo-esm-shared-store.ts diff --git a/packages/examples/vue3-demo-esm/home/vite.config.ts b/packages/examples/vue3-demo-esm/home/vite.config.ts index 0e69243b..7ad11a0a 100644 --- a/packages/examples/vue3-demo-esm/home/vite.config.ts +++ b/packages/examples/vue3-demo-esm/home/vite.config.ts @@ -24,9 +24,9 @@ export default defineConfig({ generate:false }, // This is to test if the custom library can be SHARED, there is no real point - myStore:{ - packagePath:'./src/store.js' - } + // myStore:{ + // packagePath:'./src/store.js' + // } } }), topLevelAwait({ diff --git a/packages/examples/vue3-demo-esm/layout/vite.config.ts b/packages/examples/vue3-demo-esm/layout/vite.config.ts index d57d6748..37118b8d 100644 --- a/packages/examples/vue3-demo-esm/layout/vite.config.ts +++ b/packages/examples/vue3-demo-esm/layout/vite.config.ts @@ -28,7 +28,6 @@ export default defineConfig({ shared: { vue:{ // This is an invalid configuration, because the generate attribute is not supported on the host side - generate:false }, pinia:{ } diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 889a32a2..61e92271 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -136,6 +136,12 @@ export default function federation( if (v) { return v } + if (args[0] === '\0virtual:__federation_fn_import') { + return { + id: '\0virtual:__federation_fn_import', + moduleSideEffects: true + } + } return null }, diff --git a/packages/lib/src/prod/remote-production.ts b/packages/lib/src/prod/remote-production.ts index f5e6e94b..827ca153 100644 --- a/packages/lib/src/prod/remote-production.ts +++ b/packages/lib/src/prod/remote-production.ts @@ -54,92 +54,103 @@ export function prodRemotePlugin( virtualFile: { // language=JS __federation__: ` -${createRemotesMap(remotes)} -const loadJS = async (url, fn) => { - const resolvedUrl = typeof url === 'function' ? await url() : url; - const script = document.createElement('script') - script.type = 'text/javascript'; - script.onload = fn; - script.src = resolvedUrl; - document.getElementsByTagName('head')[0].appendChild(script); -} -const scriptTypes = ['var']; -const importTypes = ['esm', 'systemjs'] -function get(name, ${REMOTE_FROM_PARAMETER}){ - return __federation_import(name).then(module => ()=> { - if (${REMOTE_FROM_PARAMETER} === 'webpack') { - return Object.prototype.toString.call(module).indexOf('Module') > -1 && module.default ? module.default : module - } - return module - }) -} -const wrapShareModule = ${REMOTE_FROM_PARAMETER} => { - return { - ${getModuleMarker('shareScope')} - } -} -async function __federation_import(name){ - return import(name); -} -const initMap = Object.create(null); -async function __federation_method_ensure(remoteId) { - const remote = remotesMap[remoteId]; - if (!remote.inited) { - if (scriptTypes.includes(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 (importTypes.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; - } -} + ${createRemotesMap(remotes)} + const loadJS = async (url, fn) => { + const resolvedUrl = typeof url === 'function' ? await url() : url; + const script = document.createElement('script') + script.type = 'text/javascript'; + script.onload = fn; + script.src = resolvedUrl; + document.getElementsByTagName('head')[0].appendChild(script); + } + const scriptTypes = ['var']; + const importTypes = ['esm', 'systemjs'] -function __federation_method_unwrapDefault(module) { - return (module?.__esModule || module?.[Symbol.toStringTag] === 'Module')?module.default:module -} + function get(name, ${REMOTE_FROM_PARAMETER}) { + return __federation_import(name).then(module => () => { + if (${REMOTE_FROM_PARAMETER} === 'webpack') { + return Object.prototype.toString.call(module).indexOf('Module') > -1 && module.default ? module.default : module + } + return module + }) + } -function __federation_method_wrapDefault(module ,need){ - if (!module?.default && need) { - let obj = Object.create(null); - obj.default = module; - obj.__esModule = true; - return obj; - } - return module; -} + const wrapShareModule = ${REMOTE_FROM_PARAMETER} => { + return { + ${getModuleMarker('shareScope')} + } + } -function __federation_method_getRemote(remoteName, componentName){ - return __federation_method_ensure(remoteName).then((remote) => remote.get(componentName).then(factory => factory())); -} -export {__federation_method_ensure, __federation_method_getRemote , __federation_method_unwrapDefault , __federation_method_wrapDefault} -` + async function __federation_import(name) { + return import(name); + } + + const initMap = Object.create(null); + + async function __federation_method_ensure(remoteId) { + const remote = remotesMap[remoteId]; + if (!remote.inited) { + if (scriptTypes.includes(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 (importTypes.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; + } + } + + function __federation_method_unwrapDefault(module) { + return (module?.__esModule || module?.[Symbol.toStringTag] === 'Module') ? module.default : module + } + + function __federation_method_wrapDefault(module, need) { + if (!module?.default && need) { + let obj = Object.create(null); + obj.default = module; + obj.__esModule = true; + return obj; + } + return module; + } + + function __federation_method_getRemote(remoteName, componentName) { + return __federation_method_ensure(remoteName).then((remote) => remote.get(componentName).then(factory => factory())); + } + + export { + __federation_method_ensure, + __federation_method_getRemote, + __federation_method_unwrapDefault, + __federation_method_wrapDefault + } + ` }, async transform(this: TransformPluginContext, code: string, id: string) { @@ -158,7 +169,7 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation }${ sharedInfo[1].root ? sharedInfo[1].root[0] + '/' : '' }${basename}`, - preserveSignature: 'allow-extension', + preserveSignature: 'strict', name: sharedInfo[0] }) sharedFileName2Prop.set(basename, sharedInfo as ConfigTypeSet) @@ -232,7 +243,9 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation }) return code.replace(getModuleMarker('shareScope'), res.join(',')) } + } + if (builderInfo.isHost || builderInfo.isShared) { let ast: AcornNode | null = null try { ast = this.parse(code) @@ -246,8 +259,53 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation const magicString = new MagicString(code) const hasStaticImported = new Map() let requiresRuntime = false + let needImportShared = false + let modify = false walk(ast, { enter(node: any) { + // handle share, eg. replace import {a} from b -> const a = importShared('b') + if (node.type === 'ImportDeclaration') { + const moduleName = node.source.value + if ( + parsedOptions.prodShared.some( + (sharedInfo) => sharedInfo[0] === moduleName + ) + ) { + const declaration: (string | never)[] = [] + if (!node.specifiers?.length) { + // invalid import , like import './__federation_shared_lib.js' , and remove it + magicString.remove(node.start, node.end) + modify = true + } else { + node.specifiers.forEach((specify) => { + declaration.push( + `${ + specify.imported?.name + ? `${ + specify.imported.name === specify.local.name + ? specify.local.name + : `${specify.imported.name}:${specify.local.name}` + }` + : `default:${specify.local.name}` + }` + ) + }) + needImportShared = true + } + if (declaration.length) { + magicString.overwrite( + node.start, + node.end, + `const {${declaration.join( + ',' + )}} = await importShared('${moduleName}');\n` + ) + needImportShared = true + } + } + } + + // handle remote import , eg replace import {a} from 'remote/b' to dynamic import if ( (node.type === 'ImportExpression' || node.type === 'ImportDeclaration' || @@ -384,7 +442,15 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation ) } - return magicString.toString() + if (needImportShared) { + magicString.prepend( + `import {importShared} from '\0virtual:__federation_fn_import';\n` + ) + } + + if (requiresRuntime || needImportShared || modify) { + return magicString.toString() + } } } } diff --git a/packages/lib/src/prod/shared-production.ts b/packages/lib/src/prod/shared-production.ts index 85ffe89b..b863c6c5 100644 --- a/packages/lib/src/prod/shared-production.ts +++ b/packages/lib/src/prod/shared-production.ts @@ -14,25 +14,11 @@ // ***************************************************************************** import type { PluginHooks } from '../../types/pluginHooks' -import { - getModuleMarker, - parseSharedOptions, - removeNonRegLetter -} from '../utils' +import { getModuleMarker, parseSharedOptions } from '../utils' import { builderInfo, EXPOSES_MAP, parsedOptions } from '../public' import type { ConfigTypeSet, VitePluginFederationOptions } from 'types' -import { walk } from 'estree-walker' -import MagicString from 'magic-string' import { basename, join, resolve } from 'path' import { readdirSync, readFileSync, statSync } from 'fs' -import type { - NormalizedOutputOptions, - OutputChunk, - PluginContext -} from 'rollup' -import { sharedFileName2Prop } from './remote-production' - -const sharedFileNameReg = /^__federation_shared_.+\.js$/ const sharedFilePathReg = /__federation_shared_.+\.js$/ export function prodSharedPlugin( @@ -51,251 +37,6 @@ export function prodSharedPlugin( let isRemote const id2Prop = new Map() - const transformImportFn = function ( - this: PluginContext, - code, - chunk: OutputChunk, - options: NormalizedOutputOptions - ) { - const ast = this.parse(code) - const magicString = new MagicString(code) - // flag import shared replace - let modify = false - // flag delete invalid import - let remove = false - switch (options.format) { - case 'es': - { - walk(ast, { - enter(node: any) { - if ( - node.type === 'ImportDeclaration' && - sharedFileNameReg.test(basename(node.source.value)) - ) { - const sharedName = sharedFileName2Prop.get( - basename(node.source.value) - )?.[0] - if (sharedName) { - const declaration: (string | never)[] = [] - if (!node.specifiers?.length) { - // invalid import , like import './__federation_shared_lib.js' , and remove it - magicString.remove(node.start, node.end) - remove = true - } else { - node.specifiers.forEach((specify) => { - declaration.push( - `${ - specify.imported?.name - ? `${ - specify.imported.name === specify.local.name - ? specify.local.name - : `${specify.imported.name}:${specify.local.name}` - }` - : `default:${specify.local.name}` - }` - ) - }) - } - if (declaration.length) { - magicString.overwrite( - node.start, - node.end, - `const {${declaration.join( - ',' - )}} = await importShared('${sharedName}');\n` - ) - modify = true - } - } - } - } - }) - if (modify) { - const prop = id2Prop.get(chunk.facadeModuleId as string) - magicString.prepend( - `import {importShared} from '${ - prop?.root ? '.' : '' - }./__federation_fn_import.js';\n` - ) - return { - code: magicString.toString(), - map: magicString.generateMap({ - source: chunk.map?.file, - hires: true - }) - } - } - if (remove) { - // only remove code , dont insert import {importShared} from 'xxx' - return { - code: magicString.toString(), - map: magicString.generateMap({ - source: chunk.map?.file, - hires: true - }) - } - } - } - break - case 'system': - { - walk(ast, { - enter(node: any) { - const expression = - node.body.length === 1 - ? node.body[0]?.expression - : node.body.find( - (item) => - item.type === 'ExpressionStatement' && - item.expression?.callee?.object?.name === 'System' && - item.expression.callee.property?.name === 'register' - )?.expression - if (expression) { - const args = expression.arguments - if ( - args[0].type === 'ArrayExpression' && - args[0].elements?.length > 0 - ) { - const importIndex: any[] = [] - let removeLast = false - chunk.imports.forEach((importName, index) => { - const baseName = basename(importName) - if (sharedFileNameReg.test(baseName)) { - importIndex.push({ - index: index, - name: sharedFileName2Prop.get(baseName)?.[0] - }) - if (index === chunk.imports.length - 1) { - removeLast = true - } - } - }) - if ( - importIndex.length && - args[1]?.type === 'FunctionExpression' - ) { - const functionExpression = args[1] - const returnStatement = functionExpression?.body?.body.find( - (item) => item.type === 'ReturnStatement' - ) - - if (returnStatement) { - // insert __federation_import variable - magicString.prependLeft( - returnStatement.start, - 'var __federation_import;\n' - ) - const setters = returnStatement.argument.properties.find( - (property) => property.key.name === 'setters' - ) - const settersElements = setters.value.elements - // insert __federation_import setter - magicString.appendRight( - setters.end - 1, - `${ - removeLast ? '' : ',' - }function (module){__federation_import=module.importShared}` - ) - const execute = returnStatement.argument.properties.find( - (property) => property.key.name === 'execute' - ) - const insertPos = execute.value.body.body[0].start - importIndex.forEach((item) => { - // remove unnecessary setters and import - const last = item.index === settersElements.length - 1 - magicString.remove( - settersElements[item.index].start, - last - ? settersElements[item.index].end - : settersElements[item.index + 1].start - 1 - ) - magicString.remove( - args[0].elements[item.index].start, - last - ? args[0].elements[item.index].end - : args[0].elements[item.index + 1].start - 1 - ) - // insert federation shared import lib - const varName = `__federation_${removeNonRegLetter( - item.name - )}` - magicString.prependLeft( - insertPos, - `var ${varName} = await __federation_import('${item.name}');\n` - ) - // get para name - const paramName = - setters.value.elements[item.index].params[0].name - // replace it with sharedImport - setters.value.elements[item.index].body.body.forEach( - (setFn) => { - if ( - setFn.expression.type === 'AssignmentExpression' - ) { - magicString.appendLeft( - insertPos, - `${setFn.expression.left.name} = ${varName}.${ - setFn.expression.right.property.name ?? - setFn.expression.right.property.value - };\n` - ) - } else if ( - setFn.expression.type === 'SequenceExpression' - ) { - setFn.expression.expressions.forEach( - (assignStatement) => { - if ( - assignStatement.right.type === - 'MemberExpression' && - assignStatement.right.object.name === - paramName - ) { - magicString.appendLeft( - insertPos, - `${ - assignStatement.left.name - } = ${varName}.${ - assignStatement.right.property.name ?? - assignStatement.right.property.value - };\n` - ) - } - } - ) - } - } - ) - }) - // add async flag to execute function - magicString.prependLeft(execute.value.start, ' async ') - // add sharedImport import declaration - magicString.appendRight( - args[0].end - 1, - `${removeLast ? '' : ','}'./__federation_fn_import.js'` - ) - modify = true - } - } - } - } - // only need to process once - this.skip() - } - }) - if (modify) { - return { - code: magicString.toString(), - map: magicString.generateMap({ - source: chunk.map?.file, - hires: true - }) - } - } - } - break - } - } - return { name: 'originjs:shared-production', virtualFile: { @@ -347,7 +88,8 @@ export function prodSharedPlugin( }, options(inputOptions) { isRemote = !!parsedOptions.prodExpose.length - isHost = !!parsedOptions.prodRemote.length && !parsedOptions.prodExpose.length + isHost = + !!parsedOptions.prodRemote.length && !parsedOptions.prodExpose.length if (shareName2Prop.size) { // remove item which is both in external and shared @@ -463,6 +205,7 @@ export function prodSharedPlugin( } } }, + outputOptions: function (outputOption) { // remove rollup generated empty imports,like import './filename.js' outputOption.hoistTransitiveImports = false @@ -492,6 +235,7 @@ export function prodSharedPlugin( return outputOption }, + generateBundle(options, bundle) { if (!isRemote) { return @@ -507,19 +251,6 @@ export function prodSharedPlugin( !shareName2Prop.get(chunk.name).generate if (removeSharedChunk) { needRemoveShared.add(key) - continue - } - const importShared = chunk.imports?.some((name) => - sharedFilePathReg.test(name) - ) - if (importShared) { - const transformedCode = transformImportFn.apply(this, [ - chunk.code, - chunk, - options - ]) - chunk.code = transformedCode?.code ?? chunk.code - chunk.map = transformedCode?.map ?? chunk.map } } }