import { DEFAULT_SCOPE } from '../constant'; import { Global, Federation } from '../global'; import { GlobalShareScopeMap, Shared, ShareArgs, ShareInfos, ShareScopeMap, LoadShareExtraOptions, UserOptions, Options, ShareStrategy, } from '../type'; import { warn, error } from './logger'; import { satisfy } from './semver'; import { SyncWaterfallHook } from './hooks'; import { arrayOptions } from './tool'; export function formatShare( shareArgs: ShareArgs, from: string, name: string, shareStrategy?: ShareStrategy, ): Shared { let get: Shared['get']; if ('get' in shareArgs) { // eslint-disable-next-line prefer-destructuring get = shareArgs.get; } else if ('lib' in shareArgs) { get = () => Promise.resolve(shareArgs.lib); } else { get = () => Promise.resolve(() => { throw new Error(`Can not get shared '${name}'!`); }); } if (shareArgs.strategy) { warn( `"shared.strategy is deprecated, please set in initOptions.shareStrategy instead!"`, ); } return { deps: [], useIn: [], from, loading: null, ...shareArgs, shareConfig: { requiredVersion: `^${shareArgs.version}`, singleton: false, eager: false, strictVersion: false, ...shareArgs.shareConfig, }, get, loaded: shareArgs?.loaded || 'lib' in shareArgs ? true : undefined, version: shareArgs.version ?? '0', scope: Array.isArray(shareArgs.scope) ? shareArgs.scope : [shareArgs.scope ?? 'default'], strategy: (shareArgs.strategy ?? shareStrategy) || 'version-first', }; } export function formatShareConfigs( globalOptions: Options, userOptions: UserOptions, ) { const shareArgs = userOptions.shared || {}; const from = userOptions.name; const shareInfos = Object.keys(shareArgs).reduce((res, pkgName) => { const arrayShareArgs = arrayOptions(shareArgs[pkgName]); res[pkgName] = res[pkgName] || []; arrayShareArgs.forEach((shareConfig) => { res[pkgName].push( formatShare(shareConfig, from, pkgName, userOptions.shareStrategy), ); }); return res; }, {} as ShareInfos); const shared = { ...globalOptions.shared, }; Object.keys(shareInfos).forEach((shareKey) => { if (!shared[shareKey]) { shared[shareKey] = shareInfos[shareKey]; } else { shareInfos[shareKey].forEach((newUserSharedOptions) => { const isSameVersion = shared[shareKey].find( (sharedVal) => sharedVal.version === newUserSharedOptions.version, ); if (!isSameVersion) { shared[shareKey].push(newUserSharedOptions); } }); } }); return { shared, shareInfos }; } export function versionLt(a: string, b: string): boolean { const transformInvalidVersion = (version: string) => { const isNumberVersion = !Number.isNaN(Number(version)); if (isNumberVersion) { const splitArr = version.split('.'); let validVersion = version; for (let i = 0; i < 3 - splitArr.length; i++) { validVersion += '.0'; } return validVersion; } return version; }; if (satisfy(transformInvalidVersion(a), `<=${transformInvalidVersion(b)}`)) { return true; } else { return false; } } export const findVersion = ( shareVersionMap: ShareScopeMap[string][string], cb?: (prev: string, cur: string) => boolean, ): string => { const callback = cb || function (prev: string, cur: string): boolean { return versionLt(prev, cur); }; return Object.keys(shareVersionMap).reduce((prev: number | string, cur) => { if (!prev) { return cur; } if (callback(prev as string, cur)) { return cur; } // default version is '0' https://github.com/webpack/webpack/blob/main/lib/sharing/ProvideSharedModule.js#L136 if (prev === '0') { return cur; } return prev; }, 0) as string; }; export const isLoaded = (shared: Shared) => { return Boolean(shared.loaded) || typeof shared.lib === 'function'; }; const isLoading = (shared: Shared) => { return Boolean(shared.loading); }; function findSingletonVersionOrderByVersion( shareScopeMap: ShareScopeMap, scope: string, pkgName: string, ): string { const versions = shareScopeMap[scope][pkgName]; const callback = function (prev: string, cur: string): boolean { return !isLoaded(versions[prev]) && versionLt(prev, cur); }; return findVersion(shareScopeMap[scope][pkgName], callback); } function findSingletonVersionOrderByLoaded( shareScopeMap: ShareScopeMap, scope: string, pkgName: string, ): string { const versions = shareScopeMap[scope][pkgName]; const callback = function (prev: string, cur: string): boolean { const isLoadingOrLoaded = (shared: Shared) => { return isLoaded(shared) || isLoading(shared); }; if (isLoadingOrLoaded(versions[cur])) { if (isLoadingOrLoaded(versions[prev])) { return Boolean(versionLt(prev, cur)); } else { return true; } } if (isLoadingOrLoaded(versions[prev])) { return false; } return versionLt(prev, cur); }; return findVersion(shareScopeMap[scope][pkgName], callback); } function getFindShareFunction(strategy: Shared['strategy']) { if (strategy === 'loaded-first') { return findSingletonVersionOrderByLoaded; } return findSingletonVersionOrderByVersion; } export function getRegisteredShare( localShareScopeMap: ShareScopeMap, pkgName: string, shareInfo: Shared, resolveShare: SyncWaterfallHook<{ shareScopeMap: ShareScopeMap; scope: string; pkgName: string; version: string; GlobalFederation: Federation; resolver: () => Shared | undefined; }>, ): Shared | void { if (!localShareScopeMap) { return; } const { shareConfig, scope = DEFAULT_SCOPE, strategy } = shareInfo; const scopes = Array.isArray(scope) ? scope : [scope]; for (const sc of scopes) { if ( shareConfig && localShareScopeMap[sc] && localShareScopeMap[sc][pkgName] ) { const { requiredVersion } = shareConfig; const findShareFunction = getFindShareFunction(strategy); const maxOrSingletonVersion = findShareFunction( localShareScopeMap, sc, pkgName, ); //@ts-ignore const defaultResolver = () => { if (shareConfig.singleton) { if ( typeof requiredVersion === 'string' && !satisfy(maxOrSingletonVersion, requiredVersion) ) { const msg = `Version ${maxOrSingletonVersion} from ${ maxOrSingletonVersion && localShareScopeMap[sc][pkgName][maxOrSingletonVersion].from } of shared singleton module ${pkgName} does not satisfy the requirement of ${ shareInfo.from } which needs ${requiredVersion})`; if (shareConfig.strictVersion) { error(msg); } else { warn(msg); } } return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; } else { if (requiredVersion === false || requiredVersion === '*') { return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; } if (satisfy(maxOrSingletonVersion, requiredVersion)) { return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; } for (const [versionKey, versionValue] of Object.entries( localShareScopeMap[sc][pkgName], )) { if (satisfy(versionKey, requiredVersion)) { return versionValue; } } } }; const params = { shareScopeMap: localShareScopeMap, scope: sc, pkgName, version: maxOrSingletonVersion, GlobalFederation: Global.__FEDERATION__, resolver: defaultResolver, }; const resolveShared = resolveShare.emit(params) || params; return resolveShared.resolver(); } } } export function getGlobalShareScope(): GlobalShareScopeMap { return Global.__FEDERATION__.__SHARE__; } export function getTargetSharedOptions(options: { pkgName: string; extraOptions?: LoadShareExtraOptions; shareInfos: ShareInfos; }) { const { pkgName, extraOptions, shareInfos } = options; const defaultResolver = (sharedOptions: ShareInfos[string]) => { if (!sharedOptions) { return undefined; } const shareVersionMap: ShareScopeMap[string][string] = {}; sharedOptions.forEach((shared) => { shareVersionMap[shared.version] = shared; }); const callback = function (prev: string, cur: string): boolean { return !isLoaded(shareVersionMap[prev]) && versionLt(prev, cur); }; const maxVersion = findVersion(shareVersionMap, callback); return shareVersionMap[maxVersion]; }; const resolver = extraOptions?.resolver ?? defaultResolver; return Object.assign( {}, resolver(shareInfos[pkgName]), extraOptions?.customShareInfo, ); }