ts-depgraph
Version:
Generate a visually stunning dependency graph from your Angular or Typescript project
208 lines (179 loc) • 5.84 kB
JavaScript
var fs = require("fs");
var fspath = require("path");
var config = {};
try {
config = require(process.cwd() + "/depgraph.config");
} catch {
console.warn(
'No local "depgraph.config" found. You can control the behaviour with it if you want. Check package folder for example.'
);
}
const sourceDirectory =
config.projectDirectory || process.cwd().replace(/\\/g, "/");
const sourceDirectoryRegex = new RegExp(`${sourceDirectory}/src/`);
const includePattern = new RegExp(config.includePattern || ".ts$");
const excludePattern = new RegExp(config.excludePattern || ".spec.ts$");
function getFileList(path) {
const getSourceFiles = (files) =>
files
.filter((file) => file.isFile())
.filter((file) => file.name.match(includePattern))
.filter((file) => !file.name.match(excludePattern));
const getDirectories = (files) => files.filter((file) => file.isDirectory());
const files = fs.readdirSync(path, { withFileTypes: true });
const fileList = getSourceFiles(files).map((file) =>
fspath.normalize(`${path}/${file.name}`)
);
getDirectories(files)
.map((subDir) => getFileList(`${path}/${subDir.name}`))
.map((dirFiles) => dirFiles.forEach((file) => fileList.push(file)));
return [...fileList];
}
function getTsConfigPaths(path) {
const mapping = {};
let tsconfig = {};
try {
tsconfig = require(config.tsconfig || `${path}/tsconfig.json`);
} catch (error) {
console.warn('tsconfig failed to load: ', error);
return mapping;
}
const paths =
(tsconfig && tsconfig.compilerOptions && tsconfig.compilerOptions.paths) ||
[];
Object.keys(paths).map((alias) => {
const pattern = alias.replace(/\*$/, "");
const replacement = paths[alias];
mapping[pattern] = replacement[0].replace(/\*$/, "");
});
return mapping;
}
const aliasMapping = getTsConfigPaths(sourceDirectory);
const fileList = getFileList(`${sourceDirectory}/src`);
function getImports(file) {
const content = fs
.readFileSync(file, { encoding: "UTF-8" })
.replace(/[\r\n]/gi, " ");
const nodes = content.match(/import\s+.*?\s+from\s+.*?;/gim) || [];
// TODO Lazy loading of modules in an Angular project
// import('./landing.module').then((m) => m.LandingModule),
// const inlineImports = content.match(/import\((.*?)\).*/);
return nodes;
}
function resolveAlias(path) {
const foundPattern = Object.keys(aliasMapping)
.map((alias) => {
const regex = new RegExp(`^${alias}`);
if (regex.test(path)) {
return { regex, replacement: aliasMapping[alias] };
}
})
.filter(Boolean);
return foundPattern.length > 0
? path.replace(foundPattern[0].regex, foundPattern[0].replacement)
: path;
}
function normalizImportPath(path, origin) {
const currentDir = fspath.dirname(origin);
const resolved = resolveAlias(path);
let absolutePath;
if (path !== resolved) {
absolutePath = fspath.normalize(`${sourceDirectory}/${resolved}`);
} else {
absolutePath = fspath.normalize(`${currentDir}/${path}`);
}
if (!(fs.existsSync(absolutePath) || fs.existsSync(absolutePath + ".ts"))) {
absolutePath = path;
}
return absolutePath;
}
function parseImport(imp, path) {
const data = imp.match(/import\b(.*?)\bfrom\b['" ]*(.*?)['" ]*;/);
return {
to: fspath.normalize(path),
imports: data[1]
.replace(/[\{\}]/g, "")
.split(/,/)
.map((v) => v.trim()),
from: normalizImportPath(data[2], path),
};
}
function getNodeColor(path) {
if (path.match(/module$/)) {
return "#ffcfcf";
}
if (path.match(/component$/)) {
return "#cfffcf";
}
if (path.match(/service$/)) {
return "#ffcfff";
}
if (path.match(/(effects?|selectors?|actions?|reducers?|state|facade)$/)) {
return "#cfcfcf";
}
return undefined;
}
function createNodes(graph) {
const nodes = {};
graph.forEach((node) => {
nodes[node.to] = {
id: node.to,
label: `*${fspath.basename(node.to)}*\n${fspath.dirname(node.to)}`,
shape: "box",
color: getNodeColor(node.to),
font: { multi: "md", size: 14 },
};
nodes[node.from] = {
id: node.from,
label: `*${fspath.basename(node.from)}*\n${fspath.dirname(node.from)}`,
shape: "box",
color: getNodeColor(node.from),
font: { multi: "md", size: 14 },
};
});
return Object.values(nodes);
}
function createEdges(graph) {
const edges = [];
graph.forEach((node) => {
edges.push({
to: node.to,
from: node.from,
arrows: "from",
label: node.imports.join("\n"),
font: { align: "horizontal" },
});
});
return edges;
}
function clearPath(path) {
return path.replace(/\.ts$/, "").replace(sourceDirectoryRegex, "");
}
function filterFile(path) {
return sourceDirectoryRegex.test(path);
}
const graph = fileList
.map((fileName) =>
getImports(fileName).map((imp) => parseImport(imp, fileName))
)
.reduce((acc, curr) => [...acc, ...curr], [])
.map((node) => ({
from: node.from.replace(/\\/g, "/"),
to: node.to.replace(/\\/g, "/"),
imports: node.imports,
}))
.filter((node) => filterFile(node.from))
.map((node) => ({
from: clearPath(node.from),
to: clearPath(node.to),
imports: node.imports,
}));
const nodes = createNodes(graph);
const edges = createEdges(graph);
fs.writeFileSync("depgraph.data.js", `\
const nodes = ${JSON.stringify(nodes)};
const edges = ${JSON.stringify(edges)};
`);
const runtime = __dirname.replace(/\\/g, "/");
fs.copyFileSync(`${runtime}/depgraph.html`, "./depgraph.html");
console.log("Open depgraph.html to view the dependency graph");