generateExposesPlugin.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import type { RouteRecordRaw } from 'vue-router';
  2. interface ExposeItem {
  3. key: string;
  4. value: string;
  5. meta?: {
  6. title?: string;
  7. description?: string;
  8. category?: string;
  9. icon?: string;
  10. tags?: string[];
  11. };
  12. }
  13. /**
  14. * 递归遍历路由,提取所有需要暴露的组件
  15. * @param routes 路由配置
  16. * @param parentPath 父路径
  17. * @param onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
  18. */
  19. function extractExposesFromRoutes(
  20. routes: RouteRecordRaw[],
  21. parentPath = '',
  22. onlyLeafNodes = true,
  23. ): ExposeItem[] {
  24. const exposes: ExposeItem[] = [];
  25. routes.forEach((route) => {
  26. // 构建当前路径(去掉开头的 / 和结尾的 /)
  27. const currentPath = route.path.replace(/^\//, '').replace(/\/$/, '');
  28. const fullPath = parentPath
  29. ? `${parentPath}/${currentPath}`.replace(/\/+/g, '/')
  30. : currentPath;
  31. // 检查是否有子路由
  32. const hasChildren = route.children && route.children.length > 0;
  33. // 判断是否应该暴露当前路由
  34. const shouldExpose =
  35. route.component &&
  36. (!onlyLeafNodes || // 如果不是只展示叶子节点,所有有组件的路由都暴露
  37. !hasChildren); // 如果只展示叶子节点,只有没有子路由的才暴露
  38. if (shouldExpose) {
  39. // 获取组件路径
  40. const componentPath = getComponentPath(
  41. route.component,
  42. route.name as string,
  43. );
  44. if (componentPath) {
  45. exposes.push({
  46. key: `./${fullPath}`,
  47. value: componentPath,
  48. meta: route.meta as any,
  49. });
  50. } else {
  51. console.warn(
  52. `⚠️ Cannot extract component path for route: ${route.name} (${fullPath})`,
  53. );
  54. }
  55. }
  56. // 递归处理子路由
  57. if (hasChildren) {
  58. const childExposes = extractExposesFromRoutes(
  59. route.children,
  60. fullPath,
  61. onlyLeafNodes,
  62. );
  63. exposes.push(...childExposes);
  64. }
  65. });
  66. return exposes;
  67. }
  68. /**
  69. * 获取组件路径
  70. */
  71. function getComponentPath(component: any, routeName?: string): string | null {
  72. // 处理异步组件 () => import(...)
  73. if (typeof component === 'function') {
  74. try {
  75. // 尝试从函数中提取 import 路径
  76. const componentStr = component.toString();
  77. // 🔍 调试:打印函数字符串(仅在调试时)
  78. if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
  79. console.log(`\n🔍 ===== Route: ${routeName} =====`);
  80. console.log(`🔍 Component function string:`, componentStr);
  81. }
  82. // 尝试多种匹配模式
  83. let importPath: string | null = null;
  84. // 模式 1: 标准 import('...') 或 import("...")
  85. let importMatch = componentStr.match(/import\s*\(\s*['"](.+?)['"]\s*\)/);
  86. if (importMatch && importMatch[1]) {
  87. importPath = importMatch[1];
  88. }
  89. // 模式 2: jitiImport('...') 或其他打包工具转换的形式
  90. if (!importPath) {
  91. importMatch = componentStr.match(
  92. /jitiImport\s*\(\s*['"](.+?)['"]\s*\)/,
  93. );
  94. if (importMatch && importMatch[1]) {
  95. importPath = importMatch[1];
  96. }
  97. }
  98. // 模式 3: __webpack_require__ 或其他类似的模式
  99. if (!importPath) {
  100. importMatch = componentStr.match(
  101. /\bwebpackImport\s*\(\s*['"](.+?)['"]\s*\)/,
  102. );
  103. if (importMatch && importMatch[1]) {
  104. importPath = importMatch[1];
  105. }
  106. }
  107. if (importPath) {
  108. // 处理相对路径,确保以 ./src 开头
  109. if (importPath.startsWith('../')) {
  110. const result = `./src/${importPath.replace(/\.\.\//g, '')}`;
  111. if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
  112. console.log(`✅ Extracted path: ${importPath} -> ${result}`);
  113. }
  114. return result;
  115. }
  116. if (importPath.startsWith('./')) {
  117. const result = `./src/${importPath.replace('./', '')}`;
  118. if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
  119. console.log(`✅ Extracted path: ${importPath} -> ${result}`);
  120. }
  121. return result;
  122. }
  123. if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
  124. console.log(`✅ Extracted path: ${importPath}`);
  125. }
  126. return importPath;
  127. } else {
  128. if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
  129. console.log(`❌ No import path match found`);
  130. }
  131. }
  132. } catch (error) {
  133. console.error(
  134. `Error processing component for route ${routeName}:`,
  135. error,
  136. );
  137. }
  138. }
  139. return null;
  140. }
  141. /**
  142. * 生成 Module Federation exposes 配置
  143. * @param routes 路由配置
  144. * @param options 配置选项
  145. * @param options.onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
  146. */
  147. export function generateExposesFromRoutes(
  148. routes: RouteRecordRaw[],
  149. options: { onlyLeafNodes?: boolean } = {},
  150. ) {
  151. const { onlyLeafNodes = true } = options;
  152. const exposeItems = extractExposesFromRoutes(routes, '', onlyLeafNodes);
  153. // 转换为 exposes 对象
  154. const exposes: Record<string, string> = {};
  155. exposeItems.forEach((item) => {
  156. exposes[item.key] = item.value;
  157. });
  158. return {
  159. exposes,
  160. exposeItems, // 返回详细信息,可用于生成文档或其他用途
  161. };
  162. }
  163. /**
  164. * Rsbuild 插件:自动从路由生成 exposes 并注入到 Module Federation 配置
  165. * @param routes 路由配置
  166. * @param options 配置选项
  167. * @param options.onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
  168. */
  169. export function generateExposesPlugin(
  170. routes: RouteRecordRaw[],
  171. options: { onlyLeafNodes?: boolean } = {},
  172. ) {
  173. return {
  174. name: 'generate-exposes-plugin',
  175. apply: 'serve',
  176. setup(build: any) {
  177. // 生成 exposes 配置
  178. const { exposes, exposeItems } = generateExposesFromRoutes(
  179. routes,
  180. options,
  181. );
  182. // 打印生成的配置(开发时可见)
  183. console.log('📦 Auto-generated Module Federation exposes:');
  184. console.table(
  185. exposeItems.map((item) => ({
  186. 'Expose Key': item.key,
  187. 'Component Path': item.value,
  188. Title: item.meta?.title || '-',
  189. })),
  190. );
  191. // 初始化钩子:修改 Module Federation 配置
  192. build.onBeforeBuild?.(() => {
  193. // 这里可以添加构建前的逻辑
  194. });
  195. // 返回 exposes 配置供外部使用
  196. return exposes;
  197. },
  198. };
  199. }