mp-lens
Version:
微信小程序分析工具 (Unused Code, Dependencies, Visualization)
511 lines • 26 kB
JavaScript
"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.ProjectStructureBuilder = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const file_parser_1 = require("../parser/file-parser");
const debug_logger_1 = require("../utils/debug-logger");
class ProjectStructureBuilder {
// --- End: Add tracking for parsed dependencies --- //
constructor(projectRoot, miniappRoot, appJsonPath, appJsonContent,
// --- Start: Added allFiles param ---
allFiles,
// --- End: Added allFiles param ---
options) {
this.nodes = new Map();
this.links = [];
this.rootNodeId = null;
this.processedJsonFiles = new Set(); // Avoid infinite loops
// --- End: Add allFiles ---
// --- Start: Add tracking for parsed dependencies --- //
this.parsedModules = new Set();
this.projectRoot = projectRoot;
this.miniappRoot = miniappRoot;
this.options = options;
this.appJsonPath = appJsonPath;
this.appJsonContent = appJsonContent;
// --- Start: Store allFiles ---
this.allFiles = allFiles;
// --- End: Store allFiles ---
// Pass necessary options to FileParser
this.fileParser = new file_parser_1.FileParser(projectRoot, {
...options,
miniappRoot: miniappRoot,
});
// --- Start: Initialize all nodes first --- //
debug_logger_1.logger.debug(`Initializing nodes for ${this.allFiles.length} found files.`);
for (const filePath of this.allFiles) {
this.addNodeForFile(filePath, 'Module', false); // Add as Module, don't log yet
}
debug_logger_1.logger.debug(`Initialized ${this.nodes.size} nodes from file scan.`);
// --- End: Initialize all nodes first --- //
}
async build() {
var _a;
debug_logger_1.logger.info('开始项目结构分析...');
// 1. Find and parse app.json - REMOVED (using constructor values)
// const appJsonInfo = this.findAndParseAppJson();
// if (!appJsonInfo) {
// // Error logged in findAndParseAppJson
// throw new Error(
// 'Failed to initialize structure: Could not find or parse app.json/entry content.',
// );
// }
// // Destructure potentially null path and guaranteed content (or {})
// const { appJsonPath, appJsonContent } = appJsonInfo;
// Use values passed to constructor
const appJsonPath = this.appJsonPath;
const appJsonContent = this.appJsonContent;
// 2. Create App node
this.rootNodeId = 'app';
this.addNode({
id: this.rootNodeId,
type: 'App',
label: 'App',
// Store path only if it exists
properties: { path: appJsonPath ? appJsonPath : undefined },
});
// Add app.json itself as a module node linked to App, only if path exists
if (appJsonPath) {
const appJsonNode = this.addNodeForFile(appJsonPath, 'Module');
if (appJsonNode) {
this.addLink(this.rootNodeId, appJsonNode.id, 'Config');
}
}
// 3. Process app.json content (pages, subpackages, etc.)
// This uses appJsonContent which is guaranteed to be an object (even if empty)
await this.processAppJsonContent(appJsonContent);
// 4. Process implicit global files (app.js/ts/wxss)
this.processImplicitGlobalFiles();
// --- Start: Final pass to parse all remaining files --- //
debug_logger_1.logger.debug(`Starting final pass to parse dependencies for all ${this.nodes.size} nodes...`);
const initialParsedCount = this.parsedModules.size;
for (const node of this.nodes.values()) {
// Only parse modules that haven't been touched yet AND are not JSON files
const filePath = (_a = node.properties) === null || _a === void 0 ? void 0 : _a.absolutePath;
const fileExt = filePath ? path.extname(filePath).toLowerCase() : '';
if (node.type === 'Module' &&
filePath &&
!this.parsedModules.has(filePath) &&
['.js', '.ts', '.wxml', '.wxss'].includes(fileExt) // Check file extension
) {
// Use node.properties.absolutePath which is the ID and the key for parsedModules
await this.parseModuleDependencies(node);
}
}
debug_logger_1.logger.debug(`Final pass complete. Parsed an additional ${this.parsedModules.size - initialParsedCount} modules.`);
// --- End: Final pass to parse all remaining files --- //
// Structure is built, return it
const structure = {
nodes: Array.from(this.nodes.values()),
links: this.links,
rootNodeId: this.rootNodeId,
miniappRoot: this.miniappRoot,
};
debug_logger_1.logger.info(`项目结构分析完成。发现 ${structure.nodes.length} 个节点和 ${structure.links.length} 条链接。`);
return structure;
}
async processAppJsonContent(content) {
// Process Pages
if (content.pages && Array.isArray(content.pages)) {
for (const pagePath of content.pages) {
await this.processPage(this.rootNodeId, pagePath, this.miniappRoot);
}
}
// Process Subpackages
const subpackages = content.subpackages || content.subPackages || [];
if (Array.isArray(subpackages)) {
for (const pkg of subpackages) {
if (pkg.root && pkg.pages && Array.isArray(pkg.pages)) {
const packageRoot = path.resolve(this.miniappRoot, pkg.root);
const packageId = `pkg:${pkg.root}`;
this.addNode({
id: packageId,
type: 'Package',
label: pkg.root,
properties: { root: packageRoot },
});
this.addLink(this.rootNodeId, packageId, 'Structure');
for (const pagePath of pkg.pages) {
const fullPagePath = path.join(pkg.root, pagePath);
await this.processPage(packageId, fullPagePath, this.miniappRoot);
}
// TODO: Process subpackage-specific app.js/ts?
}
}
}
// Process Global usingComponents
if (content.usingComponents && typeof content.usingComponents === 'object') {
for (const [_name, compPath] of Object.entries(content.usingComponents)) {
if (typeof compPath === 'string' && !compPath.startsWith('plugin://')) {
await this.processComponent(this.rootNodeId, compPath, this.miniappRoot);
}
}
}
// TODO: Process TabBar, Theme, Workers etc. (similar to parseEntryContent)
this.processTabBar(content);
this.processTheme(content);
this.processWorkers(content);
}
async processPage(parentId, pageBasePath, currentRoot) {
const pageId = `page:${pageBasePath}`;
this.addNode({
id: pageId,
type: 'Page',
label: pageBasePath,
properties: { basePath: path.resolve(currentRoot, pageBasePath) },
});
this.addLink(parentId, pageId, 'Structure');
// Process related files (json, js, wxml, wxss)
await this.processRelatedFiles(pageId, pageBasePath, currentRoot);
}
async processComponent(parentId, componentBasePath, // Path from usingComponents (e.g., '/components/comp', '../../comp')
currentRoot) {
let absoluteBasePath;
// Handle absolute paths (relative to miniapp root) vs relative paths
if (componentBasePath.startsWith('/')) {
// Resolve from miniapp root, remove leading '/' for path.join/resolve
absoluteBasePath = path.resolve(this.miniappRoot, componentBasePath.substring(1));
debug_logger_1.logger.trace(`[processComponent] Resolved absolute component path '${componentBasePath}' -> '${absoluteBasePath}'`);
}
else {
// Resolve relative to the current JSON file's directory
absoluteBasePath = path.resolve(currentRoot, componentBasePath);
debug_logger_1.logger.trace(`[processComponent] Resolved relative component path '${componentBasePath}' in '${currentRoot}' -> '${absoluteBasePath}'`);
}
// Normalize the path to ensure that '.../comp' and '.../comp/index' are treated as the same entity.
// The canonical form for IDs and basePaths will be WITHOUT '/index'.
let canonicalBasePath = absoluteBasePath;
const indexSuffix = path.sep + 'index';
if (absoluteBasePath.endsWith(indexSuffix)) {
canonicalBasePath = absoluteBasePath.slice(0, -indexSuffix.length);
debug_logger_1.logger.trace(`[processComponent] Normalized component path from '${absoluteBasePath}' to '${canonicalBasePath}' for ID and basePath.`);
}
// Create a canonical ID relative to the miniapp root
const canonicalRelativePath = path.relative(this.miniappRoot, canonicalBasePath);
// Ensure canonical path doesn't start with '../' if resolution somehow failed
if (canonicalRelativePath.startsWith('..')) {
debug_logger_1.logger.warn(`[processComponent] Calculated canonical path '${canonicalRelativePath}' seems incorrect for absolute path '${absoluteBasePath}' relative to miniapp root '${this.miniappRoot}'. Skipping component.`);
return null; // Or handle error differently
}
// TODO: 使用 id-helper.ts 中的函数来生成 ID 和 label
const canonicalComponentId = `comp:${canonicalRelativePath}`;
const componentLabel = canonicalRelativePath; // Use the normalized path for label
// Check if the canonical node already exists
if (this.nodes.has(canonicalComponentId)) {
// Node exists, just add the link from the current parent
this.addLink(parentId, canonicalComponentId, 'Structure');
debug_logger_1.logger.trace(`[processComponent] Linking existing component ${canonicalComponentId} to parent ${parentId}`);
return this.nodes.get(canonicalComponentId);
}
debug_logger_1.logger.trace(`[processComponent] Creating new component node ${canonicalComponentId} with label ${componentLabel}`);
// Create the node using the canonical ID and label
const node = this.addNode({
id: canonicalComponentId,
type: 'Component',
label: componentLabel,
properties: { basePath: canonicalBasePath }, // Store canonical absolute path for reference
});
this.addLink(parentId, canonicalComponentId, 'Structure');
// Process related files using the canonical ID as the owner
// Call processRelatedFiles with the *original* basePath and the context it came from (currentRoot)
// processRelatedFiles will re-resolve the absolute path correctly based on its context.
await this.processRelatedFiles(canonicalComponentId, componentBasePath, currentRoot);
return node;
}
// Processes the standard set of files (.json, .js, .ts, .wxml, .wxss) for a page or component
async processRelatedFiles(ownerId, // Canonical ID of the Page or Component
basePath, // Original base path (relative or absolute depending on caller)
currentRoot) {
// Resolve absolute path based on the context provided by the caller
// Needs the same logic as processComponent to handle '/' prefix
let absoluteBasePath;
if (basePath.startsWith('/')) {
absoluteBasePath = path.resolve(this.miniappRoot, basePath.substring(1));
}
else {
absoluteBasePath = path.resolve(currentRoot, basePath);
}
const extensions = ['.json', '.js', '.ts', '.wxml', '.wxss'];
for (const ext of extensions) {
// Check both patterns: basePath.ext and basePath/index.ext
const filePathDirect = absoluteBasePath + ext;
const filePathIndex = path.join(absoluteBasePath, 'index' + ext);
let foundFilePath = null;
if (fs.existsSync(filePathDirect)) {
foundFilePath = filePathDirect;
}
else if (fs.existsSync(filePathIndex)) {
foundFilePath = filePathIndex;
}
// If either path was found, process it
if (foundFilePath) {
const moduleNode = this.addNodeForFile(foundFilePath, 'Module');
if (moduleNode) {
// Assign structural parent ID (using the canonical ownerId)
if (!moduleNode.properties)
moduleNode.properties = {};
moduleNode.properties.structuralParentId = ownerId;
// Link owner (Component/Page) -> Module
this.addLink(ownerId, moduleNode.id, 'Structure');
// If it's a JSON file, parse it for components
if (ext === '.json') {
// Pass the canonical ownerId and the absolute path to the JSON
await this.parseComponentJson(ownerId, foundFilePath);
}
// If it's a script or template, parse dependencies
else if ('.js,.ts,.wxml,.wxss'.includes(ext)) {
await this.parseModuleDependencies(moduleNode);
}
}
}
}
}
// Parses a component/page's JSON file for `usingComponents`
async parseComponentJson(ownerId, jsonPath) {
if (this.processedJsonFiles.has(jsonPath)) {
return; // Avoid redundant processing
}
this.processedJsonFiles.add(jsonPath);
try {
const content = fs.readFileSync(jsonPath, 'utf-8');
const jsonContent = JSON.parse(content);
if (jsonContent.usingComponents && typeof jsonContent.usingComponents === 'object') {
debug_logger_1.logger.verbose(`Parsing components for: ${ownerId} from ${jsonPath}`);
const componentDir = path.dirname(jsonPath);
for (const [_name, compPath] of Object.entries(jsonContent.usingComponents)) {
if (typeof compPath === 'string' && !compPath.startsWith('plugin://')) {
// Resolve component path relative to the JSON file's directory
// const absoluteCompPath = path.resolve(componentDir, compPath as string);
// processComponent now resolves absolute path and calculates canonical ID internally
// We need to provide the correct context (componentDir) for resolving the relative compPath
await this.processComponent(ownerId, compPath, componentDir); // Use componentDir as currentRoot
}
}
}
}
catch (error) {
debug_logger_1.logger.warn(`Failed to read or parse component JSON: ${jsonPath}`, error);
}
}
// Parses a module file (js, ts, wxml, wxss) for its dependencies
async parseModuleDependencies(moduleNode) {
var _a;
const filePath = (_a = moduleNode.properties) === null || _a === void 0 ? void 0 : _a.absolutePath;
if (!filePath || this.parsedModules.has(filePath)) {
return; // Skip if no path or already parsed
}
this.parsedModules.add(filePath);
const relativePath = path.relative(this.projectRoot, filePath);
debug_logger_1.logger.debug(`Parsing dependencies for: ${relativePath}`);
try {
// Assuming parseFile gives a list of absolute paths of dependencies
const dependencies = await this.fileParser.parseFile(filePath);
for (const depAbsolutePath of dependencies) {
const targetNode = this.addNodeForFile(depAbsolutePath, 'Module');
if (targetNode) {
// Use 'Import' as the default dependency link type
this.addLink(moduleNode.id, targetNode.id, 'Import');
// --- Populate referredBy ---
if (!targetNode.properties)
targetNode.properties = {};
if (!targetNode.properties.referredBy)
targetNode.properties.referredBy = [];
// Ensure referredBy stores strings and check for existence
if (!targetNode.properties.referredBy.includes(moduleNode.id)) {
targetNode.properties.referredBy.push(moduleNode.id);
}
// --- End Populate referredBy ---
// Recursively parse the dependency if it hasn't been parsed yet
const depExt = path.extname(depAbsolutePath).toLowerCase();
if (!'.json'.includes(depExt) && // Avoid parsing JSON again here
!this.parsedModules.has(depAbsolutePath)) {
await this.parseModuleDependencies(targetNode);
}
}
}
}
catch (error) {
debug_logger_1.logger.warn(`Error parsing dependencies for ${relativePath}: ${error.message}`);
}
}
processImplicitGlobalFiles() {
const implicitFiles = ['app.js', 'app.ts', 'app.wxss', 'project.config.json', 'sitemap.json'];
for (const fileName of implicitFiles) {
const filePath = path.resolve(this.miniappRoot, fileName);
if (fs.existsSync(filePath)) {
const node = this.addNodeForFile(filePath, 'Module');
if (node && this.rootNodeId) {
this.addLink(this.rootNodeId, node.id, 'Structure');
// Need to parse these too, in case app.js requires other modules
this.parseModuleDependencies(node);
}
}
}
}
// Helper to add a node, ensuring uniqueness by ID
addNode(node, log = true) {
if (!this.nodes.has(node.id)) {
if (log) {
debug_logger_1.logger.verbose(`Adding node (${node.type}): ${node.label} [${node.id}]`);
}
this.nodes.set(node.id, node);
}
else if (log) {
// Optionally log if trying to add again, maybe update type?
// logger.trace(`Node already exists: ${node.id}. Current type: ${this.nodes.get(node.id)?.type}, Attempted type: ${node.type}`);
}
return this.nodes.get(node.id);
}
// Helper to create/add a node specifically for a file path
addNodeForFile(absolutePath, type, log = true) {
if (!fs.existsSync(absolutePath))
return null;
const relativePath = path.relative(this.projectRoot, absolutePath);
const nodeId = absolutePath; // Use absolute path as unique ID for file modules
// Check if node exists, potentially update type if more specific?
const existingNode = this.nodes.get(nodeId);
if (existingNode) {
// If we find a Page/Component later, should we update the type from 'Module'?
// For now, let's just return the existing node.
// Maybe update the label if it was just a placeholder?
return existingNode;
}
// 获取文件信息统计
const fileExt = path.extname(absolutePath).toLowerCase().substring(1) || 'unknown';
let fileSize = 0;
try {
// 获取文件大小
const stats = fs.statSync(absolutePath);
fileSize = stats.size;
}
catch (error) {
debug_logger_1.logger.warn(`Failed to get file size for ${absolutePath}:`, error);
}
return this.addNode({
id: nodeId,
type: type,
label: relativePath,
properties: {
absolutePath: absolutePath,
fileSize,
fileExt,
},
}, log);
}
// Helper to add a link, preventing duplicates
addLink(sourceId, targetId, type, dependencyType) {
// Avoid self-loops
if (sourceId === targetId) {
return;
}
const link = { source: sourceId, target: targetId, type };
if (dependencyType) {
link.dependencyType = dependencyType;
}
// Optional: Check for duplicates if needed
// const exists = this.links.some(l => l.source === sourceId && l.target === targetId && l.type === type);
// if (exists) return;
this.links.push(link);
}
// --- Start: Added processing functions for TabBar, Theme, Workers ---
processTabBar(content) {
if (content.tabBar && content.tabBar.list && Array.isArray(content.tabBar.list)) {
debug_logger_1.logger.debug('Processing tabBar entries...');
content.tabBar.list.forEach((item) => {
// pagePath defines a page structure
if (item.pagePath) {
// Process page structure (json, js, wxml, wxss)
// We don't know the parent context here (App or Package), link from App root?
// This might re-process pages already found via 'pages' or 'subpackages',
// but processPage/processRelatedFiles handles duplicates.
this.processPage(this.rootNodeId, item.pagePath, this.miniappRoot);
}
// Icons are single file dependencies
if (item.iconPath) {
this.addSingleFileLink(this.rootNodeId, item.iconPath, 'Resource');
}
if (item.selectedIconPath) {
this.addSingleFileLink(this.rootNodeId, item.selectedIconPath, 'Resource');
}
});
}
}
processTheme(content) {
// Check for themeLocation first
if (content.themeLocation && typeof content.themeLocation === 'string') {
debug_logger_1.logger.debug(`Processing themeLocation: ${content.themeLocation}`);
this.addSingleFileLink(this.rootNodeId, content.themeLocation, 'Config');
}
// Always check for default theme.json
debug_logger_1.logger.debug('Checking for default theme.json');
this.addSingleFileLink(this.rootNodeId, 'theme.json', 'Config');
}
processWorkers(content) {
// Workers field defines entry points for worker threads
if (content.workers && typeof content.workers === 'string') {
debug_logger_1.logger.debug(`Processing workers entry: ${content.workers}`);
// Treat the worker root directory/file itself as a structural link from App
// We might need more sophisticated handling if it's a directory
// For now, add link to the file/dir specified
this.addSingleFileLink(this.rootNodeId, content.workers, 'WorkerEntry');
// TODO: Potentially need to parse files within the worker directory?
// This depends on how workers load dependencies.
// For now, just ensure the entry point is marked as used.
}
}
// Helper to add a link for a single file path relative to miniappRoot
addSingleFileLink(sourceId, relativePath, linkType) {
const absolutePath = path.resolve(this.miniappRoot, relativePath);
if (fs.existsSync(absolutePath)) {
const node = this.addNodeForFile(absolutePath, 'Module');
if (node) {
this.addLink(sourceId, node.id, linkType);
// Parse dependencies of this file too?
// For simple configs/resources, maybe not needed initially.
// For workers entry .js, it would be needed.
if (linkType === 'WorkerEntry') {
this.parseModuleDependencies(node);
}
}
}
else {
debug_logger_1.logger.warn(`app.json 中引用的文件未找到: ${relativePath} (解析路径: ${absolutePath})`);
}
}
}
exports.ProjectStructureBuilder = ProjectStructureBuilder;
//# sourceMappingURL=project-structure-builder.js.map