UNPKG

mp-lens

Version:

微信小程序分析工具 (Unused Code, Dependencies, Visualization)

220 lines 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.purgewxss = purgewxss; const chalk_1 = __importDefault(require("chalk")); const promises_1 = __importDefault(require("fs/promises")); const glob_1 = require("glob"); const path_1 = __importDefault(require("path")); const purgecss_1 = require("purgecss"); const command_init_1 = require("../../utils/command-init"); const debug_logger_1 = require("../../utils/debug-logger"); const errors_1 = require("../../utils/errors"); const path_resolver_1 = require("../../utils/path-resolver"); const analyzeWxmlForPurge_1 = require("./analyzeWxmlForPurge"); async function performPurge(projectRoot, scanRoot, cmdOptions, pathResolver, context) { const { wxssFilePathInput, write } = cmdOptions; let commandHadErrors = false; let writeableChangesCount = 0; let totalPotentialSavings = 0; let wxssFilesToProcess = []; if (wxssFilePathInput) { const absolutePath = path_1.default.isAbsolute(wxssFilePathInput) ? wxssFilePathInput : path_1.default.resolve(projectRoot, wxssFilePathInput); try { const statResult = await promises_1.default.stat(absolutePath); if (!statResult.isFile()) { throw new errors_1.HandledError(`指定的 WXSS 输入不是一个文件: ${absolutePath}`); } } catch (e) { if (e.code === 'ENOENT') { throw new errors_1.HandledError(`WXSS 文件未找到: ${absolutePath}`); } throw new errors_1.HandledError(`无法访问 WXSS 文件: ${absolutePath} (${e.message})`); } if (path_1.default.extname(absolutePath) !== '.wxss') { throw new errors_1.HandledError(`输入文件不是 .wxss 文件: ${absolutePath}`); } wxssFilesToProcess.push(absolutePath); } else { const pattern = path_1.default.join(scanRoot, '**/*.wxss').replace(/\\/g, '/'); const excludePatterns = context.excludePatterns || []; const ignorePatterns = [ path_1.default.join(scanRoot, 'node_modules/**').replace(/\\/g, '/'), path_1.default.join(scanRoot, 'dist/**').replace(/\\/g, '/'), path_1.default.join(scanRoot, '**/iconfont.wxss').replace(/\\/g, '/'), path_1.default.join(scanRoot, '**/custom-theme.wxss').replace(/\\/g, '/'), ...excludePatterns.map((p) => path_1.default.join(scanRoot, p).replace(/\\/g, '/')), ]; try { wxssFilesToProcess = await (0, glob_1.glob)(pattern, { ignore: ignorePatterns, nodir: true, absolute: true, cwd: scanRoot, }); } catch (err) { throw new Error(`扫描 WXSS 文件时出错: ${err.message}`); } if (wxssFilesToProcess.length === 0) { debug_logger_1.logger.info(chalk_1.default.yellow('在指定目录未找到 WXSS 文件 (或所有文件都被忽略)。')); return; } } if (wxssFilesToProcess.length > 0 && !wxssFilePathInput) { const rootForDisplay = path_1.default.relative(projectRoot, scanRoot) || '.'; debug_logger_1.logger.info(`将在 ${rootForDisplay} 目录中分析 ${wxssFilesToProcess.length} 个 WXSS 文件...`); } for (const wxssFilePath of wxssFilesToProcess) { const relativeWxssPath = path_1.default.relative(projectRoot, wxssFilePath); let statusMessage = ''; const wxmlFilePath = wxssFilePath.replace(/\.wxss$/, '.wxml'); try { await promises_1.default.access(wxmlFilePath); } catch (_a) { statusMessage = chalk_1.default.yellow('跳过 (WXML 未找到)'); debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); continue; } try { const wxmlAnalysisResult = await (0, analyzeWxmlForPurge_1.analyzeWxmlForPurge)(wxmlFilePath, pathResolver, new Set()); if (wxmlAnalysisResult.wxmlFilePaths.size === 0) { try { await promises_1.default.access(wxmlFilePath); } catch (_b) { statusMessage = chalk_1.default.yellow('跳过 (WXML 未找到)'); debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); continue; } statusMessage = chalk_1.default.yellow('跳过 (WXML 分析失败或为空)'); debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); continue; } // Skip WXSS processing if its corresponding WXML has risky dynamic class patterns if (wxmlAnalysisResult.riskyDynamicClassPatterns && wxmlAnalysisResult.riskyDynamicClassPatterns.length > 0) { statusMessage = chalk_1.default.yellow('跳过 (WXML 中检测到有风险的动态类名用法)'); debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); debug_logger_1.logger.warn(` 详情: WXML 文件 ${wxmlFilePath} 或其导入包含以下风险用法:`); wxmlAnalysisResult.riskyDynamicClassPatterns.forEach((pattern) => { // Show relative path to the specific WXML file containing the risky pattern const riskyFilePathRelative = path_1.default.relative(projectRoot, pattern.filePath); debug_logger_1.logger.warn(` - 文件: ${riskyFilePathRelative}, 表达式: ${pattern.expression}`); }); continue; } const wxssContent = await promises_1.default.readFile(wxssFilePath, 'utf-8'); if (!wxssContent.trim()) { statusMessage = chalk_1.default.gray('跳过 (WXSS 文件为空)'); debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); continue; } const safelistStandard = [ ...wxmlAnalysisResult.tagNames, ...wxmlAnalysisResult.staticClassNames, ]; wxmlAnalysisResult.dynamicClassValues.forEach((dynValue) => { const innerContent = dynValue.substring(2, dynValue.length - 2); const words = innerContent.match(/[a-zA-Z0-9_-]+/g) || []; words.forEach((word) => { if (word.length > 1 && !safelistStandard.includes(word)) { safelistStandard.push(word); } }); }); const commonTagsToExclude = new Set(['block', 'template', 'slot']); const finalSafeList = safelistStandard.filter((s) => typeof s === 'string' ? !commonTagsToExclude.has(s) : true); const purger = new purgecss_1.PurgeCSS(); const purgeResults = await purger.purge({ content: await Promise.all(Array.from(wxmlAnalysisResult.wxmlFilePaths).map(async (fp) => ({ raw: await promises_1.default.readFile(fp, 'utf-8'), extension: 'wxml', }))), css: [{ raw: wxssContent, name: path_1.default.basename(wxssFilePath) }], safelist: { standard: finalSafeList }, }); const originalSize = Buffer.byteLength(wxssContent, 'utf-8'); if (purgeResults.length > 0 && purgeResults[0].css) { const purgedCss = purgeResults[0].css; const newSize = Buffer.byteLength(purgedCss, 'utf-8'); const diff = originalSize - newSize; if (diff > 0) { statusMessage = chalk_1.default.green(`节省 ${diff}B`); if (write) { await promises_1.default.writeFile(wxssFilePath, purgedCss); statusMessage += chalk_1.default.blue(' (已写入)'); } else { writeableChangesCount++; totalPotentialSavings += diff; } } else if (diff === 0) { statusMessage = chalk_1.default.gray('无变化'); } else { statusMessage = chalk_1.default.yellow(`增大 ${-diff}B (检查 safelist)`); } } else if (purgeResults.length > 0 && purgeResults[0].css === '') { if (wxssContent.trim().length > 0) { statusMessage = chalk_1.default.yellow('可清空'); if (write) { await promises_1.default.writeFile(wxssFilePath, ''); statusMessage += chalk_1.default.blue(' (已写入)'); } else { writeableChangesCount++; totalPotentialSavings += originalSize; } } else { statusMessage = chalk_1.default.gray('无变化 (文件已为空)'); } } else { statusMessage = chalk_1.default.red('PurgeCSS 处理失败'); } debug_logger_1.logger.info(`${relativeWxssPath} ${statusMessage}`); } catch (error) { debug_logger_1.logger.error(chalk_1.default.red(` 处理文件 ${relativeWxssPath} 时出错: ${error.message}`)); commandHadErrors = true; } } if (!write && writeableChangesCount > 0) { const savingsInKb = (totalPotentialSavings / 1024).toFixed(1); debug_logger_1.logger.info(chalk_1.default.yellow(`检测到 ${writeableChangesCount} 个文件有可优化空间 (合计约 ${savingsInKb}KB)。请使用 --write 参数实际写入更改。`)); } if (commandHadErrors) { debug_logger_1.logger.error(chalk_1.default.red('PurgeWXSS 命令执行完毕,但出现错误。')); if (!process.exitCode) process.exitCode = 1; } else { if (wxssFilesToProcess.length > 0 && !commandHadErrors) { // Consider if this final message is needed if all files were skipped or had individual statuses } debug_logger_1.logger.info(chalk_1.default.green('PurgeWXSS 分析完成。')); } } async function purgewxss(cliOptions, wxssFilePath, cmdOptions) { const context = await (0, command_init_1.initializeCommandContext)(cliOptions); const { projectRoot, miniappRoot } = context; const scanRoot = miniappRoot || projectRoot; const pathResolver = new path_resolver_1.PathResolver(projectRoot, context); const enhancedCmdOptions = { ...cmdOptions, wxssFilePathInput: wxssFilePath || (cmdOptions === null || cmdOptions === void 0 ? void 0 : cmdOptions.wxssFilePathInput), }; await performPurge(projectRoot, scanRoot, enhancedCmdOptions, pathResolver, context); } //# sourceMappingURL=index.js.map