UNPKG

mp-lens

Version:

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

447 lines 19.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.lint = lint; const chalk_1 = __importDefault(require("chalk")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const analyzer_1 = require("../../analyzer/analyzer"); const command_init_1 = require("../../utils/command-init"); const debug_logger_1 = require("../../utils/debug-logger"); const errors_1 = require("../../utils/errors"); const id_helper_1 = require("../../utils/id-helper"); const path_resolver_1 = require("../../utils/path-resolver"); const analyzeWxmlTags_1 = require("./analyzeWxmlTags"); const component_linter_1 = require("./component-linter"); /** * Reads global components from app.json * * @param pathResolver PathResolver instance * @param miniappRoot Miniapp root directory * @returns Object with global component definitions */ async function readGlobalComponents(pathResolver, miniappRoot) { try { const appJsonPath = path.join(miniappRoot ? miniappRoot : pathResolver['projectRoot'], 'app.json'); if (!fs.existsSync(appJsonPath)) { debug_logger_1.logger.warn('Could not find app.json in miniapp root'); return {}; } const appJsonContent = fs.readFileSync(appJsonPath, 'utf-8'); const appJson = JSON.parse(appJsonContent); return appJson.usingComponents || {}; } catch (err) { debug_logger_1.logger.warn(`Error reading global components: ${err}`); return {}; } } /** * Processes a WXML/JSON file pair * * @param ctx FilePairContext object */ async function processFilePair(ctx) { const { wxmlPath, jsonPath, pathResolver, globalComponents, result, miniappRoot } = ctx; try { debug_logger_1.logger.debug(`Processing file pair: ${wxmlPath} and ${jsonPath}`); result.summary.filesScanned++; // Analyze WXML tags const usedTagToFiles = await (0, analyzeWxmlTags_1.analyzeWxmlTags)(wxmlPath, pathResolver); // Lint component usage const lintIssue = (0, component_linter_1.lintComponentUsage)(wxmlPath, jsonPath, usedTagToFiles, globalComponents); // Read and parse JSON file to determine type and generate canonical ID const jsonContent = fs.readFileSync(jsonPath, 'utf-8'); const jsonData = JSON.parse(jsonContent); const isComponent = jsonData.component === true; const type = isComponent ? 'Component' : 'Page'; const { id } = (0, id_helper_1.generateNodeIdAndLabel)(type, wxmlPath, miniappRoot); lintIssue.id = id; if (lintIssue.declaredNotUsed.length > 0 || lintIssue.usedNotDeclared.length > 0) { result.summary.filesWithIssues++; result.summary.declaredNotUsedCount += lintIssue.declaredNotUsed.length; result.summary.usedNotDeclaredCount += lintIssue.usedNotDeclared.length; result.issues.push(lintIssue); } } catch (err) { debug_logger_1.logger.error(`Error processing file pair: ${err}`); } } /** * Processes a directory to find all WXML/JSON file pairs * * @param dirPath Directory path * @param pathResolver PathResolver instance * @param globalComponents Global component definitions * @param result Result object to populate */ async function processDirectory(dirPath, pathResolver, globalComponents, result, miniappRoot, // absolute path projectRoot) { try { debug_logger_1.logger.info(`正在处理目录: ${dirPath}`); const files = fs.readdirSync(dirPath); const wxmlFiles = files.filter((file) => path.extname(file) === '.wxml'); for (const wxmlFile of wxmlFiles) { const wxmlPath = path.join(dirPath, wxmlFile); const baseName = path.basename(wxmlFile, '.wxml'); const jsonPath = path.join(dirPath, `${baseName}.json`); if (fs.existsSync(jsonPath)) { await processFilePair({ wxmlPath, jsonPath, pathResolver, globalComponents, result, miniappRoot, projectRoot, }); } else { debug_logger_1.logger.warn(`No corresponding JSON file found for ${wxmlPath}`); } } for (const file of files) { const filePath = path.join(dirPath, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { await processDirectory(filePath, pathResolver, globalComponents, result, miniappRoot, projectRoot); } } } catch (err) { debug_logger_1.logger.error(`Error processing directory ${dirPath}: ${err}`); } } /** * Processes a single file (WXML or JSON) * * @param filePath File path * @param pathResolver PathResolver instance * @param globalComponents Global component definitions * @param result Result object to populate */ async function processFile(filePath, pathResolver, globalComponents, result, miniappRoot, // absolute path projectRoot) { const ext = path.extname(filePath); const baseName = path.basename(filePath, ext); const dirPath = path.dirname(filePath); let wxmlPath = null; let jsonPath = null; if (ext === '.wxml') { wxmlPath = filePath; jsonPath = path.join(dirPath, `${baseName}.json`); } else if (ext === '.json') { jsonPath = filePath; wxmlPath = path.join(dirPath, `${baseName}.wxml`); } else { throw new errors_1.HandledError(`不支持的文件类型: ${filePath}. 只支持 .wxml 或 .json 文件。`); } if (!fs.existsSync(wxmlPath)) { throw new errors_1.HandledError(`对应的 WXML 文件未找到: ${wxmlPath}`); } if (!fs.existsSync(jsonPath)) { throw new errors_1.HandledError(`对应的 JSON 文件未找到: ${jsonPath}`); } await processFilePair({ wxmlPath, jsonPath, pathResolver, globalComponents, result, miniappRoot, projectRoot, }); } /** * Processes a specific target path (file or directory) * * @param targetPath Path to process * @param pathResolver PathResolver instance * @param globalComponents Global component definitions * @param result Result object to populate */ async function processTargetPath(targetPath, pathResolver, globalComponents, result, miniappRoot, // absolute path projectRoot) { // 直接将 targetPath 转为绝对路径 const absTargetPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath); if (!fs.existsSync(absTargetPath)) { throw new errors_1.HandledError(`目标路径未找到: ${targetPath}`); } const stats = fs.statSync(absTargetPath); if (stats.isDirectory()) { await processDirectory(absTargetPath, pathResolver, globalComponents, result, miniappRoot, projectRoot); } else { await processFile(absTargetPath, pathResolver, globalComponents, result, miniappRoot, projectRoot); } } /** * Processes the entire miniapp project * * @param projectRoot Project root directory * @param options Analyzer options * @param pathResolver PathResolver instance * @param globalComponents Global component definitions * @param result Result object to populate */ async function processWholeProject(projectRoot, options, pathResolver, globalComponents, result, miniappRoot) { var _a; try { debug_logger_1.logger.info('正在处理整个项目...'); const projectAnalysis = await (0, analyzer_1.analyzeProject)(projectRoot, options); const nodes = projectAnalysis.projectStructure.nodes.filter((node) => node.type === 'Page' || node.type === 'Component'); debug_logger_1.logger.info(`发现 ${nodes.length} 个页面/组件需要分析`); for (const node of nodes) { const basePath = (_a = node.properties) === null || _a === void 0 ? void 0 : _a.basePath; if (!basePath) continue; let wxmlFilePath; let jsonFilePath; // Check for basePath.wxml const directWxmlPath = basePath + '.wxml'; const directJsonPath = basePath + '.json'; if (fs.existsSync(directWxmlPath) && fs.existsSync(directJsonPath)) { wxmlFilePath = directWxmlPath; jsonFilePath = directJsonPath; } else { // Check for basePath/index.wxml const indexWxmlPath = path.join(basePath, 'index.wxml'); const indexJsonPath = path.join(basePath, 'index.json'); if (fs.existsSync(indexWxmlPath) && fs.existsSync(indexJsonPath)) { wxmlFilePath = indexWxmlPath; jsonFilePath = indexJsonPath; } } if (wxmlFilePath && jsonFilePath) { await processFilePair({ wxmlPath: wxmlFilePath, jsonPath: jsonFilePath, pathResolver, globalComponents, result, miniappRoot, projectRoot, }); } } } catch (err) { debug_logger_1.logger.error(`Error processing whole project: ${err}`); } } /** * Generates a report based on the lint result * * @param result Lint result */ function generateReport(result, miniappRoot, projectRoot) { const sep = '─'.repeat(60); console.log('\n组件使用情况分析结果:'); console.log('================================\n'); if (result.issues.length === 0) { console.log(chalk_1.default.green('✓ 未发现问题。所有组件声明均与使用情况匹配。')); } else { for (const issue of result.issues) { // Print block header console.log(sep); console.log(issue.id || path.basename(issue.wxmlFile)); console.log(sep + '\n'); // Declared but not used (grouped by JSON file) if (issue.declaredNotUsed.length > 0) { const jsonPath = projectRoot ? path.relative(projectRoot, issue.jsonFile) : issue.jsonFile; console.log(jsonPath); console.log(' 已在 JSON 中声明但在 WXML 中未使用:'); for (const component of issue.declaredNotUsed) { console.log(chalk_1.default.yellow(` - ${component.componentTag}`)); } console.log(''); } // Used but not declared - display issue at the specific WXML file where it occurs if (issue.usedNotDeclared.length > 0) { // Group tags by WXML file where they're used const fileToTags = new Map(); for (const component of issue.usedNotDeclared) { for (const file of component.usedIn) { if (!fileToTags.has(file)) { fileToTags.set(file, []); } fileToTags.get(file).push(component.componentTag); } } // Display each WXML file with its undeclared tags for (const [wxmlFile, tags] of fileToTags.entries()) { const wxmlPath = projectRoot ? path.relative(projectRoot, wxmlFile) : wxmlFile; console.log(wxmlPath); console.log(' 已在 WXML 中使用但在 JSON 中未声明:'); for (const tag of tags) { console.log(chalk_1.default.red(` - ${tag}`)); } console.log(''); } } } // Summary console.log(chalk_1.default.blue('总结:')); console.log(chalk_1.default.blue(` - 已扫描文件数: ${result.summary.filesScanned}`)); console.log(chalk_1.default.blue(` - 存在问题的文件数: ${result.summary.filesWithIssues}`)); console.log(chalk_1.default.blue(` - "已声明但未使用"总数: ${result.summary.declaredNotUsedCount}处`)); console.log(chalk_1.default.blue(` - "已使用但未声明"总数: ${result.summary.usedNotDeclaredCount}处`)); // Tips console.log(chalk_1.default.cyan('\n解决建议:')); console.log(chalk_1.default.cyan(' - 对于"已声明但未使用"的情况: 请从 .json 文件的 \'usingComponents\' 部分移除相应的组件条目。')); console.log(chalk_1.default.cyan(' - 对于"已使用但未声明"的情况:')); console.log(chalk_1.default.cyan(' 1. 请检查 .wxml 中的标签是否拼写错误。')); console.log(chalk_1.default.cyan(" 2. 如果是有效的自定义组件, 请将其以正确的路径添加到 .json 文件的 'usingComponents' 中。")); console.log(chalk_1.default.cyan(' 3. 如果您正在手动检查, 请确保您有一份全面的原生 WXML 标签列表。')); } } /** * Applies auto-fixes for "declared but not used" components based on lint results. * * @param result The lint result containing issues to fix. * @param projectRoot The absolute path to the project root. */ async function applyLintFixes(result, projectRoot) { if (result.issues.length === 0) { debug_logger_1.logger.info('\n未发现问题,无需修复。'); return; } debug_logger_1.logger.info('\n尝试自动修复"已声明但未使用"的组件...'); let totalFilesModified = 0; let totalComponentsRemoved = 0; for (const issue of result.issues) { if (issue.declaredNotUsed.length > 0) { let jsonData; const jsonFilePath = issue.jsonFile; const relativeJsonPath = path.relative(projectRoot, jsonFilePath); try { const jsonContentStr = fs.readFileSync(jsonFilePath, 'utf-8'); jsonData = JSON.parse(jsonContentStr); } catch (parseError) { debug_logger_1.logger.error(`Error parsing ${relativeJsonPath} for fix: ${parseError.message}. Skipping this file.`); continue; } let modifiedInThisFile = false; let removalsInThisFile = 0; for (const compSpec of issue.declaredNotUsed) { const tagToRemove = compSpec.componentTag; let tagWasRemoved = false; if (jsonData.usingComponents && Object.prototype.hasOwnProperty.call(jsonData.usingComponents, tagToRemove)) { delete jsonData.usingComponents[tagToRemove]; debug_logger_1.logger.info(` [已修复] 已从 ${relativeJsonPath} 的 usingComponents 中移除 '${tagToRemove}'`); tagWasRemoved = true; } if (jsonData.componentGenerics && Object.prototype.hasOwnProperty.call(jsonData.componentGenerics, tagToRemove)) { delete jsonData.componentGenerics[tagToRemove]; if (!tagWasRemoved) { debug_logger_1.logger.info(` [已修复] 已从 ${relativeJsonPath} 的 componentGenerics 中移除 '${tagToRemove}'`); } tagWasRemoved = true; } if (tagWasRemoved) { removalsInThisFile++; } } if (removalsInThisFile > 0) { modifiedInThisFile = true; totalComponentsRemoved += removalsInThisFile; } if (modifiedInThisFile) { try { fs.writeFileSync(jsonFilePath, JSON.stringify(jsonData, null, 2) + '\n'); totalFilesModified++; } catch (writeError) { debug_logger_1.logger.error(`Error writing fixes to ${relativeJsonPath}: ${writeError.message}`); } } } } if (totalComponentsRemoved > 0) { debug_logger_1.logger.info(`\n自动修复完成。在 ${totalFilesModified} 个文件中移除了 ${totalComponentsRemoved} 个未使用的组件引用。`); } else { debug_logger_1.logger.info('\n未发现"已声明但未使用"的组件可供自动修复。'); } debug_logger_1.logger.info('建议重新运行 lint 命令以验证更改并检查其他潜在问题。'); } /** * Main lint command implementation (now uses initializeCommandContext) */ async function lint(cliOptions, targetPath, // 位置参数 cmdOptions) { debug_logger_1.logger.info('开始组件使用情况检查流程...'); const context = await (0, command_init_1.initializeCommandContext)(cliOptions); const { projectRoot, miniappRoot } = context; const pathResolver = new path_resolver_1.PathResolver(projectRoot, context); const globalComponents = await readGlobalComponents(pathResolver, miniappRoot || ''); const result = { summary: { filesScanned: 0, filesWithIssues: 0, declaredNotUsedCount: 0, usedNotDeclaredCount: 0, }, issues: [], }; // 使用位置参数,如果没有则使用 cmdOptions.path,最后默认为空字符串 const finalTargetPath = targetPath || (cmdOptions === null || cmdOptions === void 0 ? void 0 : cmdOptions.path) || ''; const miniappRootAbs = miniappRoot ? path.resolve(projectRoot, miniappRoot) : projectRoot; if (finalTargetPath) { await processTargetPath(finalTargetPath, pathResolver, globalComponents, result, miniappRootAbs, projectRoot); } else { await processWholeProject(projectRoot, context, pathResolver, globalComponents, result, miniappRootAbs); } generateReport(result, miniappRootAbs, projectRoot); // Apply fixes if --fix is enabled if (cmdOptions === null || cmdOptions === void 0 ? void 0 : cmdOptions.fix) { await applyLintFixes(result, projectRoot); } } //# sourceMappingURL=index.js.map