feat: add make-deps script for function dependency graph generation
Some checks failed
build / build (push) Failing after 15s
Some checks failed
build / build (push) Failing after 15s
This commit is contained in:
90
package-lock.json
generated
90
package-lock.json
generated
@@ -22,8 +22,9 @@
|
|||||||
"sk-az-tools": "dist/cli.js"
|
"sk-az-tools": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": ">=24.0.0",
|
||||||
"typescript": "^5.8.2"
|
"ts-morph": ">=27.0.0",
|
||||||
|
"typescript": ">=5.8.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
@@ -281,6 +282,18 @@
|
|||||||
"sk-tools": "dist/cli.js"
|
"sk-tools": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ts-morph/common": {
|
||||||
|
"version": "0.28.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz",
|
||||||
|
"integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.0.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"tinyglobby": "^0.2.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.11.0",
|
"version": "24.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||||
@@ -459,6 +472,13 @@
|
|||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/code-block-writer": {
|
||||||
|
"version": "13.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||||
|
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
@@ -664,6 +684,24 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@@ -1113,6 +1151,26 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-browserify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
@@ -1413,6 +1471,34 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-morph": {
|
||||||
|
"version": "27.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz",
|
||||||
|
"integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ts-morph/common": "~0.28.1",
|
||||||
|
"code-block-writer": "^13.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -8,11 +8,10 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist",
|
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||||
"build": "npm run clean && tsc && chmod +x dist/cli.js",
|
"create-pca": "node dist/create-pca.js",
|
||||||
"build:watch": "tsc --watch",
|
"make-deps": "node scripts/make-mermaid-func-deps.mjs",
|
||||||
"prepublishOnly": "npm run build",
|
"clean": "rm -rf dist"
|
||||||
"create-pca": "node dist/create-pca.js"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
@@ -29,8 +28,9 @@
|
|||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": ">=24.0.0",
|
||||||
"typescript": "^5.8.2"
|
"ts-morph": ">=27.0.0",
|
||||||
|
"typescript": ">=5.8.2"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Sławomir Koszewski",
|
"name": "Sławomir Koszewski",
|
||||||
|
|||||||
210
scripts/make-mermaid-func-deps.mjs
Normal file
210
scripts/make-mermaid-func-deps.mjs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { parseArgs } from "node:util";
|
||||||
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
||||||
|
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
const MAX_EDGES = Number(process.env.MAX_GRAPH_EDGES ?? "450");
|
||||||
|
|
||||||
|
const {
|
||||||
|
values: { source: sourceArgsRaw, output: outputArg },
|
||||||
|
} = parseArgs({
|
||||||
|
options: {
|
||||||
|
source: {
|
||||||
|
type: "string",
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function collectFunctionEntries(sourceFile) {
|
||||||
|
const entries = [];
|
||||||
|
const namesSeen = new Set();
|
||||||
|
|
||||||
|
for (const fn of sourceFile.getFunctions()) {
|
||||||
|
const name = fn.getName();
|
||||||
|
if (!name || namesSeen.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
namesSeen.add(name);
|
||||||
|
entries.push({ name, node: fn });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const declaration of sourceFile.getVariableDeclarations()) {
|
||||||
|
const name = declaration.getName();
|
||||||
|
if (namesSeen.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializer = declaration.getInitializer();
|
||||||
|
if (!initializer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Node.isArrowFunction(initializer) && !Node.isFunctionExpression(initializer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
namesSeen.add(name);
|
||||||
|
entries.push({ name, node: initializer });
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeId(filePath, functionName) {
|
||||||
|
return `${filePath}__${functionName}`.replace(/[^A-Za-z0-9_]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelativePath(filePath) {
|
||||||
|
return filePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSourceArg(sourceArg) {
|
||||||
|
const projectRelative = path.isAbsolute(sourceArg)
|
||||||
|
? path.relative(projectRoot, sourceArg)
|
||||||
|
: sourceArg;
|
||||||
|
return normalizeRelativePath(projectRelative).replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionsByFile = new Map();
|
||||||
|
const allFunctionNames = new Set();
|
||||||
|
const functionEntriesByFile = new Map();
|
||||||
|
|
||||||
|
const project = new Project({
|
||||||
|
tsConfigFilePath: path.join(projectRoot, "tsconfig.json"),
|
||||||
|
});
|
||||||
|
const sourceFiles = project
|
||||||
|
.getSourceFiles()
|
||||||
|
.filter((sourceFile) => !sourceFile.isDeclarationFile())
|
||||||
|
.sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
|
||||||
|
|
||||||
|
const sourceContext = sourceFiles.map((sourceFile) => ({
|
||||||
|
sourceFile,
|
||||||
|
relativePath: normalizeRelativePath(path.relative(projectRoot, sourceFile.getFilePath())),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredSourceFiles = (() => {
|
||||||
|
if (!sourceArgsRaw || sourceArgsRaw.length === 0) {
|
||||||
|
return sourceContext.map((item) => item.sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceArgs = sourceArgsRaw.map((sourceArg) => normalizeSourceArg(sourceArg));
|
||||||
|
const matchedSourceFiles = new Set();
|
||||||
|
|
||||||
|
for (const sourceArg of sourceArgs) {
|
||||||
|
const hasMatch = sourceContext.some((item) => (
|
||||||
|
item.relativePath === sourceArg
|
||||||
|
|| item.relativePath.startsWith(`${sourceArg}/`)
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!hasMatch) {
|
||||||
|
throw new Error(`No source file matched --source=${sourceArg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of sourceContext) {
|
||||||
|
if (item.relativePath === sourceArg || item.relativePath.startsWith(`${sourceArg}/`)) {
|
||||||
|
matchedSourceFiles.add(item.sourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...matchedSourceFiles];
|
||||||
|
})();
|
||||||
|
|
||||||
|
for (const sourceFile of filteredSourceFiles) {
|
||||||
|
const absolutePath = sourceFile.getFilePath();
|
||||||
|
|
||||||
|
const relativePath = path.relative(projectRoot, absolutePath).replaceAll("\\", "/");
|
||||||
|
const functionEntries = collectFunctionEntries(sourceFile);
|
||||||
|
const functionNames = functionEntries.map((entry) => entry.name);
|
||||||
|
|
||||||
|
functionsByFile.set(relativePath, functionNames);
|
||||||
|
functionEntriesByFile.set(relativePath, functionEntries);
|
||||||
|
for (const functionName of functionNames) {
|
||||||
|
allFunctionNames.add(functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstNodeForFunction = new Map();
|
||||||
|
for (const [relativePath, functionNames] of functionsByFile.entries()) {
|
||||||
|
for (const functionName of functionNames) {
|
||||||
|
if (!firstNodeForFunction.has(functionName)) {
|
||||||
|
firstNodeForFunction.set(functionName, nodeId(relativePath, functionName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeSet = new Set();
|
||||||
|
for (const functionEntries of functionEntriesByFile.values()) {
|
||||||
|
for (const { name: sourceName, node } of functionEntries) {
|
||||||
|
const body = node.getBody();
|
||||||
|
if (!body) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceNode = firstNodeForFunction.get(sourceName);
|
||||||
|
if (!sourceNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
||||||
|
const expression = call.getExpression();
|
||||||
|
let targetName;
|
||||||
|
|
||||||
|
if (Node.isIdentifier(expression)) {
|
||||||
|
targetName = expression.getText();
|
||||||
|
} else if (Node.isPropertyAccessExpression(expression)) {
|
||||||
|
targetName = expression.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetName || targetName === sourceName || !allFunctionNames.has(targetName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNode = firstNodeForFunction.get(targetName);
|
||||||
|
if (targetNode) {
|
||||||
|
edgeSet.add(`${sourceNode} --> ${targetNode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagram = "%% Function Dependency Graph (`src/**/*.ts`)\n";
|
||||||
|
diagram += "%% Generated from current package source files.\n";
|
||||||
|
diagram += "flowchart LR\n";
|
||||||
|
|
||||||
|
for (const [relativePath, functionNames] of functionsByFile.entries()) {
|
||||||
|
if (functionNames.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const subgraphId = relativePath.replace(/[^A-Za-z0-9_]/g, "_");
|
||||||
|
diagram += ` subgraph ${subgraphId}[\"${relativePath}\"]\n`;
|
||||||
|
for (const functionName of functionNames) {
|
||||||
|
diagram += ` ${nodeId(relativePath, functionName)}[\"${functionName}\"]\n`;
|
||||||
|
}
|
||||||
|
diagram += " end\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedEdges = [...edgeSet].sort();
|
||||||
|
const emittedEdges = sortedEdges.slice(0, MAX_EDGES);
|
||||||
|
for (const edge of emittedEdges) {
|
||||||
|
diagram += ` ${edge}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortedEdges.length > MAX_EDGES) {
|
||||||
|
diagram += `%% Note: showing ${MAX_EDGES} of ${sortedEdges.length} detected edges to stay within Mermaid default edge limits.\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputArg) {
|
||||||
|
const outputPath = path.resolve(projectRoot, outputArg);
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
fs.writeFileSync(outputPath, diagram, "utf8");
|
||||||
|
} else {
|
||||||
|
process.stdout.write(diagram);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user