mp-lens
Version:
微信小程序分析工具 (Unused Code, Dependencies, Visualization)
220 lines • 11.2 kB
JavaScript
;
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