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