UNPKG

mp-lens

Version:

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

337 lines 15.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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeProject = analyzeProject; const fs = __importStar(require("fs")); const glob = __importStar(require("glob")); const path = __importStar(require("path")); const debug_logger_1 = require("../utils/debug-logger"); const errors_1 = require("../utils/errors"); const typescript_helper_1 = require("../utils/typescript-helper"); const project_structure_builder_1 = require("./project-structure-builder"); // --- Start: Helper Function Definitions --- // /** * Finds all .d.ts files that are pure ambient declarations. */ // function findPureAmbientDeclarationFiles(projectRoot: string, allFiles: string[]): string[] { // Implementation might be complex, assume it exists and works // return []; // } /** * Resolves user-provided essential file paths relative to project/miniapp roots. */ // function resolveEssentialFiles( // projectRoot: string, // miniappRoot: string, // essentialFilesUser: string[], // ): Set<string> { // Implementation needs file system access // return new Set<string>(); // } /** * Performs reachability analysis (BFS) on the ProjectStructure graph. */ function findReachableNodes(structure, entryNodeIds) { var _a, _b; const reachable = new Set(); const queue = []; const linksFrom = new Map(); const nodeMap = new Map(structure.nodes.map((n) => [n.id, n])); // Precompute outgoing links for faster lookup structure.links.forEach((link) => { if (!linksFrom.has(link.source)) { linksFrom.set(link.source, []); } linksFrom.get(link.source).push(link); }); // Initialize queue with entry points that exist in the graph entryNodeIds.forEach((id) => { if (nodeMap.has(id)) { queue.push(id); reachable.add(id); } else { debug_logger_1.logger.warn(`Entry node ID specified but not found in graph: ${id}`); } }); debug_logger_1.logger.trace(`findReachableNodes: Starting BFS with initial queue size ${queue.length}`); while (queue.length > 0) { const currentNodeId = queue.shift(); debug_logger_1.logger.trace(`BFS: Processing node ${(_a = nodeMap.get(currentNodeId)) === null || _a === void 0 ? void 0 : _a.label} [${currentNodeId}]`); // Find nodes reachable from the current node const outgoingLinks = linksFrom.get(currentNodeId) || []; debug_logger_1.logger.trace(`BFS: Found ${outgoingLinks.length} outgoing links from ${currentNodeId}`); for (const link of outgoingLinks) { const targetNodeId = link.target; const targetNodeLabel = (_b = nodeMap.get(targetNodeId)) === null || _b === void 0 ? void 0 : _b.label; debug_logger_1.logger.trace(`BFS: Checking neighbor ${targetNodeLabel} [${targetNodeId}] via link type ${link.type}`); if (nodeMap.has(targetNodeId) && !reachable.has(targetNodeId)) { reachable.add(targetNodeId); queue.push(targetNodeId); debug_logger_1.logger.trace(`BFS: Added ${targetNodeLabel} [${targetNodeId}] to reachable set and queue.`); } else if (!nodeMap.has(targetNodeId)) { debug_logger_1.logger.trace(`BFS: Neighbor node not found in map: ${targetNodeId}`); } else if (reachable.has(targetNodeId)) { debug_logger_1.logger.trace(`BFS: Neighbor already reachable: ${targetNodeLabel} [${targetNodeId}]`); } } } debug_logger_1.logger.debug(`Reachability analysis complete. Found ${reachable.size} reachable nodes.`); return reachable; } // --- End: New Helper Function --- // // --- Start: Re-added findAllFiles --- // /** * 在指定目录中查找所有符合条件的文件 */ function findAllFiles(rootDir, fileTypes, excludePatterns) { // Ensure fileTypes are valid for glob pattern const safeFileTypes = fileTypes.filter((t) => t && /^[a-zA-Z0-9]+$/.test(t)); if (safeFileTypes.length === 0) { debug_logger_1.logger.warn('No valid file types specified for glob search.'); return []; } const globPattern = `**/*.{${safeFileTypes.join(',')}}`; // Default ignore patterns (consider refining these) const defaultIgnorePatterns = [ '**/node_modules/**', '**/miniprogram_npm/**', // Add other common build/output dirs if necessary, e.g., '**/dist/**'? // Keep output patterns from original '**/output/dependency-graph.*', '**/output/unused-files.*', 'dependency-graph.*', 'unused-files.*', ]; const globOptions = { cwd: rootDir, absolute: true, ignore: [...defaultIgnorePatterns, ...excludePatterns], nodir: true, dot: true, // Include hidden files/folders if not excluded }; debug_logger_1.logger.debug(`Glob pattern: ${globPattern}`); debug_logger_1.logger.debug(`Glob options:`, globOptions); try { const files = glob.sync(globPattern, globOptions); debug_logger_1.logger.debug(`Found ${files.length} files via initial scan in ${rootDir}`); return files; } catch (error) { debug_logger_1.logger.error(`Error during initial file scan:`, error); return []; } } // --- End: Re-added findAllFiles --- // // --- Start: Exported analyzeProject function --- // async function analyzeProject(projectRoot, options) { const { fileTypes = ['js', 'ts', 'wxml', 'wxss', 'json'], excludePatterns = [], miniappRoot, appJsonPath, appJsonContent, essentialFiles = [], includeAssets = false, } = options; debug_logger_1.logger.debug('Project Root:', projectRoot); debug_logger_1.logger.debug('MiniApp Root:', miniappRoot); debug_logger_1.logger.debug('Exclude patterns:', excludePatterns); debug_logger_1.logger.debug('Include assets in cleanup:', includeAssets); debug_logger_1.logger.debug('Essential files:', essentialFiles); // 验证 appJsonContent 存在 if (!appJsonContent || typeof appJsonContent !== 'object' || Object.keys(appJsonContent).length === 0) { const errorMsg = '分析失败: 没有找到有效的 app.json 内容。请确保小程序项目根目录中存在 app.json 文件,或通过配置提供 appJsonContent。'; debug_logger_1.logger.error(errorMsg); throw new errors_1.HandledError(errorMsg); } // --- Initial File Scan --- // // Scan should happen within the miniapp root if specified const allFoundFiles = findAllFiles(miniappRoot, fileTypes, excludePatterns); if (allFoundFiles.length === 0) { // If no files found, analysis will be based solely on appJsonContent debug_logger_1.logger.warn('在指定的 miniapp 根目录中未找到匹配的文件。'); } // Add app.json path if it exists and wasn't found by glob (e.g., different extension) if (appJsonPath && !allFoundFiles.includes(appJsonPath)) { debug_logger_1.logger.debug(`Manually adding app.json path to found files: ${appJsonPath}`); allFoundFiles.push(appJsonPath); } // --- Build Project Structure --- const builder = new project_structure_builder_1.ProjectStructureBuilder(projectRoot, miniappRoot, appJsonPath, appJsonContent, allFoundFiles, // Pass the list of all files options); const projectStructure = await builder.build(); // --- Calculate Reachable Nodes FIRST --- // const nodeMap = new Map(projectStructure.nodes.map((n) => [n.id, n])); const entryNodeIdsSet = new Set(); if (projectStructure.rootNodeId) { entryNodeIdsSet.add(projectStructure.rootNodeId); } else { debug_logger_1.logger.warn('Project structure has no root node ID defined.'); } // Add essential files as entry points const essentialFilePaths = resolveEssentialFiles(projectRoot, miniappRoot, essentialFiles); essentialFilePaths.forEach((filePath) => { if (nodeMap.has(filePath)) { entryNodeIdsSet.add(filePath); debug_logger_1.logger.trace(`Added essential file path as entry point: ${filePath}`); } else { debug_logger_1.logger.trace(`Essential file path specified but not found as node: ${filePath}`); } }); // Add pure ambient declaration files as entry points const allFilePathsInStructure = projectStructure.nodes .filter((n) => { var _a; return n.type === 'Module' && ((_a = n.properties) === null || _a === void 0 ? void 0 : _a.absolutePath); }) .map((n) => n.properties.absolutePath); const pureAmbientDtsFiles = (0, typescript_helper_1.findPureAmbientDeclarationFiles)(projectRoot, allFilePathsInStructure); pureAmbientDtsFiles.forEach((filePath) => { if (nodeMap.has(filePath)) { entryNodeIdsSet.add(filePath); debug_logger_1.logger.trace(`Added pure ambient d.ts as entry point: ${filePath}`); } else { debug_logger_1.logger.warn(`Ambient d.ts file found but not present as node in structure: ${filePath}`); } }); debug_logger_1.logger.debug(`Starting reachability analysis from ${entryNodeIdsSet.size} entry nodes.`); const reachableNodeIds = findReachableNodes(projectStructure, Array.from(entryNodeIdsSet)); debug_logger_1.logger.debug(`Found ${reachableNodeIds.size} total reachable nodes.`); // --- Find Unused Files using the Calculated Reachable Nodes --- // const unusedFiles = findUnusedFiles(projectStructure, projectRoot, reachableNodeIds, // <-- Pass calculated reachable nodes includeAssets); return { projectStructure, unusedFiles, reachableNodeIds, // <-- Return calculated reachable nodes }; } /** * Finds unused files using the ProjectStructure. * Relies on pre-calculated reachableNodeIds. */ function findUnusedFiles(structure, projectRoot, reachableNodeIds, // <-- Receive reachable nodes includeAssets = false) { // --- Step 1: Determine Unused FILES --- // const allModuleNodes = structure.nodes.filter((n) => n.type === 'Module'); const unusedModuleNodes = allModuleNodes.filter((node) => !reachableNodeIds.has(node.id)); let unusedFiles = unusedModuleNodes .map((node) => { var _a; return (_a = node.properties) === null || _a === void 0 ? void 0 : _a.absolutePath; }) .filter((filePath) => !!filePath); // --- Step 2: Filter out asset files unless includeAssets is true --- // if (!includeAssets) { const assetExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; debug_logger_1.logger.debug(`Filtering out asset files with extensions: ${assetExtensions.join(', ')}`); unusedFiles = unusedFiles.filter((absolutePath) => { const fileExt = path.extname(absolutePath).toLowerCase(); const isAsset = assetExtensions.includes(fileExt); if (isAsset) { const relativePath = path.relative(projectRoot, absolutePath); debug_logger_1.logger.verbose(`Keeping asset file (assets are excluded by default): ${relativePath}`); } // Keep the file in the unused list only if it's NOT an asset or if we explicitly want to include assets return !isAsset; }); } // Debugging output (adjusted) debug_logger_1.logger.verbose('Total Module Nodes:', allModuleNodes.length); // logger.verbose('Entry Node IDs for BFS:', entryNodeIds.size); // Removed, happens before debug_logger_1.logger.verbose('Reachable Node IDs:', reachableNodeIds.size); debug_logger_1.logger.info(`发现 ${unusedFiles.length} 个未使用的文件。`); return unusedFiles; } /** * Resolves essential files provided by the user to absolute paths. * (Implementation likely remains the same - it deals with file paths) */ function resolveEssentialFiles(projectRoot, miniappRoot, essentialFilesUser) { const essentialFiles = new Set(); // --- Start: Added Default Essential Files --- const projectLevelFiles = [ 'tsconfig.json', 'jsconfig.json', 'mp-lens.config.json', 'mp-lens.config.js', 'package.json', '.eslintrc.js', '.eslintrc.json', '.prettierrc', '.prettierrc.js', '.babelrc', 'babel.config.js', ]; const miniappLevelFiles = [ 'app.json', // Often the source of truth for entries, essential itself 'project.config.json', 'project.private.config.json', 'sitemap.json', 'theme.json', 'ext.json', ]; // Add default project-level files projectLevelFiles.forEach((file) => { const absPath = path.resolve(projectRoot, file); // Check existence? Optional, depends if we want to add only existing ones. // Adding regardless simplifies logic, reachability check will handle non-existent nodes later. // if (fs.existsSync(absPath)) { essentialFiles.add(absPath); // } }); // Add default miniapp-level files miniappLevelFiles.forEach((file) => { const absPath = path.resolve(miniappRoot, file); // if (fs.existsSync(absPath)) { essentialFiles.add(absPath); // } }); debug_logger_1.logger.verbose(`Added ${projectLevelFiles.length + miniappLevelFiles.length} default essential file paths.`); // --- End: Added Default Essential Files --- // Add user-specified essential files essentialFilesUser.forEach((file) => { // Try resolving relative to miniapp root first, then project root const pathFromMiniApp = path.resolve(miniappRoot, file); const pathFromProject = path.resolve(projectRoot, file); if (fs.existsSync(pathFromMiniApp)) { essentialFiles.add(pathFromMiniApp); debug_logger_1.logger.verbose(`Resolved essential file (from miniapp root): ${file} -> ${pathFromMiniApp}`); } else if (fs.existsSync(pathFromProject)) { essentialFiles.add(pathFromProject); debug_logger_1.logger.verbose(`Resolved essential file (from project root): ${file} -> ${pathFromProject}`); } else { debug_logger_1.logger.warn(`Specified essential file not found: ${file}`); } }); return essentialFiles; } //# sourceMappingURL=analyzer.js.map