| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import { type RouteRecordRaw } from 'vue-router'
- import fs from 'node:fs'
- import path from 'node:path'
- interface ExposeItem {
- key: string
- value: string
- meta?: {
- title?: string
- description?: string
- category?: string
- icon?: string
- tags?: string[]
- }
- }
- // wrapper 文件生成目录(相对于项目根目录)
- const GENERATED_DIR = './src/.generated'
- // remote-setup.ts 的路径(相对于 wrapper 文件)
- const REMOTE_SETUP_RELATIVE = '../remote-setup'
- /**
- * 将 expose key(如 ./home/dashboard)转为合法的文件名(如 home_dashboard)
- */
- function exposeKeyToFileName(key: string): string {
- return key
- .replace(/^\.\//, '') // 去掉 ./
- .replace(/\//g, '_') // / → _
- .replace(/-/g, '_') // - → _
- }
- /**
- * 生成 wrapper 文件并返回 wrapper 的路径
- * wrapper 文件内容:import remote-setup + re-export Vue 组件
- */
- function generateWrapperFile(componentPath: string, fileName: string): string {
- // 确保生成目录存在
- const generatedDir = path.resolve(process.cwd(), GENERATED_DIR)
- if (!fs.existsSync(generatedDir)) {
- fs.mkdirSync(generatedDir, { recursive: true })
- }
- // 计算从 wrapper 到 Vue 组件的相对路径
- // wrapper 在 src/.generated/xxx.ts,组件在 src/views/XxxView.vue
- // 所以相对路径是 ../views/XxxView.vue
- const relativeComponentPath = componentPath
- .replace(/^\.\//, '../') // ./src/views/Xxx.vue → ../src/views/Xxx.vue
- .replace(/src\//, '') // ../src/views/Xxx.vue → ../views/Xxx.vue
- // wrapper 文件内容
- const content = [
- '// ⚠️ 自动生成文件,请勿手动修改',
- `import { ensureRemoteSetup } from '${REMOTE_SETUP_RELATIVE}'`,
- 'ensureRemoteSetup()',
- `export { default } from '${relativeComponentPath}'`,
- '',
- ].join('\n')
- const wrapperPath = path.join(generatedDir, `${fileName}.ts`)
- fs.writeFileSync(wrapperPath, content, 'utf-8')
- // 返回相对于项目根目录的路径,供 exposes 配置使用
- return `${GENERATED_DIR}/${fileName}.ts`
- }
- /**
- * 递归遍历路由,提取所有需要暴露的组件
- * @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 对象
- // ⭐ 关键改动:每个 expose 指向自动生成的 wrapper 文件,而不是直接指向 Vue 组件
- const exposes: Record<string, string> = {}
- exposeItems.forEach((item) => {
- const fileName = exposeKeyToFileName(item.key)
- const wrapperPath = generateWrapperFile(item.value, fileName)
- exposes[item.key] = wrapperPath
- })
- 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
- },
- }
- }
|