UNPKG

mp-lens

Version:

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

510 lines 23.6 kB
"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