@module-federation/vite 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import defu from 'defu';
2
2
  import * as fs from 'fs';
3
3
  import { mkdirSync, writeFileSync, existsSync, writeFile } from 'fs';
4
4
  import * as path from 'pathe';
5
- import path__default, { resolve, basename, parse, join, dirname, relative } from 'pathe';
5
+ import path__default, { resolve, basename, parse, join, dirname } from 'pathe';
6
6
  import { createFilter } from '@rollup/pluginutils';
7
7
  import { walk } from 'estree-walker';
8
8
  import MagicString from 'magic-string';
@@ -97,7 +97,7 @@ const addEntry = ({
97
97
  emitFileOptions.fileName = fileName;
98
98
  }
99
99
  emitFileId = this.emitFile(emitFileOptions);
100
- if (htmlFilePath) {
100
+ if (htmlFilePath && fs.existsSync(htmlFilePath)) {
101
101
  const htmlContent = fs.readFileSync(htmlFilePath, 'utf-8');
102
102
  const scriptRegex = /<script\s+[^>]*src=["']([^"']+)["'][^>]*>/gi;
103
103
  let match;
@@ -138,10 +138,14 @@ const addEntry = ({
138
138
  */
139
139
  function PluginDevProxyModuleTopLevelAwait() {
140
140
  const filterFunction = createFilter();
141
+ const processedFlag = '/* already-processed-by-dev-proxy-module-top-level-await */';
141
142
  return {
142
143
  name: 'dev-proxy-module-top-level-await',
143
144
  apply: 'serve',
144
145
  transform(code, id) {
146
+ if (code.includes(processedFlag)) {
147
+ return null;
148
+ }
145
149
  if (!code.includes('/*mf top-level-await placeholder replacement mf*/')) {
146
150
  return null;
147
151
  }
@@ -200,8 +204,9 @@ function PluginDevProxyModuleTopLevelAwait() {
200
204
  }
201
205
  }
202
206
  });
207
+ const transformedCode = magicString.toString();
203
208
  return {
204
- code: magicString.toString(),
209
+ code: `${processedFlag}\n${transformedCode}`,
205
210
  map: magicString.generateMap({
206
211
  hires: true
207
212
  })
@@ -700,7 +705,7 @@ function generateLocalSharedImportMap() {
700
705
  const options = getNormalizeModuleFederationOptions();
701
706
  return `
702
707
  const importMap = {
703
- ${Array.from(getUsedShares()).map(pkg => `
708
+ ${Array.from(getUsedShares()).sort().map(pkg => `
704
709
  ${JSON.stringify(pkg)}: async () => {
705
710
  let pkg = await import("${getPreBuildLibImportId(pkg)}")
706
711
  return pkg
@@ -708,7 +713,7 @@ function generateLocalSharedImportMap() {
708
713
  `).join(',')}
709
714
  }
710
715
  const usedShared = {
711
- ${Array.from(getUsedShares()).map(key => {
716
+ ${Array.from(getUsedShares()).sort().map(key => {
712
717
  const shareItem = getNormalizeShareItem(key);
713
718
  if (!shareItem) return null;
714
719
  return `
@@ -843,6 +848,129 @@ function initVirtualModules() {
843
848
  writeRuntimeInitStatus();
844
849
  }
845
850
 
851
+ const ASSET_TYPES = ['js', 'css'];
852
+ const LOAD_TIMINGS = ['sync', 'async'];
853
+ const JS_EXTENSIONS = ['.ts', '.tsx', '.jsx', '.mjs', '.cjs'];
854
+ /**
855
+ * Creates an empty asset map structure for tracking JS and CSS assets
856
+ * @returns Initialized asset map with sync/async arrays for JS and CSS
857
+ */
858
+ const createEmptyAssetMap = () => ({
859
+ js: {
860
+ sync: [],
861
+ async: []
862
+ },
863
+ css: {
864
+ sync: [],
865
+ async: []
866
+ }
867
+ });
868
+ /**
869
+ * Tracks an asset in the preload map with deduplication
870
+ * @param map - The preload map to update
871
+ * @param key - The module key to track under
872
+ * @param fileName - The asset filename to track
873
+ * @param isAsync - Whether the asset is loaded async
874
+ * @param type - The asset type ('js' or 'css')
875
+ */
876
+ const trackAsset = (map, key, fileName, isAsync, type) => {
877
+ if (!map[key]) {
878
+ map[key] = createEmptyAssetMap();
879
+ }
880
+ const target = isAsync ? map[key][type].async : map[key][type].sync;
881
+ if (!target.includes(fileName)) {
882
+ target.push(fileName);
883
+ }
884
+ };
885
+ /**
886
+ * Checks if a file is a CSS file by extension
887
+ * @param fileName - The filename to check
888
+ * @returns True if file has a CSS extension (.css, .scss, .less)
889
+ */
890
+ const isCSSFile = fileName => {
891
+ return fileName.endsWith('.css') || fileName.endsWith('.scss') || fileName.endsWith('.less');
892
+ };
893
+ /**
894
+ * Collects all CSS assets from the bundle
895
+ * @param bundle - The Rollup output bundle
896
+ * @returns Set of CSS asset filenames
897
+ */
898
+ const collectCssAssets = bundle => {
899
+ const cssAssets = new Set();
900
+ for (const [fileName, fileData] of Object.entries(bundle)) {
901
+ if (fileData.type === 'asset' && isCSSFile(fileName)) {
902
+ cssAssets.add(fileName);
903
+ }
904
+ }
905
+ return cssAssets;
906
+ };
907
+ /**
908
+ * Processes module assets and tracks them in the files map
909
+ * @param bundle - The Rollup output bundle
910
+ * @param filesMap - The preload map to populate
911
+ * @param moduleMatcher - Function that matches module paths to keys
912
+ */
913
+ const processModuleAssets = (bundle, filesMap, moduleMatcher) => {
914
+ for (const [fileName, fileData] of Object.entries(bundle)) {
915
+ if (fileData.type !== 'chunk') continue;
916
+ if (!fileData.modules) continue;
917
+ for (const modulePath of Object.keys(fileData.modules)) {
918
+ const matchKey = moduleMatcher(modulePath);
919
+ if (!matchKey) continue;
920
+ // Track main JS chunk
921
+ trackAsset(filesMap, matchKey, fileName, false, 'js');
922
+ // Handle dynamic imports
923
+ if (fileData.dynamicImports) {
924
+ for (const dynamicImport of fileData.dynamicImports) {
925
+ const importData = bundle[dynamicImport];
926
+ if (!importData) continue;
927
+ const isCss = isCSSFile(dynamicImport);
928
+ trackAsset(filesMap, matchKey, dynamicImport, true, isCss ? 'css' : 'js');
929
+ }
930
+ }
931
+ }
932
+ }
933
+ };
934
+ /**
935
+ * Deduplicates assets in the files map
936
+ * @param filesMap - The preload map to deduplicate
937
+ * @returns New deduplicated preload map
938
+ */
939
+ const deduplicateAssets = filesMap => {
940
+ const result = {};
941
+ for (const [key, assetMaps] of Object.entries(filesMap)) {
942
+ result[key] = createEmptyAssetMap();
943
+ for (const type of ASSET_TYPES) {
944
+ for (const timing of LOAD_TIMINGS) {
945
+ result[key][type][timing] = Array.from(new Set(assetMaps[type][timing]));
946
+ }
947
+ }
948
+ }
949
+ return result;
950
+ };
951
+ /**
952
+ * Builds a mapping between module files and their share keys
953
+ * @param shareKeys - Set of share keys to map
954
+ * @param resolveFn - Function to resolve module paths
955
+ * @returns Map of file paths to their corresponding share keys
956
+ */
957
+ const buildFileToShareKeyMap = async (shareKeys, resolveFn) => {
958
+ const fileToShareKey = new Map();
959
+ const resolutions = await Promise.all(Array.from(shareKeys).map(shareKey => resolveFn(getPreBuildLibImportId(shareKey)).then(resolution => {
960
+ var _resolution$id;
961
+ return {
962
+ shareKey,
963
+ file: resolution == null || (_resolution$id = resolution.id) == null ? void 0 : _resolution$id.split('?')[0]
964
+ };
965
+ }).catch(() => null)));
966
+ for (const resolution of resolutions) {
967
+ if (resolution != null && resolution.file) {
968
+ fileToShareKey.set(resolution.file, resolution.shareKey);
969
+ }
970
+ }
971
+ return fileToShareKey;
972
+ };
973
+
846
974
  const Manifest = () => {
847
975
  const mfOptions = getNormalizeModuleFederationOptions();
848
976
  const {
@@ -856,21 +984,43 @@ const Manifest = () => {
856
984
  mfManifestName = 'mf-manifest.json';
857
985
  }
858
986
  if (typeof manifestOptions !== 'boolean') {
859
- mfManifestName = join((manifestOptions == null ? void 0 : manifestOptions.filePath) || '', (manifestOptions == null ? void 0 : manifestOptions.fileName) || '');
987
+ mfManifestName = path.join((manifestOptions == null ? void 0 : manifestOptions.filePath) || '', (manifestOptions == null ? void 0 : manifestOptions.fileName) || '');
860
988
  }
861
- let extensions;
862
989
  let root;
863
990
  let remoteEntryFile;
864
991
  let publicPath;
865
992
  let _command;
866
993
  let _originalConfigBase;
867
994
  let viteConfig;
995
+ /**
996
+ * Adds global CSS assets to all module exports
997
+ * @param filesMap - The preload map to update
998
+ * @param cssAssets - Set of CSS asset filenames to add
999
+ */
1000
+ const addCssAssetsToAllExports = (filesMap, cssAssets) => {
1001
+ Object.keys(filesMap).forEach(key => {
1002
+ cssAssets.forEach(cssAsset => {
1003
+ trackAsset(filesMap, key, cssAsset, false, 'css');
1004
+ });
1005
+ });
1006
+ };
868
1007
  return [{
869
1008
  name: 'module-federation-manifest',
870
1009
  apply: 'serve',
1010
+ /**
1011
+ * Stores resolved Vite config for later use
1012
+ */
1013
+ /**
1014
+ * Finalizes configuration after all plugins are resolved
1015
+ * @param config - Fully resolved Vite config
1016
+ */
871
1017
  configResolved(config) {
872
1018
  viteConfig = config;
873
1019
  },
1020
+ /**
1021
+ * Configures dev server middleware to handle manifest requests
1022
+ * @param server - Vite dev server instance
1023
+ */
874
1024
  configureServer(server) {
875
1025
  server.middlewares.use((req, res, next) => {
876
1026
  var _req$url;
@@ -918,119 +1068,85 @@ const Manifest = () => {
918
1068
  }, {
919
1069
  name: 'module-federation-manifest',
920
1070
  enforce: 'post',
1071
+ /**
1072
+ * Initial plugin configuration
1073
+ * @param config - Vite config object
1074
+ * @param command - Current Vite command (serve/build)
1075
+ */
921
1076
  config(config, {
922
1077
  command
923
1078
  }) {
924
1079
  if (!config.build) config.build = {};
925
- if (!config.build.manifest) config.build.manifest = config.build.manifest || !!manifestOptions;
1080
+ if (!config.build.manifest) {
1081
+ config.build.manifest = config.build.manifest || !!manifestOptions;
1082
+ }
926
1083
  _command = command;
927
1084
  _originalConfigBase = config.base;
928
1085
  },
929
1086
  configResolved(config) {
930
1087
  root = config.root;
931
- extensions = config.resolve.extensions || ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'];
932
1088
  let base = config.base;
933
1089
  if (_command === 'serve') {
934
1090
  base = (config.server.origin || '') + config.base;
935
1091
  }
936
1092
  publicPath = _originalConfigBase === '' ? 'auto' : base ? base.replace(/\/?$/, '/') : 'auto';
937
1093
  },
1094
+ /**
1095
+ * Generates the module federation manifest file
1096
+ * @param options - Rollup output options
1097
+ * @param bundle - Generated bundle assets
1098
+ */
938
1099
  async generateBundle(options, bundle) {
939
1100
  if (!mfManifestName) return;
940
- const exposesModules = Object.keys(mfOptions.exposes).map(item => mfOptions.exposes[item].import); // 获取你提供的 moduleIds
941
- const filesContainingModules = {};
942
- // 帮助函数:检查模块路径是否匹配
943
- const isModuleMatched = (relativeModulePath, preloadModule) => {
944
- // 先尝试直接匹配
945
- if (relativeModulePath === preloadModule) return true;
946
- // 如果 preloadModule 没有后缀,尝试添加可能的后缀进行匹配
947
- for (const ext of extensions) {
948
- if (relativeModulePath === `${preloadModule}${ext}`) {
949
- return true;
950
- }
951
- }
952
- return false;
953
- };
954
- // 遍历打包生成的每个文件
955
- for (const [fileName, fileData] of Object.entries(bundle)) {
1101
+ let filesMap = {};
1102
+ // First pass: Find remoteEntry file
1103
+ for (const [_, fileData] of Object.entries(bundle)) {
956
1104
  if (mfOptions.filename.replace(/[\[\]]/g, '_').replace(/\.[^/.]+$/, '') === fileData.name || fileData.name === 'remoteEntry') {
957
1105
  remoteEntryFile = fileData.fileName;
958
- }
959
- if (fileData.type === 'chunk') {
960
- // 遍历该文件的所有模块
961
- for (const modulePath of Object.keys(fileData.modules)) {
962
- // 将绝对路径转换为相对于 Vite root 的相对路径
963
- const relativeModulePath = relative(root, modulePath);
964
- // 检查模块是否在 preloadModules 列表中
965
- for (const preloadModule of exposesModules) {
966
- const formatPreloadModule = preloadModule.replace('./', '');
967
- if (isModuleMatched(relativeModulePath, formatPreloadModule)) {
968
- if (!filesContainingModules[preloadModule]) {
969
- filesContainingModules[preloadModule] = {
970
- sync: [],
971
- async: []
972
- };
973
- }
974
- console.log(Object.keys(fileData.modules));
975
- filesContainingModules[preloadModule].sync.push(fileName);
976
- filesContainingModules[preloadModule].async.push(...(fileData.dynamicImports || []));
977
- findSynchronousImports(fileName, filesContainingModules[preloadModule].sync);
978
- break; // 如果找到匹配,跳出循环
979
- }
980
- }
981
- }
1106
+ break; // We can break early since we only need to find remoteEntry once
982
1107
  }
983
1108
  }
984
- // 递归查找模块的同步导入文件
985
- function findSynchronousImports(fileName, array) {
986
- const fileData = bundle[fileName];
987
- if (fileData && fileData.type === 'chunk') {
988
- array.push(fileName); // 将当前文件加入预加载列表
989
- // 遍历该文件的同步导入文件
990
- fileData.imports.forEach(importedFile => {
991
- if (array.indexOf(importedFile) === -1) {
992
- findSynchronousImports(importedFile, array); // 递归查找同步导入的文件
993
- }
994
- });
995
- }
996
- }
997
- const fileToShareKey = {};
998
- await Promise.all(Array.from(getUsedShares()).map(async shareKey => {
999
- const file = (await this.resolve(getPreBuildLibImportId(shareKey))).id.split('?')[0];
1000
- fileToShareKey[file] = shareKey;
1001
- }));
1002
- // 遍历打包生成的每个文件
1003
- for (const [fileName, fileData] of Object.entries(bundle)) {
1004
- if (fileData.type === 'chunk') {
1005
- // 遍历该文件的所有模块
1006
- for (const modulePath of Object.keys(fileData.modules)) {
1007
- const sharedKey = fileToShareKey[modulePath];
1008
- if (sharedKey) {
1009
- if (!filesContainingModules[sharedKey]) {
1010
- filesContainingModules[sharedKey] = {
1011
- sync: [],
1012
- async: []
1013
- };
1014
- }
1015
- filesContainingModules[sharedKey].sync.push(fileName);
1016
- filesContainingModules[sharedKey].async.push(...(fileData.dynamicImports || []));
1017
- findSynchronousImports(fileName, filesContainingModules[sharedKey].sync);
1018
- break; // 如果找到匹配,跳出循环
1019
- }
1109
+ // Second pass: Collect all CSS assets
1110
+ const allCssAssets = collectCssAssets(bundle);
1111
+ const exposesModules = Object.keys(mfOptions.exposes).map(item => mfOptions.exposes[item].import);
1112
+ // Process exposed modules
1113
+ processModuleAssets(bundle, filesMap, modulePath => {
1114
+ const absoluteModulePath = path.resolve(root, modulePath);
1115
+ return exposesModules.find(exposeModule => {
1116
+ const exposePath = path.resolve(root, exposeModule);
1117
+ // First try exact path match
1118
+ if (absoluteModulePath === exposePath) {
1119
+ return true;
1020
1120
  }
1021
- }
1022
- }
1023
- Object.keys(filesContainingModules).forEach(key => {
1024
- filesContainingModules[key].sync = Array.from(new Set(filesContainingModules[key].sync));
1025
- filesContainingModules[key].async = Array.from(new Set(filesContainingModules[key].async));
1121
+ // Then try path match without known extensions
1122
+ const getPathWithoutKnownExt = filePath => {
1123
+ const ext = path.extname(filePath);
1124
+ return JS_EXTENSIONS.includes(ext) ? path.join(path.dirname(filePath), path.basename(filePath, ext)) : filePath;
1125
+ };
1126
+ const modulePathNoExt = getPathWithoutKnownExt(absoluteModulePath);
1127
+ const exposePathNoExt = getPathWithoutKnownExt(exposePath);
1128
+ return modulePathNoExt === exposePathNoExt;
1129
+ });
1026
1130
  });
1131
+ // Process shared modules
1132
+ const fileToShareKey = await buildFileToShareKeyMap(getUsedShares(), this.resolve.bind(this));
1133
+ processModuleAssets(bundle, filesMap, modulePath => fileToShareKey.get(modulePath));
1134
+ // Add all CSS assets to every export
1135
+ addCssAssetsToAllExports(filesMap, allCssAssets);
1136
+ // Final deduplication of all assets
1137
+ filesMap = deduplicateAssets(filesMap);
1027
1138
  this.emitFile({
1028
1139
  type: 'asset',
1029
1140
  fileName: mfManifestName,
1030
- source: JSON.stringify(generateMFManifest(filesContainingModules))
1141
+ source: JSON.stringify(generateMFManifest(filesMap))
1031
1142
  });
1032
1143
  }
1033
1144
  }];
1145
+ /**
1146
+ * Generates the final manifest JSON structure
1147
+ * @param preloadMap - Map of module assets to include
1148
+ * @returns Complete manifest object
1149
+ */
1034
1150
  function generateMFManifest(preloadMap) {
1035
1151
  const options = getNormalizeModuleFederationOptions();
1036
1152
  const {
@@ -1041,23 +1157,17 @@ const Manifest = () => {
1041
1157
  path: '',
1042
1158
  type: 'module'
1043
1159
  };
1044
- const remotes = [];
1045
- const usedRemotesMap = getUsedRemotesMap();
1046
- Object.keys(usedRemotesMap).forEach(remoteKey => {
1047
- const usedModules = Array.from(usedRemotesMap[remoteKey]);
1048
- usedModules.forEach(moduleKey => {
1049
- remotes.push({
1050
- federationContainerName: options.remotes[remoteKey].entry,
1051
- moduleName: moduleKey.replace(remoteKey, '').replace('/', ''),
1052
- alias: remoteKey,
1053
- entry: '*'
1054
- });
1055
- });
1056
- });
1057
- // @ts-ignore
1160
+ // Process remotes
1161
+ const remotes = Array.from(Object.entries(getUsedRemotesMap())).flatMap(([remoteKey, modules]) => Array.from(modules).map(moduleKey => ({
1162
+ federationContainerName: options.remotes[remoteKey].entry,
1163
+ moduleName: moduleKey.replace(remoteKey, '').replace('/', ''),
1164
+ alias: remoteKey,
1165
+ entry: '*'
1166
+ })));
1167
+ // Process shared dependencies
1058
1168
  const shared = Array.from(getUsedShares()).map(shareKey => {
1059
- var _preloadMap$shareKey, _preloadMap$shareKey2;
1060
1169
  const shareItem = getNormalizeShareItem(shareKey);
1170
+ const assets = preloadMap[shareKey] || createEmptyAssetMap();
1061
1171
  return {
1062
1172
  id: `${name}:${shareKey}`,
1063
1173
  name: shareKey,
@@ -1065,42 +1175,42 @@ const Manifest = () => {
1065
1175
  requiredVersion: shareItem.shareConfig.requiredVersion,
1066
1176
  assets: {
1067
1177
  js: {
1068
- async: (preloadMap == null || (_preloadMap$shareKey = preloadMap[shareKey]) == null ? void 0 : _preloadMap$shareKey.async) || [],
1069
- sync: (preloadMap == null || (_preloadMap$shareKey2 = preloadMap[shareKey]) == null ? void 0 : _preloadMap$shareKey2.sync) || []
1178
+ async: assets.js.async,
1179
+ sync: assets.js.sync
1070
1180
  },
1071
1181
  css: {
1072
- async: [],
1073
- sync: []
1182
+ async: assets.css.async,
1183
+ sync: assets.css.sync
1074
1184
  }
1075
1185
  }
1076
1186
  };
1077
- }).filter(item => item);
1078
- const exposes = Object.keys(options.exposes).map(key => {
1079
- var _preloadMap$sourceFil, _preloadMap$sourceFil2;
1080
- // assets(.css, .jpg, .svg等)其他资源, 不重要, 暂未处理
1187
+ }).filter(Boolean);
1188
+ // Process exposed modules
1189
+ const exposes = Object.entries(options.exposes).map(([key, value]) => {
1081
1190
  const formatKey = key.replace('./', '');
1082
- const sourceFile = options.exposes[key].import;
1191
+ const sourceFile = value.import;
1192
+ const assets = preloadMap[sourceFile] || createEmptyAssetMap();
1083
1193
  return {
1084
- id: name + ':' + formatKey,
1194
+ id: `${name}:${formatKey}`,
1085
1195
  name: formatKey,
1086
1196
  assets: {
1087
1197
  js: {
1088
- async: (preloadMap == null || (_preloadMap$sourceFil = preloadMap[sourceFile]) == null ? void 0 : _preloadMap$sourceFil.async) || [],
1089
- sync: (preloadMap == null || (_preloadMap$sourceFil2 = preloadMap[sourceFile]) == null ? void 0 : _preloadMap$sourceFil2.sync) || []
1198
+ async: assets.js.async,
1199
+ sync: assets.js.sync
1090
1200
  },
1091
1201
  css: {
1092
- sync: [],
1093
- async: []
1202
+ async: assets.css.async,
1203
+ sync: assets.css.sync
1094
1204
  }
1095
1205
  },
1096
1206
  path: key
1097
1207
  };
1098
- }).filter(item => item); // Filter out any null values
1099
- const result = {
1208
+ }).filter(Boolean);
1209
+ return {
1100
1210
  id: name,
1101
- name: name,
1211
+ name,
1102
1212
  metaData: _extends({
1103
- name: name,
1213
+ name,
1104
1214
  type: 'app',
1105
1215
  buildInfo: {
1106
1216
  buildVersion: '1.0.0',
@@ -1111,8 +1221,6 @@ const Manifest = () => {
1111
1221
  types: {
1112
1222
  path: '',
1113
1223
  name: ''
1114
- // "zip": "@mf-types.zip",
1115
- // "api": "@mf-types.d.ts"
1116
1224
  },
1117
1225
  globalName: name,
1118
1226
  pluginVersion: '0.2.5'
@@ -1125,7 +1233,6 @@ const Manifest = () => {
1125
1233
  remotes,
1126
1234
  exposes
1127
1235
  };
1128
- return result;
1129
1236
  }
1130
1237
  };
1131
1238