UNPKG

vue-hook-optimizer

Version:

a tool that helps refactor and optimize hook abstractions in Vue components

1 lines 171 kB
{"version":3,"file":"index.cjs","names":["traverse: typeof _traverse","_traverse","analyze","traverse: typeof _traverse","_traverse","spread: string[]","state: LexerState","analyze","traverse: typeof _traverse","_traverse","analyze","traverse: typeof _traverse","_traverse","res: t.Identifier[]","path: NodePath<t.ObjectMethod>","spread: string[]","node","parentPath","params","result: IReturnData | undefined","analyze","direction: 'TB' | 'BT' | 'LR' | 'RL'","usedNodes: Set<string>","mermaidText: string","shape: string","closeShape: string","unusedSuffix: string","dfs","graph","result: Set<TypedNode>[]","visited: Set<TypedNode>","onStack: Set<TypedNode>","stack: TypedNode[]","cycleNodes: TypedNode[]","dfs","suggestions: Suggestion[]","nodes: CustomNode[]","edges: Edge[]","inDegreeMap: Record<string, number>","outDegreeMap: Record<string, number>"],"sources":["../src/analyze/utils.ts","../src/analyze/setupScript.ts","../src/analyze/options.ts","../src/analyze/style.ts","../src/analyze/template.ts","../src/utils/traverse.ts","../src/analyze/tsx.ts","../src/mermaid.ts","../src/suggest/filter.ts","../src/suggest/split.ts","../src/suggest/utils.ts","../src/suggest/index.ts","../src/vis.ts"],"sourcesContent":["import type { NodePath } from '@babel/traverse';\nimport type * as t from '@babel/types';\n\nexport interface TypedNode {\n label: string\n type: NodeType\n info?: Partial<{\n line: number\n column: number\n comment: string\n used: Set<string>\n }>\n};\n\nexport enum NodeType {\n var = 'var',\n fun = 'fun',\n}\n\ninterface Options {\n isComputed: boolean\n isMethod: boolean\n comment: string\n};\n\nexport type RelationType = 'get' | 'set' | 'call';\n\nexport class NodeCollection {\n lineOffset = 0;\n addInfo = true;\n constructor(_lineOffset = 0, _addInfo = true) {\n this.lineOffset = _lineOffset;\n this.addInfo = _addInfo;\n }\n\n nodes = new Map<string, TypedNode>();\n\n addNode(\n label: string,\n node: t.Node,\n options: Partial<Options> = { isComputed: false, isMethod: false, comment: '' },\n ) {\n if (this.nodes.has(label)) {\n return;\n }\n if (\n (!options.isComputed && (\n (node.type === 'VariableDeclarator' && [\n 'ArrowFunctionExpression',\n 'FunctionDeclaration',\n 'FunctionExpression',\n ].includes(node.init?.type || ''))\n || (node.type === 'ObjectProperty' && [\n 'ArrowFunctionExpression',\n 'FunctionDeclaration',\n 'FunctionExpression',\n ].includes(node.value?.type || ''))\n || node.type === 'FunctionDeclaration'\n || node.type === 'ObjectMethod'\n || node.type === 'ArrowFunctionExpression'\n || node.type === 'FunctionExpression'\n ))\n || options.isMethod\n ) {\n this.nodes.set(label, {\n label,\n type: NodeType.fun,\n ...(this.addInfo\n ? {\n info: {\n line: (node.loc?.start.line || 1) - 1 + this.lineOffset,\n column: node.loc?.start.column || 0,\n ...options.comment\n ? { comment: options.comment }\n : {},\n },\n }\n : {}),\n });\n }\n else {\n this.nodes.set(label, {\n label,\n type: NodeType.var,\n ...(this.addInfo\n ? {\n info: {\n line: (node.loc?.start.line || 1) - 1 + this.lineOffset,\n column: node.loc?.start.column || 0,\n ...options.comment\n ? { comment: options.comment }\n : {},\n },\n }\n : {}),\n });\n }\n }\n\n addTypedNode(label: string, node: TypedNode) {\n this.nodes.set(label, {\n label,\n type: node.type,\n ...(this.addInfo\n ? {\n info: {\n ...(node.info || {}),\n },\n }\n : {}),\n });\n }\n\n getNode(label: string) {\n return this.nodes.get(label);\n }\n\n map(graph: {\n nodes: Set<string>\n edges: Map<string, Set<{ label: string, type: RelationType }>>\n }) {\n const nodes = new Set(Array.from(graph.nodes).map((node) => {\n return this.nodes.get(node)!;\n }).filter(node => !!node));\n\n const edges = new Map(Array.from(graph.edges).map(([from, to]) => {\n // dedupe by node label, preferring 'set' over 'get'\n const labelMap = new Map<string, { node: TypedNode, type: RelationType }>();\n for (const item of to) {\n const node = this.nodes.get(item.label)!;\n if (!node) {\n continue;\n }\n const existing = labelMap.get(item.label);\n if (!existing || (existing.type === 'get' && item.type === 'set')) {\n labelMap.set(item.label, { node, type: item.type });\n }\n }\n const items = Array.from(labelMap.values());\n return [this.nodes.get(from)!, new Set(items)];\n }));\n\n return {\n nodes,\n edges,\n };\n }\n}\n\nexport function getComment(node: t.Node) {\n let comment = '';\n\n node.leadingComments?.forEach((_comment) => {\n if (_comment.loc!.end.line > node.loc!.start.line) {\n return;\n }\n if (_comment.value.trim().startsWith('*')) {\n comment += `${_comment.value.trim().replace(/^\\s*\\*+\\s*\\**/gm, '').trim()}\\n`;\n }\n });\n\n node.trailingComments?.forEach((_comment) => {\n if (_comment.loc!.end.line > node.loc!.start.line) {\n return;\n }\n if (_comment.value.trim().startsWith('*')) {\n comment += `${_comment.value.trim().replace(/^\\s*\\*+\\s*\\**/gm, '').trim()}\\n`;\n }\n else {\n comment += `${_comment.value.trim()}\\n`;\n }\n });\n\n return comment.trim();\n}\n\nexport function isWritingNode(path: NodePath<t.Node>) {\n // Check if the current node is inside an assignment expression (including nested MemberExpression)\n const assignParent = path.findParent(p => p.isAssignmentExpression()) as NodePath<t.AssignmentExpression> | null;\n if (assignParent) {\n const leftNode = assignParent.node.left;\n if (\n leftNode.start != null\n && path.node.start! >= leftNode.start\n && path.node.end! <= (leftNode.end as number)\n ) {\n return true;\n }\n }\n\n // Check if the current node is inside an update expression (including nested MemberExpression)\n const updateParent = path.findParent(p => p.isUpdateExpression()) as NodePath<t.UpdateExpression> | null;\n if (updateParent) {\n const argNode = updateParent.node.argument as t.Node;\n if (\n argNode.start != null\n && path.node.start! >= argNode.start\n && path.node.end! <= (argNode.end as number)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function isCallingNode(path: NodePath<t.Identifier>) {\n const parent = path.parentPath;\n // 判断父节点是否为 CallExpression,并且当前节点是 callee\n if (parent && parent.isCallExpression()) {\n return parent.node.callee === path.node;\n }\n return false;\n}\n\nexport function getRelationType(path: NodePath<t.Node>) {\n if (path.node.type === 'Identifier' && isCallingNode(path as NodePath<t.Identifier>)) {\n return 'call';\n }\n if (isWritingNode(path)) {\n return 'set';\n }\n return 'get';\n}\n","import type { NodePath, Scope, VisitNode } from '@babel/traverse';\nimport type * as t from '@babel/types';\nimport type { RelationType } from './utils';\nimport _traverse from '@babel/traverse';\nimport { babelParse } from '@vue/compiler-sfc';\nimport { getComment, getRelationType, NodeCollection, NodeType } from './utils';\n\nconst traverse: typeof _traverse\n // @ts-expect-error unwarp default\n = _traverse.default?.default || _traverse.default || _traverse;\n\nconst ignoreFunctionsName = ['defineProps', 'defineEmits', 'withDefaults'];\n\nexport const watchHooks = [\n 'watch',\n\n // from `@vueuse/core`\n 'watchArray',\n 'watchAtMost',\n 'watchDebounced',\n 'watchDeep',\n 'watchIgnorable',\n 'watchImmediate',\n 'watchOnce',\n 'watchPausable',\n 'watchThrottled',\n 'watchTriggerable',\n 'watchWithFilter',\n];\n\nexport function processSetup(\n ast: t.Node,\n parentScope?: Scope,\n parentPath?: t.Node,\n _spread?: string[],\n _lineOffset = 0,\n) {\n const spread = _spread || [];\n\n const nodeCollection = new NodeCollection(_lineOffset);\n\n const graph = {\n nodes: new Set<string>(),\n edges: new Map<string, Set<{ label: string, type: RelationType }>>(),\n spread: new Map<string, Set<string>>(),\n };\n\n traverse(ast, {\n VariableDeclaration(path) {\n path.node.declarations.forEach((declaration) => {\n if (declaration.id.type === 'ArrayPattern') {\n declaration.id.elements.forEach((element) => {\n if (element?.type === 'Identifier') {\n const name = element.name;\n const binding = path.scope.getBinding(name);\n\n if (\n binding\n && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )\n && !(declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && ignoreFunctionsName.includes(declaration.init?.callee.name)\n )\n ) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, element, {\n comment: getComment(path.node),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n if (element?.type === 'RestElement' && element.argument.type === 'Identifier') {\n const name = element.argument.name;\n const binding = path.scope.getBinding(name);\n\n if (\n binding\n && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )\n && !(declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && ignoreFunctionsName.includes(declaration.init?.callee.name)\n )\n ) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, element.argument, {\n comment: getComment(path.node),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n });\n }\n if (declaration.id.type === 'ObjectPattern') {\n declaration.id.properties.forEach((property) => {\n if (property.type === 'ObjectProperty' && property.value.type === 'Identifier') {\n const name = property.value.name;\n const binding = path.scope.getBinding(name);\n if (\n binding\n && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )\n && !(declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && ignoreFunctionsName.includes(declaration.init?.callee.name)\n )\n ) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, property.value, {\n comment: getComment(property),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n\n if (property.type === 'RestElement' && property.argument.type === 'Identifier') {\n const name = property.argument.name;\n const binding = path.scope.getBinding(name);\n if (\n binding\n && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )\n && !(declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && ignoreFunctionsName.includes(declaration.init?.callee.name)\n )\n ) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, property.argument, {\n comment: getComment(property),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n });\n }\n if (declaration.id?.type === 'Identifier') {\n const name = declaration.id.name;\n const binding = path.scope.getBinding(name);\n if (\n binding\n && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )\n && !(declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && ignoreFunctionsName.includes(declaration.init?.callee.name)\n )\n ) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, declaration, {\n comment: getComment(path.node),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n\n if (spread.includes(name)) {\n if (declaration.init?.type === 'ObjectExpression') {\n declaration.init?.properties.forEach((prop) => {\n if (\n (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod')\n && prop.key.type === 'Identifier'\n ) {\n const keyName = prop.key.name;\n graph.nodes.add(keyName);\n nodeCollection.addNode(keyName, prop, {\n comment: getComment(prop),\n });\n if (!graph.edges.get(keyName)) {\n graph.edges.set(keyName, new Set());\n }\n if (graph.spread.has(name)) {\n graph.spread.get(name)?.add(keyName);\n }\n else {\n graph.spread.set(name, new Set([keyName]));\n }\n }\n else if (prop.type === 'SpreadElement') {\n console.warn('not support spread in spread');\n }\n });\n }\n if (\n declaration.init?.type === 'CallExpression'\n && declaration.init?.callee.type === 'Identifier'\n && declaration.init?.callee.name === 'reactive'\n ) {\n const arg = declaration.init?.arguments[0];\n if (arg.type === 'ObjectExpression') {\n arg.properties.forEach((prop) => {\n if (\n (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod')\n && prop.key.type === 'Identifier'\n ) {\n const keyName = prop.key.name;\n graph.nodes.add(keyName);\n nodeCollection.addNode(keyName, prop, {\n comment: getComment(prop),\n });\n if (!graph.edges.get(keyName)) {\n graph.edges.set(keyName, new Set());\n }\n if (graph.spread.has(name)) {\n graph.spread.get(name)?.add(keyName);\n }\n else {\n graph.spread.set(name, new Set([keyName]));\n }\n }\n else if (prop.type === 'SpreadElement') {\n console.warn('not support spread in spread');\n }\n });\n }\n }\n }\n }\n }\n });\n },\n FunctionDeclaration(path) {\n const name = path.node.id?.name;\n if (name) {\n const binding = path.scope.getBinding(name);\n if (binding && (path.parent.type === 'Program'\n || (parentPath?.type === 'ObjectMethod' && parentPath.body === path.parent)\n )) {\n graph.nodes.add(name);\n nodeCollection.addNode(name, path.node.id!, {\n isMethod: true,\n comment: getComment(path.node),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n },\n }, parentScope, parentPath);\n\n function traverseHooks(node: t.ExpressionStatement | t.CallExpression, patentScope: Scope) {\n if (\n (\n node.type === 'ExpressionStatement'\n && node.expression.type === 'CallExpression'\n && node.expression.callee.type === 'Identifier'\n ) || (\n node.type === 'CallExpression'\n && node.callee.type === 'Identifier'\n )\n ) {\n const hookName = (() => {\n if (node.type === 'ExpressionStatement'\n && node.expression.type === 'CallExpression'\n && node.expression.callee.type === 'Identifier') {\n return node.expression.callee.name;\n }\n if (node.type === 'CallExpression'\n && node.callee.type === 'Identifier') {\n return node.callee.name;\n }\n })() || '';\n\n if (!hookName) {\n return;\n }\n\n const hookBinding = patentScope.getBinding(hookName);\n if (!(hookBinding === undefined || hookBinding?.scope.block.type === 'Program'\n || parentScope === hookBinding?.scope)) {\n return;\n }\n\n const expression = (node.type === 'ExpressionStatement'\n ? node.expression\n : node) as t.CallExpression;\n\n const watchArgs = new Set<t.Identifier>();\n\n if (hookName === 'provide') {\n traverse(expression, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n const _node = nodeCollection.getNode(path1.node.name);\n if (_node?.info?.used) {\n _node?.info?.used?.add(hookName);\n }\n else if (_node) {\n _node.info = {\n ..._node?.info,\n used: new Set([hookName]),\n };\n }\n }\n },\n }, patentScope, node);\n }\n else if (watchHooks.includes(hookName)) {\n if (expression.arguments[0].type === 'Identifier') {\n const binding = patentScope.getBinding(expression.arguments[0].name);\n if (\n graph.nodes.has(expression.arguments[0].name)\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n watchArgs.add(expression.arguments[0]);\n }\n }\n else {\n traverse(expression.arguments[0], {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n watchArgs.add(path1.node);\n }\n },\n }, patentScope, node);\n }\n }\n else if (hookName === 'useEffect' && expression.arguments[1].type === 'ArrayExpression') {\n traverse(expression.arguments[1], {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n watchArgs.add(path1.node);\n }\n },\n }, patentScope, node);\n }\n expression.arguments.forEach((argNode, index) => {\n if (watchHooks.includes(hookName) && index === 0 && argNode.type === 'Identifier') {\n const _node = nodeCollection.getNode(argNode.name);\n if (_node?.info?.used) {\n _node?.info?.used?.add(hookName);\n }\n else if (_node) {\n _node.info = {\n ..._node?.info,\n used: new Set([hookName]),\n };\n }\n return;\n }\n if (argNode.type === 'Identifier') {\n const binding = patentScope.getBinding(argNode.name);\n if (\n graph.nodes.has(argNode.name)\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n const _node = nodeCollection.getNode(argNode.name);\n if (_node?.info?.used) {\n _node?.info?.used?.add(hookName);\n }\n else if (_node) {\n _node.info = {\n ..._node?.info,\n used: new Set([hookName]),\n };\n }\n }\n }\n else {\n traverse(argNode, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || parentScope === binding?.scope)\n ) {\n if ([...watchHooks, 'useEffect'].includes(hookName) && watchArgs.size > 0) {\n const watchArgsNames = Array.from(watchArgs).map(arg => arg.name);\n watchArgs.forEach((watchArg) => {\n if (!watchArgsNames.includes(path1.node.name)) {\n graph.edges.get(watchArg.name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n });\n }\n const _node = nodeCollection.getNode(path1.node.name);\n if (_node?.info?.used) {\n _node?.info?.used?.add(hookName);\n }\n else if (_node) {\n _node.info = {\n ..._node?.info,\n used: new Set([hookName]),\n };\n }\n }\n },\n }, patentScope, node);\n }\n });\n }\n }\n\n // get the relation between the variable and the function\n traverse(ast, {\n FunctionDeclaration(path) {\n const name = path.node.id?.name;\n if (name && graph.nodes.has(name)) {\n traverse(path.node.body, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n },\n\n VariableDeclarator(path) {\n if (path.node.init) {\n if (path.node.id.type === 'ArrayPattern') {\n path.node.id.elements.forEach((element) => {\n if (element?.type === 'Identifier') {\n const name = element.name;\n if (name && graph.nodes.has(name) && path.node.init?.type === 'CallExpression') {\n traverse(path.node.init, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n }\n });\n }\n else if (path.node.id.type === 'ObjectPattern') {\n path.node.id.properties.forEach((property) => {\n if (property.type === 'ObjectProperty' && property.value.type === 'Identifier') {\n const name = property.value.name;\n if (name && graph.nodes.has(name) && path.node.init) {\n traverse(path.node.init, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n }\n });\n }\n else if ([\n 'CallExpression',\n 'ArrowFunctionExpression',\n 'FunctionDeclaration',\n ].includes(path.node.init.type)\n && path.node.id.type === 'Identifier'\n ) {\n if (path.node.init.type === 'CallExpression' && path.node.init.callee.type === 'Identifier' && [...watchHooks, 'watchEffect'].includes(path.node.init.callee.name)) {\n traverseHooks(path.node.init, path.scope);\n }\n const name = path.node.id?.name;\n if (name && graph.nodes.has(name)) {\n traverse(path.node.init, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n }\n else if (path.node.id.type === 'Identifier') {\n const name = path.node.id.name;\n if (path.node.init.type === 'Identifier') {\n const binding = path.scope.getBinding(path.node.init.name);\n if (\n graph.nodes.has(path.node.init.name)\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path.node.init.name,\n type: getRelationType(path),\n });\n }\n }\n else {\n traverse(path.node.init, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n }, path.scope, path);\n }\n }\n }\n },\n\n ObjectMethod(path) {\n if (path.node.key.type === 'Identifier' && graph.nodes.has(path.node.key.name)) {\n const name = path.node.key.name;\n\n traverse(path.node.body, {\n Identifier(path1) {\n const binding = path1.scope.getBinding(path1.node.name);\n if (\n graph.nodes.has(path1.node.name)\n && (\n (path1.parent.type !== 'MemberExpression'\n && path1.parent.type !== 'OptionalMemberExpression')\n || path1.parent.object === path1.node\n )\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.name,\n type: getRelationType(path1),\n });\n }\n },\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n },\n\n ObjectProperty(path) {\n if (path.node.key.type === 'Identifier' && graph.nodes.has(path.node.key.name)) {\n const name = path.node.key.name;\n\n traverse(path.node.value, {\n MemberExpression(path1) {\n if (\n path1.node.object.type === 'Identifier'\n && spread.includes(path1.node.object.name)\n ) {\n const binding = path1.scope.getBinding(path1.node.object.name);\n if (\n spread.includes(path1.node.object.name)\n && path1.node.property.type === 'Identifier'\n && (binding?.scope.block.type === 'Program'\n || (parentScope === binding?.scope)\n )\n ) {\n graph.edges.get(name)?.add({\n label: path1.node.property.name,\n type: getRelationType(path1),\n });\n }\n }\n },\n }, path.scope, path);\n }\n },\n ExpressionStatement(path) {\n if (path.type === 'ExpressionStatement'\n && path.node.expression.type === 'CallExpression'\n && path.node.expression.callee.type === 'Identifier'\n ) {\n const name = path.node.expression.callee.name;\n if (\n graph.nodes.has(name)\n && (path.scope.block.type === 'Program')\n ) {\n const _node = nodeCollection.getNode(name);\n if (_node?.info?.used) {\n _node?.info?.used?.add('Call Expression');\n }\n else if (_node) {\n _node.info = {\n ..._node?.info,\n used: new Set(['Call Expression']),\n };\n }\n }\n else {\n traverseHooks(path.node.expression, path.scope);\n }\n }\n if (path.type === 'ExpressionStatement'\n && path.node.expression.type === 'AssignmentExpression'\n && path.node.expression.right.type === 'CallExpression'\n && path.node.expression.right.callee.type === 'Identifier'\n ) {\n traverseHooks(path.node.expression.right, path.scope);\n }\n },\n }, parentScope, parentPath);\n\n return {\n graph,\n nodeCollection,\n };\n}\n\nexport function analyze(\n content: string,\n lineOffset = 0,\n jsx = false,\n) {\n // console.log(content);\n const ast = babelParse(content, { sourceType: 'module', plugins: [\n 'typescript',\n ...jsx\n ? ['jsx' as const]\n : [],\n ] });\n\n // ---\n const { graph, nodeCollection } = processSetup(ast, undefined, undefined, undefined, lineOffset);\n return nodeCollection.map(graph);\n}\n","import type { NodePath } from '@babel/traverse';\nimport type * as t from '@babel/types';\nimport type { RelationType } from './utils';\nimport _traverse from '@babel/traverse';\nimport { babelParse } from '@vue/compiler-sfc';\nimport { processSetup, watchHooks } from './setupScript';\nimport { getComment, getRelationType, NodeCollection } from './utils';\n\nconst traverse: typeof _traverse\n // @ts-expect-error unwarp default\n = _traverse.default?.default || _traverse.default || _traverse;\n\nconst vueLifeCycleHooks = [\n 'beforeCreate',\n 'created',\n 'beforeMount',\n 'mounted',\n 'beforeUpdate',\n 'updated',\n 'beforeDestroy',\n 'destroyed',\n 'activated',\n 'deactivated',\n 'errorCaptured',\n 'renderTracked',\n 'renderTriggered',\n 'provide',\n];\n\nexport function analyze(\n content: string,\n lineOffset = 0,\n jsx = false,\n) {\n // console.log({lineOffset});\n // console.log(content);\n const ast = babelParse(content, { sourceType: 'module', plugins: [\n 'typescript',\n ...jsx\n ? ['jsx' as const]\n : [],\n ] });\n\n // ---\n\n let nodeCollection = new NodeCollection(lineOffset);\n\n const tNodes = new Map<string, t.Identifier>();\n const graph = {\n nodes: new Set<string>(),\n edges: new Map<string, Set<{ label: string, type: RelationType }>>(),\n };\n\n /** used in render block or setup return */\n const nodesUsedInTemplate = new Set<string>();\n\n function process(node: t.ObjectExpression, path: NodePath<t.ExportDefaultDeclaration>) {\n traverse(node, {\n ObjectProperty(path1) {\n if (\n (\n path.node.declaration.type === 'ObjectExpression'\n && path1.parent === path.node.declaration\n ) || (\n path.node.declaration.type === 'CallExpression'\n && path1.parent === path.node.declaration.arguments[0]\n )\n ) {\n // data\n if (\n path1.node.key.type === 'Identifier'\n && path1.node.key.name === 'data'\n && (\n path1.node.value.type === 'ArrowFunctionExpression'\n || path1.node.value.type === 'FunctionExpression'\n )\n ) {\n const dataNode = path1.node.value;\n\n traverse(dataNode, {\n ReturnStatement(path2) {\n if (path2.parent === dataNode.body) {\n if (path2.node.argument?.type === 'ObjectExpression') {\n path2.node.argument.properties.forEach((prop) => {\n if (prop.type === 'ObjectProperty') {\n if (prop.key.type === 'Identifier') {\n const name = prop.key.name;\n graph.nodes.add(name);\n tNodes.set(name, prop.key);\n nodeCollection.addNode(name, prop, {\n comment: getComment(prop),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n });\n }\n }\n },\n }, path1.scope, path1);\n }\n\n // computed\n if (path1.node.key.type === 'Identifier' && path1.node.key.name === 'computed') {\n const computedNode = path1.node;\n if (computedNode.value.type === 'ObjectExpression') {\n computedNode.value.properties.forEach((prop) => {\n if (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') {\n if (prop.key.type === 'Identifier') {\n const name = prop.key.name;\n graph.nodes.add(name);\n tNodes.set(name, prop.key);\n nodeCollection.addNode(name, prop, {\n isComputed: true,\n comment: getComment(prop),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n });\n }\n }\n\n // methods\n if (path1.node.key.type === 'Identifier' && path1.node.key.name === 'methods') {\n const methodsNode = path1.node;\n if (methodsNode.value.type === 'ObjectExpression') {\n methodsNode.value.properties.forEach((prop) => {\n if (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') {\n if (prop.key.type === 'Identifier') {\n const name = prop.key.name;\n graph.nodes.add(name);\n tNodes.set(name, prop.key);\n nodeCollection.addNode(name, prop, {\n isMethod: true,\n comment: getComment(prop),\n });\n if (!graph.edges.get(name)) {\n graph.edges.set(name, new Set());\n }\n }\n }\n });\n }\n }\n\n if (\n path1.node.key.type === 'Identifier'\n && path1.node.key.name === 'render'\n && (\n path1.node.value.type === 'ArrowFunctionExpression'\n || path1.node.value.type === 'FunctionExpression'\n )\n ) {\n traverse(path1.node.value, {\n ReturnStatement(path2) {\n const templateNode = path2.node;\n traverse(templateNode, {\n MemberExpression(path3) {\n if (path3.node.object && path3.node.object.type === 'ThisExpression') {\n if (path3.node.property && path3.node.property.type === 'Identifier') {\n nodesUsedInTemplate.add(path3.node.property.name);\n }\n }\n },\n }, path2.scope, path2);\n },\n }, path1.scope, path1);\n }\n }\n },\n ObjectMethod(path1) {\n if (\n (\n path.node.declaration.type === 'ObjectExpression'\n && path1.parent === path.node.declaration\n ) || (\n path.node.declaration.type === 'CallExpression'\n && path1.parent === path.node.declaration.arguments[0]\n )\n ) {\n // setup\n if (path1.node.key.type === 'Identifier' && path1.node.key.name === 'setup') {\n const setupNode = path1.node;\n\n const spread: string[] = [];\n\n traverse(setupNode, {\n ReturnStatement(path2) {\n if (path2.node.argument?.type === 'ObjectExpression') {\n const returnNode = path2.node.argument;\n traverse(returnNode, {\n SpreadElement(path3) {\n // ...toRefs(xxx)\n if (\n path3.node.argument.type === 'CallExpression'\n && path3.node.argument.callee.type === 'Identifier'\n && path3.node.argument.callee.name === 'toRefs'\n && path3.node.argument.arguments[0].type === 'Identifier'\n ) {\n spread.push(path3.node.argument.arguments[0].name);\n }\n // ...xxx\n else if (\n path3.node.argument.type === 'Identifier'\n ) {\n spread.push(path3.node.argument.name);\n }\n },\n }, path2.scope, path2);\n }\n if (\n path2.node.argument?.type === 'FunctionExpression'\n || path2.node.argument?.type === 'ArrowFunctionExpression'\n ) {\n const templateNode = path2.node.argument.body;\n traverse(templateNode, {\n Identifier(path3) {\n const binding = path3.scope.getBinding(path3.node.name);\n if (binding?.scope === path1.scope) {\n nodesUsedInTemplate.add(path3.node.name);\n }\n },\n JSXIdentifier(path3) {\n const binding = path3.scope.getBinding(path3.node.name);\n if (binding?.scope === path1.scope) {\n nodesUsedInTemplate.add(path3.node.name);\n }\n },\n }, path2.scope, path2);\n }\n },\n }, path1.scope, path1);\n\n const {\n graph: {\n nodes: tempNodes,\n edges: tempEdges,\n spread: tempSpread,\n },\n nodeCollection: tempNodeCollection,\n } = processSetup(setupNode, path1.scope, setupNode, spread, lineOffset);\n\n // 3 filter data by return\n traverse(setupNode, {\n ReturnStatement(path2) {\n // only process return in setupNode scope\n if (path2.scope !== path1.scope) {\n return;\n }\n\n if (path2.node.argument?.type === 'ObjectExpression') {\n const returnNode = path2.node.argument;\n traverse(returnNode, {\n ObjectProperty(path3) {\n if (path3.parent === returnNode) {\n if (\n path3.node.key.type === 'Identifier'\n && path3.node.value.type === 'Identifier'\n && tempNodes.has(path3.node.value.name)\n ) {\n const valName = path3.node.value.name;\n if (!graph.nodes.has(valName)) {\n graph.nodes.add(valName);\n tNodes.set(valName, path3.node.value);\n nodeCollection.addTypedNode(\n valName,\n tempNodeCollection.nodes.get(valName)!,\n );\n }\n if (!graph.edges.has(valName)) {\n graph.edges.set(valName, new Set([...Array.from(\n tempEdges.get(valName) || new Set<{ label: string, type: RelationType }>(),\n )]));\n }\n\n const name = path3.node.key.name;\n if (name !== valName) {\n graph.nodes.add(name);\n tNodes.set(name, path3.node.key);\n nodeCollection.addNode(name, path3.node.key, {\n comment: getComment(path3.node),\n