UNPKG

@rx-now/analysis

Version:

analysis tool for visualizing for code dependencies in typescript

397 lines (384 loc) 16.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('path'), require('child_process'), require('mustache'), require('ts-morph'), require('minimatch'), require('@rx-now/analysis-core')) : typeof define === 'function' && define.amd ? define(['exports', 'fs', 'path', 'child_process', 'mustache', 'ts-morph', 'minimatch', '@rx-now/analysis-core'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.rxNowAnalysis = {}, global.fs, global.path, global.child_process, global.Mustache, global.tsMorph, global.minimatch, global.analysisCore)); })(this, (function (exports, fs, path, child_process, Mustache, tsMorph, minimatch, analysisCore) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var Mustache__default = /*#__PURE__*/_interopDefaultLegacy(Mustache); var minimatch__default = /*#__PURE__*/_interopDefaultLegacy(minimatch); class Graph { constructor() { this.graph = null; this.vertices = new Set(); } exists(value) { return !!Array.from(this.vertices).find((o) => o.value === value); } getNodeByValue(value) { return Array.from(this.vertices).find((o) => o.value === value); } getEdges() { return Array.from(this.vertices) .map((v) => Array.from(v.children).map((c) => ({ source: c, target: v }))) .reduce((previousValue, currentValue) => previousValue.concat(currentValue), []); } } function getName(file) { const baseName = file.getBaseName(); return baseName; } class Workspace extends Graph { constructor(entryPath, tsConfigFilePath, ignoreList) { super(); this.ignoreList = []; this.project = new tsMorph.Project({ tsConfigFilePath }); const entryFile = this.project.getSourceFileOrThrow(entryPath); this.ignoreList = ignoreList; this.graph = { value: entryFile, parent: new Set(), children: new Set(), }; this.vertices.add(this.graph); this.collectDescendants(this.graph); } getG6Data() { throw new Error("Method not implemented."); } generateHtmlFile() { throw new Error("Method not implemented."); } getId(v) { if (v instanceof tsMorph.MethodDeclaration) { return `${v.getSourceFile().getFilePath()}.${v.getName()}`; } return v.getFilePath(); } getName(v) { if (v instanceof tsMorph.MethodDeclaration) { return `${getName(v.getSourceFile())}.${v.getName()}`; } return getName(v); } collectDescendants(current) { const sourceFile = current.value; const allImports = []; // static import statements sourceFile .getImportDeclarations() .map((i) => i .getNamedImports() .map((o) => o.getNameNode().getDefinitionNodes()) .reduce((prev, curr) => prev.concat(curr), [])) .reduce((prev, curr) => prev.concat(curr), []) .filter((n) => !this.ignoreFile(n.getSourceFile())) .forEach((n) => { allImports.push(n.getSourceFile()); }); // dynamic import statements sourceFile .getVariableStatements() .filter((o) => o.getDescendantsOfKind(tsMorph.SyntaxKind.ImportKeyword).length > 0) .forEach((o) => { const dynamicImports = o.getDescendantsOfKind(tsMorph.SyntaxKind.ImportKeyword); if (dynamicImports.length > 0) { dynamicImports.forEach((d) => { var _a; const str = d .getParentOrThrow() .getDescendantsOfKind(tsMorph.SyntaxKind.StringLiteral)[0]; (_a = str .getSymbol()) === null || _a === void 0 ? void 0 : _a.getDeclarations().filter((p) => !this.ignoreFile(p.getSourceFile())).map((p) => p.getSourceFile()).forEach((p) => allImports.push(p)); }); } }); // eslint-disable-next-line no-param-reassign current.children = new Set(); allImports.forEach((dest) => { const node = (this.getNodeByValue(dest) || { value: dest, parent: new Set(), children: new Set(), }); node.parent.add(current); current.children.add(node); if (!this.vertices.has(node)) { this.vertices.add(node); this.collectDescendants(node); } }); } ignoreFile(source) { return !!this.ignoreList.find((rule) => minimatch__default["default"](source.getFilePath(), rule)); } toString() { const res = []; Array.from(this.vertices).forEach((o) => { res.push(`${o.value.getSourceFile().getFilePath()} => ${Array.from(o.children) .map((p) => p.value.getSourceFile().getFilePath()) .join(", ")}`); }); return res.join("\n"); } } function appendPath(tree, items, equals = (a, b) => a === b) { let curr = tree; items.forEach((item) => { var _a; let next = (_a = curr.children) === null || _a === void 0 ? void 0 : _a.find((c) => equals(c.node, item)); if (!next) { if (!curr.children) { curr.children = []; } next = { node: item, count: 0, children: [] }; curr.children.push(next); } curr = next; }); curr.count += 1; return tree; } function dfs(tree, func, ext) { var _a; const data = func(tree, ext); (_a = tree.children) === null || _a === void 0 ? void 0 : _a.forEach((c) => dfs(c, func, data)); } const DEFAULT_LAYER_COLORS = [ { fill: "#C4E3B2", stroke: "#C4E3B2", }, { stroke: "#99C0ED", fill: "#99C0ED", }, ]; const DEFAULT_LAYER_ID = "other-components"; class HierarchicalGraph extends Workspace { constructor(config) { super(config.entryPath, config.tsConfigFilePath, config.ignoreList); this.config = config; this.layers = []; this.collapseTrivialGroups = false; this.groupsMap = {}; this.groups = []; const l = config.layerConfig || []; l.sort((a, b) => a.index - b.index); this.layers = l.map((o, i) => ({ id: o.name, label: o.name, style: { fill: o.fill || DEFAULT_LAYER_COLORS[i % DEFAULT_LAYER_COLORS.length].fill, stroke: o.stroke || DEFAULT_LAYER_COLORS[i % DEFAULT_LAYER_COLORS.length].stroke, }, match: o.rules || [], extractMethod: o.extractMethods, })); this.layers.push({ id: DEFAULT_LAYER_ID, label: "components", style: { fill: "eee", stroke: "eee" }, }); this.collectMethods(); this.collectGroups(); this.collapseTrivialGroups = config.collapseTrivialGroups || false; } collectMethods() { const methods = []; Array.from(this.vertices) .filter((v) => { var _a; return (_a = this.layers.find((l) => this.belongsTo(v.value.getSourceFile(), l))) === null || _a === void 0 ? void 0 : _a.extractMethod; }) .forEach((v) => { const sourceFile = v.value; const classes = sourceFile.getClasses(); classes.forEach((c) => c.getMethods().forEach((m) => { var _a; const methodNode = { value: m, children: new Set().add(v), parent: new Set(), }; methods.push(methodNode); (_a = this.getNodeByValue(sourceFile)) === null || _a === void 0 ? void 0 : _a.parent.add(methodNode); })); }); methods.forEach((m) => this.vertices.add(m)); methods.forEach((m) => { const methodDeclaration = m.value; methodDeclaration .findReferencesAsNodes() .filter((r) => !this.ignoreFile(r.getSourceFile())) .forEach((r) => { let node = r.getParentOrThrow(); // eslint-disable-next-line no-constant-condition while (true) { if (node.getKindName() === "MethodDeclaration") { const source = this.getNodeByValue(node.asKindOrThrow(tsMorph.SyntaxKind.MethodDeclaration)); if (source) { m.parent.add(source); source.children.add(m); break; } } if (node.getKindName() === "SourceFile") { const source = this.getNodeByValue(node.asKindOrThrow(tsMorph.SyntaxKind.SourceFile)); if (source) { m.parent.add(source); source.children.add(m); } break; } node = node.getParentOrThrow(); } }); }); } belongsTo(source, combo) { var _a; if (combo.match === undefined) return false; return !!((_a = combo.match) === null || _a === void 0 ? void 0 : _a.find((rule) => minimatch__default["default"](source.getFilePath(), rule))); } generateHtmlFile() { const template = fs.readFileSync(path.resolve(__dirname, "../templates/dag.mustache"), "utf-8"); const content = Mustache__default["default"].render(template, { g6Data: this.getG6Data(), }, undefined, { escape: (str) => str }); fs.writeFileSync(path.resolve(__dirname, "g6x.html"), content, "utf-8"); child_process.execSync(`open ${path.resolve(__dirname, "g6x.html")}`); } getLayerId(source) { var _a; return ((_a = this.layers.find((l) => this.belongsTo(source, l))) === null || _a === void 0 ? void 0 : _a.id) || ""; } getComboId(source) { var _a; const layerId = this.getLayerId(source) || this.layers[this.layers.length - 1].id; return (((_a = this.groups.find(({ id }) => `${layerId}-${source.getDirectoryPath()}` === id)) === null || _a === void 0 ? void 0 : _a.id) || ""); } getNodesData() { return Array.from(this.vertices).map((o) => ({ id: this.getId(o.value), item: this.getName(o.value), comboId: this.getComboId(o.value.getSourceFile()), })); } getEdgesData() { const edges = this.getEdges(); return edges.map(({ source, target }) => ({ source: this.getId(source.value), target: this.getId(target.value), error: this.getEdgeErrorInfo(source, target), })); } getEdgeErrorInfo(source, target) { const error = analysisCore.checkDomain(target.value.getSourceFile().getFilePath(), source.value.getSourceFile().getFilePath(), this.config.allowedCommonParentNames, this.config.layerConfig); // let errorType = 0; // switch (error.error) { // case DomainImportError.IGNORE: // break; // case DomainImportError.SUB_DOMAIN_IMPORTED: // errorType = 1; // break; // case DomainImportError.SIBLING_FOLDER_IMPORTED: // errorType = 2; // break; // case DomainImportError.UPPER_LAYER_IMPORTED: // errorType = 3; // } // return errorType; return error.error; } getG6Data() { const nodes = this.getNodesData(); const edges = this.getEdgesData(); const combos = this.groups.map((o) => ({ id: o.id, label: o.label, parentId: o.parentId, style: o.style, collapsed: false, })); if (this.collapseTrivialGroups) { const comboSet = new Set(); // eslint-disable-next-line no-constant-condition while (true) { let boo = true; combos .filter((c) => !comboSet.has(c.id)) .filter((combo) => !combos.some((c) => c.parentId === combo.id && c.collapsed === false) && !nodes.some((n) => n.comboId === combo.id && edges.some((e) => (e.source === n.id || e.target === n.id) && e.error > 0))) .forEach((combo) => { boo = false; comboSet.add(combo.id); const curr = combo; curr.collapsed = true; }); if (boo) { break; } } } const data = { nodes, edges, combos, }; return JSON.stringify(data, null, 4); } collectGroups() { this.groups = []; const verticesGroups = this.layers.map(() => []); Array.from(this.vertices).forEach((v) => { let idx = this.layers.findIndex((c) => this.belongsTo(v.value.getSourceFile(), c)); if (idx === -1) { idx = this.layers.length - 1; } verticesGroups[idx].push(v); }); // 根据不同layer区分文件夹 verticesGroups.forEach((vertices, index) => { var _a; let tree = { node: "", count: 0, children: [] }; vertices.forEach((v) => { const f = v.value.getSourceFile(); const path = f.getDirectoryPath(); const arr = path.split("/").filter((x) => !!x); appendPath(tree, arr); }); const arr = []; while (tree.count === 0 && ((_a = tree.children) === null || _a === void 0 ? void 0 : _a.length) === 1) { arr.push(tree.node); [tree] = tree.children; } dfs(tree, (t, name) => { const curr = [...name, t.node]; this.groups.push({ id: `${this.layers[index].id}-${curr.join("/")}`, label: curr[curr.length - 1], style: { stroke: "#666", fill: "#fff", }, collapsed: false, parentId: name === arr ? this.layers[index].id : `${this.layers[index].id}-${name.join("/")}`, }); return curr; }, arr); }); this.groups = this.groups.concat(this.layers); } } function analyseProject(config) { const workspace = new HierarchicalGraph(config); workspace.generateHtmlFile(); } exports.analyseProject = analyseProject; Object.defineProperty(exports, '__esModule', { value: true }); }));