Compare commits
2 Commits
fff80047c2
...
d39fdb3e33
| Author | SHA1 | Date | |
|---|---|---|---|
| d39fdb3e33 | |||
| 2a0b49effe |
90
package-lock.json
generated
90
package-lock.json
generated
@@ -22,8 +22,9 @@
|
||||
"sk-az-tools": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.8.2"
|
||||
"@types/node": ">=24.0.0",
|
||||
"ts-morph": ">=27.0.0",
|
||||
"typescript": ">=5.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
@@ -281,6 +282,18 @@
|
||||
"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": {
|
||||
"version": "24.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||
@@ -459,6 +472,13 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"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": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
@@ -664,6 +684,24 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@@ -1113,6 +1151,26 @@
|
||||
"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": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -1413,6 +1471,34 @@
|
||||
"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": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
||||
14
package.json
14
package.json
@@ -8,11 +8,10 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"build": "npm run clean && tsc && chmod +x dist/cli.js",
|
||||
"build:watch": "tsc --watch",
|
||||
"prepublishOnly": "npm run build",
|
||||
"create-pca": "node dist/create-pca.js"
|
||||
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||
"create-pca": "node dist/create-pca.js",
|
||||
"make-deps": "node scripts/make-mermaid-func-deps.mjs",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
@@ -29,8 +28,9 @@
|
||||
"open": "^10.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.8.2"
|
||||
"@types/node": ">=24.0.0",
|
||||
"ts-morph": ">=27.0.0",
|
||||
"typescript": ">=5.8.2"
|
||||
},
|
||||
"author": {
|
||||
"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);
|
||||
}
|
||||
21
src/cli.ts
21
src/cli.ts
@@ -18,9 +18,6 @@ import {
|
||||
parseHeaderSpec,
|
||||
renderOutput,
|
||||
} from "@slawek/sk-tools";
|
||||
import {
|
||||
omitPermissionGuidColumns,
|
||||
} from "./cli/permission-utils.ts";
|
||||
|
||||
type CliValues = {
|
||||
help?: boolean;
|
||||
@@ -89,6 +86,16 @@ function usageCommand(command: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function omitRecords(record: Record<string, unknown>, names: Set<string>): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(record).filter(([key]) => !names.has(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const argv = process.argv.slice(2);
|
||||
const command = argv[0];
|
||||
@@ -138,9 +145,11 @@ async function main(): Promise<void> {
|
||||
const outputFormat = normalizeOutputFormat(typedValues.output);
|
||||
const result = await runCommand(command, typedValues);
|
||||
const filtered = outputFiltered(result, typedValues.query);
|
||||
const output = command === "list-app-permissions" && typedValues.short
|
||||
? omitPermissionGuidColumns(filtered)
|
||||
: filtered;
|
||||
let output: unknown = filtered;
|
||||
if (command === "list-app-permissions" && typedValues.short && Array.isArray(filtered) && filtered.every(isRecord)) {
|
||||
const names = new Set(["resourceAppId", "permissionId"]);
|
||||
output = filtered.map((item) => omitRecords(item, names));
|
||||
}
|
||||
const headerSpec = command === "rest"
|
||||
? parseHeaderSpec(undefined)
|
||||
: parseHeaderSpec(typedValues.header);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export function omitPermissionGuidColumns(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => omitPermissionGuidColumns(item));
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const { resourceAppId, permissionId, ...rest } = value as Record<string, unknown>;
|
||||
void resourceAppId;
|
||||
void permissionId;
|
||||
return rest;
|
||||
}
|
||||
Reference in New Issue
Block a user