mp-lens
Version:
微信小程序分析工具 (Unused Code, Dependencies, Visualization)
1,397 lines (1,379 loc) • 99.1 kB
JavaScript
"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");
}