| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import type { RouteRecordRaw } from 'vue-router';
- interface ExposeItem {
- key: string;
- value: string;
- meta?: {
- title?: string;
- description?: string;
- category?: string;
- icon?: string;
- tags?: string[];
- };
- }
- /**
- * 递归遍历路由,提取所有需要暴露的组件
- * @param routes 路由配置
- * @param parentPath 父路径
- * @param onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
- */
- function extractExposesFromRoutes(
- routes: RouteRecordRaw[],
- parentPath = '',
- onlyLeafNodes = true,
- ): ExposeItem[] {
- const exposes: ExposeItem[] = [];
- routes.forEach((route) => {
- // 构建当前路径(去掉开头的 / 和结尾的 /)
- const currentPath = route.path.replace(/^\//, '').replace(/\/$/, '');
- const fullPath = parentPath
- ? `${parentPath}/${currentPath}`.replace(/\/+/g, '/')
- : currentPath;
- // 检查是否有子路由
- const hasChildren = route.children && route.children.length > 0;
- // 判断是否应该暴露当前路由
- const shouldExpose =
- route.component &&
- (!onlyLeafNodes || // 如果不是只展示叶子节点,所有有组件的路由都暴露
- !hasChildren); // 如果只展示叶子节点,只有没有子路由的才暴露
- if (shouldExpose) {
- // 获取组件路径
- const componentPath = getComponentPath(
- route.component,
- route.name as string,
- );
- if (componentPath) {
- exposes.push({
- key: `./${fullPath}`,
- value: componentPath,
- meta: route.meta as any,
- });
- } else {
- console.warn(
- `⚠️ Cannot extract component path for route: ${route.name} (${fullPath})`,
- );
- }
- }
- // 递归处理子路由
- if (hasChildren) {
- const childExposes = extractExposesFromRoutes(
- route.children,
- fullPath,
- onlyLeafNodes,
- );
- exposes.push(...childExposes);
- }
- });
- return exposes;
- }
- /**
- * 获取组件路径
- */
- function getComponentPath(component: any, routeName?: string): string | null {
- // 处理异步组件 () => import(...)
- if (typeof component === 'function') {
- try {
- // 尝试从函数中提取 import 路径
- const componentStr = component.toString();
- // 🔍 调试:打印函数字符串(仅在调试时)
- if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
- console.log(`\n🔍 ===== Route: ${routeName} =====`);
- console.log(`🔍 Component function string:`, componentStr);
- }
- // 尝试多种匹配模式
- let importPath: string | null = null;
- // 模式 1: 标准 import('...') 或 import("...")
- let importMatch = componentStr.match(/import\s*\(\s*['"](.+?)['"]\s*\)/);
- if (importMatch && importMatch[1]) {
- importPath = importMatch[1];
- }
- // 模式 2: jitiImport('...') 或其他打包工具转换的形式
- if (!importPath) {
- importMatch = componentStr.match(
- /jitiImport\s*\(\s*['"](.+?)['"]\s*\)/,
- );
- if (importMatch && importMatch[1]) {
- importPath = importMatch[1];
- }
- }
- // 模式 3: __webpack_require__ 或其他类似的模式
- if (!importPath) {
- importMatch = componentStr.match(
- /\bwebpackImport\s*\(\s*['"](.+?)['"]\s*\)/,
- );
- if (importMatch && importMatch[1]) {
- importPath = importMatch[1];
- }
- }
- if (importPath) {
- // 处理相对路径,确保以 ./src 开头
- if (importPath.startsWith('../')) {
- const result = `./src/${importPath.replace(/\.\.\//g, '')}`;
- if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
- console.log(`✅ Extracted path: ${importPath} -> ${result}`);
- }
- return result;
- }
- if (importPath.startsWith('./')) {
- const result = `./src/${importPath.replace('./', '')}`;
- if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
- console.log(`✅ Extracted path: ${importPath} -> ${result}`);
- }
- return result;
- }
- if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
- console.log(`✅ Extracted path: ${importPath}`);
- }
- return importPath;
- } else {
- if (process.env.DEBUG_EXPOSES === 'true' && routeName) {
- console.log(`❌ No import path match found`);
- }
- }
- } catch (error) {
- console.error(
- `Error processing component for route ${routeName}:`,
- error,
- );
- }
- }
- return null;
- }
- /**
- * 生成 Module Federation exposes 配置
- * @param routes 路由配置
- * @param options 配置选项
- * @param options.onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
- */
- export function generateExposesFromRoutes(
- routes: RouteRecordRaw[],
- options: { onlyLeafNodes?: boolean } = {},
- ) {
- const { onlyLeafNodes = true } = options;
- const exposeItems = extractExposesFromRoutes(routes, '', onlyLeafNodes);
- // 转换为 exposes 对象
- const exposes: Record<string, string> = {};
- exposeItems.forEach((item) => {
- exposes[item.key] = item.value;
- });
- return {
- exposes,
- exposeItems, // 返回详细信息,可用于生成文档或其他用途
- };
- }
- /**
- * Rsbuild 插件:自动从路由生成 exposes 并注入到 Module Federation 配置
- * @param routes 路由配置
- * @param options 配置选项
- * @param options.onlyLeafNodes 是否只展示叶子节点(默认 true,只展示最底层的子路由)
- */
- export function generateExposesPlugin(
- routes: RouteRecordRaw[],
- options: { onlyLeafNodes?: boolean } = {},
- ) {
- return {
- name: 'generate-exposes-plugin',
- apply: 'serve',
- setup(build: any) {
- // 生成 exposes 配置
- const { exposes, exposeItems } = generateExposesFromRoutes(
- routes,
- options,
- );
- // 打印生成的配置(开发时可见)
- console.log('📦 Auto-generated Module Federation exposes:');
- console.table(
- exposeItems.map((item) => ({
- 'Expose Key': item.key,
- 'Component Path': item.value,
- Title: item.meta?.title || '-',
- })),
- );
- // 初始化钩子:修改 Module Federation 配置
- build.onBeforeBuild?.(() => {
- // 这里可以添加构建前的逻辑
- });
- // 返回 exposes 配置供外部使用
- return exposes;
- },
- };
- }
|