UNPKG

mp-lens

Version:

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

1,397 lines (1,379 loc) 99.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { DependencyGraph: () => DependencyGraph, HtmlGeneratorPreact: () => HtmlGeneratorPreact, analyzeProject: () => analyzeProject, findMiniProgramEntryPoints: () => findMiniProgramEntryPoints, findUnusedAssets: () => findUnusedAssets, parseJson: () => parseJson, parseWxml: () => parseWxml, parseWxs: () => parseWxs, parseWxss: () => parseWxss }); module.exports = __toCommonJS(index_exports); // src/analyzer/analyzer.ts var fs6 = __toESM(require("fs")); var glob = __toESM(require("glob")); var path7 = __toESM(require("path")); // src/utils/debug-logger.ts var DebugLogger = class { /** * Create a new debug logger */ constructor(options = {}) { this.options = { level: options.level ?? (options.level === 0 ? 0 : 0 /* ESSENTIAL */), projectRoot: options.projectRoot, useRelativePaths: options.useRelativePaths ?? true, useColors: options.useColors ?? true, language: options.language ?? "en" }; } /** * Update logger options */ setOptions(options) { this.options = { ...this.options, ...options }; } /** * Set the verbosity level */ setLevel(level) { if (typeof level === "boolean") { this.options.level = level ? 1 /* NORMAL */ : 0 /* ESSENTIAL */; } else { this.options.level = level; } } /** * Get the current verbosity level */ getLevel() { return this.options.level; } /** * Set the project root for path normalization */ setProjectRoot(projectRoot) { this.options.projectRoot = projectRoot; } /** * Log a message at ESSENTIAL level (always shown) */ info(message, ...args) { this._log("INFO", 0 /* ESSENTIAL */, message, ...args); } /** * Log a warning message (always shown) */ warn(message, ...args) { this._log("WARN", 0 /* ESSENTIAL */, message, ...args); } /** * Log an error message (always shown) */ error(message, ...args) { this._log("ERROR", 0 /* ESSENTIAL */, message, ...args); } /** * Log a message at NORMAL level * Only shown when verbose flag is enabled */ debug(message, ...args) { this._log("DEBUG", 1 /* NORMAL */, message, ...args); } /** * Log a message at VERBOSE level * Requires verbosity level of 2 or higher */ verbose(message, ...args) { this._log("VERBOSE", 2 /* VERBOSE */, message, ...args); } /** * Log a message at TRACE level * Requires verbosity level of 3 */ trace(message, ...args) { this._log("TRACE", 3 /* TRACE */, message, ...args); } /** * Normalize paths in log messages if needed */ normalizePath(input) { if (!this.options.useRelativePaths || !this.options.projectRoot) { return input; } return input.replace(new RegExp(this.options.projectRoot.replace(/\\/g, "\\\\"), "g"), "."); } /** * Internal logging implementation */ _log(prefix, level, message, ...args) { if (this.options.level < level) { return; } if (this.options.projectRoot) { message = this.normalizePath(message); args = args.map((arg) => { if (typeof arg === "string") { return this.normalizePath(arg); } else if (typeof arg === "object" && arg !== null) { try { const str = JSON.stringify(arg); return JSON.parse(this.normalizePath(str)); } catch (e) { return arg; } } return arg; }); } let formatted = message; if (args.length > 0) { if (args.length === 1 && typeof args[0] === "object") { formatted = `${message} ${JSON.stringify(args[0], null, 2)}`; } else { formatted = `${message} ${args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")}`; } } console.log(`${prefix} - ${formatted}`); } }; var logger = new DebugLogger(); // src/utils/errors.ts var HandledError = class _HandledError extends Error { constructor(message) { super(message); this.name = "HandledError"; Object.setPrototypeOf(this, _HandledError.prototype); } }; // src/utils/typescript-helper.ts var fs = __toESM(require("fs")); function isPureAmbientDeclarationFile(filePath) { if (!filePath.endsWith(".d.ts")) { return false; } try { const content = fs.readFileSync(filePath, "utf-8"); const hasModuleElements = /\b(import|export)\b/.test(content); if (hasModuleElements) { return false; } const hasAmbientDeclarations = /\bdeclare\b/.test(content); return hasAmbientDeclarations; } catch (err) { logger.debug(`Error reading d.ts file ${filePath}: ${err.message}`); return false; } } function findPureAmbientDeclarationFiles(projectRoot, allFiles) { const declarationFiles = allFiles.filter((file) => file.endsWith(".d.ts")); const ambientDeclarationFiles = declarationFiles.filter(isPureAmbientDeclarationFile); if (ambientDeclarationFiles.length > 0) { logger.debug( `Found ${ambientDeclarationFiles.length} pure ambient declaration files that will be preserved` ); } return ambientDeclarationFiles; } // src/analyzer/project-structure-builder.ts var fs5 = __toESM(require("fs")); var path6 = __toESM(require("path")); // src/parser/file-parser.ts var fs4 = __toESM(require("fs")); var path5 = __toESM(require("path")); // src/utils/alias-resolver.ts var fs2 = __toESM(require("fs")); var path = __toESM(require("path")); var AliasResolver = class { constructor(projectRoot) { this.aliases = {}; this.initialized = false; this.projectRoot = projectRoot; } /** * 初始化别名解析器 * @returns 是否找到有效的别名配置 */ initialize() { if (this.initialized) return Object.keys(this.aliases).length > 0; const foundTsConfig = this.loadFromTsConfig(); const foundCustomConfig = this.loadFromCustomConfig(); if (foundTsConfig) { logger.info(`\u5DF2\u4ECEtsconfig.json\u52A0\u8F7D\u522B\u540D\u914D\u7F6E`); } if (foundCustomConfig) { logger.info(`\u5DF2\u4ECEmp-lens.config.json\u52A0\u8F7D\u522B\u540D\u914D\u7F6E`); } if (this.projectRoot) { logger.info(`alias\u89E3\u6790\u7684\u6839\u76EE\u5F55: ${this.projectRoot}`); } this.initialized = true; return foundTsConfig || foundCustomConfig; } /** * 解析别名路径 * @param importPath 导入路径 * @param currentFile 当前文件路径 * @returns 解析后的路径,如果找不到匹配的别名则返回null */ resolve(importPath, currentFile) { if (!this.initialized) { this.initialize(); } logger.trace(`Resolving alias prefix for import '${importPath}' in file '${currentFile}'`); for (const [alias, targets] of Object.entries(this.aliases)) { const aliasPrefix = alias + "/"; if (importPath.startsWith(aliasPrefix)) { logger.trace(`Found matching alias prefix: ${alias} => ${targets.join(" or ")}`); if (targets.length > 0) { const target = targets[0]; const resolvedBaseDir = path.isAbsolute(target) ? target : path.resolve(this.projectRoot, target); const remainingPath = importPath.substring(aliasPrefix.length); const potentialPath = path.join(resolvedBaseDir, remainingPath); logger.trace(`Alias resolved to potential base path: ${potentialPath}`); return potentialPath; } else { logger.warn(`Alias '${alias}' found but has no target paths defined.`); } return null; } else if (importPath === alias) { logger.trace(`Found matching alias (exact match): ${alias} => ${targets.join(" or ")}`); if (targets.length > 0) { const target = targets[0]; const potentialPath = path.isAbsolute(target) ? target : path.resolve(this.projectRoot, target); logger.trace(`Alias resolved to potential base path: ${potentialPath}`); return potentialPath; } else { logger.warn(`Alias '${alias}' found but has no target paths defined.`); } return null; } } logger.trace(`No matching alias prefix found for ${importPath}`); return null; } /** * 从tsconfig.json加载路径别名 * @returns 是否成功加载到别名配置 */ loadFromTsConfig() { let tsconfigPath = path.join(this.projectRoot, "tsconfig.json"); if (!fs2.existsSync(tsconfigPath)) { let currentDir = this.projectRoot; let found = false; for (let i = 0; i < 3; i++) { currentDir = path.dirname(currentDir); const testPath = path.join(currentDir, "tsconfig.json"); if (fs2.existsSync(testPath)) { tsconfigPath = testPath; found = true; break; } } if (!found) { return false; } } try { logger.debug(`\u5C1D\u8BD5\u4ECE${tsconfigPath}\u52A0\u8F7D\u522B\u540D\u914D\u7F6E`); const tsconfig = JSON.parse(fs2.readFileSync(tsconfigPath, "utf-8")); if (tsconfig.compilerOptions && tsconfig.compilerOptions.paths) { const tsconfigDir = path.dirname(tsconfigPath); const baseUrl = tsconfig.compilerOptions.baseUrl || "."; const baseDir = path.resolve(tsconfigDir, baseUrl); logger.debug(`tsconfig.json\u7684baseUrl: ${baseUrl}, \u89E3\u6790\u4E3A: ${baseDir}`); for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) { const normalizedAlias = alias.replace(/\/\*$/, ""); this.aliases[normalizedAlias] = targets.map((target) => { const targetPath = target.replace(/\/\*$/, ""); const absoluteTargetPath = path.resolve(baseDir, targetPath); logger.verbose( `Mapping alias '${normalizedAlias}' target '${target}' -> '${absoluteTargetPath}'` ); return absoluteTargetPath; }); } logger.debug("\u4ECEtsconfig.json\u52A0\u8F7D\u7684\u522B\u540D:", this.aliases); return Object.keys(this.aliases).length > 0; } } catch (error) { logger.warn(`\u65E0\u6CD5\u89E3\u6790 tsconfig.json: ${error.message}`); } return false; } /** * 从自定义配置文件加载路径别名 * @returns 是否成功加载到别名配置 */ loadFromCustomConfig() { const configPath = path.join(this.projectRoot, "mp-lens.config.json"); if (!fs2.existsSync(configPath)) { return false; } try { const config = JSON.parse(fs2.readFileSync(configPath, "utf-8")); if (config.aliases && typeof config.aliases === "object") { const initialAliasCount = Object.keys(this.aliases).length; for (const [alias, targets] of Object.entries(config.aliases)) { this.aliases[alias] = Array.isArray(targets) ? targets : [targets]; } return Object.keys(this.aliases).length > initialAliasCount; } } catch (error) { logger.warn(`Failed to parse mp-lens.config.json: ${error.message}`); } return false; } /** * 获取所有配置的别名 */ getAliases() { if (!this.initialized) { this.initialize(); } return this.aliases; } }; // src/utils/path-resolver.ts var fs3 = __toESM(require("fs")); var path2 = __toESM(require("path")); var PathResolver = class { constructor(projectRoot, options, aliasResolver, hasAliasConfig) { this.projectRoot = projectRoot; this.options = options; this.aliasResolver = aliasResolver; this.hasAliasConfig = hasAliasConfig; } /** * Resolves an import path (which could be relative, absolute, alias, or implicit root) * to an existing file path, considering context-specific allowed extensions. * * @param importPath The original import string (e.g., './utils', '/pages/index', '@/comp', 'image.png'). * @param sourcePath The absolute path of the file containing the import. * @param allowedExtensions An ordered array of extensions to check (e.g., ['.js', '.ts'] or ['.wxml']). * @returns The absolute path of the resolved existing file, or null if not found. */ resolveAnyPath(importPath, sourcePath, allowedExtensions) { logger.trace( `Resolving import '${importPath}' from '${sourcePath}' with allowed extensions: [${allowedExtensions.join( ", " )}]` ); if (/^data:/.test(importPath) || /^(http|https|\/\/):/.test(importPath)) { logger.trace(`Skipping resolution for data URI or remote URL: ${importPath}`); return null; } if (this.isNpmPackageImport(importPath)) { logger.trace(`Skipping resolution for npm package import: ${importPath}`); return null; } if (path2.isAbsolute(importPath)) { logger.trace( `Input importPath '${importPath}' is absolute. Checking direct existence first.` ); const existingAbsolutePath = this.findExistingPath(importPath, allowedExtensions); if (existingAbsolutePath) { logger.trace( `Found existing file at true absolute path: ${existingAbsolutePath}. Returning directly.` ); return existingAbsolutePath; } else { logger.trace( `Absolute path '${importPath}' not found directly. Will proceed to normal resolution (might be root-relative).` ); } } let potentialBasePath = null; let isAlias = false; if (this.isAliasPath(importPath) && this.aliasResolver) { isAlias = true; potentialBasePath = this.aliasResolver.resolve(importPath, sourcePath); if (potentialBasePath) { logger.trace(`Alias resolved to base path: ${potentialBasePath}`); } else { logger.warn(`Alias '${importPath}' detected but could not be resolved by AliasResolver.`); return null; } } if (!potentialBasePath) { const sourceDir = path2.dirname(sourcePath); const miniappRoot = this.options.miniappRoot || this.projectRoot; if (importPath.startsWith("/")) { potentialBasePath = path2.resolve(miniappRoot, importPath.slice(1)); } else if (importPath.startsWith(".")) { potentialBasePath = path2.resolve(sourceDir, importPath); } else { potentialBasePath = path2.resolve(miniappRoot, importPath); } logger.trace(`Path resolved to potential base path: ${potentialBasePath}`); } if (potentialBasePath) { const existingPath = this.findExistingPath(potentialBasePath, allowedExtensions); if (existingPath) { logger.trace(`Resolved '${importPath}' to existing file: ${existingPath}`); return existingPath; } else if (isAlias) { logger.warn( `Alias resolved to '${potentialBasePath}', but no existing file found with extensions [${allowedExtensions.join( ", " )}]` ); } } if (!isAlias) { logger.warn(`Failed to resolve import '${importPath}' from '${sourcePath}'.`); } return null; } /** * Given a potential absolute base path (without extension or index), finds the * actual existing file path by checking for the path itself, adding allowed * extensions, or checking for directory index files with allowed extensions. * * @param potentialPath Absolute path, possibly without extension (e.g., '/path/to/file' or '/path/to/dir') * @param allowedExtensions Ordered list of extensions to check (e.g., ['.js', '.ts']) * @returns The existing absolute file path, or null. */ findExistingPath(potentialPath, allowedExtensions) { logger.trace( `Looking for existing path: ${potentialPath} with extensions: ${allowedExtensions}` ); let potentialPathIsDir = false; try { const stats = fs3.statSync(potentialPath); if (stats.isFile()) { logger.trace(`Check 1: Exact path exists and is a file: ${potentialPath}`); return potentialPath; } else if (stats.isDirectory()) { logger.trace(`Check 1: Exact path exists and is a directory: ${potentialPath}`); potentialPathIsDir = true; } } catch (e) { logger.trace(`Check 1: Exact path does not exist: ${potentialPath}`); } if (!potentialPathIsDir) { logger.trace(`Check 2: Trying extensions for base path: ${potentialPath}`); for (const ext of allowedExtensions) { const pathWithExt = potentialPath + ext; logger.trace(`Check 2a: Checking path with extension: ${pathWithExt}`); try { const stats = fs3.statSync(pathWithExt); if (stats.isFile()) { logger.trace(`Check 2b: Found file with extension: ${pathWithExt}`); return pathWithExt; } else { logger.trace(`Check 2b: Path with extension not found or not a file: ${pathWithExt}`); } } catch (e) { logger.trace(`Check 2b: Error stating path ${pathWithExt}: ${e.message}`); } } } logger.trace(`Check 3: Checking for index files in directory: ${potentialPath}`); for (const ext of allowedExtensions) { const indexFilePath = path2.join(potentialPath, "index" + ext); logger.trace(`Check 3a: Checking index file: ${indexFilePath}`); try { const stats = fs3.statSync(indexFilePath); if (stats.isFile()) { logger.trace(`Check 3b: Found index file: ${indexFilePath}`); return indexFilePath; } else { logger.trace(`Check 3b: Index file not found or not a file: ${indexFilePath}`); } } catch (e) { logger.trace(`Check 3b: Error stating index file ${indexFilePath}: ${e.message}`); } } logger.trace(`Failed to find existing path for: ${potentialPath}`); return null; } /** * Check if the import path looks like an alias based on the loaded configuration. */ isAliasPath(importPath) { if (!this.hasAliasConfig || !this.aliasResolver) { return false; } const aliases = this.aliasResolver.getAliases(); if (Object.keys(aliases).length === 0) { return false; } if (importPath in aliases) { return true; } for (const alias of Object.keys(aliases)) { if (importPath === alias || importPath.startsWith(`${alias}/`)) { return true; } } return false; } /** * Check if the import path looks like an npm package that we shouldn't try to resolve * on the file system or with aliases. */ isNpmPackageImport(importPath) { if (path2.isAbsolute(importPath)) { return false; } if (importPath.startsWith("@")) { const scope = importPath.split("/")[0]; if (this.hasAliasConfig && this.aliasResolver) { const aliases = this.aliasResolver.getAliases(); if (scope in aliases || Object.keys(aliases).some((alias) => alias === scope || alias.startsWith(`${scope}/`))) { return false; } } return true; } if (!importPath.startsWith(".") && !importPath.startsWith("/") && !this.isAliasPath(importPath)) { logger.trace( `Path '${importPath}' is non-relative, non-absolute, non-alias. Considering as NPM package.` ); return true; } return false; } }; // src/parser/javascript-parser.ts var import_parser = require("@babel/parser"); var import_traverse = __toESM(require("@babel/traverse")); var t = __toESM(require("@babel/types")); var path3 = __toESM(require("path")); var JavaScriptParser = class { constructor() { } async parse(content, filePath) { try { const dependencies = /* @__PURE__ */ new Set(); const ast = this.parseToAST(content, filePath); this.traverseAST(ast, dependencies); return Array.from(dependencies); } catch (e) { logger.warn(`Error parsing JavaScript file ${filePath}: ${e.message}`); throw e; } } parseToAST(content, filePath) { const isTypeScript = path3.extname(filePath) === ".ts"; const basePlugins = [ "jsx", "objectRestSpread", "functionBind", "exportDefaultFrom", "exportNamespaceFrom", "decorators-legacy", "classProperties", "asyncGenerators", "functionSent", "dynamicImport", "numericSeparator", "optionalChaining", "importMeta", "bigInt", "optionalCatchBinding", "throwExpressions", "nullishCoalescingOperator", "topLevelAwait" ]; const plugins = isTypeScript ? [...basePlugins, "typescript"] : basePlugins; try { return (0, import_parser.parse)(content, { sourceType: "module", allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins }); } catch (parseError) { const hasImportExport = /\b(import|export)\b/.test(content); if (hasImportExport) { throw parseError; } logger.trace(`Failed to parse ${filePath} as module, trying as script: ${parseError}`); return (0, import_parser.parse)(content, { sourceType: "script", allowReturnOutsideFunction: true, plugins }); } } traverseAST(ast, dependencies) { (0, import_traverse.default)(ast, { // Handle ES6 import statements ImportDeclaration: (path15) => { const source = path15.node.source; if (t.isStringLiteral(source)) { const importPath = source.value; dependencies.add(importPath); } }, // Handle CommonJS require() calls CallExpression: (path15) => { const { node } = path15; if (t.isIdentifier(node.callee) && node.callee.name === "require" && node.arguments.length === 1 && t.isStringLiteral(node.arguments[0])) { const requirePath = node.arguments[0].value; dependencies.add(requirePath); } }, // Handle dynamic imports Import: (path15) => { const parent = path15.parent; if (t.isCallExpression(parent) && parent.arguments.length === 1 && t.isStringLiteral(parent.arguments[0])) { const importPath = parent.arguments[0].value; dependencies.add(importPath); } } }); } }; // src/parser/json-parser.ts var path4 = __toESM(require("path")); var JSONParser = class { constructor() { } async parse(content, filePath) { try { const jsonContent = JSON.parse(content); const dependencies = /* @__PURE__ */ new Set(); this.processPages(jsonContent, dependencies); this.processSubPackages(jsonContent, dependencies); this.processTabBar(jsonContent, dependencies); this.processUsingComponents(jsonContent, dependencies); this.processComponentGenerics(jsonContent, dependencies); return Array.from(dependencies); } catch (e) { if (e instanceof SyntaxError) { logger.error(`Error parsing JSON file ${filePath}: ${e.message}`); } else { logger.warn(`Error processing JSON file ${filePath}: ${e.message}`); } return []; } } processPages(content, dependencies) { if (content.pages && Array.isArray(content.pages)) { for (const pagePath of content.pages) { if (typeof pagePath === "string") { dependencies.add("/" + pagePath); } } } } processSubPackages(content, dependencies) { const subpackages = content.subPackages || content.subpackages; if (subpackages && Array.isArray(subpackages)) { for (const subpackage of subpackages) { const root = subpackage.root; const subPages = subpackage.pages; if (typeof root === "string" && Array.isArray(subPages)) { for (const pagePath of subPages) { if (typeof pagePath === "string") { const fullPagePath = "/" + path4.posix.join(root, pagePath); dependencies.add(fullPagePath); } } } } } } processTabBar(content, dependencies) { var _a; if (((_a = content.tabBar) == null ? void 0 : _a.list) && Array.isArray(content.tabBar.list)) { for (const item of content.tabBar.list) { if (item && typeof item.iconPath === "string") { dependencies.add(item.iconPath); } if (item && typeof item.selectedIconPath === "string") { dependencies.add(item.selectedIconPath); } } } } processUsingComponents(content, dependencies) { if (content.usingComponents && typeof content.usingComponents === "object") { for (const [_componentName, componentPath] of Object.entries(content.usingComponents)) { if (typeof componentPath === "string" && !componentPath.startsWith("plugin://")) { dependencies.add(componentPath); } } } } processComponentGenerics(content, dependencies) { if (content.componentGenerics && typeof content.componentGenerics === "object") { for (const genericName in content.componentGenerics) { const genericInfo = content.componentGenerics[genericName]; if (typeof genericInfo === "object" && genericInfo.default) { if (typeof genericInfo.default === "string") { dependencies.add(genericInfo.default); } } } } } }; // src/parser/wxml-parser.ts var import_parser2 = require("@wxml/parser"); // src/utils/wxml-path.ts function normalizeWxmlImportPath(raw) { if (!raw || raw.startsWith("/") || raw.startsWith("./") || raw.startsWith("../") || /^(http|https|data):/.test(raw) || /{{.*?}}/.test(raw)) { return raw; } return "./" + raw; } // src/parser/wxml-parser.ts var WXMLParser = class { constructor() { } async parse(content, filePath) { try { const dependencies = /* @__PURE__ */ new Set(); const ast = (0, import_parser2.parse)(content); this.processImportIncludeTags(ast, dependencies); this.processWxsTags(ast, dependencies); this.processImageSources(ast, dependencies); return Array.from(dependencies); } catch (e) { logger.warn(`Error parsing WXML file ${filePath}: ${e.message}`); throw e; } } /** * Processes import and include tags to extract template dependencies */ processImportIncludeTags(ast, dependencies) { this.findImportIncludeTags(ast, (path15) => { const normalizedPath = normalizeWxmlImportPath(path15); logger.debug(`Found import/include: ${path15} -> normalized: ${normalizedPath}`); dependencies.add(normalizedPath); }); } /** * Processes wxs tags to extract WXS script dependencies */ processWxsTags(ast, dependencies) { this.findWxsTags(ast, (path15) => { const normalizedPath = normalizeWxmlImportPath(path15); logger.debug(`Found wxs: ${path15} -> normalized: ${normalizedPath}`); dependencies.add(normalizedPath); }); } /** * Processes image tags to extract image dependencies */ processImageSources(ast, dependencies) { this.findImageTags(ast, (src) => { if (src.startsWith("data:") || /^(http|https):\/\//.test(src) || /{{.*?}}/.test(src)) { return; } const normalizedPath = normalizeWxmlImportPath(src); logger.debug(`Found image: ${src} -> normalized: ${normalizedPath}`); dependencies.add(normalizedPath); }); } /** * Recursively finds import and include tags in the AST */ findImportIncludeTags(ast, callback) { var _a; if (ast.type === "WXElement" && (ast.name === "import" || ast.name === "include")) { const attrs = (_a = ast.startTag) == null ? void 0 : _a.attributes; if (attrs && Array.isArray(attrs)) { const srcAttr = attrs.find((attr) => attr.key === "src"); if (srcAttr && srcAttr.value) { callback(srcAttr.value); } } } if (ast.type === "WXElement" && Array.isArray(ast.children)) { for (const child of ast.children) { this.findImportIncludeTags(child, callback); } } if (ast.type === "Program" && Array.isArray(ast.body)) { for (const node of ast.body) { this.findImportIncludeTags(node, callback); } } } /** * Recursively finds wxs tags in the AST */ findWxsTags(ast, callback) { var _a, _b; if (ast.type === "WXScript" && ast.name === "wxs") { const attrs = (_a = ast.startTag) == null ? void 0 : _a.attributes; if (attrs && Array.isArray(attrs)) { const srcAttr = attrs.find((attr) => attr.key === "src"); if (srcAttr && srcAttr.value) { callback(srcAttr.value); } } } if (ast.type === "WXElement" && ast.name === "wxs") { const attrs = (_b = ast.startTag) == null ? void 0 : _b.attributes; if (attrs && Array.isArray(attrs)) { const srcAttr = attrs.find((attr) => attr.key === "src"); if (srcAttr && srcAttr.value) { callback(srcAttr.value); } } } if (ast.type === "WXElement" && Array.isArray(ast.children)) { for (const child of ast.children) { this.findWxsTags(child, callback); } } if (ast.type === "Program" && Array.isArray(ast.body)) { for (const node of ast.body) { this.findWxsTags(node, callback); } } } /** * Recursively finds image tags in the AST */ findImageTags(ast, callback) { var _a; if (ast.type === "WXElement" && ast.name === "image") { const attrs = (_a = ast.startTag) == null ? void 0 : _a.attributes; if (attrs && Array.isArray(attrs)) { const srcAttr = attrs.find((attr) => attr.key === "src"); if (srcAttr && srcAttr.value) { callback(srcAttr.value); } } } if (ast.type === "WXElement" && Array.isArray(ast.children)) { for (const child of ast.children) { this.findImageTags(child, callback); } } if (ast.type === "Program" && Array.isArray(ast.body)) { for (const node of ast.body) { this.findImageTags(node, callback); } } } }; // src/parser/wxss-parser.ts var WXSSParser = class { constructor() { } async parse(content, filePath) { try { const dependencies = /* @__PURE__ */ new Set(); const importRegex = /@import\s+['"]([^'"]+)['"]/g; const urlRegex = /url\(['"]?([^'")]+)['"]?\)/g; let match; while ((match = importRegex.exec(content)) !== null) { if (match[1]) { const importPath = match[1]; dependencies.add(importPath); } } while ((match = urlRegex.exec(content)) !== null) { if (match[1]) { const urlPath = match[1].trim(); if (urlPath.startsWith("data:") || /^(http|https):\/\//.test(urlPath) || /{{.*?}}/.test(urlPath)) { continue; } dependencies.add(urlPath); } } return Array.from(dependencies); } catch (e) { logger.warn(`Error parsing WXSS file ${filePath}: ${e.message}`); throw e; } } }; // src/parser/file-parser.ts var FileParser = class { constructor(projectRoot, options) { const actualRoot = options.miniappRoot || projectRoot; if (options.miniappRoot) { logger.debug(`FileParser using custom miniapp root: ${options.miniappRoot}`); } const aliasResolver = new AliasResolver(actualRoot); const hasAliasConfig = aliasResolver.initialize(); if (hasAliasConfig) { logger.debug("Alias configuration detected, automatically enabling alias resolution"); logger.debug("Alias configuration:", aliasResolver.getAliases()); } this.pathResolver = new PathResolver(projectRoot, options, aliasResolver, hasAliasConfig); this.javaScriptParser = new JavaScriptParser(); this.wxmlParser = new WXMLParser(); this.wxssParser = new WXSSParser(); this.jsonParser = new JSONParser(); } /** * Parses a single file by reading its content and delegating text analysis to the appropriate parser. * Handles path resolution centrally. * Returns a list of absolute paths to the file's dependencies. */ async parseFile(filePath) { const ext = path5.extname(filePath).toLowerCase(); try { const content = fs4.readFileSync(filePath, "utf-8"); let rawDependencies = []; switch (ext) { case ".js": case ".ts": case ".wxs": rawDependencies = await this.javaScriptParser.parse(content, filePath); break; case ".wxml": rawDependencies = await this.wxmlParser.parse(content, filePath); break; case ".wxss": rawDependencies = await this.wxssParser.parse(content, filePath); break; case ".json": rawDependencies = await this.jsonParser.parse(content, filePath); break; case ".png": case ".jpg": case ".jpeg": case ".gif": case ".svg": return []; // Image files have no dependencies default: logger.trace(`Unsupported file type for parsing: ${filePath}`); return []; } const resolvedDependencies = []; for (const rawPath of rawDependencies) { const resolvedPath = this.resolveDependencyPath(rawPath, filePath, ext); if (resolvedPath) { resolvedDependencies.push(resolvedPath); } } return resolvedDependencies; } catch (e) { logger.warn(`Error parsing file ${filePath}: ${e.message}`); return []; } } /** * Resolves a raw dependency path to an absolute path based on file type context */ resolveDependencyPath(rawPath, sourcePath, sourceExt) { let allowedExtensions; switch (sourceExt) { case ".js": case ".ts": allowedExtensions = [".js", ".ts", ".json"]; break; case ".wxs": allowedExtensions = [".wxs"]; break; case ".wxml": if (this.isImagePath(rawPath)) { allowedExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]; } else if (rawPath.includes(".wxs") || rawPath.endsWith(".wxs")) { allowedExtensions = [".wxs"]; } else { allowedExtensions = [".wxml"]; } break; case ".wxss": if (this.isImagePath(rawPath)) { allowedExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]; } else { allowedExtensions = [".wxss"]; } break; case ".json": if (this.isImagePath(rawPath)) { allowedExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]; } else { allowedExtensions = [".js", ".ts", ".wxml", ".wxss", ".json"]; } break; default: allowedExtensions = []; } return this.pathResolver.resolveAnyPath(rawPath, sourcePath, allowedExtensions); } /** * Determines if a path refers to an image file based on its extension */ isImagePath(filePath) { const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]; const ext = path5.extname(filePath).toLowerCase(); return imageExtensions.includes(ext); } }; // src/analyzer/project-structure-builder.ts var ProjectStructureBuilder = class { // --- End: Add tracking for parsed dependencies --- // constructor(projectRoot, miniappRoot, appJsonPath, appJsonContent, allFiles, options) { this.nodes = /* @__PURE__ */ new Map(); this.links = []; this.rootNodeId = null; this.processedJsonFiles = /* @__PURE__ */ new Set(); // --- End: Add allFiles --- // --- Start: Add tracking for parsed dependencies --- // this.parsedModules = /* @__PURE__ */ new Set(); this.projectRoot = projectRoot; this.miniappRoot = miniappRoot; this.options = options; this.appJsonPath = appJsonPath; this.appJsonContent = appJsonContent; this.allFiles = allFiles; this.fileParser = new FileParser(projectRoot, { ...options, miniappRoot }); logger.debug(`Initializing nodes for ${this.allFiles.length} found files.`); for (const filePath of this.allFiles) { this.addNodeForFile(filePath, "Module", false); } logger.debug(`Initialized ${this.nodes.size} nodes from file scan.`); } async build() { var _a; logger.info("\u5F00\u59CB\u9879\u76EE\u7ED3\u6784\u5206\u6790..."); const appJsonPath = this.appJsonPath; const appJsonContent = this.appJsonContent; this.rootNodeId = "app"; this.addNode({ id: this.rootNodeId, type: "App", label: "App", // Store path only if it exists properties: { path: appJsonPath ? appJsonPath : void 0 } }); if (appJsonPath) { const appJsonNode = this.addNodeForFile(appJsonPath, "Module"); if (appJsonNode) { this.addLink(this.rootNodeId, appJsonNode.id, "Config"); } } await this.processAppJsonContent(appJsonContent); this.processImplicitGlobalFiles(); 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()) { const filePath = (_a = node.properties) == null ? void 0 : _a.absolutePath; const fileExt = filePath ? path6.extname(filePath).toLowerCase() : ""; if (node.type === "Module" && filePath && !this.parsedModules.has(filePath) && [".js", ".ts", ".wxml", ".wxss"].includes(fileExt)) { await this.parseModuleDependencies(node); } } logger.debug( `Final pass complete. Parsed an additional ${this.parsedModules.size - initialParsedCount} modules.` ); const structure = { nodes: Array.from(this.nodes.values()), links: this.links, rootNodeId: this.rootNodeId, miniappRoot: this.miniappRoot }; logger.info( `\u9879\u76EE\u7ED3\u6784\u5206\u6790\u5B8C\u6210\u3002\u53D1\u73B0 ${structure.nodes.length} \u4E2A\u8282\u70B9\u548C ${structure.links.length} \u6761\u94FE\u63A5\u3002` ); return structure; } async processAppJsonContent(content) { if (content.pages && Array.isArray(content.pages)) { for (const pagePath of content.pages) { await this.processPage(this.rootNodeId, pagePath, this.miniappRoot); } } 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 = path6.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 = path6.join(pkg.root, pagePath); await this.processPage(packageId, fullPagePath, this.miniappRoot); } } } } 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); } } } 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: path6.resolve(currentRoot, pageBasePath) } }); this.addLink(parentId, pageId, "Structure"); await this.processRelatedFiles(pageId, pageBasePath, currentRoot); } async processComponent(parentId, componentBasePath, currentRoot) { let absoluteBasePath; if (componentBasePath.startsWith("/")) { absoluteBasePath = path6.resolve(this.miniappRoot, componentBasePath.substring(1)); logger.trace( `[processComponent] Resolved absolute component path '${componentBasePath}' -> '${absoluteBasePath}'` ); } else { absoluteBasePath = path6.resolve(currentRoot, componentBasePath); logger.trace( `[processComponent] Resolved relative component path '${componentBasePath}' in '${currentRoot}' -> '${absoluteBasePath}'` ); } const canonicalRelativePath = path6.relative(this.miniappRoot, absoluteBasePath); if (canonicalRelativePath.startsWith("..")) { logger.warn( `[processComponent] Calculated canonical path '${canonicalRelativePath}' seems incorrect for absolute path '${absoluteBasePath}' relative to miniapp root '${this.miniappRoot}'. Skipping component.` ); return null; } const canonicalComponentId = `comp:${canonicalRelativePath}`; const componentLabel = canonicalRelativePath; if (this.nodes.has(canonicalComponentId)) { this.addLink(parentId, canonicalComponentId, "Structure"); logger.trace( `[processComponent] Linking existing component ${canonicalComponentId} to parent ${parentId}` ); return this.nodes.get(canonicalComponentId); } logger.trace( `[processComponent] Creating new component node ${canonicalComponentId} with label ${componentLabel}` ); const node = this.addNode({ id: canonicalComponentId, type: "Component", label: componentLabel, properties: { basePath: absoluteBasePath } // Store absolute path for reference }); this.addLink(parentId, canonicalComponentId, "Structure"); 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, basePath, currentRoot) { let absoluteBasePath; if (basePath.startsWith("/")) { absoluteBasePath = path6.resolve(this.miniappRoot, basePath.substring(1)); } else { absoluteBasePath = path6.resolve(currentRoot, basePath); } const extensions = [".json", ".js", ".ts", ".wxml", ".wxss"]; for (const ext of extensions) { const filePathDirect = absoluteBasePath + ext; const filePathIndex = path6.join(absoluteBasePath, "index" + ext); let foundFilePath = null; if (fs5.existsSync(filePathDirect)) { foundFilePath = filePathDirect; } else if (fs5.existsSync(filePathIndex)) { foundFilePath = filePathIndex; } if (foundFilePath) { const moduleNode = this.addNodeForFile(foundFilePath, "Module"); if (moduleNode) { if (!moduleNode.properties) moduleNode.properties = {}; moduleNode.properties.structuralParentId = ownerId; this.addLink(ownerId, moduleNode.id, "Structure"); if (ext === ".json") { await this.parseComponentJson(ownerId, foundFilePath); } 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; } this.processedJsonFiles.add(jsonPath); try { const content = fs5.readFileSync(jsonPath, "utf-8"); const jsonContent = JSON.parse(content); if (jsonContent.usingComponents && typeof jsonContent.usingComponents === "object") { logger.verbose(`Parsing components for: ${ownerId} from ${jsonPath}`); const componentDir = path6.dirname(jsonPath); for (const [_name, compPath] of Object.entries(jsonContent.usingComponents)) { if (typeof compPath === "string" && !compPath.startsWith("plugin://")) { await this.processComponent(ownerId, compPath, componentDir); } } } } catch (error) { 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 ? void 0 : _a.absolutePath; if (!filePath || this.parsedModules.has(filePath)) { return; } this.parsedModules.add(filePath); const relativePath = path6.relative(this.projectRoot, filePath); logger.debug(`Parsing dependencies for: ${relativePath}`); try { const dependencies = await this.fileParser.parseFile(filePath); for (const depAbsolutePath of dependencies) { const targetNode = this.addNodeForFile(depAbsolutePath, "Module"); if (targetNode) { this.addLink(moduleNode.id, targetNode.id, "Import"); if (!targetNode.properties) targetNode.properties = {}; if (!targetNode.properties.referredBy) targetNode.properties.referredBy = []; if (!targetNode.properties.referredBy.includes(moduleNode.id)) { targetNode.properties.referredBy.push(moduleNode.id); } const depExt = path6.extname(depAbsolutePath).toLowerCase(); if (!".json".includes(depExt) && // Avoid parsing JSON again here !this.parsedModules.has(depAbsolutePath)) { await this.parseModuleDependencies(targetNode); } } } } catch (error) { 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 = path6.resolve(this.miniappRoot, fileName); if (fs5.existsSync(filePath)) { const node = this.addNodeForFile(filePath, "Module"); if (node && this.rootNodeId) { this.addLink(this.rootNodeId, node.id, "Structure"); this.parseModuleDependencies(node); } } } } // Helper to add a node, ensuring uniqueness by ID addNode(node, log = true) { if (!this.nodes.has(node.id)) { if (log) { logger.verbose(`Adding node (${node.type}): ${node.label} [${node.id}]`); } this.nodes.set(node.id, node); } else if (log) { } return this.nodes.get(node.id); } // Helper to create/add a node specifically for a file path addNodeForFile(absolutePath, type, log = true) { if (!fs5.existsSync(absolutePath)) return null; const relativePath = path6.relative(this.projectRoot, absolutePath); const nodeId = absolutePath; const existingNode = this.nodes.get(nodeId); if (existingNode) { return existingNode; } const fileExt = path6.extname(absolutePath).toLowerCase().substring(1) || "unknown"; let fileSize = 0; try { const stats = fs5.statSync(absolutePath); fileSize = stats.size; } catch (error) { logger.warn(`Failed to get file size for ${absolutePath}:`, error); } return this.addNode( { id: nodeId, type, label: relativePath, properties: { absolutePath, fileSize, fileExt } }, log ); } // Helper to add a link, preventing duplicates addLink(sourceId, targetId, type, dependencyType) { if (sourceId === targetId) { return; } const link = { source: sourceId, target: targetId, type }; if (dependencyType) { link.dependencyType = dependencyType; } 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)) { logger.debug("Processing tabBar entries..."); content.tabBar.list.forEach((item) => { if (item.pagePath) { this.processPage(this.rootNodeId, item.pagePath, this.miniappRoot); } if (item.iconPath) { this.addSingleFileLink(this.rootNodeId, item.iconPath, "Resource"); } if (item.selectedIconPath) { this.addSingleFileLink(this.rootNodeId, item.selectedIconPath, "Resource"); } }); } } processTheme(content) { if (content.themeLocation && typeof content.themeLocation === "string") { logger.debug(`Processing themeLocation: ${content.themeLocation}`); this.addSingleFileLink(this.rootNodeId, content.themeLocation, "Config"); } logger.debug("Checking for default theme.json"); this.addSingleFileLink(this.rootNodeId, "theme.json", "Config"); }