UNPKG

@nodeboot/aot

Version:

Node-Boot module for Ahead-of-Time (AOT) compilation. Generates node-boot beans and OpenAPI schemas at compile time

197 lines 7.67 kB
#!/usr/bin/env node "use strict"; /** * Node-Boot Circular Dependency Analyzer * * This script statically analyzes the TypeScript source code under the `src/` directory * to detect **circular dependencies** between services/components using class constructor * injection and NodeBoot decorators like `@Service` or `@Component`. * * ### Features: * - **Supports TypeScript 4 & 5**: Uses a compatibility wrapper for accessing decorators. * - **Decorator Filtering**: Only classes decorated with known injectable decorators are analyzed. * - **Dependency Graph**: Builds a constructor-based dependency graph of injected types. * - **Cycle Detection**: Uses depth-first search (DFS) to find cycles in the graph. * - **Fast & CI-Ready**: Designed to run post-lint/build in CI pipelines to fail on architecture violations. * * ### Usage: * Run manually or from `package.json` scripts: * ```sh * node node-boot-cycle-detector.js * ``` * * ### Requirements: * - Your classes should use NodeBoot decorators (`@Service`, `@Component`, etc.) * - TypeScript must be configured with appropriate type information (e.g., not erased before analysis). * - The analysis is performed **before compilation**, directly on `.ts` files in `src/`. * * --- * Part of the Node-Boot developer toolchain for enforcing architectural soundness. * * @author Manuel Santos <https://github.com/manusant> */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * Node-Boot Circular Dependency Analyzer * * This script statically analyzes the TypeScript source code under the `src/` directory * to detect **circular dependencies** between services/components using class constructor * injection and NodeBoot decorators like `@Service` or `@Component`. * * ### Features: * - **Supports TypeScript 4 & 5**: Uses a compatibility wrapper for accessing decorators. * - **Decorator Filtering**: Only classes decorated with known injectable decorators are analyzed. * - **Dependency Graph**: Builds a constructor-based dependency graph of injected types. * - **Cycle Detection**: Uses depth-first search (DFS) to find cycles in the graph. * - **Fast & CI-Ready**: Designed to run post-lint/build in CI pipelines to fail on architecture violations. * * ### Usage: * Run manually or from `package.json` scripts: * ```sh * node node-boot-cycle-detector.js * ``` * * ### Requirements: * - Your classes should use NodeBoot decorators (`@Service`, `@Component`, etc.) * - TypeScript must be configured with appropriate type information (e.g., not erased before analysis). * - The analysis is performed **before compilation**, directly on `.ts` files in `src/`. * * --- * Part of the Node-Boot developer toolchain for enforcing architectural soundness. * * @author Manuel Santos <https://github.com/manusant> */ const typescript_1 = __importDefault(require("typescript")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); // Define where to scan source files const PROJECT_DIR = path_1.default.resolve(process.cwd(), "src"); // List of decorator names to track as injectable const decoratorsToTrack = ["Service", "Component"]; /** * Recursively collects all TypeScript files in a directory, * excluding `.d.ts` files. */ function getAllTsFiles(dir) { const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); let files = []; for (const entry of entries) { const fullPath = path_1.default.join(dir, entry.name); if (entry.isDirectory()) { files = files.concat(getAllTsFiles(fullPath)); } else if (fullPath.endsWith(".ts") && !fullPath.endsWith(".d.ts")) { files.push(fullPath); } } return files; } /** * TypeScript 4/5 compatible way to get decorators from a node. */ function getDecorators(node) { if (typescript_1.default.canHaveDecorators && typescript_1.default.getDecorators) { // TS 5+ if (typescript_1.default.canHaveDecorators(node)) { return typescript_1.default.getDecorators(node); } return undefined; } // TS 4 fallback return node.decorators; } /** * Checks if a class declaration is decorated with any of the tracked decorator names. */ function hasDecorator(node, names) { const decorators = getDecorators(node); return (decorators || []).some(d => { const call = d.expression; return typescript_1.default.isCallExpression(call) && typescript_1.default.isIdentifier(call.expression) && names.includes(call.expression.text); }); } /** * Extracts type names from the constructor parameters as dependency identifiers. */ function getConstructorDependencies(node) { const ctor = node.members.find(typescript_1.default.isConstructorDeclaration); if (!ctor) return []; return ctor.parameters .map(param => { if (param.type && typescript_1.default.isTypeReferenceNode(param.type) && typescript_1.default.isIdentifier(param.type.typeName)) { return param.type.typeName.text; } return null; }) .filter((x) => !!x); } /** * Parses all files and builds a dependency graph of decorated services/components. */ function buildServiceGraph(files) { const graph = {}; for (const file of files) { const code = fs_1.default.readFileSync(file, "utf-8"); const source = typescript_1.default.createSourceFile(file, code, typescript_1.default.ScriptTarget.Latest, true); typescript_1.default.forEachChild(source, node => { if (typescript_1.default.isClassDeclaration(node) && node.name && hasDecorator(node, decoratorsToTrack)) { const className = node.name.text; const deps = getConstructorDependencies(node); graph[className] = new Set(deps); } }); } return graph; } /** * Runs a depth-first search on the graph to detect cycles. */ function detectCycles(graph) { const visited = new Set(); const pathStack = new Set(); const cycles = []; function dfs(node, path) { if (pathStack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; } if (visited.has(node)) return; visited.add(node); pathStack.add(node); const deps = graph[node] || new Set(); for (const dep of deps) { dfs(dep, [...path, node]); } pathStack.delete(node); } for (const node in graph) { dfs(node, []); } return cycles; } // ─────────────────────────────── // Execute the analysis // ─────────────────────────────── console.info("===================== NodeBoot Cycle Detector ====================="); const files = getAllTsFiles(PROJECT_DIR); const graph = buildServiceGraph(files); const cycles = detectCycles(graph); // Print results if (cycles.length) { console.error(`❌ Detected ${cycles.length} circular service dependency(ies):\n`); for (const cycle of cycles) { console.error(" 🔁 " + cycle.join(" → ")); } process.exit(1); } else { console.log("✅ No circular service dependencies found."); } console.info("===================================================================="); //# sourceMappingURL=node-boot-cycle-detector.js.map