vite-plugin-resource-waste 1.0.0
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.
- package/README.md +290 -0
- package/dist/index.cjs +929 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/plugin.ts","../src/constants.ts","../src/utils/fs.ts","../src/analyzers/cache-analyzer.ts","../src/analyzers/duplicate-deps-analyzer.ts","../src/analyzers/import-pattern-analyzer.ts","../src/analyzers/unreachable-analyzer.ts","../src/analyzers/index.ts","../src/analyzers/module-graph-analyzer.ts","../src/report/reporter.ts"],"sourcesContent":["/**\r\n * 包入口:对外暴露插件工厂函数与类型定义\r\n */\r\nexport { resourceWaste, default } from './plugin'\r\nexport type {\r\n ResourceWastePluginOptions,\r\n ResourceWasteReport,\r\n ResourceIssue,\r\n ResourceWasteSummary,\r\n IssueCategory,\r\n IssueSeverity,\r\n} from './types'\r\n","/**\r\n * Vite 插件主入口 —— vite-plugin-resource-waste\r\n *\r\n * 前端资源浪费分析插件,在构建期检测无效 JS、不可达组件、缓存失效等问题并生成报告。\r\n * 1. configResolved → 读取项目根目录、输出目录、构建入口\r\n * 2. buildStart → glob 扫描全部源文件\r\n * 3. moduleParsed → 记录模块依赖图(import / 被 import)\r\n * 4. generateBundle → 缓存 Rollup 产出 bundle\r\n * 5. closeBundle → 运行分析器、生成 JSON/HTML 报告\r\n * 6. configureServer → dev 模式下做轻量静态分析,写入 cache\r\n */\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport type { OutputBundle } from 'rollup'\r\nimport type { Plugin as VitePlugin } from 'vite'\r\nimport pc from 'picocolors'\r\nimport { runAllAnalyzers, sortIssues } from './analyzers'\r\nimport { extractBundleFiles, recordModuleParsed } from './analyzers/module-graph-analyzer'\r\nimport { collectSourceFiles } from './analyzers/import-pattern-analyzer'\r\nimport { DEFAULT_OPTIONS } from './constants'\r\nimport { buildReport, printTerminalSummary, renderHtmlReport } from './report/reporter'\r\nimport type { AnalysisContext, ParsedModuleInfo, ResourceWastePluginOptions } from './types'\r\nimport { ensureDir, normalizePath, resolveFromRoot } from './utils/fs'\r\n\r\n/** 插件内部状态:保存 generateBundle 阶段的 bundle 供 closeBundle 使用 */\r\ntype PluginState = {\r\n bundle?: OutputBundle\r\n}\r\n\r\nexport function resourceWaste(options: ResourceWastePluginOptions = {}): VitePlugin {\r\n const opts = { ...DEFAULT_OPTIONS, ...options }\r\n const state: PluginState = {}\r\n\r\n // 以下变量在 configResolved 后赋值,供整个构建周期使用\r\n let projectRoot = process.cwd()\r\n let outDir = 'dist'\r\n let mode = 'production'\r\n let entries: string[] = []\r\n let sourceFiles: string[] = []\r\n\r\n // 模块依赖图:key 为模块绝对路径,value 为 ParsedModuleInfo\r\n const parsedModules = new Map<string, ParsedModuleInfo>()\r\n\r\n /** 构造分析上下文,供各 analyzer 读取共享数据 */\r\n const createContext = (bundleFiles: AnalysisContext['bundleFiles'] = []): AnalysisContext => ({\r\n root: opts.root ?? 'src',\r\n projectRoot,\r\n outDir,\r\n mode,\r\n entries,\r\n sourceFiles,\r\n parsedModules,\r\n bundleFiles,\r\n options: opts,\r\n })\r\n\r\n /**\r\n * 构建完成后:运行全部分析器 → 写报告 → 可选 fail build\r\n */\r\n async function writeReport(): Promise<void> {\r\n if (!state.bundle) return\r\n\r\n const bundleFiles = extractBundleFiles(state.bundle)\r\n const ctx = createContext(bundleFiles)\r\n const issues = sortIssues(await runAllAnalyzers(ctx))\r\n const report = buildReport(issues, { projectRoot, mode, outDir })\r\n\r\n const reportBaseDir = path.resolve(projectRoot, outDir, opts.reportDir ?? 'resource-waste')\r\n ensureDir(reportBaseDir)\r\n\r\n // 写入 JSON 报告(供 CI / 程序消费)\r\n const jsonPath = path.join(reportBaseDir, 'report.json')\r\n fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf-8')\r\n\r\n // 可选:写入 HTML 可视化报告\r\n if (opts.htmlReport) {\r\n fs.writeFileSync(path.join(reportBaseDir, 'report.html'), renderHtmlReport(report), 'utf-8')\r\n }\r\n\r\n if (!opts.silent) {\r\n printTerminalSummary(report, opts.wasteThresholdKb ?? 300)\r\n console.log(` Report JSON : ${normalizePath(path.relative(projectRoot, jsonPath))}`)\r\n if (opts.htmlReport) {\r\n console.log(\r\n ` Report HTML : ${normalizePath(path.relative(projectRoot, path.join(reportBaseDir, 'report.html')))}`,\r\n )\r\n }\r\n }\r\n\r\n // CI 构建阈值拦截:浪费超过阈值则中断构建\r\n if (opts.failOnThreshold && report.summary.totalWasteTransferKb > (opts.wasteThresholdKb ?? 300)) {\r\n throw new Error(\r\n `[vite-plugin-resource-waste] Estimated waste ${report.summary.totalWasteTransferKb} KB exceeds threshold ${opts.wasteThresholdKb} KB`,\r\n )\r\n }\r\n }\r\n\r\n return {\r\n name: 'vite-plugin-resource-waste',\r\n enforce: 'post', // 在其他插件之后执行,确保 module graph 和 bundle 已完整\r\n\r\n /** Vite 配置解析完成后,收集构建入口等信息 */\r\n configResolved(config) {\r\n projectRoot = config.root\r\n outDir = config.build.outDir\r\n mode = config.mode\r\n\r\n // 解析 rollup input 配置,支持 string / string[] / Record 三种形式\r\n const rollupInput = config.build.rollupOptions.input\r\n if (typeof rollupInput === 'string') {\r\n entries = [normalizePath(resolveFromRoot(projectRoot, rollupInput))]\r\n } else if (Array.isArray(rollupInput)) {\r\n entries = rollupInput.map((e) => normalizePath(resolveFromRoot(projectRoot, e)))\r\n } else if (rollupInput && typeof rollupInput === 'object') {\r\n entries = Object.values(rollupInput).map((e) => normalizePath(resolveFromRoot(projectRoot, e)))\r\n } else {\r\n // 未显式配置 input 时,默认以 index.html 为入口\r\n entries = [normalizePath(resolveFromRoot(projectRoot, 'index.html'))]\r\n }\r\n },\r\n\r\n /** 构建开始时 glob 扫描 src 下全部源文件 */\r\n async buildStart() {\r\n sourceFiles = await collectSourceFiles(createContext())\r\n },\r\n\r\n /**\r\n * 每个模块被 Rollup 解析后触发\r\n * 用于构建 import 依赖图(谁引用了谁)\r\n */\r\n moduleParsed(moduleInfo) {\r\n if (!moduleInfo.id) return\r\n\r\n recordModuleParsed(\r\n parsedModules,\r\n moduleInfo.id,\r\n moduleInfo.code ?? null,\r\n [...moduleInfo.importedIds, ...(moduleInfo.dynamicallyImportedIds ?? [])],\r\n moduleInfo.isEntry,\r\n )\r\n },\r\n\r\n /** 构建产出阶段:缓存 bundle 对象,closeBundle 时再分析 */\r\n generateBundle(_options, bundle) {\r\n state.bundle = bundle\r\n },\r\n\r\n /** 构建结束:执行分析并写报告 */\r\n async closeBundle() {\r\n await writeReport()\r\n },\r\n\r\n /**\r\n * dev server 启动后:做轻量静态分析(无 bundle 数据)\r\n * 报告写入 node_modules/.cache/resource-waste/dev-report.json\r\n */\r\n configureServer(server) {\r\n server.httpServer?.once('listening', async () => {\r\n if (mode === 'production') return\r\n\r\n sourceFiles = await collectSourceFiles(createContext())\r\n const issues = sortIssues(await runAllAnalyzers(createContext()))\r\n\r\n if (!opts.silent && issues.length > 0) {\r\n console.log(\r\n pc.cyan('\\n[vite-plugin-resource-waste]') +\r\n pc.yellow(\r\n ` Dev mode: found ${issues.length} potential resource waste issue(s). Run \"vite build\" for full report.\\n`,\r\n ),\r\n )\r\n }\r\n\r\n const cacheDir = path.resolve(projectRoot, 'node_modules/.cache/resource-waste')\r\n ensureDir(cacheDir)\r\n const report = buildReport(issues, { projectRoot, mode, outDir })\r\n fs.writeFileSync(path.join(cacheDir, 'dev-report.json'), JSON.stringify(report, null, 2), 'utf-8')\r\n })\r\n },\r\n }\r\n}\r\n\r\nexport default resourceWaste\r\n\r\nexport type {\r\n ResourceWastePluginOptions,\r\n ResourceWasteReport,\r\n ResourceIssue,\r\n ResourceWasteSummary,\r\n} from './types'\r\n","/**\r\n * 插件常量与默认配置\r\n * 包含:默认选项、扫描规则、导入模式黑名单、报告展示文案等\r\n */\r\nimport type { IssueCategory, IssueSeverity } from './types'\r\n\r\n/** 每 KB JS 估算解析耗时(ms),基于中端移动设备经验值 */\r\nexport const PARSE_MS_PER_KB = 0.45\r\n\r\n/** 插件默认配置,用户传入的 options 会与之 merge */\r\nexport const DEFAULT_OPTIONS = {\r\n root: 'src',\r\n reportDir: 'resource-waste',\r\n wasteThresholdKb: 300,\r\n failOnThreshold: false,\r\n htmlReport: true,\r\n scanImportPatterns: true,\r\n scanUnreachable: true,\r\n scanCacheStrategy: true,\r\n scanDuplicateDeps: true,\r\n include: [] as string[],\r\n exclude: ['**/*.spec.*', '**/*.test.*', '**/__tests__/**', '**/node_modules/**'],\r\n silent: false,\r\n}\r\n\r\n/** glob 扫描时匹配的源码扩展名 */\r\nexport const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte']\r\n\r\n/** 需要关注缓存策略的静态资源扩展名 */\r\nexport const CACHEABLE_EXTENSIONS = [\r\n '.js',\r\n '.css',\r\n '.woff',\r\n '.woff2',\r\n '.ttf',\r\n '.png',\r\n '.jpg',\r\n '.jpeg',\r\n '.gif',\r\n '.webp',\r\n '.svg',\r\n '.ico',\r\n]\r\n\r\n/** 文件名中含 content hash 的常见模式,用于判断是否可安全长期缓存 */\r\nexport const HASH_IN_FILENAME =\r\n /\\.[a-f0-9]{8,}\\.(js|css|woff2?|ttf|png|jpe?g|gif|webp|svg|ico)$/i\r\n\r\n/**\r\n * 已知「容易导致 tree-shake 失效」的 import 模式规则库\r\n * 通过正则匹配源码,命中则生成 import-pattern 类 issue\r\n */\r\nexport const FULL_IMPORT_PATTERNS: Array<{\r\n pattern: RegExp\r\n packageName: string\r\n suggestion: string\r\n severity: IssueSeverity\r\n wasteKb: number // 未 gzip 的预估浪费体积,报告时会乘以 0.35 估算 gzip 后体积\r\n}> = [\r\n {\r\n pattern: /import\\s+_\\s+from\\s+['\"]lodash['\"]/,\r\n packageName: 'lodash',\r\n suggestion: \"改为 import debounce from 'lodash-es/debounce' 或按需导入\",\r\n severity: 'high',\r\n wasteKb: 68,\r\n },\r\n {\r\n pattern: /import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['\"]lodash['\"]/,\r\n packageName: 'lodash',\r\n suggestion: \"改为 lodash-es 按需导入\",\r\n severity: 'high',\r\n wasteKb: 68,\r\n },\r\n {\r\n pattern: /import\\s+\\w+\\s+from\\s+['\"]moment['\"]/,\r\n packageName: 'moment',\r\n suggestion: \"改用 dayjs 或 date-fns 按需导入\",\r\n severity: 'high',\r\n wasteKb: 65,\r\n },\r\n {\r\n pattern: /import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['\"]antd['\"]/,\r\n packageName: 'antd',\r\n suggestion: '改用 antd 按需导入或 unplugin-vue-components 自动导入',\r\n severity: 'high',\r\n wasteKb: 200,\r\n },\r\n {\r\n pattern: /import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['\"]@ant-design\\/icons['\"]/,\r\n packageName: '@ant-design/icons',\r\n suggestion: '改为 @ant-design/icons-vue 按需导入具体图标',\r\n severity: 'medium',\r\n wasteKb: 80,\r\n },\r\n {\r\n pattern: /import\\s+['\"][^'\"]+\\.css['\"]\\s*;\\s*\\/\\/\\s*unused/i,\r\n packageName: 'css-side-effect',\r\n suggestion: '移除未使用的 CSS 副作用 import',\r\n severity: 'medium',\r\n wasteKb: 10,\r\n },\r\n]\r\n\r\n/** HTML 报告中问题分类的中文标签 */\r\nexport const CATEGORY_LABELS: Record<IssueCategory, string> = {\r\n 'static-unused-js': '静态未使用 JS',\r\n 'unreachable-component': '不可达组件/页面',\r\n 'cache-miss': '缓存策略风险',\r\n 'duplicate-dependency': '重复依赖',\r\n 'import-pattern': '导入模式浪费',\r\n 'prefetch-waste': '预加载浪费',\r\n}\r\n\r\n/** HTML 报告中严重级别的中文标签 */\r\nexport const SEVERITY_LABELS: Record<IssueSeverity, string> = {\r\n high: '高',\r\n medium: '中',\r\n low: '低',\r\n}\r\n","/**\r\n * 文件系统与路径相关的通用工具函数\r\n * 供各 analyzer 和 plugin 主逻辑复用\r\n */\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\n\r\n/** 统一路径分隔符为 `/`,便于跨平台比较 */\r\nexport function normalizePath(p: string): string {\r\n return p.split(path.sep).join('/')\r\n}\r\n\r\n/** 判断是否为前端源码文件 */\r\nexport function isSourceFile(filePath: string): boolean {\r\n return /\\.(tsx?|jsx?|vue|svelte)$/.test(filePath)\r\n}\r\n\r\n/** 安全读取文本文件,失败时返回空字符串而不抛错 */\r\nexport function readTextSafe(filePath: string): string {\r\n try {\r\n return fs.readFileSync(filePath, 'utf-8')\r\n } catch {\r\n return ''\r\n }\r\n}\r\n\r\n/** 获取文件体积(KB),保留两位小数 */\r\nexport function fileSizeKb(filePath: string): number {\r\n try {\r\n const stat = fs.statSync(filePath)\r\n return Math.round((stat.size / 1024) * 100) / 100\r\n } catch {\r\n return 0\r\n }\r\n}\r\n\r\n/** 根据体积估算 JS 解析耗时(ms) */\r\nexport function estimateParseMs(kb: number): number {\r\n return Math.round(kb * 0.45 * 10) / 10\r\n}\r\n\r\n/**\r\n * 从 Rollup module id 中提取 npm 包名与版本\r\n * 例:/project/node_modules/lodash/lodash.js → { name: 'lodash', version: '4.17.21' }\r\n */\r\nexport function extractPackageFromModuleId(moduleId: string): { name: string; version?: string } | null {\r\n const normalized = normalizePath(moduleId)\r\n const nodeModulesIdx = normalized.lastIndexOf('node_modules/')\r\n if (nodeModulesIdx === -1) return null\r\n\r\n const nodeModulesRoot = normalized.slice(0, nodeModulesIdx + 'node_modules/'.length)\r\n const rest = normalized.slice(nodeModulesIdx + 'node_modules/'.length)\r\n const parts = rest.split('/')\r\n\r\n // scoped 包:@scope/name\r\n if (parts[0]?.startsWith('@') && parts[1]) {\r\n const name = `${parts[0]}/${parts[1]}`\r\n const version = readPackageVersion(path.join(nodeModulesRoot, name))\r\n return { name, version }\r\n }\r\n\r\n // 普通包:lodash、vue 等\r\n if (parts[0]) {\r\n const name = parts[0]\r\n const version = readPackageVersion(path.join(nodeModulesRoot, name))\r\n return { name, version }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/** 读取 node_modules 中某包的 package.json version 字段 */\r\nfunction readPackageVersion(packageDir: string): string | undefined {\r\n try {\r\n const pkgPath = path.join(packageDir, 'package.json')\r\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string }\r\n return pkg.version\r\n } catch {\r\n return undefined\r\n }\r\n}\r\n\r\n/** 将相对路径解析为基于项目根的绝对路径 */\r\nexport function resolveFromRoot(projectRoot: string, maybeRelative: string): string {\r\n if (path.isAbsolute(maybeRelative)) return maybeRelative\r\n return path.resolve(projectRoot, maybeRelative)\r\n}\r\n\r\n/** 生成 issue 唯一 id */\r\nexport function uniqueId(prefix: string, index: number): string {\r\n return `${prefix}-${index}`\r\n}\r\n\r\n/** 格式化体积显示(KB 或 MB) */\r\nexport function formatKb(kb: number): string {\r\n if (kb >= 1024) return `${(kb / 1024).toFixed(2)} MB`\r\n return `${kb.toFixed(2)} KB`\r\n}\r\n\r\n/** 递归创建目录 */\r\nexport function ensureDir(dir: string): void {\r\n fs.mkdirSync(dir, { recursive: true })\r\n}\r\n\r\n/** 绝对路径转相对项目根的路径 */\r\nexport function toRelative(projectRoot: string, absPath: string): string {\r\n return normalizePath(path.relative(projectRoot, absPath))\r\n}\r\n","/**\r\n * 缓存策略分析器\r\n *\r\n * 检测构建产物中:\r\n * 1. 静态资源文件名是否包含 content hash(无 hash → 难以长期缓存)\r\n * 2. HTML 入口文件的缓存策略建议(信息类,不计入浪费总量)\r\n */\r\nimport { CACHEABLE_EXTENSIONS, HASH_IN_FILENAME } from '../constants'\r\nimport type { AnalysisContext, ResourceIssue } from '../types'\r\nimport { estimateParseMs, uniqueId } from '../utils/fs'\r\n\r\nexport function analyzeCacheStrategy(ctx: AnalysisContext): ResourceIssue[] {\r\n if (!ctx.options.scanCacheStrategy) return []\r\n\r\n const issues: ResourceIssue[] = []\r\n let index = 0\r\n\r\n for (const file of ctx.bundleFiles) {\r\n if (file.type !== 'asset') continue\r\n\r\n const ext = getExtension(file.fileName)\r\n if (!CACHEABLE_EXTENSIONS.includes(ext)) continue\r\n\r\n const sizeKb = Math.round((file.size / 1024) * 100) / 100\r\n const hasHash = HASH_IN_FILENAME.test(file.fileName)\r\n\r\n if (!hasHash) {\r\n const isFont = /\\.(woff2?|ttf|otf)$/i.test(file.fileName)\r\n const isLargeAsset = sizeKb > 100\r\n\r\n issues.push({\r\n id: uniqueId('cache-miss', index++),\r\n category: 'cache-miss',\r\n severity: isLargeAsset || isFont ? 'high' : 'medium',\r\n title: '产物文件名缺少 content hash',\r\n file: file.fileName,\r\n detail: `静态资源 ${file.fileName} (${sizeKb} KB) 未包含 content hash,浏览器难以安全长期缓存,回访用户可能重复下载。`,\r\n suggestion: isFont\r\n ? '配置 vite build.assetsInlineLimit 与 rollup assetFileNames 含 [hash];CDN 设置 immutable 缓存。'\r\n : '在 vite.config 中设置 build.rollupOptions.output.assetFileNames 包含 [hash] 占位符。',\r\n cost: {\r\n transferKb: sizeKb,\r\n parseMsEstimate: isFont ? estimateParseMs(sizeKb * 0.1) : 0,\r\n description: isLargeAsset\r\n ? `回访用户可能重复传输 ${sizeKb} KB`\r\n : '缓存命中率低导致重复验证成本',\r\n },\r\n metadata: {\r\n sizeKb,\r\n extension: ext,\r\n hasContentHash: false,\r\n },\r\n })\r\n }\r\n }\r\n\r\n // HTML 入口:给出部署层面的缓存建议(informational 不计入浪费总量)\r\n const htmlFiles = ctx.bundleFiles.filter((f) => f.fileName.endsWith('.html'))\r\n for (const html of htmlFiles) {\r\n issues.push({\r\n id: uniqueId('cache-info', index++),\r\n category: 'cache-miss',\r\n severity: 'low',\r\n title: 'HTML 入口应使用短缓存',\r\n file: html.fileName,\r\n detail: `${html.fileName} 是 HTML 入口,通常应配置较短 max-age 或 no-cache,以确保用户获取最新资源引用。`,\r\n suggestion: '部署时为 HTML 设置 Cache-Control: no-cache;JS/CSS 等带 hash 资源设置长期缓存。',\r\n cost: {\r\n transferKb: Math.round((html.size / 1024) * 100) / 100,\r\n parseMsEstimate: 0,\r\n description: '配置建议,非浪费',\r\n },\r\n metadata: { informational: true },\r\n })\r\n }\r\n\r\n return issues\r\n}\r\n\r\n/** 提取文件扩展名(含点,小写) */\r\nfunction getExtension(fileName: string): string {\r\n const idx = fileName.lastIndexOf('.')\r\n return idx === -1 ? '' : fileName.slice(idx).toLowerCase()\r\n}\r\n","/**\r\n * 重复依赖 & 过多 Export 分析器\r\n *\r\n * - analyzeDuplicateDependencies:同一 npm 包在产物中出现多个版本\r\n * - analyzeUnusedExports:启发式检测 export 过多但引用者极少的模块\r\n */\r\nimport type { AnalysisContext, ResourceIssue } from '../types'\r\nimport { estimateParseMs, extractPackageFromModuleId, uniqueId } from '../utils/fs'\r\n\r\n/**\r\n * 遍历 bundle 中各 chunk 的 modules,统计 npm 包版本\r\n * 若同一包名对应多个 version → 报告重复依赖问题\r\n */\r\nexport function analyzeDuplicateDependencies(ctx: AnalysisContext): ResourceIssue[] {\r\n if (!ctx.options.scanDuplicateDeps) return []\r\n\r\n // packageName → (version → 引用次数)\r\n const packageVersions = new Map<string, Map<string, number>>()\r\n let index = 0\r\n const issues: ResourceIssue[] = []\r\n\r\n for (const bundleFile of ctx.bundleFiles) {\r\n if (bundleFile.type !== 'chunk') continue\r\n\r\n for (const moduleId of bundleFile.modules ?? []) {\r\n const pkg = extractPackageFromModuleId(moduleId)\r\n if (!pkg?.version) continue\r\n\r\n if (!packageVersions.has(pkg.name)) {\r\n packageVersions.set(pkg.name, new Map())\r\n }\r\n const versions = packageVersions.get(pkg.name)!\r\n versions.set(pkg.version, (versions.get(pkg.version) ?? 0) + 1)\r\n }\r\n }\r\n\r\n for (const [name, versions] of packageVersions) {\r\n if (versions.size <= 1) continue\r\n\r\n const versionList = [...versions.entries()]\r\n const totalRefs = versionList.reduce((sum, [, count]) => sum + count, 0)\r\n // 粗略估算:每个重复引用约 3KB 浪费\r\n const wasteKb = Math.max(5, Math.round(totalRefs * 3))\r\n\r\n issues.push({\r\n id: uniqueId('duplicate-dep', index++),\r\n category: 'duplicate-dependency',\r\n severity: 'medium',\r\n title: `重复依赖版本: ${name}`,\r\n file: name,\r\n detail: `依赖 ${name} 在产物中出现多个版本: ${versionList.map(([v, c]) => `${v}(${c}次)`).join(', ')}。`,\r\n suggestion: `使用 package.json overrides / pnpm.overrides 统一 ${name} 版本,避免重复打包与缓存失效。`,\r\n cost: {\r\n transferKb: wasteKb,\r\n parseMsEstimate: estimateParseMs(wasteKb),\r\n description: `预估重复传输约 ${wasteKb} KB`,\r\n },\r\n metadata: {\r\n packageName: name,\r\n versions: Object.fromEntries(versionList),\r\n },\r\n })\r\n }\r\n\r\n return issues\r\n}\r\n\r\n/**\r\n * 启发式检测「疑似过多未使用 export」\r\n * 条件:export 数量 >= 8 且 importers <= 1\r\n * 注意:这是保守信号,需结合 Knip/ESLint 进一步确认\r\n */\r\nexport function analyzeUnusedExports(ctx: AnalysisContext): ResourceIssue[] {\r\n const issues: ResourceIssue[] = []\r\n let index = 0\r\n\r\n for (const [moduleId, mod] of ctx.parsedModules) {\r\n if (moduleId.includes('node_modules') || moduleId.startsWith('\\0')) continue\r\n if (!mod.code) continue\r\n\r\n const exportMatches = [...mod.code.matchAll(/export\\s+(?:const|function|class|type|interface)\\s+(\\w+)/g)]\r\n if (exportMatches.length === 0) continue\r\n\r\n if (exportMatches.length >= 8 && mod.importers.size <= 1) {\r\n const sizeEstimate = Math.round((mod.code.length / 1024) * 0.3)\r\n\r\n issues.push({\r\n id: uniqueId('unused-export', index++),\r\n category: 'static-unused-js',\r\n severity: 'low',\r\n title: '疑似过多未使用 export',\r\n file: moduleId,\r\n detail: `${moduleId} 导出了 ${exportMatches.length} 个符号,但仅被 ${mod.importers.size} 个模块引用,可能存在未使用的 export。`,\r\n suggestion: '使用 Knip 或 ESLint 进一步确认,移除未使用的 export 以改善 tree-shaking。',\r\n cost: {\r\n transferKb: sizeEstimate,\r\n parseMsEstimate: estimateParseMs(sizeEstimate),\r\n description: '保守估计,需结合静态分析工具确认',\r\n },\r\n metadata: {\r\n exportCount: exportMatches.length,\r\n importerCount: mod.importers.size,\r\n },\r\n })\r\n }\r\n }\r\n\r\n return issues\r\n}\r\n","/**\r\n * Import 模式分析器\r\n *\r\n * 检测内容:\r\n * 1. 已知全量导入模式(lodash、moment、antd 等)\r\n * 2. barrel 文件(export * from)导致的 tree-shake 风险\r\n * 3. 疑似空组件文件\r\n *\r\n * 同时提供 collectSourceFiles / getReachableModules 供其他 analyzer 使用\r\n */\r\nimport path from 'node:path'\r\nimport fg from 'fast-glob'\r\nimport { FULL_IMPORT_PATTERNS, SOURCE_EXTENSIONS } from '../constants'\r\nimport type { AnalysisContext, ResourceIssue } from '../types'\r\nimport {\r\n estimateParseMs,\r\n normalizePath,\r\n readTextSafe,\r\n toRelative,\r\n uniqueId,\r\n} from '../utils/fs'\r\n\r\n/** 扫描源码中的低效 import 模式与 barrel 文件等问题 */\r\nexport async function analyzeImportPatterns(ctx: AnalysisContext): Promise<ResourceIssue[]> {\r\n if (!ctx.options.scanImportPatterns) return []\r\n\r\n const issues: ResourceIssue[] = []\r\n let index = 0\r\n\r\n for (const file of ctx.sourceFiles) {\r\n const content = readTextSafe(file)\r\n if (!content) continue\r\n\r\n const relativeFile = toRelative(ctx.projectRoot, file)\r\n\r\n // 逐条匹配 FULL_IMPORT_PATTERNS 规则库\r\n for (const rule of FULL_IMPORT_PATTERNS) {\r\n if (!rule.pattern.test(content)) continue\r\n\r\n // wasteKb 为未压缩估计值,gzip 后约为 35%\r\n const gzipEstimate = Math.round(rule.wasteKb * 0.35)\r\n issues.push({\r\n id: uniqueId('import-pattern', index++),\r\n category: 'import-pattern',\r\n severity: rule.severity,\r\n title: `全量/低效导入: ${rule.packageName}`,\r\n file: relativeFile,\r\n detail: `文件 ${relativeFile} 中存在可能导致 tree-shake 失效的导入模式(${rule.packageName})。`,\r\n suggestion: rule.suggestion,\r\n cost: {\r\n transferKb: gzipEstimate,\r\n parseMsEstimate: estimateParseMs(gzipEstimate),\r\n description: `预估 gzip 后额外传输约 ${gzipEstimate} KB`,\r\n },\r\n metadata: {\r\n packageName: rule.packageName,\r\n pattern: rule.pattern.source,\r\n },\r\n })\r\n }\r\n\r\n // 辅助信号:检测 export 形态,识别 barrel 文件等\r\n const hasDefaultExport = /export\\s+default/.test(content)\r\n const hasNamedExport = /export\\s+(const|function|class|type|interface|\\{)/.test(content)\r\n const isIndexBarrel = /export\\s+\\*\\s+from/.test(content)\r\n\r\n if (isIndexBarrel) {\r\n issues.push({\r\n id: uniqueId('import-pattern', index++),\r\n category: 'import-pattern',\r\n severity: 'medium',\r\n title: 'Barrel 文件可能导致 tree-shake 失效',\r\n file: relativeFile,\r\n detail: `${relativeFile} 使用了 export * from 模式,容易导致未使用的模块被一并打包。`,\r\n suggestion: '改为显式具名导出,或直接从源文件按需导入。',\r\n cost: {\r\n transferKb: 15,\r\n parseMsEstimate: estimateParseMs(15),\r\n description: 'Barrel 文件间接引入的冗余体积难以精确量化,此为保守估计',\r\n },\r\n })\r\n }\r\n\r\n // 疑似空组件:在 components 目录下且内容极少\r\n if (!hasDefaultExport && !hasNamedExport && relativeFile.includes('/components/')) {\r\n if (content.trim().length < 20) {\r\n issues.push({\r\n id: uniqueId('static-unused', index++),\r\n category: 'static-unused-js',\r\n severity: 'low',\r\n title: '疑似空组件文件',\r\n file: relativeFile,\r\n detail: `${relativeFile} 几乎没有有效内容,可能是遗留文件。`,\r\n suggestion: '确认是否仍需要,不需要则删除。',\r\n cost: {\r\n transferKb: 0,\r\n parseMsEstimate: 0,\r\n description: '维护成本浪费',\r\n },\r\n })\r\n }\r\n }\r\n }\r\n\r\n return issues\r\n}\r\n\r\n/**\r\n * 使用 fast-glob 收集 src 下全部源码文件\r\n * 结果写入 ctx.sourceFiles,供不可达分析等使用\r\n */\r\nexport async function collectSourceFiles(ctx: AnalysisContext): Promise<string[]> {\r\n const rootDir = path.resolve(ctx.projectRoot, ctx.options.root ?? 'src')\r\n const patterns = [\r\n ...SOURCE_EXTENSIONS.map((ext) => `**/*${ext}`),\r\n ...(ctx.options.include ?? []),\r\n ]\r\n\r\n const files = await fg(patterns, {\r\n cwd: rootDir,\r\n absolute: true,\r\n ignore: ctx.options.exclude,\r\n onlyFiles: true,\r\n })\r\n\r\n return files.map((f) => normalizePath(f))\r\n}\r\n\r\n/**\r\n * 从构建入口出发 BFS 遍历 module graph,得到所有静态可达模块\r\n * 用于判断源文件是否「不可达」\r\n */\r\nexport function getReachableModules(ctx: AnalysisContext): Set<string> {\r\n const reachable = new Set<string>()\r\n const queue = [...ctx.entries]\r\n\r\n while (queue.length) {\r\n const current = queue.shift()!\r\n if (reachable.has(current)) continue\r\n reachable.add(current)\r\n\r\n const mod = ctx.parsedModules.get(current)\r\n if (!mod) continue\r\n\r\n for (const dep of mod.importedIds) {\r\n if (!reachable.has(dep)) queue.push(dep)\r\n }\r\n }\r\n\r\n return reachable\r\n}\r\n","/**\r\n * 不可达文件 / 异步 Chunk 分析器\r\n *\r\n * - analyzeUnreachableFiles:对比「glob 扫到的源文件」与「module graph 可达集合」\r\n * - analyzeOrphanChunks:找出仅被 dynamic import 引用、未被静态引用的 chunk\r\n */\r\nimport path from 'node:path'\r\nimport type { AnalysisContext, ResourceIssue } from '../types'\r\nimport {\r\n estimateParseMs,\r\n fileSizeKb,\r\n normalizePath,\r\n toRelative,\r\n uniqueId,\r\n} from '../utils/fs'\r\nimport { getReachableModules } from './import-pattern-analyzer'\r\n\r\n/**\r\n * 检测静态不可达源文件\r\n * 逻辑:src 中存在,但不在 entry → module graph 可达集合中,且无 importers\r\n */\r\nexport function analyzeUnreachableFiles(ctx: AnalysisContext): ResourceIssue[] {\r\n if (!ctx.options.scanUnreachable) return []\r\n\r\n const issues: ResourceIssue[] = []\r\n const reachable = getReachableModules(ctx)\r\n let index = 0\r\n\r\n // 将 module graph 中的模块 id 统一转为绝对路径集合\r\n const reachableAbsPaths = new Set<string>()\r\n for (const moduleId of reachable) {\r\n // 跳过 Rollup 虚拟模块\r\n if (moduleId.startsWith('\\0') || moduleId.includes('virtual:')) continue\r\n const abs = path.isAbsolute(moduleId)\r\n ? normalizePath(moduleId)\r\n : normalizePath(path.resolve(ctx.projectRoot, moduleId))\r\n reachableAbsPaths.add(abs)\r\n }\r\n\r\n for (const sourceFile of ctx.sourceFiles) {\r\n const normalized = normalizePath(sourceFile)\r\n\r\n // 构建入口本身不算不可达\r\n const isEntry = ctx.entries.some((e) => normalizePath(path.resolve(e)) === normalized)\r\n if (isEntry) continue\r\n\r\n // 路径模糊匹配:处理 Vite 解析前后路径差异\r\n const isReachable = [...reachableAbsPaths].some(\r\n (r) => r === normalized || normalized.endsWith(r) || r.endsWith(normalized),\r\n )\r\n\r\n // 双重保险:若 module graph 中有 importers 记录,也视为可达\r\n const mod = ctx.parsedModules.get(normalized) ?? ctx.parsedModules.get(sourceFile)\r\n const hasImporters = mod && mod.importers.size > 0\r\n\r\n if (isReachable || hasImporters) continue\r\n\r\n const sizeKb = fileSizeKb(sourceFile)\r\n const relativeFile = toRelative(ctx.projectRoot, sourceFile)\r\n\r\n // 页面/组件类文件不可达 → 严重级别更高(更可能是遗留代码)\r\n const isPageOrComponent =\r\n /pages?|views?|components?|routes?/i.test(relativeFile) &&\r\n /\\.(vue|tsx|jsx)$/.test(relativeFile)\r\n\r\n issues.push({\r\n id: uniqueId('unreachable', index++),\r\n category: 'unreachable-component',\r\n severity: isPageOrComponent ? 'high' : 'medium',\r\n title: isPageOrComponent ? '不可达页面/组件' : '不可达源文件',\r\n file: relativeFile,\r\n detail: `${relativeFile} 未出现在任何构建入口的 module graph 中,属于静态不可达代码。`,\r\n suggestion: isPageOrComponent\r\n ? '若业务已下线,删除该页面/组件及关联静态资源。'\r\n : '确认是否仍被动态路径引用;若无,请删除或移出 src。',\r\n cost: {\r\n transferKb: sizeKb,\r\n parseMsEstimate: estimateParseMs(sizeKb),\r\n description: `文件本体 ${sizeKb} KB;若被 glob 扫入可能产生更大 chunk 浪费`,\r\n },\r\n metadata: {\r\n isPageOrComponent,\r\n },\r\n })\r\n }\r\n\r\n return issues\r\n}\r\n\r\n/**\r\n * 检测「独立异步 chunk」\r\n * 从 entry chunk 静态 BFS 无法到达的 chunk,通常仅由 dynamic import 加载\r\n * 本身不是浪费,但若被 prefetch 则会产生加载浪费\r\n */\r\nexport function analyzeOrphanChunks(ctx: AnalysisContext): ResourceIssue[] {\r\n const issues: ResourceIssue[] = []\r\n let index = 0\r\n\r\n const entryChunks = ctx.bundleFiles.filter((f) => f.isEntry)\r\n const allChunkNames = new Set(ctx.bundleFiles.filter((f) => f.type === 'chunk').map((f) => f.fileName))\r\n\r\n // 从 entry chunk 出发,沿静态 imports 边 BFS\r\n const reachableChunks = new Set<string>()\r\n const queue = entryChunks.map((c) => c.fileName)\r\n\r\n while (queue.length) {\r\n const name = queue.shift()!\r\n if (reachableChunks.has(name)) continue\r\n reachableChunks.add(name)\r\n\r\n const chunk = ctx.bundleFiles.find((f) => f.fileName === name)\r\n if (!chunk) continue\r\n\r\n for (const imp of chunk.imports ?? []) {\r\n if (allChunkNames.has(imp) && !reachableChunks.has(imp)) queue.push(imp)\r\n }\r\n // dynamicImports 不参与静态可达性(lazy chunk 设计上就是按需加载)\r\n }\r\n\r\n for (const chunk of ctx.bundleFiles) {\r\n if (chunk.type !== 'chunk' || chunk.isEntry) continue\r\n if (reachableChunks.has(chunk.fileName)) continue\r\n\r\n const sizeKb = Math.round((chunk.size / 1024) * 100) / 100\r\n // 启发式:文件名含 legacy/admin 的更可能被误 prefetch\r\n const isDynamicOnly = chunk.fileName.includes('legacy') || chunk.fileName.includes('admin')\r\n\r\n issues.push({\r\n id: uniqueId('orphan-chunk', index++),\r\n category: 'static-unused-js',\r\n severity: 'low',\r\n title: '独立异步 chunk(未被静态引用)',\r\n file: chunk.fileName,\r\n detail: `产物 chunk ${chunk.fileName} (${sizeKb} KB) 仅通过动态 import 加载,本身不是浪费,但若被全站 prefetch 则会产生加载浪费。`,\r\n suggestion: '检查是否对该 chunk 配置了不必要的 prefetch/preload。',\r\n cost: {\r\n transferKb: 0,\r\n parseMsEstimate: 0,\r\n description: '本身为按需加载;浪费取决于 prefetch 策略',\r\n },\r\n metadata: {\r\n chunkSizeKb: sizeKb,\r\n dynamicOnly: true,\r\n suspiciousPrefetch: isDynamicOnly,\r\n },\r\n })\r\n }\r\n\r\n return issues\r\n}\r\n","/**\r\n * 分析器调度中心\r\n * 负责并行运行各 analyzer、去重、按优先级排序\r\n */\r\nimport { analyzeCacheStrategy } from './cache-analyzer'\r\nimport { analyzeDuplicateDependencies, analyzeUnusedExports } from './duplicate-deps-analyzer'\r\nimport { analyzeImportPatterns } from './import-pattern-analyzer'\r\nimport { analyzeOrphanChunks, analyzeUnreachableFiles } from './unreachable-analyzer'\r\nimport type { AnalysisContext, ResourceIssue } from '../types'\r\n\r\n/** 并行运行所有分析器,合并结果并去重 */\r\nexport async function runAllAnalyzers(ctx: AnalysisContext): Promise<ResourceIssue[]> {\r\n const results = await Promise.all([\r\n analyzeImportPatterns(ctx), // 低效 import 模式、barrel 文件等\r\n Promise.resolve(analyzeUnreachableFiles(ctx)), // 静态不可达源文件\r\n Promise.resolve(analyzeOrphanChunks(ctx)), // 仅动态引用的 async chunk\r\n Promise.resolve(analyzeCacheStrategy(ctx)), // 产物 cache 策略\r\n Promise.resolve(analyzeDuplicateDependencies(ctx)), // 重复 npm 依赖版本\r\n Promise.resolve(analyzeUnusedExports(ctx)), // 疑似过多 export\r\n ])\r\n\r\n return dedupeIssues(results.flat())\r\n}\r\n\r\n/** 按 category + file + title 去重,避免同一问题重复报告 */\r\nfunction dedupeIssues(issues: ResourceIssue[]): ResourceIssue[] {\r\n const seen = new Set<string>()\r\n return issues.filter((issue) => {\r\n const key = `${issue.category}:${issue.file}:${issue.title}`\r\n if (seen.has(key)) return false\r\n seen.add(key)\r\n return true\r\n })\r\n}\r\n\r\n/**\r\n * 问题排序:先按严重级别(high → low),再按浪费体积降序\r\n * 便于报告 Top issues 展示最需要优先处理的问题\r\n */\r\nexport function sortIssues(issues: ResourceIssue[]): ResourceIssue[] {\r\n const severityOrder = { high: 0, medium: 1, low: 2 }\r\n return [...issues].sort((a, b) => {\r\n const s = severityOrder[a.severity] - severityOrder[b.severity]\r\n if (s !== 0) return s\r\n return b.cost.transferKb - a.cost.transferKb\r\n })\r\n}\r\n","/**\r\n * Module Graph 与 Bundle 产物解析\r\n *\r\n * - recordModuleParsed:在 moduleParsed 钩子中累积模块依赖关系\r\n * - extractBundleFiles:从 Rollup OutputBundle 提取 chunk/asset 元信息\r\n */\r\nimport type { OutputBundle, OutputChunk, OutputAsset } from 'rollup'\r\nimport type { AnalysisContext, BundleFileInfo, ParsedModuleInfo } from '../types'\r\nimport { normalizePath } from '../utils/fs'\r\n\r\n/**\r\n * 记录单个模块的解析结果,并维护双向依赖关系\r\n * @param parsedModules 全局模块图 Map\r\n * @param id 模块 id(通常是绝对路径)\r\n * @param code 模块源码(可选,用于 export 分析)\r\n * @param importedIds 该模块 import 的模块 id 列表\r\n * @param isEntry 是否为构建入口\r\n */\r\nexport function recordModuleParsed(\r\n parsedModules: Map<string, ParsedModuleInfo>,\r\n id: string,\r\n code: string | null,\r\n importedIds: string[],\r\n isEntry: boolean,\r\n): void {\r\n const normalizedId = normalizePath(id)\r\n\r\n // 获取或初始化当前模块节点\r\n const existing = parsedModules.get(normalizedId) ?? {\r\n id: normalizedId,\r\n importers: new Set<string>(),\r\n importedIds: new Set<string>(),\r\n dynamicImporters: new Set<string>(),\r\n isEntry: false,\r\n code: undefined,\r\n }\r\n\r\n existing.isEntry = existing.isEntry || isEntry\r\n if (code) existing.code = code\r\n\r\n // 记录本模块 → 依赖模块 的边\r\n for (const dep of importedIds) {\r\n existing.importedIds.add(normalizePath(dep))\r\n }\r\n parsedModules.set(normalizedId, existing)\r\n\r\n // 反向记录 依赖模块 → 本模块 的 importers 边\r\n for (const dep of importedIds) {\r\n const depId = normalizePath(dep)\r\n const depMod = parsedModules.get(depId) ?? {\r\n id: depId,\r\n importers: new Set<string>(),\r\n importedIds: new Set<string>(),\r\n dynamicImporters: new Set<string>(),\r\n isEntry: false,\r\n }\r\n depMod.importers.add(normalizedId)\r\n parsedModules.set(depId, depMod)\r\n }\r\n}\r\n\r\n/**\r\n * 将 Rollup 的 OutputBundle 转为统一的 BundleFileInfo 数组\r\n * 供 cache-analyzer、duplicate-deps-analyzer 等使用\r\n */\r\nexport function extractBundleFiles(bundle: OutputBundle): BundleFileInfo[] {\r\n const files: BundleFileInfo[] = []\r\n\r\n for (const [fileName, item] of Object.entries(bundle)) {\r\n if (item.type === 'chunk') {\r\n const chunk = item as OutputChunk\r\n files.push({\r\n fileName,\r\n type: 'chunk',\r\n size: Buffer.byteLength(chunk.code, 'utf-8'),\r\n modules: Object.keys(chunk.modules ?? {}),\r\n isEntry: chunk.isEntry ?? false,\r\n imports: chunk.imports,\r\n dynamicImports: chunk.dynamicImports,\r\n })\r\n } else {\r\n const asset = item as OutputAsset\r\n const source = asset.source\r\n // asset 可能是 string 或 Uint8Array\r\n const size =\r\n typeof source === 'string'\r\n ? Buffer.byteLength(source, 'utf-8')\r\n : source instanceof Uint8Array\r\n ? source.byteLength\r\n : 0\r\n\r\n files.push({\r\n fileName,\r\n type: 'asset',\r\n size,\r\n })\r\n }\r\n }\r\n\r\n return files\r\n}\r\n\r\n/** 将 bundle 文件信息合并进分析上下文(工具函数,便于扩展) */\r\nexport function mergeAnalysisContext(\r\n ctx: AnalysisContext,\r\n bundleFiles: BundleFileInfo[],\r\n): AnalysisContext {\r\n return {\r\n ...ctx,\r\n bundleFiles,\r\n }\r\n}\r\n","/**\r\n * 报告生成模块\r\n *\r\n * - buildReport:汇总 issues 为 ResourceWasteReport 结构\r\n * - renderHtmlReport:生成自包含 HTML 可视化报告\r\n * - printTerminalSummary:构建完成后在终端输出摘要\r\n */\r\nimport type { IssueCategory, IssueSeverity, ResourceWasteReport, ResourceIssue } from '../types'\r\nimport { CATEGORY_LABELS, SEVERITY_LABELS } from '../constants'\r\n\r\n/**\r\n * 将 issue 列表聚合为完整报告对象\r\n * 同时计算 summary 中的分类计数与浪费总量\r\n */\r\nexport function buildReport(\r\n issues: ResourceIssue[],\r\n meta: {\r\n projectRoot: string\r\n mode: string\r\n outDir: string\r\n },\r\n): ResourceWasteReport {\r\n const byCategory = emptyCategoryCount()\r\n const bySeverity: Record<IssueSeverity, number> = { high: 0, medium: 0, low: 0 }\r\n\r\n let totalWasteTransferKb = 0\r\n let totalParseMsEstimate = 0\r\n\r\n for (const issue of issues) {\r\n byCategory[issue.category]++\r\n bySeverity[issue.severity]++\r\n\r\n // metadata.informational 为 true 的 issue 仅为建议,不计入浪费总量\r\n if (issue.metadata?.informational) continue\r\n totalWasteTransferKb += issue.cost.transferKb\r\n totalParseMsEstimate += issue.cost.parseMsEstimate\r\n }\r\n\r\n return {\r\n generatedAt: new Date().toISOString(),\r\n projectRoot: meta.projectRoot,\r\n mode: meta.mode,\r\n outDir: meta.outDir,\r\n summary: {\r\n totalIssues: issues.length,\r\n totalWasteTransferKb: Math.round(totalWasteTransferKb * 100) / 100,\r\n totalParseMsEstimate: Math.round(totalParseMsEstimate * 10) / 10,\r\n byCategory,\r\n bySeverity,\r\n },\r\n issues,\r\n }\r\n}\r\n\r\n/** 初始化各分类计数为 0 */\r\nfunction emptyCategoryCount(): Record<IssueCategory, number> {\r\n return {\r\n 'static-unused-js': 0,\r\n 'unreachable-component': 0,\r\n 'cache-miss': 0,\r\n 'duplicate-dependency': 0,\r\n 'import-pattern': 0,\r\n 'prefetch-waste': 0,\r\n }\r\n}\r\n\r\n/** 生成自包含 HTML 报告(无外部 CDN 依赖,可直接双击打开) */\r\nexport function renderHtmlReport(report: ResourceWasteReport): string {\r\n const { summary, issues } = report\r\n\r\n const issueRows = issues\r\n .map(\r\n (issue) => `\r\n <tr class=\"severity-${issue.severity}\">\r\n <td><span class=\"badge ${issue.severity}\">${SEVERITY_LABELS[issue.severity]}</span></td>\r\n <td>${CATEGORY_LABELS[issue.category]}</td>\r\n <td><strong>${escapeHtml(issue.title)}</strong><br/><small>${escapeHtml(issue.detail)}</small></td>\r\n <td><code>${escapeHtml(issue.file ?? '-')}</code></td>\r\n <td>${issue.cost.transferKb > 0 ? `${issue.cost.transferKb} KB` : '-'}</td>\r\n <td>${issue.cost.parseMsEstimate > 0 ? `${issue.cost.parseMsEstimate} ms` : '-'}</td>\r\n <td>${escapeHtml(issue.suggestion)}</td>\r\n </tr>`,\r\n )\r\n .join('')\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"zh-CN\">\r\n<head>\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>Resource Cost Report</title>\r\n <style>\r\n :root {\r\n --bg: #0f172a;\r\n --card: #1e293b;\r\n --text: #e2e8f0;\r\n --muted: #94a3b8;\r\n --high: #ef4444;\r\n --medium: #f59e0b;\r\n --low: #22c55e;\r\n --accent: #38bdf8;\r\n }\r\n * { box-sizing: border-box; }\r\n body {\r\n margin: 0;\r\n font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;\r\n background: var(--bg);\r\n color: var(--text);\r\n line-height: 1.5;\r\n }\r\n .container { max-width: 1200px; margin: 0 auto; padding: 24px; }\r\n h1 { margin: 0 0 8px; font-size: 28px; }\r\n .sub { color: var(--muted); margin-bottom: 24px; }\r\n .cards {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\r\n gap: 16px;\r\n margin-bottom: 24px;\r\n }\r\n .card {\r\n background: var(--card);\r\n border-radius: 12px;\r\n padding: 16px;\r\n border: 1px solid #334155;\r\n }\r\n .card .label { color: var(--muted); font-size: 13px; }\r\n .card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }\r\n table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n background: var(--card);\r\n border-radius: 12px;\r\n overflow: hidden;\r\n border: 1px solid #334155;\r\n }\r\n th, td {\r\n padding: 12px;\r\n text-align: left;\r\n vertical-align: top;\r\n border-bottom: 1px solid #334155;\r\n font-size: 14px;\r\n }\r\n th { background: #0b1220; color: var(--muted); font-weight: 600; }\r\n tr:last-child td { border-bottom: none; }\r\n code { background: #0b1220; padding: 2px 6px; border-radius: 4px; font-size: 12px; }\r\n .badge {\r\n display: inline-block;\r\n padding: 2px 8px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n }\r\n .badge.high { background: rgba(239,68,68,.2); color: var(--high); }\r\n .badge.medium { background: rgba(245,158,11,.2); color: var(--medium); }\r\n .badge.low { background: rgba(34,197,94,.2); color: var(--low); }\r\n .legend { margin-top: 16px; color: var(--muted); font-size: 13px; }\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"container\">\r\n <h1>前端资源浪费分析报告</h1>\r\n <p class=\"sub\">生成时间: ${report.generatedAt} · 模式: ${report.mode}</p>\r\n\r\n <div class=\"cards\">\r\n <div class=\"card\"><div class=\"label\">问题总数</div><div class=\"value\">${summary.totalIssues}</div></div>\r\n <div class=\"card\"><div class=\"label\">预估浪费传输</div><div class=\"value\">${summary.totalWasteTransferKb} KB</div></div>\r\n <div class=\"card\"><div class=\"label\">预估浪费解析</div><div class=\"value\">${summary.totalParseMsEstimate} ms</div></div>\r\n <div class=\"card\"><div class=\"label\">高危问题</div><div class=\"value\">${summary.bySeverity.high}</div></div>\r\n </div>\r\n\r\n <table>\r\n <thead>\r\n <tr>\r\n <th>级别</th>\r\n <th>类别</th>\r\n <th>问题</th>\r\n <th>文件</th>\r\n <th>传输浪费</th>\r\n <th>解析浪费</th>\r\n <th>建议</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n ${issueRows || '<tr><td colspan=\"7\">未检测到资源浪费问题 🎉</td></tr>'}\r\n </tbody>\r\n </table>\r\n\r\n <p class=\"legend\">\r\n 说明:报告包含构建期静态分析结果。未使用的 export 在 tree-shake 有效时不一定增大首包;\r\n 缓存类问题在回访用户场景下浪费更显著。\r\n </p>\r\n </div>\r\n</body>\r\n</html>`\r\n}\r\n\r\n/** 转义 HTML 特殊字符,防止 XSS(报告数据来自本地构建分析) */\r\nfunction escapeHtml(input: string): string {\r\n return input\r\n .replace(/&/g, '&')\r\n .replace(/</g, '<')\r\n .replace(/>/g, '>')\r\n .replace(/\"/g, '"')\r\n}\r\n\r\n/** 在终端打印构建摘要与 Top 5 问题 */\r\nexport function printTerminalSummary(report: ResourceWasteReport, thresholdKb: number): void {\r\n const { summary, issues } = report\r\n const top = issues.slice(0, 5)\r\n\r\n console.log('\\n[vite-plugin-resource-waste] Resource waste analysis complete\\n')\r\n console.log(` Issues : ${summary.totalIssues}`)\r\n console.log(` Waste (est.) : ${summary.totalWasteTransferKb} KB transfer / ${summary.totalParseMsEstimate} ms parse`)\r\n console.log(` Threshold : ${thresholdKb} KB`)\r\n\r\n if (top.length > 0) {\r\n console.log('\\n Top issues:')\r\n for (const issue of top) {\r\n console.log(` [${issue.severity.toUpperCase()}] ${issue.title}${issue.file ? ` (${issue.file})` : ''}`)\r\n }\r\n }\r\n\r\n console.log('')\r\n}\r\n"],"mappings":"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","names":["import_node_fs","import_node_path","path","fs","path","fs","path","import_node_path","path","fg","import_node_path","path","path","fs","pc"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 插件类型定义
|
|
5
|
+
* 包含:配置项、问题项、报告结构、分析上下文等核心数据结构
|
|
6
|
+
*/
|
|
7
|
+
/** 问题严重级别:高 / 中 / 低 */
|
|
8
|
+
type IssueSeverity = 'high' | 'medium' | 'low';
|
|
9
|
+
/** 问题分类,对应不同分析器产出的 issue 类型 */
|
|
10
|
+
type IssueCategory = 'static-unused-js' | 'unreachable-component' | 'cache-miss' | 'duplicate-dependency' | 'import-pattern' | 'prefetch-waste';
|
|
11
|
+
/** 插件用户可配置项 */
|
|
12
|
+
interface ResourceWastePluginOptions {
|
|
13
|
+
/** 源码根目录,默认 src */
|
|
14
|
+
root?: string;
|
|
15
|
+
/** 报告输出目录,相对 outDir,默认 resource-waste */
|
|
16
|
+
reportDir?: string;
|
|
17
|
+
/** 预估浪费阈值(KB),超出则在终端提示,配合 failOnThreshold 可用于 CI 拦截 */
|
|
18
|
+
wasteThresholdKb?: number;
|
|
19
|
+
/** 超出阈值是否在构建时抛出错误(用于 CI 构建阈值拦截) */
|
|
20
|
+
failOnThreshold?: boolean;
|
|
21
|
+
/** 是否生成 HTML 报告 */
|
|
22
|
+
htmlReport?: boolean;
|
|
23
|
+
/** 是否扫描 import 模式问题 */
|
|
24
|
+
scanImportPatterns?: boolean;
|
|
25
|
+
/** 是否扫描不可达文件 */
|
|
26
|
+
scanUnreachable?: boolean;
|
|
27
|
+
/** 是否分析产物 cache 策略 */
|
|
28
|
+
scanCacheStrategy?: boolean;
|
|
29
|
+
/** 是否检测重复依赖 */
|
|
30
|
+
scanDuplicateDeps?: boolean;
|
|
31
|
+
/** glob 扫描额外包含的文件模式 */
|
|
32
|
+
include?: string[];
|
|
33
|
+
/** 排除扫描的路径 */
|
|
34
|
+
exclude?: string[];
|
|
35
|
+
/** 静默模式,不输出终端摘要 */
|
|
36
|
+
silent?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** 单项问题的成本估算 */
|
|
39
|
+
interface CostEstimate {
|
|
40
|
+
transferKb: number;
|
|
41
|
+
parseMsEstimate: number;
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
/** 单条资源浪费问题 */
|
|
45
|
+
interface ResourceIssue {
|
|
46
|
+
id: string;
|
|
47
|
+
category: IssueCategory;
|
|
48
|
+
severity: IssueSeverity;
|
|
49
|
+
title: string;
|
|
50
|
+
file?: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
suggestion: string;
|
|
53
|
+
cost: CostEstimate;
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/** 报告摘要统计 */
|
|
57
|
+
interface ResourceWasteSummary {
|
|
58
|
+
totalIssues: number;
|
|
59
|
+
totalWasteTransferKb: number;
|
|
60
|
+
totalParseMsEstimate: number;
|
|
61
|
+
byCategory: Record<IssueCategory, number>;
|
|
62
|
+
bySeverity: Record<IssueSeverity, number>;
|
|
63
|
+
}
|
|
64
|
+
/** 完整分析报告结构(JSON 输出格式) */
|
|
65
|
+
interface ResourceWasteReport {
|
|
66
|
+
generatedAt: string;
|
|
67
|
+
projectRoot: string;
|
|
68
|
+
mode: string;
|
|
69
|
+
outDir: string;
|
|
70
|
+
summary: ResourceWasteSummary;
|
|
71
|
+
issues: ResourceIssue[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare function resourceWaste(options?: ResourceWastePluginOptions): Plugin;
|
|
75
|
+
|
|
76
|
+
export { type IssueCategory, type IssueSeverity, type ResourceIssue, type ResourceWastePluginOptions, type ResourceWasteReport, type ResourceWasteSummary, resourceWaste as default, resourceWaste };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 插件类型定义
|
|
5
|
+
* 包含:配置项、问题项、报告结构、分析上下文等核心数据结构
|
|
6
|
+
*/
|
|
7
|
+
/** 问题严重级别:高 / 中 / 低 */
|
|
8
|
+
type IssueSeverity = 'high' | 'medium' | 'low';
|
|
9
|
+
/** 问题分类,对应不同分析器产出的 issue 类型 */
|
|
10
|
+
type IssueCategory = 'static-unused-js' | 'unreachable-component' | 'cache-miss' | 'duplicate-dependency' | 'import-pattern' | 'prefetch-waste';
|
|
11
|
+
/** 插件用户可配置项 */
|
|
12
|
+
interface ResourceWastePluginOptions {
|
|
13
|
+
/** 源码根目录,默认 src */
|
|
14
|
+
root?: string;
|
|
15
|
+
/** 报告输出目录,相对 outDir,默认 resource-waste */
|
|
16
|
+
reportDir?: string;
|
|
17
|
+
/** 预估浪费阈值(KB),超出则在终端提示,配合 failOnThreshold 可用于 CI 拦截 */
|
|
18
|
+
wasteThresholdKb?: number;
|
|
19
|
+
/** 超出阈值是否在构建时抛出错误(用于 CI 构建阈值拦截) */
|
|
20
|
+
failOnThreshold?: boolean;
|
|
21
|
+
/** 是否生成 HTML 报告 */
|
|
22
|
+
htmlReport?: boolean;
|
|
23
|
+
/** 是否扫描 import 模式问题 */
|
|
24
|
+
scanImportPatterns?: boolean;
|
|
25
|
+
/** 是否扫描不可达文件 */
|
|
26
|
+
scanUnreachable?: boolean;
|
|
27
|
+
/** 是否分析产物 cache 策略 */
|
|
28
|
+
scanCacheStrategy?: boolean;
|
|
29
|
+
/** 是否检测重复依赖 */
|
|
30
|
+
scanDuplicateDeps?: boolean;
|
|
31
|
+
/** glob 扫描额外包含的文件模式 */
|
|
32
|
+
include?: string[];
|
|
33
|
+
/** 排除扫描的路径 */
|
|
34
|
+
exclude?: string[];
|
|
35
|
+
/** 静默模式,不输出终端摘要 */
|
|
36
|
+
silent?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** 单项问题的成本估算 */
|
|
39
|
+
interface CostEstimate {
|
|
40
|
+
transferKb: number;
|
|
41
|
+
parseMsEstimate: number;
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
/** 单条资源浪费问题 */
|
|
45
|
+
interface ResourceIssue {
|
|
46
|
+
id: string;
|
|
47
|
+
category: IssueCategory;
|
|
48
|
+
severity: IssueSeverity;
|
|
49
|
+
title: string;
|
|
50
|
+
file?: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
suggestion: string;
|
|
53
|
+
cost: CostEstimate;
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/** 报告摘要统计 */
|
|
57
|
+
interface ResourceWasteSummary {
|
|
58
|
+
totalIssues: number;
|
|
59
|
+
totalWasteTransferKb: number;
|
|
60
|
+
totalParseMsEstimate: number;
|
|
61
|
+
byCategory: Record<IssueCategory, number>;
|
|
62
|
+
bySeverity: Record<IssueSeverity, number>;
|
|
63
|
+
}
|
|
64
|
+
/** 完整分析报告结构(JSON 输出格式) */
|
|
65
|
+
interface ResourceWasteReport {
|
|
66
|
+
generatedAt: string;
|
|
67
|
+
projectRoot: string;
|
|
68
|
+
mode: string;
|
|
69
|
+
outDir: string;
|
|
70
|
+
summary: ResourceWasteSummary;
|
|
71
|
+
issues: ResourceIssue[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
declare function resourceWaste(options?: ResourceWastePluginOptions): Plugin;
|
|
75
|
+
|
|
76
|
+
export { type IssueCategory, type IssueSeverity, type ResourceIssue, type ResourceWastePluginOptions, type ResourceWasteReport, type ResourceWasteSummary, resourceWaste as default, resourceWaste };
|