@rx-now/analysis
Version:
analysis tool for visualizing for code dependencies in typescript
397 lines (384 loc) • 16.8 kB
JavaScript
(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 });
}));