UNPKG

@fchc8/vite-plugin-multi-page

Version:

A powerful Vite plugin for building multi-page applications with smart file routing and multi-strategy builds

1 lines 88 kB
{"version":3,"sources":["../src/index.ts","../src/dev-server.ts","../src/file-filter.ts","../src/utils.ts","../src/page-config.ts","../src/build-config.ts","../src/config-loader.ts","../src/defaults.ts","../src/types.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { Plugin } from 'vite';\nimport { mergeConfig } from 'vite';\nimport { setupDevMiddleware } from './dev-server';\nimport { generateBuildConfig } from './build-config';\nimport { loadUserConfig, hasCustomConfig } from './config-loader';\nimport { mergeWithDefaults } from './defaults';\nimport type { Options, ConfigTransformFunction } from './types';\nimport * as glob from 'glob';\n\n// 导出类型和工具函数\nexport { defineConfig, defineConfigTransform } from './types';\nexport type {\n ConfigFunction,\n ConfigTransformFunction,\n PluginContext,\n PageContext,\n PageConfig,\n} from './types';\n\n/**\n * 重组构建产物,实现不同的merge模式\n */\nfunction reorganizeAssets(\n distDir: string,\n mode: 'strategy' | 'page',\n options: Options,\n log: (...args: any[]) => void\n) {\n const assetsDir = path.resolve(distDir, 'assets');\n\n if (!fs.existsSync(assetsDir)) {\n log('assets目录不存在,跳过重组');\n return;\n }\n\n // 分析所有HTML文件和它们的资源依赖\n const htmlFiles = fs\n .readdirSync(distDir)\n .filter(file => file.endsWith('.html'))\n .map(file => path.resolve(distDir, file));\n\n if (htmlFiles.length === 0) {\n log('未找到HTML文件,但仍需清理assets目录');\n\n // 即使没有HTML文件,在strategy/page模式下也要清理空的assets目录\n if ((mode === 'strategy' || mode === 'page') && fs.existsSync(assetsDir)) {\n try {\n fs.rmSync(assetsDir, { recursive: true, force: true });\n log('强制清理整个根目录assets目录 (strategy/page模式)');\n } catch (error) {\n log('清理根目录assets失败:', error);\n }\n }\n\n return;\n }\n\n const bundleInfo = new Map<string, { assets: string[]; targetDir: string; strategy?: string }>();\n const allAssetUsage = new Map<string, string[]>(); // 记录每个资源被哪些页面使用\n\n // 第一阶段:分析每个页面的资源依赖\n htmlFiles.forEach(htmlFile => {\n let fileName = path.basename(htmlFile, '.html');\n\n // 如果是临时文件名,提取真实的页面名\n if (fileName.startsWith('.temp.mp.')) {\n fileName = fileName.replace('.temp.mp.', '');\n }\n\n const htmlContent = fs.readFileSync(htmlFile, 'utf-8');\n const assets: string[] = [];\n\n // 匹配 src 和 href 属性中的 /assets/ 路径\n const assetRegex = /(?:src|href)=\"\\/assets\\/([^\"]+)\"/g;\n let match;\n\n while ((match = assetRegex.exec(htmlContent)) !== null) {\n const assetFile = match[1];\n assets.push(assetFile);\n\n // 记录这个资源被哪些页面使用\n if (!allAssetUsage.has(assetFile)) {\n allAssetUsage.set(assetFile, []);\n }\n allAssetUsage.get(assetFile)?.push(fileName);\n }\n\n // 确定目标目录\n let targetSubDir = '';\n if (mode === 'page') {\n targetSubDir = fileName;\n }\n\n const targetDir = path.resolve(distDir, targetSubDir);\n bundleInfo.set(fileName, { assets, targetDir });\n log(`页面 ${fileName} 依赖资源:`, assets);\n });\n\n // 添加public目录资源处理\n log('第一阶段补充:分析public目录资源');\n const publicAssets = new Set<string>();\n const publicDir = path.resolve(process.cwd(), 'public');\n\n if (fs.existsSync(publicDir)) {\n const publicFiles = glob.sync('**/*', { cwd: publicDir, nodir: true });\n for (const file of publicFiles) {\n publicAssets.add(file);\n log(`发现public资源: ${file}`);\n }\n }\n\n // 第二阶段:识别共享资源\n allAssetUsage.forEach((users, assetFile) => {\n if (users.length > 1) {\n log(`共享资源 ${assetFile} 被页面使用:`, users);\n } else {\n log(`独占资源 ${assetFile} 仅被页面 ${users[0]} 使用`);\n }\n });\n\n // 第三阶段:复制每个页面需要的所有资源文件(包括共享资源)\n bundleInfo.forEach(({ assets, targetDir }, pageName) => {\n // 创建目标目录和assets子目录\n const pageAssetsDir = path.resolve(targetDir, 'assets');\n if (!fs.existsSync(pageAssetsDir)) {\n fs.mkdirSync(pageAssetsDir, { recursive: true });\n }\n\n // 复制该页面需要的所有资源文件(包括共享资源)\n assets.forEach(assetFile => {\n const sourcePath = path.resolve(assetsDir, assetFile);\n const targetPath = path.resolve(pageAssetsDir, assetFile);\n\n if (fs.existsSync(sourcePath)) {\n fs.copyFileSync(sourcePath, targetPath);\n const users = allAssetUsage.get(assetFile) || [];\n const resourceType = users.length > 1 ? '共享资源' : '独占资源';\n log(\n `复制${resourceType}到 ${pageName}: assets/${assetFile} -> ${path.relative(distDir, targetPath)}`\n );\n } else {\n log(`警告: 资源文件不存在: ${sourcePath}`);\n }\n });\n\n // 第三阶段补充:复制所有剩余的资源文件\n if (fs.existsSync(assetsDir)) {\n const allAssetFiles = fs.readdirSync(assetsDir);\n\n // 复制所有不在HTML中直接引用的资源文件(如通过JS动态引入的资源)\n allAssetFiles.forEach(assetFile => {\n if (!assets.includes(assetFile)) {\n const sourcePath = path.resolve(assetsDir, assetFile);\n const targetPath = path.resolve(pageAssetsDir, assetFile);\n\n if (fs.existsSync(sourcePath)) {\n fs.copyFileSync(sourcePath, targetPath);\n log(\n `复制其他资源文件到 ${pageName}: assets/${assetFile} -> ${path.relative(distDir, targetPath)}`\n );\n }\n }\n });\n }\n });\n\n log('第三阶段补充:处理public目录资源');\n // 在每个策略/页面目录中创建public资源的副本\n const uniqueTargetDirs = new Set<string>();\n bundleInfo.forEach(({ targetDir }) => {\n uniqueTargetDirs.add(targetDir);\n });\n\n for (const targetDir of uniqueTargetDirs) {\n for (const publicAsset of publicAssets) {\n const sourceFile = path.resolve(distDir, publicAsset);\n const targetFile = path.resolve(targetDir, publicAsset);\n\n if (fs.existsSync(sourceFile)) {\n const targetAssetDir = path.dirname(targetFile);\n if (!fs.existsSync(targetAssetDir)) {\n fs.mkdirSync(targetAssetDir, { recursive: true });\n }\n\n fs.copyFileSync(sourceFile, targetFile);\n log(`复制public资源: ${publicAsset} -> ${path.relative(distDir, targetFile)}`);\n }\n }\n }\n\n // 第四阶段:移动HTML文件并更新资源引用\n bundleInfo.forEach(({ targetDir }, pageName) => {\n // 查找实际的HTML文件(可能是临时文件名或正常文件名)\n let originalHtmlPath = path.resolve(distDir, `${pageName}.html`);\n let actualPageName = pageName;\n\n // 如果正常文件名不存在,尝试临时文件名\n if (!fs.existsSync(originalHtmlPath)) {\n originalHtmlPath = path.resolve(distDir, `.temp.mp.${pageName}.html`);\n // 从临时文件名中提取页面名\n if (fs.existsSync(originalHtmlPath)) {\n actualPageName = pageName; // 保持原页面名\n }\n }\n\n if (fs.existsSync(originalHtmlPath)) {\n let htmlContent = fs.readFileSync(originalHtmlPath, 'utf-8');\n\n // 更新资源引用路径:所有资源都使用本地路径\n htmlContent = htmlContent.replace(/(?:src|href)=\"\\/assets\\//g, match => {\n return match.replace('/assets/', './assets/');\n });\n\n // 确定最终的HTML文件路径(使用正常的文件名)\n let finalHtmlPath: string;\n if (mode === 'strategy') {\n finalHtmlPath = path.resolve(targetDir, `${actualPageName}.html`);\n } else if (mode === 'page') {\n finalHtmlPath = path.resolve(targetDir, 'index.html');\n } else {\n finalHtmlPath = originalHtmlPath;\n }\n\n // 写入更新后的HTML文件\n fs.writeFileSync(finalHtmlPath, htmlContent);\n\n // 删除原始文件(如果位置发生了变化)\n if (finalHtmlPath !== originalHtmlPath) {\n fs.unlinkSync(originalHtmlPath);\n }\n\n const relativePath = path.relative(distDir, finalHtmlPath);\n log(`按${mode}分组移动HTML文件: ${actualPageName}.html -> ${relativePath}`);\n } else {\n log(`警告: 未找到HTML文件: ${pageName}.html 或 .temp.mp.${pageName}.html`);\n }\n });\n\n // 第五阶段:清理原始assets目录\n if (fs.existsSync(assetsDir)) {\n // 在strategy或page模式下,强制清理整个根目录assets(因为资源已经复制到各个页面目录)\n if (mode === 'strategy' || mode === 'page') {\n try {\n fs.rmSync(assetsDir, { recursive: true, force: true });\n log('强制清理整个根目录assets目录 (strategy/page模式)');\n } catch (error) {\n log('清理根目录assets失败:', error);\n }\n } else {\n // 默认模式:只删除已处理的资源文件\n allAssetUsage.forEach((users, assetFile) => {\n const originalPath = path.resolve(assetsDir, assetFile);\n if (fs.existsSync(originalPath)) {\n fs.unlinkSync(originalPath);\n log(`清理原始资源文件: assets/${assetFile}`);\n }\n });\n\n // 如果assets目录为空则删除\n const finalRemainingFiles = fs.readdirSync(assetsDir);\n if (finalRemainingFiles.length === 0) {\n fs.rmdirSync(assetsDir);\n log('清理空的assets目录');\n } else {\n log('assets目录中还有未处理的文件:', finalRemainingFiles);\n }\n }\n }\n\n // 第五阶段补充:清理根目录的public资源(在strategy/page模式下)\n if ((mode === 'strategy' || mode === 'page') && publicAssets.size > 0) {\n log('第五阶段补充:清理根目录的public资源');\n publicAssets.forEach(publicAsset => {\n const rootPublicFile = path.resolve(distDir, publicAsset);\n if (fs.existsSync(rootPublicFile)) {\n try {\n fs.unlinkSync(rootPublicFile);\n log(`删除根目录public资源: ${publicAsset}`);\n } catch (error) {\n log(`删除根目录public资源失败: ${publicAsset}`, error);\n }\n }\n });\n\n // 尝试删除空的public相关目录结构\n publicAssets.forEach(publicAsset => {\n const rootPublicFile = path.resolve(distDir, publicAsset);\n let parentDir = path.dirname(rootPublicFile);\n\n // 逐级向上检查并删除空目录,直到dist根目录\n while (parentDir !== distDir && parentDir !== path.dirname(parentDir)) {\n try {\n if (fs.existsSync(parentDir) && fs.readdirSync(parentDir).length === 0) {\n fs.rmdirSync(parentDir);\n log(`删除空目录: ${path.relative(distDir, parentDir)}`);\n parentDir = path.dirname(parentDir);\n } else {\n break; // 目录不空或不存在,停止向上检查\n }\n } catch (error) {\n // 忽略删除目录失败的错误,可能是权限问题或目录不空\n break;\n }\n }\n });\n }\n}\n\nexport function viteMultiPage(transform?: ConfigTransformFunction): Plugin {\n let resolvedOptions: Options;\n const tempFiles: string[] = [];\n let log: (...args: any[]) => void = () => {}; // 默认为空函数\n\n return {\n name: 'vite-multi-page',\n\n async configResolved(config) {\n // 加载用户配置文件(如果存在)\n let userConfig: Options | null = null;\n\n if (hasCustomConfig()) {\n userConfig = await loadUserConfig({\n mode: config.command === 'serve' ? 'development' : 'production',\n command: config.command,\n isCLI: false,\n });\n }\n\n // 合并用户配置和默认配置\n const mergedConfig = mergeWithDefaults(userConfig);\n\n // 应用配置变换函数(如果提供)\n resolvedOptions = transform\n ? transform(mergedConfig, {\n mode: config.command === 'serve' ? 'development' : 'production',\n command: config.command,\n isCLI: false,\n })\n : mergedConfig;\n\n // 设置debug日志\n const debug = resolvedOptions.debug ?? false;\n log = debug ? console.log.bind(console, '[vite-multi-page]') : () => {};\n\n log('Vite配置已解析, 使用配置:', {\n strategies: Object.keys(resolvedOptions.strategies || {}),\n entry: resolvedOptions.entry,\n });\n },\n\n async config(config, { command }) {\n // 处理开发模式下的策略参数\n if (command === 'serve') {\n // 检查命令行参数中的策略设置\n const args = process.argv;\n\n // 查找 --strategy=value 格式的参数\n const strategyArg = args.find(arg => arg.startsWith('--strategy='));\n if (strategyArg) {\n const strategy = strategyArg.split('=')[1];\n if (strategy) {\n process.env.VITE_MULTI_PAGE_STRATEGY = strategy;\n }\n }\n // 查找 --strategy value 格式的参数\n else {\n const strategyIndex = args.findIndex(arg => arg === '--strategy');\n if (strategyIndex !== -1 && strategyIndex + 1 < args.length) {\n const strategy = args[strategyIndex + 1];\n process.env.VITE_MULTI_PAGE_STRATEGY = strategy;\n }\n }\n\n // 确保有默认策略\n if (!process.env.VITE_MULTI_PAGE_STRATEGY) {\n process.env.VITE_MULTI_PAGE_STRATEGY = 'default';\n }\n }\n if (command === 'build') {\n // 在config钩子中临时加载配置,因为configResolved还没运行\n if (!resolvedOptions) {\n // 加载用户配置文件(如果存在)\n let userConfig: Options | null = null;\n\n if (hasCustomConfig()) {\n userConfig = await loadUserConfig({\n mode: 'production',\n command: 'build',\n isCLI: false,\n });\n }\n\n // 合并用户配置和默认配置\n const mergedConfig = mergeWithDefaults(userConfig);\n\n // 应用配置变换函数(如果提供)\n resolvedOptions = transform\n ? transform(mergedConfig, {\n mode: 'production',\n command: 'build',\n isCLI: false,\n })\n : mergedConfig;\n const debug = resolvedOptions.debug ?? false;\n log = debug ? console.log.bind(console, '[vite-multi-page]') : () => {};\n }\n\n log('配置构建模式');\n\n // 检查是否是单页面构建模式\n const buildSinglePage = process.env.VITE_MULTI_PAGE_BUILD_SINGLE_PAGE;\n\n if (buildSinglePage) {\n // 单页面构建模式:只构建指定的页面\n log(`单页面构建模式: ${buildSinglePage}`);\n\n // 生成只包含指定页面的构建配置\n const buildConfigs = generateBuildConfig({\n entry: resolvedOptions.entry || 'src/pages/**/*.{ts,js}',\n exclude: resolvedOptions.exclude || [],\n template: resolvedOptions.template || 'index.html',\n placeholder: resolvedOptions.placeholder || '{{ENTRY_FILE}}',\n merge: resolvedOptions.merge || 'all',\n strategies: resolvedOptions.strategies || {},\n pageConfigs: resolvedOptions.pageConfigs || {},\n forceBuildStrategy: undefined, // 不使用策略过滤\n forceBuildPage: buildSinglePage, // 使用页面过滤\n });\n\n // 应用单页面构建配置\n const configs = Object.values(buildConfigs);\n if (configs.length > 0) {\n const singlePageConfig = configs[0];\n const mergedConfig = mergeConfig(config, singlePageConfig);\n Object.assign(config, mergedConfig);\n log(`已应用单页面构建配置: ${buildSinglePage}`);\n return;\n } else {\n throw new Error(`未找到页面: ${buildSinglePage}`);\n }\n }\n\n // 策略构建模式:生成构建配置\n const forceBuildStrategy = process.env.VITE_MULTI_PAGE_STRATEGY;\n const buildConfigs = generateBuildConfig({\n entry: resolvedOptions.entry || 'src/pages/**/*.{ts,js}',\n exclude: resolvedOptions.exclude || [],\n template: resolvedOptions.template || 'index.html',\n placeholder: resolvedOptions.placeholder || '{{ENTRY_FILE}}',\n merge: resolvedOptions.merge || 'all',\n strategies: resolvedOptions.strategies || {},\n pageConfigs: resolvedOptions.pageConfigs || {},\n forceBuildStrategy,\n });\n\n // 应用构建配置中的策略(如果有forceBuildStrategy,buildConfigs只会包含该策略)\n const targetStrategy = Object.keys(buildConfigs)[0];\n\n if (targetStrategy && buildConfigs[targetStrategy]) {\n log(`应用构建策略: ${targetStrategy}`);\n const strategyConfig = buildConfigs[targetStrategy];\n\n // 使用Vite的mergeConfig进行智能深度合并\n const mergedConfig = mergeConfig(config, strategyConfig);\n\n // 将合并结果复制回config对象\n Object.assign(config, mergedConfig);\n\n log(`已应用策略 \"${targetStrategy}\" 的配置:`, {\n build: !!strategyConfig.build,\n define: !!strategyConfig.define,\n plugins: strategyConfig.plugins?.length || 0,\n });\n } else {\n log('未找到可用的构建策略,使用默认配置');\n\n throw new Error(\n '❌ 构建失败: 未找到任何构建策略\\n\\n' +\n '可能的原因:\\n' +\n ' 1. 配置文件返回空对象 {}\\n' +\n ' 2. 未找到匹配的入口文件\\n' +\n ' 3. 模板文件不存在\\n' +\n ' 4. 未配置 strategies 对象\\n\\n' +\n '最小配置示例:\\n' +\n 'export default () => ({\\n' +\n ' entry: \"src/pages/**/*.{ts,js}\",\\n' +\n ' template: \"index.html\",\\n' +\n ' strategies: {\\n' +\n ' default: {}\\n' +\n ' }\\n' +\n '});'\n );\n }\n }\n },\n\n configureServer(server) {\n if (server.config.command === 'serve') {\n log('配置开发服务器');\n\n // 处理开发模式下的策略参数\n // 从环境变量中获取策略,默认为 default\n const devStrategy = process.env.VITE_MULTI_PAGE_STRATEGY || 'default';\n\n log(`开发模式策略: ${devStrategy}`);\n\n setupDevMiddleware(\n server,\n {\n entry: resolvedOptions.entry || 'src/pages/**/*.{ts,js}',\n exclude: resolvedOptions.exclude || [],\n template: resolvedOptions.template || 'index.html',\n placeholder: resolvedOptions.placeholder || '{{ENTRY_FILE}}',\n strategies: resolvedOptions.strategies || {},\n pageConfigs: resolvedOptions.pageConfigs || {},\n devStrategy: devStrategy, // 传递策略给开发服务器\n },\n log\n );\n }\n },\n\n writeBundle(options: any) {\n // 只在构建模式下处理merge功能\n if (!resolvedOptions?.merge || resolvedOptions.merge === 'all') {\n // 默认模式:所有文件保持在根目录\n return;\n }\n\n const distDir = options.dir || 'dist';\n const merge = resolvedOptions.merge;\n\n log(`应用构建产物合并模式: ${merge}`);\n\n try {\n // 执行资源重组\n reorganizeAssets(distDir, merge, resolvedOptions, log);\n } catch (error) {\n log('资源重组失败:', error);\n throw error;\n }\n },\n\n buildEnd() {\n // 清理临时文件\n if (tempFiles.length > 0) {\n log(`清理 ${tempFiles.length} 个临时文件`);\n tempFiles.forEach(file => {\n try {\n if (fs.existsSync(file)) {\n fs.unlinkSync(file);\n log(`删除临时文件: ${file}`);\n }\n } catch (error) {\n log(`删除临时文件失败: ${file}`, error);\n }\n });\n tempFiles.length = 0;\n }\n },\n };\n}\n\n/**\n * CLI工具专用的资源重组函数\n */\nexport function reorganizeAssetsInCLI(\n distDir: string,\n mode: 'strategy' | 'page',\n options: Options,\n log: (...args: any[]) => void\n): void {\n return reorganizeAssets(distDir, mode, options, log);\n}\n\nexport default viteMultiPage;\nexport type { Options } from './types';\nexport {\n generateBuildConfig,\n getAvailableStrategies,\n getViteOutputDirectory,\n cleanViteOutputDirectory,\n} from './build-config';\nexport { mergeWithDefaults } from './defaults';\n","import type { ViteDevServer } from 'vite';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { glob } from 'glob';\nimport { filterEntryFiles } from './file-filter';\nimport { escapeRegExp } from './utils';\nimport { DevServerOptions, PageConfigContext } from './types';\nimport { getPageConfig } from './page-config';\n\nexport function configureDevServer(\n server: ViteDevServer,\n options: DevServerOptions,\n log: (...args: any[]) => void\n) {\n try {\n const allFiles = glob.sync(options.entry, { cwd: process.cwd() });\n let entryFiles = filterEntryFiles(allFiles, options.entry, options.exclude, log);\n\n if (entryFiles.length === 0) {\n log('警告: 未找到匹配的入口文件');\n return;\n }\n\n // 获取指定的策略,优先使用开发模式传入的策略\n const cliStrategy =\n options.devStrategy ||\n (((server.config as any).__cliStrategy || (server.config as any).strategy) as\n | string\n | undefined);\n\n // 如果指定了策略,则只显示该策略下的页面或没有指定策略的默认页面\n if (cliStrategy) {\n log(`开发服务器使用指定的策略: ${cliStrategy}`);\n\n // 过滤入口文件,只保留匹配策略的页面\n entryFiles = entryFiles.filter(file => {\n // 动态获取页面策略\n const pageContext = {\n pageName: file.name,\n filePath: file.file,\n relativePath: path.relative(process.cwd(), file.file),\n strategy: undefined,\n isMatched: false,\n } as PageConfigContext;\n\n const pageConfig = getPageConfig(options.pageConfigs, pageContext, log);\n const pageStrategy = pageConfig?.strategy || 'default';\n\n // 在指定策略为default时,包含所有没有指定策略的页面\n if (cliStrategy === 'default') {\n return pageStrategy === 'default';\n }\n\n // 其他策略,只包含匹配的页面\n return pageStrategy === cliStrategy;\n });\n\n log(`策略 \"${cliStrategy}\" 下可用的页面: ${entryFiles.map(f => f.name).join(', ') || '无'}`);\n }\n\n log('开发服务器应用的入口文件:', entryFiles);\n\n // 修改中间件来处理HTML请求\n server.middlewares.use(async (req, res, next) => {\n try {\n const url = req.url || '';\n const pathWithoutQuery = url.split('?')[0];\n\n // 处理根路径请求 - 显示所有页面的索引\n if (pathWithoutQuery === '/') {\n const indexHtml = generateIndexHtml(entryFiles, options, log);\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html');\n res.end(indexHtml);\n return;\n }\n\n // 跳过明显的静态资源请求\n if (\n pathWithoutQuery.match(/\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map)$/) &&\n !pathWithoutQuery.endsWith('.html')\n ) {\n return next();\n }\n\n // 跳过以@开头的特殊路径(如Vite的特殊路径)\n if (pathWithoutQuery.startsWith('/@')) {\n return next();\n }\n\n // 跳过 __vite_ping 和其他 Vite 内部路径\n if (pathWithoutQuery.includes('__vite') || pathWithoutQuery.startsWith('/node_modules')) {\n return next();\n }\n\n // 提取页面名称,支持 history 路由\n let pageName = '';\n\n // 1. 处理带 .html 后缀的请求\n if (pathWithoutQuery.endsWith('.html')) {\n pageName = path.basename(pathWithoutQuery, '.html');\n }\n // 2. 处理精确匹配的页面路径,如 /mobile\n else if (pathWithoutQuery.startsWith('/')) {\n const cleanPath = pathWithoutQuery.substring(1); // 移除开头的斜杠\n\n // 首先尝试精确匹配\n if (entryFiles.find(file => file.name === cleanPath)) {\n pageName = cleanPath;\n }\n // 然后尝试 history 路由匹配,如 /home/login -> home\n else {\n const segments = cleanPath.split('/');\n if (segments.length > 1) {\n const possiblePageName = segments[0];\n if (entryFiles.find(file => file.name === possiblePageName)) {\n pageName = possiblePageName;\n log(`History 路由匹配: ${pathWithoutQuery} -> ${possiblePageName}`);\n }\n }\n }\n }\n\n if (!pageName) {\n return next();\n }\n\n const matchedFile = entryFiles.find(file => file.name === pageName);\n\n if (!matchedFile) {\n return next();\n }\n\n return servePageHtml(res, matchedFile, options, log);\n } catch (error) {\n log(`开发服务器处理请求失败: ${error}`);\n next(error);\n }\n });\n\n log('开发服务器配置完成');\n } catch (error) {\n log(`配置开发服务器失败: ${error}`);\n throw error;\n }\n}\n\n// 提取页面HTML服务逻辑\nfunction servePageHtml(\n res: any,\n matchedFile: { name: string; file: string },\n options: DevServerOptions,\n log: (...args: any[]) => void\n) {\n // 获取页面配置\n const pageContext = {\n pageName: matchedFile.name,\n filePath: matchedFile.file,\n relativePath: path.relative(process.cwd(), matchedFile.file),\n strategy: undefined,\n isMatched: false,\n } as PageConfigContext;\n\n const pageConfig = getPageConfig(options.pageConfigs, pageContext, log);\n\n // 应用配置策略\n if (pageConfig?.strategy) {\n pageContext.strategy = pageConfig.strategy;\n } else if (options.appliedStrategies?.has(matchedFile.name)) {\n // 使用缓存的策略信息\n const strategyName = options.appliedStrategies.get(matchedFile.name);\n if (strategyName) {\n pageContext.strategy = strategyName;\n }\n }\n\n // 获取模板文件路径\n // 首先检查是否有页面特定的模板(例如mobile.html对应mobile页面)\n let templatePath = '';\n\n // 尝试以页面名称查找匹配的模板\n const pageSpecificTemplate = path.resolve(process.cwd(), `${matchedFile.name}.html`);\n if (fs.existsSync(pageSpecificTemplate)) {\n templatePath = pageSpecificTemplate;\n }\n // 然后尝试使用页面配置中指定的模板\n else if (pageConfig?.template) {\n templatePath = path.resolve(process.cwd(), pageConfig.template);\n }\n // 最后使用默认模板\n else {\n templatePath = path.resolve(process.cwd(), options.template);\n }\n\n if (!fs.existsSync(templatePath)) {\n res.statusCode = 404;\n res.end('Template not found');\n return;\n }\n\n // 读取并修改模板\n let html = fs.readFileSync(templatePath, 'utf-8');\n\n // 检查模板中是否包含占位符\n const containsPlaceholder = html.includes(options.placeholder);\n\n // 替换占位符为入口文件路径\n if (containsPlaceholder) {\n const originalHtml = html;\n\n // 方式1: 直接字符串替换\n html = html.split(options.placeholder).join(`/${matchedFile.file}`);\n\n // 检查替换结果\n if (html === originalHtml) {\n // 方式2: 正则表达式替换\n const escapedPlaceholder = escapeRegExp(options.placeholder);\n const placeholderRegex = new RegExp(escapedPlaceholder, 'g');\n html = originalHtml.replace(placeholderRegex, `/${matchedFile.file}`);\n\n // 检查替换结果\n if (html === originalHtml) {\n // 方式3: 硬编码替换具体的占位符格式\n html = originalHtml.replace(/\\{\\{ENTRY_FILE\\}\\}/g, `/${matchedFile.file}`);\n }\n }\n }\n\n // 添加页面级define变量\n if (pageConfig?.define) {\n const defineScript = Object.entries(pageConfig.define)\n .map(([key, value]) => {\n const stringValue = typeof value === 'string' ? `\"${value}\"` : JSON.stringify(value);\n return `window.${key} = ${stringValue};`;\n })\n .join('\\n');\n\n if (defineScript) {\n // 注入到head标签底部\n html = html.replace(\n /<\\/head>/i,\n `<script type=\"text/javascript\">\\n${defineScript}\\n</script>\\n</head>`\n );\n }\n }\n\n // 发送响应\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html');\n res.end(html);\n}\n\n// 为了兼容性,导出setupDevMiddleware作为configureDevServer的别名\nexport const setupDevMiddleware = configureDevServer;\n\n// 生成索引页面HTML\nfunction generateIndexHtml(\n entryFiles: { name: string; file: string }[],\n options: DevServerOptions,\n log: (...args: any[]) => void\n): string {\n try {\n const pageItems = entryFiles\n .map(file => {\n // 获取页面配置和策略\n const pageContext = {\n pageName: file.name,\n filePath: file.file,\n relativePath: path.relative(process.cwd(), file.file),\n strategy: undefined,\n isMatched: false,\n };\n\n const pageConfig = getPageConfig(options.pageConfigs, pageContext, log);\n\n // 确定策略\n let strategy = 'default';\n if (pageConfig?.strategy) {\n strategy = pageConfig.strategy;\n } else if (options.appliedStrategies?.has(file.name)) {\n const strategyName = options.appliedStrategies.get(file.name);\n if (strategyName) {\n strategy = strategyName;\n }\n }\n\n const strategyBadge =\n strategy !== 'default' ? `<span class=\"badge\">${strategy}</span>` : '';\n\n return `\n <div class=\"page-item\">\n <a href=\"${file.name}.html\" class=\"page-link\">\n ${file.name}${strategyBadge}\n </a>\n <div class=\"page-path\">${file.file}</div>\n </div>`;\n })\n .join('');\n\n return `\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>多页面应用索引</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n max-width: 1200px;\n margin: 0 auto;\n padding: 20px;\n background-color: #f5f5f7;\n }\n h1 {\n font-size: 24px;\n margin-bottom: 20px;\n color: #111;\n }\n .page-list {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n gap: 16px;\n }\n .page-item {\n background-color: white;\n border-radius: 8px;\n padding: 16px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n transition: transform 0.2s, box-shadow 0.2s;\n }\n .page-item:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n }\n .page-link {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 18px;\n font-weight: 500;\n color: #0066cc;\n text-decoration: none;\n margin-bottom: 8px;\n }\n .page-path {\n font-size: 14px;\n color: #666;\n word-break: break-all;\n }\n .badge {\n display: inline-block;\n font-size: 12px;\n padding: 2px 8px;\n border-radius: 12px;\n background-color: #e6f2ff;\n color: #0066cc;\n margin-left: 8px;\n }\n .stats {\n margin-bottom: 20px;\n font-size: 14px;\n color: #666;\n }\n </style>\n </head>\n <body>\n <h1>多页面应用索引</h1>\n <div class=\"stats\">\n 找到 ${entryFiles.length} 个页面\n </div>\n <div class=\"page-list\">\n ${pageItems}\n </div>\n </body>\n </html>\n `;\n } catch (error) {\n log(`生成索引页失败: ${error}`);\n return `\n <!DOCTYPE html>\n <html>\n <head>\n <title>错误</title>\n </head>\n <body>\n <h1>生成索引页时发生错误</h1>\n <p>${error}</p>\n </body>\n </html>\n `;\n }\n}\n","import * as path from 'node:path';\nimport type { EntryFile, CandidateFile } from './types';\n\nexport function filterEntryFiles(\n files: string[],\n entry: string,\n exclude: string[],\n _log: (...args: any[]) => void\n): EntryFile[] {\n const result: EntryFile[] = [];\n const nameToFile = new Map<string, { file: string; priority: number }>();\n\n // 从entry模式中提取基础目录\n let basePattern = entry.replace(/\\/\\*.*$/, ''); // 去掉glob部分\n // 如果基础模式为空或不合理,使用默认处理\n if (!basePattern || basePattern === entry) {\n basePattern = path.dirname(entry.split('*')[0]);\n }\n const candidateFiles: CandidateFile[] = [];\n\n for (const file of files) {\n if (exclude.includes(file)) {\n continue;\n }\n\n // 统一使用正斜杠处理路径,确保Windows兼容性\n const normalizedFile = file.replace(/\\\\/g, '/');\n const normalizedBasePattern = basePattern.replace(/\\\\/g, '/');\n\n const relativePath = path.posix.relative(normalizedBasePattern, normalizedFile);\n const pathParts = relativePath.split('/'); // 使用正斜杠分割\n\n if (pathParts.length === 1) {\n // 第一级文件:src/pages/about.js -> /about.html\n const fileName = pathParts[0];\n const name = path.posix.basename(fileName, path.posix.extname(fileName));\n candidateFiles.push({ name, file, priority: 1 });\n } else if (pathParts.length >= 2) {\n // 目录下的文件\n const fileName = path.posix.basename(normalizedFile, path.posix.extname(normalizedFile));\n const dirName = pathParts[0];\n\n if (fileName === 'main') {\n // 目录下的main文件:src/pages/mobile/main.ts -> /mobile.html\n candidateFiles.push({ name: dirName, file, priority: 2 });\n }\n }\n }\n\n // 按照优先级处理冲突:目录优先覆盖文件(优先级2 > 优先级1)\n for (const candidate of candidateFiles) {\n const existing = nameToFile.get(candidate.name);\n\n if (!existing) {\n nameToFile.set(candidate.name, { file: candidate.file, priority: candidate.priority });\n } else {\n if (candidate.priority > existing.priority) {\n nameToFile.set(candidate.name, { file: candidate.file, priority: candidate.priority });\n }\n }\n }\n\n for (const [name, { file }] of nameToFile.entries()) {\n result.push({ name, file });\n }\n\n return result;\n}\n","export function escapeRegExp(string: string): string {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport function createLogger(debug: boolean) {\n return (...args: any[]) => {\n if (debug) {\n console.log('[vite-plugin-multi-page]', ...args);\n }\n };\n}\n","import type { PageConfig, PageConfigFunction, PageConfigContext } from './types';\n\n/**\n * 根据页面上下文获取页面配置\n */\nexport function getPageConfig(\n pageConfigs: Record<string, PageConfig> | PageConfigFunction | undefined,\n context: PageConfigContext,\n log: (...args: any[]) => void\n): PageConfig | null {\n if (!pageConfigs) return null;\n\n // 如果是函数,直接调用\n if (typeof pageConfigs === 'function') {\n const result = pageConfigs(context);\n return result;\n }\n\n // 对象配置:支持精确匹配和模式匹配\n for (const [key, config] of Object.entries(pageConfigs)) {\n // 精确匹配页面名称\n if (key === context.pageName) {\n log(`精确匹配页面 ${context.pageName}:`, config);\n return config;\n }\n\n // 模式匹配\n if (config.match) {\n const patterns = Array.isArray(config.match) ? config.match : [config.match];\n const isMatched = patterns.some(\n pattern =>\n simpleMatch(pattern, context.pageName) ||\n simpleMatch(pattern, context.relativePath) ||\n simpleMatch(pattern, context.filePath)\n );\n\n if (isMatched) {\n log(`模式匹配页面 ${context.pageName} (模式: ${config.match}):`, config);\n return { ...config, match: undefined };\n }\n }\n\n // glob 模式匹配页面名称\n if (simpleMatch(key, context.pageName)) {\n log(`Glob匹配页面 ${context.pageName} (模式: ${key}):`, config);\n return config;\n }\n }\n\n return null;\n}\n\n/**\n * 简单的模式匹配函数\n */\nfunction simpleMatch(pattern: string, text: string): boolean {\n const regexPattern = pattern\n .replace(/\\*\\*/g, '__DOUBLE_STAR__')\n .replace(/\\*/g, '[^/]*')\n .replace(/__DOUBLE_STAR__/g, '.*');\n const regex = new RegExp(`^${regexPattern}$`);\n return regex.test(text);\n}\n","import type { UserConfig } from 'vite';\nimport { mergeConfig } from 'vite';\nimport { glob } from 'glob';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { filterEntryFiles } from './file-filter';\nimport { getPageConfig } from './page-config';\nimport type { BuildConfigOptions, PageConfigContext, ConfigStrategy } from './types';\nimport { createLogger } from './utils';\n\n/**\n * 构建时配置生成器\n * 根据策略和页面配置生成多页面构建配置\n */\nexport function generateBuildConfig(options: BuildConfigOptions): Record<string, UserConfig> {\n const {\n entry = 'src/pages/*/main.{ts,js}',\n exclude = [],\n template = 'index.html',\n placeholder = '<!--VITE_MULTI_PAGE_ENTRY-->',\n strategies = {},\n pageConfigs = {},\n forceBuildStrategy,\n forceBuildPage,\n } = options;\n\n const log = createLogger(true);\n const buildConfigs: Record<string, UserConfig> = {};\n\n try {\n // 1. 发现所有页面入口文件\n const allFiles = glob.sync(entry, { cwd: process.cwd() });\n const entryFiles = filterEntryFiles(allFiles, entry, exclude, log);\n\n if (entryFiles.length === 0) {\n log('警告: 未找到匹配的入口文件');\n return {};\n }\n\n // 2. 为每个页面分析配置和策略\n const pageStrategies = new Map<string, string>();\n const strategyPages = new Map<string, string[]>();\n\n for (const entryFile of entryFiles) {\n const pageContext = {\n pageName: entryFile.name,\n filePath: entryFile.file,\n relativePath: path.relative(process.cwd(), entryFile.file),\n } as PageConfigContext;\n\n // 获取页面配置\n const pageConfig = getPageConfig(pageConfigs, pageContext, log);\n const strategyName = pageConfig?.strategy || 'default';\n\n pageStrategies.set(entryFile.name, strategyName);\n\n if (!strategyPages.has(strategyName)) {\n strategyPages.set(strategyName, []);\n }\n strategyPages.get(strategyName)?.push(entryFile.name);\n }\n\n log(`📄 发现 ${entryFiles.length} 个页面: ${entryFiles.map(f => f.name).join(', ')}`);\n\n // 3. 如果指定了强制页面,只构建该页面\n if (forceBuildPage) {\n const targetEntry = entryFiles.find(f => f.name === forceBuildPage);\n if (!targetEntry) {\n log(`警告: 未找到页面 \"${forceBuildPage}\"`);\n return {};\n }\n\n // 获取该页面的策略\n const pageStrategy = pageStrategies.get(forceBuildPage) || 'default';\n const strategyConfig = strategies[pageStrategy] || {};\n\n const config = generateStrategyConfig(\n `single-${forceBuildPage}`,\n [forceBuildPage],\n entryFiles,\n strategyConfig,\n pageConfigs,\n template,\n placeholder,\n log\n );\n\n buildConfigs[`single-${forceBuildPage}`] = config;\n return buildConfigs;\n }\n\n // 4. 如果指定了强制策略,只构建该策略的页面\n if (forceBuildStrategy) {\n const targetPages = strategyPages.get(forceBuildStrategy) || [];\n if (targetPages.length === 0) {\n log(`警告: 策略 \"${forceBuildStrategy}\" 下没有页面`);\n return {};\n }\n\n log(`强制构建策略: ${forceBuildStrategy}, 页面: ${targetPages.join(', ')}`);\n\n const config = generateStrategyConfig(\n forceBuildStrategy,\n targetPages,\n entryFiles,\n strategies[forceBuildStrategy],\n pageConfigs,\n template,\n placeholder,\n log\n );\n\n buildConfigs[forceBuildStrategy] = config;\n return buildConfigs;\n }\n\n // 5. 为每个策略生成构建配置\n for (const [strategyName, pages] of strategyPages) {\n if (pages.length === 0) continue;\n\n // 获取策略配置,如果没有定义则使用空配置(允许默认策略)\n const strategyConfig = strategies[strategyName] || {};\n const config = generateStrategyConfig(\n strategyName,\n pages,\n entryFiles,\n strategyConfig,\n pageConfigs,\n template,\n placeholder,\n log\n );\n\n buildConfigs[strategyName] = config;\n }\n\n // 确保至少有一个构建配置\n if (Object.keys(buildConfigs).length === 0) {\n log('警告: 未生成任何构建配置,创建默认配置');\n\n // 如果没有任何策略,创建一个默认策略包含所有页面\n const allPageNames = entryFiles.map(f => f.name);\n const defaultConfig = generateStrategyConfig(\n 'default',\n allPageNames,\n entryFiles,\n {},\n pageConfigs,\n template,\n placeholder,\n log\n );\n\n buildConfigs['default'] = defaultConfig;\n }\n\n const strategyNames = Object.keys(buildConfigs);\n log(`📦 构建策略: ${strategyNames.join(', ')}`);\n return buildConfigs;\n } catch (error) {\n log('生成构建配置失败:', error);\n throw error;\n }\n}\n\n/**\n * 为特定策略生成构建配置\n */\nfunction generateStrategyConfig(\n strategyName: string,\n pages: string[],\n entryFiles: Array<{ name: string; file: string }>,\n strategyConfig: ConfigStrategy | undefined,\n pageConfigs: any,\n defaultTemplate: string,\n placeholder: string,\n log: (...args: any[]) => void\n): UserConfig {\n const htmlInputs: Record<string, string> = {};\n const tempFiles: string[] = [];\n\n // 收集所有页面的 define 变量\n const allPageDefines: Record<string, any> = {};\n\n // 为每个页面确定使用的HTML模板并创建临时文件\n for (const pageName of pages) {\n const entryFile = entryFiles.find(f => f.name === pageName);\n if (!entryFile) continue;\n\n // 获取页面配置\n const pageContext = {\n pageName,\n filePath: entryFile.file,\n relativePath: path.relative(process.cwd(), entryFile.file),\n strategy: strategyName,\n } as PageConfigContext;\n\n const pageConfig = getPageConfig(pageConfigs, pageContext, log);\n\n // 收集页面级 define 变量\n if (pageConfig?.define) {\n Object.assign(allPageDefines, pageConfig.define);\n }\n\n // 确定HTML模板\n let templatePath = defaultTemplate;\n\n // 1. 页面特定模板(如 mobile.html 对应 mobile 页面)\n const pageSpecificTemplate = `${pageName}.html`;\n if (fs.existsSync(path.resolve(process.cwd(), pageSpecificTemplate))) {\n templatePath = pageSpecificTemplate;\n }\n // 2. 页面配置中指定的模板\n else if (pageConfig?.template) {\n templatePath = pageConfig.template;\n }\n\n // 读取模板内容\n const templateFullPath = path.resolve(process.cwd(), templatePath);\n if (!fs.existsSync(templateFullPath)) {\n log(`警告: 模板文件不存在: ${templatePath}`);\n continue;\n }\n\n let templateContent = fs.readFileSync(templateFullPath, 'utf-8');\n\n // 替换占位符\n if (templateContent.includes(placeholder)) {\n // 临时HTML在项目根目录中,使用相对路径\n const entryPath = `./${entryFile.file}`;\n templateContent = templateContent.replace(\n new RegExp(placeholder.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'),\n entryPath\n );\n }\n\n // 创建临时HTML文件,使用新的命名规则:.temp.mp.[name].html\n const tempHtmlPath = path.resolve(process.cwd(), `.temp.mp.${pageName}.html`);\n fs.writeFileSync(tempHtmlPath, templateContent);\n tempFiles.push(tempHtmlPath);\n\n htmlInputs[pageName] = tempHtmlPath;\n }\n\n // 构建基础配置 - 不设置 outDir,让 Vite 使用默认配置\n const baseConfig: UserConfig = {\n build: {\n rollupOptions: {\n input: htmlInputs, // 使用临时HTML文件作为输入\n output: {\n entryFileNames: 'assets/[name]-[hash].js',\n chunkFileNames: 'assets/[name]-[hash].js',\n assetFileNames: 'assets/[name]-[hash][extname]',\n },\n },\n emptyOutDir: false, // 不清空输出目录,避免删除临时HTML文件\n },\n define: {},\n };\n\n // 使用Vite的mergeConfig进行智能深度合并\n let config: UserConfig = baseConfig;\n\n if (strategyConfig) {\n config = mergeConfig(baseConfig, strategyConfig);\n }\n\n // 合并页面级 define 变量到 Vite 的 define 配置中\n // 页面级 define 优先级高于策略级 define\n if (Object.keys(allPageDefines).length > 0) {\n config.define = {\n ...config.define,\n ...allPageDefines,\n };\n }\n\n // 手动处理需要特殊控制的配置项,防止被mergeConfig覆盖\n if (!config.build) config.build = {};\n if (!config.build.rollupOptions) config.build.rollupOptions = {};\n\n // 确保关键配置不被覆盖\n config.build.rollupOptions.input = htmlInputs; // 强制使用临时HTML文件作为输入\n config.build.emptyOutDir = false; // 不清空输出目录,避免删除临时HTML文件\n\n // 简化日志输出\n log(`策略 \"${strategyName}\" - ${pages.length} 个页面`);\n\n return config;\n}\n\n/**\n * 获取Vite配置的输出目录\n * 需要传入已解析的Vite配置或命令行参数\n */\nexport function getViteOutputDirectory(viteBuildArgs: string[] = []): string {\n // 1. 首先检查命令行参数中的 --outDir\n const outDirIndex = viteBuildArgs.findIndex(arg => arg === '--outDir');\n if (outDirIndex !== -1 && outDirIndex + 1 < viteBuildArgs.length) {\n const outDir = viteBuildArgs[outDirIndex + 1];\n return path.resolve(process.cwd(), outDir);\n }\n\n // 2. 检查 --outDir=value 格式\n const outDirArg = viteBuildArgs.find(arg => arg.startsWith('--outDir='));\n if (outDirArg) {\n const outDir = outDirArg.split('=')[1];\n return path.resolve(process.cwd(), outDir);\n }\n\n // 3. 如果没有命令行参数,使用 Vite 默认值\n // 注意:如果用户在 vite.config.ts 中配置了 build.outDir,\n // Vite 会自动使用该配置,我们这里只处理命令行参数的情况\n return path.resolve(process.cwd(), 'dist');\n}\n\n/**\n * 清理Vite配置的输出目录\n */\nexport function cleanViteOutputDirectory(viteBuildArgs: string[] = []): void {\n const outputDir = getViteOutputDirectory(viteBuildArgs);\n const log = createLogger(true);\n\n try {\n if (fs.existsSync(outputDir)) {\n fs.rmSync(outputDir, { recursive: true, force: true });\n log(`🧹 清理输出目录: ${path.relative(process.cwd(), outputDir)}`);\n }\n } catch (error) {\n log(`⚠️ 清理输出目录失败: ${outputDir}`, error);\n }\n}\n\n/**\n * 获取所有可用的构建策略\n */\nexport function discoverPages(options: BuildConfigOptions): Array<{ name: string; file: string }> {\n const { entry = 'src/pages/*/main.{ts,js}', exclude = [] } = options;\n\n const log = createLogger(true);\n\n try {\n // 发现所有页面入口文件\n const allFiles = glob.sync(entry, { cwd: process.cwd() });\n const entryFiles = filterEntryFiles(allFiles, entry, exclude, log);\n\n return entryFiles;\n } catch (error) {\n log('发现页面失败:', error);\n throw error;\n }\n}\n\nexport function getAvailableStrategies(options: BuildConfigOptions): string[] {\n const { entry = 'src/pages/*/main.{ts,js}', exclude = [], pageConfigs = {} } = options;\n\n const log = createLogger(false); // 静默模式\n const strategySet = new Set<string>();\n\n // 发现所有页面入口文件\n const allFiles = glob.sync(entry, { cwd: process.cwd() });\n const entryFiles = filterEntryFiles(allFiles, entry, exclude, log);\n\n if (entryFiles.length === 0) {\n throw new Error(`未找到匹配的入口文件: ${entry}`);\n }\n\n try {\n // 分析每个页面的策略\n for (const entryFile of entryFiles) {\n const pageContext = {\n pageName: entryFile.name,\n filePath: entryFile.file,\n relativePath: path.relative(process.cwd(), entryFile.file),\n } as PageConfigContext;\n\n const pageConfig = getPageConfig(pageConfigs, pageContext, log);\n const strategyName = pageConfig?.strategy || 'default';\n strategySet.add(strategyName);\n }\n\n // 只返回实际有页面的策略,不添加空策略\n return Array.from(strategySet).sort();\n } catch (error) {\n log('获取可用策略失败:', error);\n return ['default'];\n }\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { Module } from 'node:module';\nimport type { Options } from './types';\n\n/**\n * 配置上下文\n */\nexport interface ConfigContext {\n mode: 'development' | 'production';\n command: 'serve' | 'build';\n isCLI?: boolean;\n}\n\n/**\n * 配置函数类型\n */\nexport type ConfigFunction = (context: ConfigContext) => Options;\n\n/**\n * 配置文件名列表(优先级从高到低)\n */\nconst CONFIG_FILES = [\n 'multipage.config.js',\n 'multipage.config.mjs',\n 'multipage.config.ts',\n] as const;\n\n/**\n * 检查是否存在自定义配置文件\n */\nexport function hasCustomConfig(): boolean {\n for (const filename of CONFIG_FILES) {\n const configPath = path.resolve(process.cwd(), filename);\n if (fs.existsSync(configPath)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * 加载用户的多页面配置\n */\nexport async function loadUserConfig(context: ConfigContext): Promise<Options | null> {\n // 尝试加载项目自定义配置\n const customConfig = await loadCustomConfig();\n\n if (customConfig) {\n // 使用项目自定义配置\n const result = customConfig(context);\n\n // 如果配置函数返回 undefined 或 null,视为空配置\n if (!result) {\n return {};\n }\n\n return result;\n }\n\n // 没有找到用户配置\n return null;\n}\n\n/**\n * 加载配置文件\n */\nasync function loadConfigFile(filePath: string): Promise<any> {\n // 处理 TypeScript 文件\n if (filePath.endsWith('.ts')) {\n try {\n const code = await fs.promises.readFile(filePath, 'utf-8');\n\n // 尝试动态导入 esbuild\n let esbuild: any;\n try {\n esbuild = await import('esbuild');\n } catch (importError) {\n // esbuild 不可用,给出友好的错误提示\n console.error('\\n❌ 无法加载 TypeScript 配置文件,因为找不到 esbuild 依赖。');\n console.error('\\n💡 请选择以下解决方案之一:');\n console.error(\n ' 1. 安装 esbuild (peerDependency):npm install esbuild@\">=0.19.3\" --save-dev'\n );\n console.error(' 2. 或者如果使用 Vite 项目,esbuild 通常已安装,请检查版本是否 >=0.19.3');\n console.error(\n ' 3. 使用 JavaScript 配置文件:将 multipage.config.ts 重命名为 multipage.config.js'\n );\n console.error(\n ' 4. 使用 ESM 配置文件:将 multipage.config.ts 重命名为 multipage.config.mjs\\n'\n );\n throw new Error(`需要 esbuild 依赖来处理 TypeScript 配置文件: ${path.basename(filePath)}`);\n }\n\n // 使用 esbuild 实时转译 TS → JS\n const result = await esbuild.transform(code, {\n loader: 'ts',\n format: 'cjs', // 使用 CommonJS 格式便于使用 Module._compile\n target: 'node16',\n sourcemap: false,\n });\n\n // 创建临时模块并编译\n const tempModule = new Module(filePath);\n tempModule.filename = filePath;\n tempModule.paths = (Module as any)._nodeModulePaths(path.dirname(filePath));\n\n // 编译代码\n (tempModule as any)._compile(result.code, filePath);\n\n return tempModule.exports;\n } catch (error) {\n // 如果是 esbuild 缺失的错误,直接抛出\n if (error instanceof Error && error.message.includes('需要 esbuild 依赖')) {\n throw error;\n }\n\n console.warn('esbuild 转译失败,尝试简单转换:', error);\n\n // 备选方案:简单的文本替换\n const code = await fs.promises.readFile(filePath, 'utf-8');\n const jsCode = code\n .replace(/export\\s+default\\s+/, 'module.exports = ')\n .replace(/import\\s+.*?from\\s+['\"][^'\"]*['\"];?\\s*/g, '')\n .replace(/:\\s*[^=,})\\]]+/g, ''); // 简单的类型注解移除\n\n const tempModule = new Module(filePath);\n tempModule.filename = filePath;\n tempModule.paths = (Module as any)._nodeModulePaths(path.dirname(filePath));\n\n (tempModule as any)._compile(jsCode, filePath);\n return tempModule.exports;\n }\n }\n\n // 处理 JavaScript 文件\n if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {\n const fileUrl = pathToFileURL(filePath).href;\n return import(`${fileUrl}?t=${Date.now()}`);\n }\n\n throw new Error(`不支持的配置文件类型: ${filePath}`);\n}\n\n/**\n * 加载项目自定义配置文件\n */\nasync function loadCustomConfig(): Promise<ConfigFunction | null> {\n const cwd = process.cwd();\n\n for (const configFile of CONFIG_FILES) {\n const configPath = path.resolve(cwd, configFile);\n\n if (fs.existsSync(configPath)) {\n try {\n const configModule = await loadConfigFile(configPath);\n const configFunction = configModule.default || configModule;\n\n if (typeof configFunction === 'function') {\n return configFunction;\n } else {\n console.warn(`配置文件 ${configFile} 必须默认导出一个函数`);\n }\n } catch (error) {\n if (configFile.endsWith('.ts'