mp-lens
Version:
微信小程序分析工具 (Unused Code, Dependencies, Visualization)
510 lines • 23.6 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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = DependencyGraph;
const jsx_runtime_1 = require("preact/jsx-runtime");
const g6_1 = __importStar(require("@antv/g6"));
const hooks_1 = require("preact/hooks");
const DependencyGraph_module_css_1 = __importDefault(require("./DependencyGraph.module.css"));
// Helper function to find a node in the tree by its ID (copied from App.tsx)
function findTreeNodeById(treeNode, id) {
if (!treeNode) {
return null;
}
if (treeNode.id === id) {
return treeNode;
}
if (treeNode.children) {
for (const child of treeNode.children) {
const found = findTreeNodeById(child, id);
if (found) {
return found;
}
}
}
return null;
}
// Helper function to get color by node type
function getNodeColorByType(type, isCenter = false) {
const colors = {
App: '#e6f7ff',
Module: '#e8f5e9',
Component: '#fff3e0',
Page: '#e3f2fd',
Config: '#f3e5f5',
Package: '#eceff1',
Worker: '#fce4ec',
Default: '#f5f5f5',
};
return isCenter ? '#e6f7ff' : colors[type] || colors.Default;
}
// Helper function to get border color by node type
function getBorderColorByType(type, isCenter = false) {
const colors = {
App: '#1890ff',
Module: '#a5d6a7',
Component: '#ffcc80',
Page: '#90caf9',
Config: '#ce93d8',
Package: '#b0bec5',
Worker: '#f48fb1',
Default: '#e0e0e0',
};
return isCenter ? '#1890ff' : colors[type] || colors.Default;
}
// Helper to ensure node always has valid property information
function ensureNodeProperties(properties = {}) {
return {
...properties,
fileCount: properties.fileCount !== undefined ? properties.fileCount : 1,
totalSize: properties.totalSize !== undefined
? properties.totalSize
: properties.fileSize !== undefined
? properties.fileSize
: 0,
};
}
function DependencyGraph({ selectedNode, fullGraphData, initialTreeData, onNodeSelect, }) {
const containerRef = (0, hooks_1.useRef)(null);
const graphRef = (0, hooks_1.useRef)(null);
const [isLoading, setIsLoading] = (0, hooks_1.useState)(true);
// Function to create a subgraph focused on the selected node
const createSubgraphForNode = (node) => {
const nodes = [];
const edges = [];
const nodeMap = new Map();
if (!node || !fullGraphData) {
return { nodes, edges };
}
// Add the selected node as the center node (already has full properties)
nodes.push({
id: node.id,
label: node.label || node.id,
style: {
fill: getNodeColorByType(node.type, true),
stroke: getBorderColorByType(node.type, true),
lineWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 5,
radius: 4,
cursor: 'pointer',
},
labelCfg: {
style: {
fontWeight: 'bold',
fontSize: 13,
cursor: 'pointer',
},
},
nodeType: node.type,
properties: ensureNodeProperties(node.properties), // Ensure center node has valid properties
});
nodeMap.set(node.id, true);
// Find direct dependencies (outgoing edges)
const outgoingLinks = fullGraphData.links.filter((link) => link.source === node.id);
const maxOutgoingNodes = 30;
outgoingLinks.slice(0, maxOutgoingNodes).forEach((link) => {
const rawTargetNode = fullGraphData.nodes.find((n) => n.id === link.target);
if (rawTargetNode) {
if (!nodeMap.has(rawTargetNode.id)) {
const processedTargetNode = findTreeNodeById(initialTreeData, rawTargetNode.id);
const targetNodeData = processedTargetNode || rawTargetNode; // Prioritize processed node
// Get properties, ensure they're valid
let targetProperties = (processedTargetNode === null || processedTargetNode === void 0 ? void 0 : processedTargetNode.properties) || rawTargetNode.properties || {};
targetProperties = ensureNodeProperties(targetProperties);
nodes.push({
id: targetNodeData.id,
label: targetNodeData.label || targetNodeData.id,
style: {
fill: getNodeColorByType(targetNodeData.type),
stroke: getBorderColorByType(targetNodeData.type),
lineWidth: 1,
radius: 4,
cursor: 'pointer',
},
nodeType: targetNodeData.type,
properties: targetProperties, // Use ensured properties
});
nodeMap.set(targetNodeData.id, true);
}
if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
edges.push({
source: link.source,
target: link.target,
label: link.type || '',
});
}
}
});
// Find reverse dependencies (incoming edges)
const incomingLinks = fullGraphData.links.filter((link) => link.target === node.id);
const maxIncomingNodes = 30;
incomingLinks.slice(0, maxIncomingNodes).forEach((link) => {
const rawSourceNode = fullGraphData.nodes.find((n) => n.id === link.source);
if (rawSourceNode) {
if (!nodeMap.has(rawSourceNode.id)) {
const processedSourceNode = findTreeNodeById(initialTreeData, rawSourceNode.id);
const sourceNodeData = processedSourceNode || rawSourceNode; // Prioritize processed node
// Get properties, ensure they're valid
let sourceProperties = (processedSourceNode === null || processedSourceNode === void 0 ? void 0 : processedSourceNode.properties) || rawSourceNode.properties || {};
sourceProperties = ensureNodeProperties(sourceProperties);
nodes.push({
id: sourceNodeData.id,
label: sourceNodeData.label || sourceNodeData.id,
style: {
fill: getNodeColorByType(sourceNodeData.type),
stroke: getBorderColorByType(sourceNodeData.type),
lineWidth: 1,
radius: 4,
cursor: 'pointer',
},
nodeType: sourceNodeData.type,
properties: sourceProperties, // Use ensured properties
});
nodeMap.set(sourceNodeData.id, true);
}
if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
if (!edges.some((e) => e.source === link.source && e.target === link.target)) {
edges.push({
source: link.source,
target: link.target,
label: link.type || '',
});
}
}
}
});
// Create tooltips for all nodes
nodes.forEach((n) => {
let tooltip = `<div style="padding: 5px; font-size: 12px;"><strong>${n.label}</strong><br/>Type: ${n.nodeType}`;
if (n.properties) {
// Always use fileCount from properties (now guaranteed to exist)
tooltip += `<br/>Files: ${n.properties.fileCount}`;
// Get size information, preferring totalSize, then fileSize, defaulting to 0
const sizeInBytes = n.properties.totalSize !== undefined
? n.properties.totalSize
: n.properties.fileSize !== undefined
? n.properties.fileSize
: 0;
// Convert bytes to KB for display
const sizeToDisplay = (sizeInBytes / 1024).toFixed(2) + ' KB';
tooltip += `<br/>Size: ${sizeToDisplay}`;
}
tooltip += `</div>`;
n.tooltip = tooltip; // Assign tooltip directly to node data
});
console.log(`[G6] Created subgraph with ${nodes.length} nodes and ${edges.length} edges`);
return { nodes, edges };
};
// Effect for initializing and updating the graph
(0, hooks_1.useEffect)(() => {
if (!containerRef.current) {
console.log('[G6] Container ref not available');
return;
}
setIsLoading(true);
// Initialize graph if it doesn't exist
if (!graphRef.current) {
console.log('[G6] Creating new graph instance with default elements');
const containerHeight = Math.max(containerRef.current.clientHeight, window.innerHeight * 0.6);
// Define layout options - ONLY DAGRE NOW
const dagreLayout = {
type: 'dagre',
rankdir: 'LR', // Keep Left-to-Right
align: 'UL',
nodesep: 15,
ranksep: 40,
};
// Configure G6 graph using default elements
const graph = new g6_1.Graph({
container: containerRef.current,
width: containerRef.current.clientWidth,
height: containerHeight,
fitView: true,
fitViewPadding: 30,
animate: false, // Keep animation disabled for zoom stability
layout: dagreLayout, // Use the defined dagre layout directly
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node', 'activate-relations'],
},
// *** Use default rect node ***
defaultNode: {
type: 'rect',
size: [140, 30],
// Basic default style, will be overridden by node data
style: {
radius: 4,
lineWidth: 1,
fill: '#f5f5f5',
stroke: '#e0e0e0',
cursor: 'pointer', // Add cursor pointer to indicate clickable nodes
},
// Default label config
labelCfg: {
style: {
fill: '#333',
fontSize: 11,
cursor: 'pointer',
},
},
},
// *** Use default cubic edge ***
defaultEdge: {
type: 'cubic',
// Default edge style
style: {
stroke: '#C2C8D5',
lineWidth: 1.5,
endArrow: {
path: g6_1.default.Arrow.triangle(6, 8, 3), // Standard arrow
d: 3, // Offset
fill: '#C2C8D5',
},
},
// Default edge label config
labelCfg: {
autoRotate: true,
style: {
fill: '#666',
fontSize: 9,
cursor: 'default', // Keep default cursor for edge labels
background: {
fill: '#fff',
stroke: '#efefef',
padding: [1, 3],
radius: 2,
},
},
},
},
// State styles for interaction feedback
nodeStateStyles: {
hover: {
shadowColor: 'rgba(64,158,255,0.4)',
shadowBlur: 10,
stroke: '#40a9ff', // Highlight border on hover
cursor: 'pointer', // Ensure pointer cursor on hover
},
// Add styles for the 'selected' state if needed
},
edgeStateStyles: {
active: {
stroke: '#1890ff',
lineWidth: 2,
shadowColor: '#1890ff',
shadowBlur: 5,
endArrow: {
path: g6_1.default.Arrow.triangle(6, 8, 3),
d: 3,
fill: '#1890ff', // Highlight arrow
},
},
},
// Tooltip plugin
plugins: [
new g6_1.default.Tooltip({
offsetX: 10,
offsetY: 10,
itemTypes: ['node'],
getContent: (e) => { var _a; return ((_a = e.item) === null || _a === void 0 ? void 0 : _a.getModel().tooltip) || ''; },
shouldBegin: (e) => { var _a; return (_a = e.item) === null || _a === void 0 ? void 0 : _a.getModel().tooltip; }, // Only show if tooltip data exists
}),
],
});
// *** 禁用局部刷新,解决缩小时的残影问题 ***
const canvas = graph.get('canvas');
if (canvas) {
console.log('[G6] Disabling localRefresh to fix ghosting issue');
canvas.set('localRefresh', false);
}
// Event listeners for hover effects
graph.on('node:mouseenter', (e) => {
if (!e.item)
return;
graph.setItemState(e.item, 'hover', true);
// Highlight connected edges
e.item.getEdges().forEach((edge) => graph.setItemState(edge, 'active', true));
});
graph.on('node:mouseleave', (e) => {
if (!e.item)
return;
graph.setItemState(e.item, 'hover', false);
// Reset edge highlighting
e.item.getEdges().forEach((edge) => graph.setItemState(edge, 'active', false));
});
// *** Add event listener for node click ***
graph.on('node:click', (e) => {
if (e.item) {
const clickedNodeId = e.item.getID();
console.log(`[G6] Node clicked: ${clickedNodeId}`);
// Call the callback prop if it exists
if (onNodeSelect) {
onNodeSelect(clickedNodeId);
}
}
});
// Add layout/zoom controls (existing logic)
const addControls = () => {
var _a;
const controlsContainer = document.createElement('div');
controlsContainer.style.position = 'absolute';
controlsContainer.style.top = '10px';
controlsContainer.style.right = '10px';
controlsContainer.style.background = 'rgba(255, 255, 255, 0.9)';
controlsContainer.style.borderRadius = '4px';
controlsContainer.style.padding = '8px';
controlsContainer.style.display = 'flex';
// Change to row for zoom controls only
controlsContainer.style.flexDirection = 'row';
controlsContainer.style.gap = '5px';
controlsContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
controlsContainer.style.zIndex = '10';
// KEEP ZOOM CONTROLS SECTION
const zoomControls = document.createElement('div');
zoomControls.style.display = 'flex'; // Keep flex for buttons
zoomControls.style.gap = '5px';
const zoomInBtn = document.createElement('button');
zoomInBtn.textContent = '+';
zoomInBtn.style.width = '28px';
zoomInBtn.style.height = '28px';
zoomInBtn.style.cursor = 'pointer';
zoomInBtn.style.borderRadius = '2px';
zoomInBtn.style.border = '1px solid #d9d9d9';
zoomInBtn.addEventListener('click', () => graph.zoom(1.2));
zoomControls.appendChild(zoomInBtn);
const zoomOutBtn = document.createElement('button');
zoomOutBtn.textContent = '-';
zoomOutBtn.style.width = '28px';
zoomOutBtn.style.height = '28px';
zoomOutBtn.style.cursor = 'pointer';
zoomOutBtn.style.borderRadius = '2px';
zoomOutBtn.style.border = '1px solid #d9d9d9';
zoomOutBtn.addEventListener('click', () => graph.zoom(0.8));
zoomControls.appendChild(zoomOutBtn);
const fitBtn = document.createElement('button');
fitBtn.textContent = '⇔';
fitBtn.style.width = '28px';
fitBtn.style.height = '28px';
fitBtn.style.cursor = 'pointer';
fitBtn.style.borderRadius = '2px';
fitBtn.style.border = '1px solid #d9d9d9';
fitBtn.addEventListener('click', () => graph.fitView(20));
zoomControls.appendChild(fitBtn);
// Append zoom controls directly to the main container
controlsContainer.appendChild(zoomControls);
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.appendChild(controlsContainer);
};
addControls();
graphRef.current = graph;
}
// Prepare and render graph data (existing logic)
const graph = graphRef.current;
if (!graph || graph.get('destroyed')) {
console.error('[G6] Graph instance not available or destroyed');
setIsLoading(false);
return;
}
const g6Data = createSubgraphForNode(selectedNode);
if (g6Data.nodes.length > 0) {
// Define the handler outside the try block to ensure it's in scope for catch
let afterLayoutHandler = null;
try {
// Define the handler for after layout completion
afterLayoutHandler = () => {
if (graph && !graph.get('destroyed')) {
graph.fitView(20); // Fit view AFTER layout is done
console.log('[G6] View fitted after layout.');
setIsLoading(false); // Hide loading indicator
}
// Optional: graph.off('afterlayout', afterLayoutHandler); // G6.once should handle this
};
// Register the listener ONCE before changing data
graph.once('afterlayout', afterLayoutHandler);
// Change data - this triggers the layout process
graph.changeData(g6Data);
console.log('[G6] Graph data changed, waiting for layout...');
// --- Do NOT call fitView or setIsLoading immediately here ---
}
catch (error) {
console.error('[G6] Error during graph rendering:', error);
// Ensure listener is removed on error, check if handler was defined
if (afterLayoutHandler) {
graph.off('afterlayout', afterLayoutHandler);
}
setIsLoading(false); // Still hide loading on error
}
}
else {
// Handle empty graph case
try {
graph.clear();
console.log('[G6] Graph cleared due to empty data');
setIsLoading(false); // Hide loading immediately when clearing
}
catch (error) {
console.error('[G6] Error clearing graph:', error);
setIsLoading(false);
}
}
// Cleanup function
return () => {
// No explicit graph destroy needed here if we reuse the instance
// console.log('[G6] Cleanup effect');
};
}, [selectedNode, fullGraphData, initialTreeData, onNodeSelect]);
// Resize handling (existing logic)
(0, hooks_1.useEffect)(() => {
// ... (keep existing resize observer code) ...
}, []);
return ((0, jsx_runtime_1.jsxs)("div", { style: {
position: 'relative',
height: 'calc(100vh - 250px)',
minHeight: '500px',
overflow: 'hidden',
border: '1px solid #eee',
borderRadius: '4px',
flex: 1,
display: 'flex',
flexDirection: 'column',
}, children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: DependencyGraph_module_css_1.default.graphPlaceholder, children: (0, jsx_runtime_1.jsx)("div", { className: DependencyGraph_module_css_1.default.graphPlaceholderText, children: !selectedNode
? '请从左侧选择一个节点以查看其依赖关系'
: `正在渲染 "${selectedNode.label || selectedNode.id}" 的依赖关系...` }) })), (0, jsx_runtime_1.jsx)("div", { className: DependencyGraph_module_css_1.default.graphContainer, ref: containerRef })] }));
}
//# sourceMappingURL=DependencyGraph.js.map