@viberails/graph 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-graph-worker.cjs +255 -0
- package/dist/build-graph-worker.cjs.map +1 -0
- package/dist/build-graph-worker.d.cts +2 -0
- package/dist/build-graph-worker.d.ts +2 -0
- package/dist/build-graph-worker.js +10 -0
- package/dist/build-graph-worker.js.map +1 -0
- package/dist/chunk-DQX7OULF.js +274 -0
- package/dist/chunk-DQX7OULF.js.map +1 -0
- package/dist/index.cjs +26 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +7 -244
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/build-graph-worker.ts
|
|
4
|
+
var import_node_worker_threads2 = require("worker_threads");
|
|
5
|
+
|
|
6
|
+
// src/build-graph.ts
|
|
7
|
+
var import_node_path2 = require("path");
|
|
8
|
+
var import_node_worker_threads = require("worker_threads");
|
|
9
|
+
var import_ts_morph2 = require("ts-morph");
|
|
10
|
+
|
|
11
|
+
// src/detect-cycles.ts
|
|
12
|
+
function detectCycles(edges) {
|
|
13
|
+
const graph = /* @__PURE__ */ new Map();
|
|
14
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const { source, target } of edges) {
|
|
16
|
+
nodes.add(source);
|
|
17
|
+
nodes.add(target);
|
|
18
|
+
const neighbors = graph.get(source);
|
|
19
|
+
if (neighbors) {
|
|
20
|
+
neighbors.push(target);
|
|
21
|
+
} else {
|
|
22
|
+
graph.set(source, [target]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const WHITE = 0;
|
|
26
|
+
const GRAY = 1;
|
|
27
|
+
const BLACK = 2;
|
|
28
|
+
const color = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const node of nodes) {
|
|
30
|
+
color.set(node, WHITE);
|
|
31
|
+
}
|
|
32
|
+
const cycles = [];
|
|
33
|
+
const path = [];
|
|
34
|
+
function dfs(node) {
|
|
35
|
+
color.set(node, GRAY);
|
|
36
|
+
path.push(node);
|
|
37
|
+
const neighbors = graph.get(node) ?? [];
|
|
38
|
+
for (const neighbor of neighbors) {
|
|
39
|
+
const c = color.get(neighbor);
|
|
40
|
+
if (c === GRAY) {
|
|
41
|
+
const cycleStart = path.indexOf(neighbor);
|
|
42
|
+
if (cycleStart !== -1) {
|
|
43
|
+
cycles.push(path.slice(cycleStart));
|
|
44
|
+
}
|
|
45
|
+
} else if (c === WHITE) {
|
|
46
|
+
dfs(neighbor);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
path.pop();
|
|
50
|
+
color.set(node, BLACK);
|
|
51
|
+
}
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
if (color.get(node) === WHITE) {
|
|
54
|
+
dfs(node);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return cycles;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/parse-imports.ts
|
|
61
|
+
var import_ts_morph = require("ts-morph");
|
|
62
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
63
|
+
".css",
|
|
64
|
+
".scss",
|
|
65
|
+
".less",
|
|
66
|
+
".sass",
|
|
67
|
+
".png",
|
|
68
|
+
".svg",
|
|
69
|
+
".jpg",
|
|
70
|
+
".jpeg",
|
|
71
|
+
".gif",
|
|
72
|
+
".ico",
|
|
73
|
+
".webp",
|
|
74
|
+
".json",
|
|
75
|
+
".woff",
|
|
76
|
+
".woff2",
|
|
77
|
+
".ttf",
|
|
78
|
+
".eot"
|
|
79
|
+
]);
|
|
80
|
+
function shouldSkip(specifier) {
|
|
81
|
+
const dotIndex = specifier.lastIndexOf(".");
|
|
82
|
+
if (dotIndex === -1) return false;
|
|
83
|
+
return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
function parseImports(sourceFile) {
|
|
86
|
+
const edges = [];
|
|
87
|
+
const filePath = sourceFile.getFilePath();
|
|
88
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
89
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
90
|
+
if (shouldSkip(specifier)) continue;
|
|
91
|
+
edges.push({
|
|
92
|
+
source: filePath,
|
|
93
|
+
target: specifier,
|
|
94
|
+
specifier,
|
|
95
|
+
typeOnly: decl.isTypeOnly(),
|
|
96
|
+
dynamic: false,
|
|
97
|
+
line: decl.getStartLineNumber()
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
101
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
102
|
+
if (!specifier || shouldSkip(specifier)) continue;
|
|
103
|
+
edges.push({
|
|
104
|
+
source: filePath,
|
|
105
|
+
target: specifier,
|
|
106
|
+
specifier,
|
|
107
|
+
typeOnly: decl.isTypeOnly(),
|
|
108
|
+
dynamic: false,
|
|
109
|
+
line: decl.getStartLineNumber()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const call of sourceFile.getDescendantsOfKind(import_ts_morph.SyntaxKind.CallExpression)) {
|
|
113
|
+
if (call.getExpression().getKind() !== import_ts_morph.SyntaxKind.ImportKeyword) continue;
|
|
114
|
+
const args = call.getArguments();
|
|
115
|
+
if (args.length === 0) continue;
|
|
116
|
+
const arg = args[0];
|
|
117
|
+
if (arg.getKind() !== import_ts_morph.SyntaxKind.StringLiteral) continue;
|
|
118
|
+
const specifier = arg.getText().slice(1, -1);
|
|
119
|
+
if (shouldSkip(specifier)) continue;
|
|
120
|
+
edges.push({
|
|
121
|
+
source: filePath,
|
|
122
|
+
target: specifier,
|
|
123
|
+
specifier,
|
|
124
|
+
typeOnly: false,
|
|
125
|
+
dynamic: true,
|
|
126
|
+
line: call.getStartLineNumber()
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return edges;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/resolve-import.ts
|
|
133
|
+
var import_node_module = require("module");
|
|
134
|
+
var import_node_path = require("path");
|
|
135
|
+
var BUILTINS = /* @__PURE__ */ new Set([...import_node_module.builtinModules, ...import_node_module.builtinModules.map((m) => `node:${m}`)]);
|
|
136
|
+
function resolveImport(specifier, fromFile, project, workspacePackages) {
|
|
137
|
+
if (BUILTINS.has(specifier)) {
|
|
138
|
+
return { kind: "builtin" };
|
|
139
|
+
}
|
|
140
|
+
const wsMatch = workspacePackages.find(
|
|
141
|
+
(pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)
|
|
142
|
+
);
|
|
143
|
+
if (wsMatch) {
|
|
144
|
+
return {
|
|
145
|
+
kind: "workspace",
|
|
146
|
+
resolvedPath: wsMatch.path,
|
|
147
|
+
packageName: wsMatch.name
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
151
|
+
const resolved = tryResolve(specifier, fromFile, project);
|
|
152
|
+
if (resolved) {
|
|
153
|
+
return { kind: "internal", resolvedPath: resolved };
|
|
154
|
+
}
|
|
155
|
+
return { kind: "unresolved" };
|
|
156
|
+
}
|
|
157
|
+
const aliasResolved = tryResolve(specifier, fromFile, project);
|
|
158
|
+
if (aliasResolved) {
|
|
159
|
+
return { kind: "internal", resolvedPath: aliasResolved };
|
|
160
|
+
}
|
|
161
|
+
return { kind: "external", packageName: specifier.split("/")[0] };
|
|
162
|
+
}
|
|
163
|
+
function tryResolve(specifier, fromFile, project) {
|
|
164
|
+
const sourceFile = project.getSourceFile(fromFile);
|
|
165
|
+
if (sourceFile) {
|
|
166
|
+
const dir = (0, import_node_path.dirname)(fromFile);
|
|
167
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
168
|
+
for (const ext of extensions) {
|
|
169
|
+
const candidate = specifier.startsWith(".") ? (0, import_node_path.resolve)(dir, specifier + ext) : specifier + ext;
|
|
170
|
+
const found = project.getSourceFile(candidate);
|
|
171
|
+
if (found) return found.getFilePath();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/build-graph.ts
|
|
178
|
+
var DEFAULT_IGNORE = [
|
|
179
|
+
"**/node_modules/**",
|
|
180
|
+
"**/dist/**",
|
|
181
|
+
"**/build/**",
|
|
182
|
+
"**/.next/**",
|
|
183
|
+
"**/.nuxt/**",
|
|
184
|
+
"**/coverage/**"
|
|
185
|
+
];
|
|
186
|
+
function buildImportGraphSync(projectRoot2, options2) {
|
|
187
|
+
const packages = options2?.packages ?? [];
|
|
188
|
+
const shouldDetectCycles = options2?.detectCycles !== false;
|
|
189
|
+
const ignorePatterns = options2?.ignore ?? DEFAULT_IGNORE;
|
|
190
|
+
const project = new import_ts_morph2.Project({
|
|
191
|
+
tsConfigFilePath: options2?.tsconfigPath,
|
|
192
|
+
skipAddingFilesFromTsConfig: true
|
|
193
|
+
});
|
|
194
|
+
const sourceGlobs = buildSourceGlobs(projectRoot2, packages, ignorePatterns);
|
|
195
|
+
project.addSourceFilesAtPaths(sourceGlobs);
|
|
196
|
+
const nodes = [];
|
|
197
|
+
const allEdges = [];
|
|
198
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
199
|
+
const filePath = sourceFile.getFilePath();
|
|
200
|
+
const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));
|
|
201
|
+
nodes.push({
|
|
202
|
+
filePath,
|
|
203
|
+
relativePath: (0, import_node_path2.relative)(ownerPkg?.path ?? projectRoot2, filePath),
|
|
204
|
+
packageName: ownerPkg?.name
|
|
205
|
+
});
|
|
206
|
+
const rawEdges = parseImports(sourceFile);
|
|
207
|
+
for (const edge of rawEdges) {
|
|
208
|
+
const resolved = resolveImport(edge.target, filePath, project, packages);
|
|
209
|
+
if (resolved.resolvedPath) {
|
|
210
|
+
allEdges.push({
|
|
211
|
+
...edge,
|
|
212
|
+
target: resolved.resolvedPath
|
|
213
|
+
});
|
|
214
|
+
} else if (resolved.kind === "external" || resolved.kind === "builtin") {
|
|
215
|
+
allEdges.push(edge);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let cycles = [];
|
|
220
|
+
if (shouldDetectCycles) {
|
|
221
|
+
const internalEdges = allEdges.filter(
|
|
222
|
+
(e) => e.target.startsWith("/") && !e.target.includes("node_modules")
|
|
223
|
+
);
|
|
224
|
+
cycles = detectCycles(internalEdges);
|
|
225
|
+
}
|
|
226
|
+
return { nodes, edges: allEdges, packages, cycles };
|
|
227
|
+
}
|
|
228
|
+
function buildSourceGlobs(projectRoot2, packages, ignore) {
|
|
229
|
+
const globs = [];
|
|
230
|
+
if (packages.length > 0) {
|
|
231
|
+
for (const pkg of packages) {
|
|
232
|
+
globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
globs.push(`${projectRoot2}/src/**/*.{ts,tsx,js,jsx}`);
|
|
236
|
+
globs.push(`${projectRoot2}/**/*.{ts,tsx,js,jsx}`);
|
|
237
|
+
}
|
|
238
|
+
globs.push(
|
|
239
|
+
"!**/node_modules/**",
|
|
240
|
+
"!**/dist/**",
|
|
241
|
+
"!**/build/**",
|
|
242
|
+
"!**/.next/**",
|
|
243
|
+
"!**/coverage/**"
|
|
244
|
+
);
|
|
245
|
+
for (const pattern of ignore) {
|
|
246
|
+
globs.push(`!${pattern}`);
|
|
247
|
+
}
|
|
248
|
+
return globs;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/build-graph-worker.ts
|
|
252
|
+
var { projectRoot, options } = import_node_worker_threads2.workerData;
|
|
253
|
+
var result = buildImportGraphSync(projectRoot, options);
|
|
254
|
+
import_node_worker_threads2.parentPort?.postMessage(result);
|
|
255
|
+
//# sourceMappingURL=build-graph-worker.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/build-graph-worker.ts","../src/build-graph.ts","../src/detect-cycles.ts","../src/parse-imports.ts","../src/resolve-import.ts"],"sourcesContent":["import { parentPort, workerData } from 'node:worker_threads';\nimport type { GraphOptions } from './build-graph.js';\nimport { buildImportGraphSync } from './build-graph.js';\n\nconst { projectRoot, options } = workerData as {\n projectRoot: string;\n options?: GraphOptions;\n};\n\nconst result = buildImportGraphSync(projectRoot, options);\nparentPort?.postMessage(result);\n","import { relative } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport type { ImportGraph, ImportGraphNode, WorkspacePackage } from '@viberails/types';\nimport { Project } from 'ts-morph';\nimport { detectCycles } from './detect-cycles.js';\nimport { parseImports } from './parse-imports.js';\nimport { resolveImport } from './resolve-import.js';\n\n/** Options for building an import graph. */\nexport interface GraphOptions {\n /** Workspace packages to include in resolution. */\n packages?: WorkspacePackage[];\n /** Glob patterns for files to ignore. */\n ignore?: string[];\n /** Whether to detect import cycles. @default true */\n detectCycles?: boolean;\n /** Path to tsconfig.json. Auto-detected if not provided. */\n tsconfigPath?: string;\n}\n\n/** Default glob patterns for files to ignore when building the graph. */\nconst DEFAULT_IGNORE = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/coverage/**',\n];\n\n/**\n * Builds a complete import graph for a project.\n *\n * Runs in a worker thread to keep the main thread responsive\n * (e.g. for CLI spinner animations). Falls back to in-process\n * execution if the worker cannot be spawned.\n *\n * @param projectRoot - Absolute path to the project root.\n * @param options - Configuration options.\n * @returns The complete import graph.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n try {\n return await buildImportGraphInWorker(projectRoot, options);\n } catch {\n // Worker failed (e.g. bundler issue, test environment) — run in-process\n return buildImportGraphSync(projectRoot, options);\n }\n}\n\n/**\n * Spawns a worker thread to run the graph build off the main thread.\n */\nfunction buildImportGraphInWorker(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n return new Promise((resolve, reject) => {\n // Resolve worker script path relative to this file's compiled location\n const workerPath = new URL('./build-graph-worker.js', import.meta.url);\n const worker = new Worker(workerPath, {\n workerData: { projectRoot, options },\n });\n worker.on('message', (result: ImportGraph) => {\n resolve(result);\n void worker.terminate();\n });\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker exited with code ${code}`));\n });\n });\n}\n\n/**\n * Core import graph building logic. Runs synchronously on whichever\n * thread calls it (main thread as fallback, or worker thread).\n */\nexport function buildImportGraphSync(projectRoot: string, options?: GraphOptions): ImportGraph {\n const packages = options?.packages ?? [];\n const shouldDetectCycles = options?.detectCycles !== false;\n const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;\n\n // Create ts-morph project\n const project = new Project({\n tsConfigFilePath: options?.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // Add source files from project root and workspace packages\n const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);\n project.addSourceFilesAtPaths(sourceGlobs);\n\n // Build nodes and edges\n const nodes: ImportGraphNode[] = [];\n const allEdges: ImportGraph['edges'] = [];\n\n for (const sourceFile of project.getSourceFiles()) {\n const filePath = sourceFile.getFilePath();\n\n // Determine which package this file belongs to\n const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));\n\n nodes.push({\n filePath,\n relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),\n packageName: ownerPkg?.name,\n });\n\n // Parse and resolve imports\n const rawEdges = parseImports(sourceFile);\n for (const edge of rawEdges) {\n const resolved = resolveImport(edge.target, filePath, project, packages);\n\n // Only include edges with resolved file paths (skip externals/builtins)\n if (resolved.resolvedPath) {\n allEdges.push({\n ...edge,\n target: resolved.resolvedPath,\n });\n } else if (resolved.kind === 'external' || resolved.kind === 'builtin') {\n // Keep external/builtin edges with the specifier as target\n allEdges.push(edge);\n }\n }\n }\n\n // Detect cycles among internal file edges only\n let cycles: string[][] = [];\n if (shouldDetectCycles) {\n const internalEdges = allEdges.filter(\n (e) => e.target.startsWith('/') && !e.target.includes('node_modules'),\n );\n cycles = detectCycles(internalEdges);\n }\n\n return { nodes, edges: allEdges, packages, cycles };\n}\n\n/**\n * Builds glob patterns for adding source files to the ts-morph project.\n */\nfunction buildSourceGlobs(\n projectRoot: string,\n packages: WorkspacePackage[],\n ignore: string[],\n): string[] {\n const globs: string[] = [];\n\n if (packages.length > 0) {\n // Add source files from each workspace package (src/ and other dirs like app/, pages/)\n for (const pkg of packages) {\n globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);\n }\n } else {\n // Single-package project\n globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);\n globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);\n }\n\n // Exclude build outputs and dependencies\n globs.push(\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/.next/**',\n '!**/coverage/**',\n );\n\n // Add negation patterns for ignored paths\n for (const pattern of ignore) {\n globs.push(`!${pattern}`);\n }\n\n return globs;\n}\n","/**\n * Detects import cycles in a directed graph of file dependencies.\n * Uses DFS with three-color marking (white/gray/black) to find back edges.\n *\n * @param edges - Array of directed edges with source and target file paths.\n * @returns Array of cycles, each represented as a list of file paths.\n */\nexport function detectCycles(edges: Array<{ source: string; target: string }>): string[][] {\n // Build adjacency list\n const graph = new Map<string, string[]>();\n const nodes = new Set<string>();\n\n for (const { source, target } of edges) {\n nodes.add(source);\n nodes.add(target);\n const neighbors = graph.get(source);\n if (neighbors) {\n neighbors.push(target);\n } else {\n graph.set(source, [target]);\n }\n }\n\n const WHITE = 0; // unvisited\n const GRAY = 1; // in current DFS path\n const BLACK = 2; // fully processed\n\n const color = new Map<string, number>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n const cycles: string[][] = [];\n const path: string[] = [];\n\n function dfs(node: string): void {\n color.set(node, GRAY);\n path.push(node);\n\n const neighbors = graph.get(node) ?? [];\n for (const neighbor of neighbors) {\n const c = color.get(neighbor);\n\n if (c === GRAY) {\n // Found a cycle — extract it from the path\n const cycleStart = path.indexOf(neighbor);\n if (cycleStart !== -1) {\n cycles.push(path.slice(cycleStart));\n }\n } else if (c === WHITE) {\n dfs(neighbor);\n }\n }\n\n path.pop();\n color.set(node, BLACK);\n }\n\n for (const node of nodes) {\n if (color.get(node) === WHITE) {\n dfs(node);\n }\n }\n\n return cycles;\n}\n","import type { ImportEdge } from '@viberails/types';\nimport { type SourceFile, SyntaxKind } from 'ts-morph';\n\n/** File extensions to skip (non-JS assets). */\nconst SKIP_EXTENSIONS = new Set([\n '.css',\n '.scss',\n '.less',\n '.sass',\n '.png',\n '.svg',\n '.jpg',\n '.jpeg',\n '.gif',\n '.ico',\n '.webp',\n '.json',\n '.woff',\n '.woff2',\n '.ttf',\n '.eot',\n]);\n\n/**\n * Checks whether an import specifier should be skipped (non-JS asset).\n */\nfunction shouldSkip(specifier: string): boolean {\n const dotIndex = specifier.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Parses all import statements from a ts-morph SourceFile and returns\n * them as ImportEdge objects.\n *\n * Handles static imports, default imports, namespace imports, type-only\n * imports, side-effect imports, dynamic imports, and re-exports.\n *\n * @param sourceFile - A ts-morph SourceFile to extract imports from.\n * @returns Array of ImportEdge objects for each import found.\n */\nexport function parseImports(sourceFile: SourceFile): ImportEdge[] {\n const edges: ImportEdge[] = [];\n const filePath = sourceFile.getFilePath();\n\n // Static imports (including type-only, default, namespace, side-effect)\n for (const decl of sourceFile.getImportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Re-exports: export { x } from './foo' and export * from './foo'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (!specifier || shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;\n\n const args = call.getArguments();\n if (args.length === 0) continue;\n\n const arg = args[0];\n if (arg.getKind() !== SyntaxKind.StringLiteral) continue;\n\n const specifier = arg.getText().slice(1, -1); // Remove quotes\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: false,\n dynamic: true,\n line: call.getStartLineNumber(),\n });\n }\n\n return edges;\n}\n","import { builtinModules } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type { ImportKind, WorkspacePackage } from '@viberails/types';\nimport type { Project } from 'ts-morph';\n\n/** Result of resolving an import specifier. */\nexport interface ResolvedImport {\n /** Classification of the import. */\n kind: ImportKind;\n /** Absolute path for internal/workspace imports. */\n resolvedPath?: string;\n /** Package name for workspace/external imports. */\n packageName?: string;\n}\n\n/** Set of Node.js builtin module names (with and without node: prefix). */\nconst BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);\n\n/**\n * Resolves an import specifier and classifies it.\n *\n * Classification order:\n * 1. `node:` prefix or known builtin → `builtin`\n * 2. Matches a workspace package name → `workspace`\n * 3. Relative path (`.` or `/`) → resolve via ts-morph → `internal`\n * 4. Otherwise → `external`\n * 5. If resolution fails → `unresolved`\n *\n * @param specifier - The raw import specifier as written in source.\n * @param fromFile - Absolute path of the file containing the import.\n * @param project - ts-morph Project for resolution.\n * @param workspacePackages - Known workspace packages for monorepo resolution.\n * @returns Classification and resolved path information.\n */\nexport function resolveImport(\n specifier: string,\n fromFile: string,\n project: Project,\n workspacePackages: WorkspacePackage[],\n): ResolvedImport {\n // 1. Node.js builtins\n if (BUILTINS.has(specifier)) {\n return { kind: 'builtin' };\n }\n\n // 2. Workspace packages\n const wsMatch = workspacePackages.find(\n (pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`),\n );\n if (wsMatch) {\n return {\n kind: 'workspace',\n resolvedPath: wsMatch.path,\n packageName: wsMatch.name,\n };\n }\n\n // 3. Relative or absolute imports → internal\n if (specifier.startsWith('.') || specifier.startsWith('/')) {\n const resolved = tryResolve(specifier, fromFile, project);\n if (resolved) {\n return { kind: 'internal', resolvedPath: resolved };\n }\n return { kind: 'unresolved' };\n }\n\n // 4. Check if ts-morph can resolve it (e.g. path aliases)\n const aliasResolved = tryResolve(specifier, fromFile, project);\n if (aliasResolved) {\n return { kind: 'internal', resolvedPath: aliasResolved };\n }\n\n // 5. External package\n return { kind: 'external', packageName: specifier.split('/')[0] };\n}\n\n/**\n * Attempts to resolve a specifier using ts-morph's module resolution.\n * Returns the absolute path if resolved, undefined otherwise.\n */\nfunction tryResolve(specifier: string, fromFile: string, project: Project): string | undefined {\n // Try ts-morph resolution first\n const sourceFile = project.getSourceFile(fromFile);\n if (sourceFile) {\n // Try common TypeScript extensions\n const dir = dirname(fromFile);\n const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];\n\n for (const ext of extensions) {\n const candidate = specifier.startsWith('.') ? resolve(dir, specifier + ext) : specifier + ext;\n const found = project.getSourceFile(candidate);\n if (found) return found.getFilePath();\n }\n }\n\n return undefined;\n}\n"],"mappings":"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","names":["import_node_worker_threads","import_node_path","import_ts_morph","projectRoot","options"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildImportGraphSync
|
|
3
|
+
} from "./chunk-DQX7OULF.js";
|
|
4
|
+
|
|
5
|
+
// src/build-graph-worker.ts
|
|
6
|
+
import { parentPort, workerData } from "worker_threads";
|
|
7
|
+
var { projectRoot, options } = workerData;
|
|
8
|
+
var result = buildImportGraphSync(projectRoot, options);
|
|
9
|
+
parentPort?.postMessage(result);
|
|
10
|
+
//# sourceMappingURL=build-graph-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/build-graph-worker.ts"],"sourcesContent":["import { parentPort, workerData } from 'node:worker_threads';\nimport type { GraphOptions } from './build-graph.js';\nimport { buildImportGraphSync } from './build-graph.js';\n\nconst { projectRoot, options } = workerData as {\n projectRoot: string;\n options?: GraphOptions;\n};\n\nconst result = buildImportGraphSync(projectRoot, options);\nparentPort?.postMessage(result);\n"],"mappings":";;;;;AAAA,SAAS,YAAY,kBAAkB;AAIvC,IAAM,EAAE,aAAa,QAAQ,IAAI;AAKjC,IAAM,SAAS,qBAAqB,aAAa,OAAO;AACxD,YAAY,YAAY,MAAM;","names":[]}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/detect-cycles.ts
|
|
2
|
+
function detectCycles(edges) {
|
|
3
|
+
const graph = /* @__PURE__ */ new Map();
|
|
4
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
5
|
+
for (const { source, target } of edges) {
|
|
6
|
+
nodes.add(source);
|
|
7
|
+
nodes.add(target);
|
|
8
|
+
const neighbors = graph.get(source);
|
|
9
|
+
if (neighbors) {
|
|
10
|
+
neighbors.push(target);
|
|
11
|
+
} else {
|
|
12
|
+
graph.set(source, [target]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const WHITE = 0;
|
|
16
|
+
const GRAY = 1;
|
|
17
|
+
const BLACK = 2;
|
|
18
|
+
const color = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const node of nodes) {
|
|
20
|
+
color.set(node, WHITE);
|
|
21
|
+
}
|
|
22
|
+
const cycles = [];
|
|
23
|
+
const path = [];
|
|
24
|
+
function dfs(node) {
|
|
25
|
+
color.set(node, GRAY);
|
|
26
|
+
path.push(node);
|
|
27
|
+
const neighbors = graph.get(node) ?? [];
|
|
28
|
+
for (const neighbor of neighbors) {
|
|
29
|
+
const c = color.get(neighbor);
|
|
30
|
+
if (c === GRAY) {
|
|
31
|
+
const cycleStart = path.indexOf(neighbor);
|
|
32
|
+
if (cycleStart !== -1) {
|
|
33
|
+
cycles.push(path.slice(cycleStart));
|
|
34
|
+
}
|
|
35
|
+
} else if (c === WHITE) {
|
|
36
|
+
dfs(neighbor);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
path.pop();
|
|
40
|
+
color.set(node, BLACK);
|
|
41
|
+
}
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
if (color.get(node) === WHITE) {
|
|
44
|
+
dfs(node);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return cycles;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/parse-imports.ts
|
|
51
|
+
import { SyntaxKind } from "ts-morph";
|
|
52
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
53
|
+
".css",
|
|
54
|
+
".scss",
|
|
55
|
+
".less",
|
|
56
|
+
".sass",
|
|
57
|
+
".png",
|
|
58
|
+
".svg",
|
|
59
|
+
".jpg",
|
|
60
|
+
".jpeg",
|
|
61
|
+
".gif",
|
|
62
|
+
".ico",
|
|
63
|
+
".webp",
|
|
64
|
+
".json",
|
|
65
|
+
".woff",
|
|
66
|
+
".woff2",
|
|
67
|
+
".ttf",
|
|
68
|
+
".eot"
|
|
69
|
+
]);
|
|
70
|
+
function shouldSkip(specifier) {
|
|
71
|
+
const dotIndex = specifier.lastIndexOf(".");
|
|
72
|
+
if (dotIndex === -1) return false;
|
|
73
|
+
return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());
|
|
74
|
+
}
|
|
75
|
+
function parseImports(sourceFile) {
|
|
76
|
+
const edges = [];
|
|
77
|
+
const filePath = sourceFile.getFilePath();
|
|
78
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
79
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
80
|
+
if (shouldSkip(specifier)) continue;
|
|
81
|
+
edges.push({
|
|
82
|
+
source: filePath,
|
|
83
|
+
target: specifier,
|
|
84
|
+
specifier,
|
|
85
|
+
typeOnly: decl.isTypeOnly(),
|
|
86
|
+
dynamic: false,
|
|
87
|
+
line: decl.getStartLineNumber()
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
91
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
92
|
+
if (!specifier || shouldSkip(specifier)) continue;
|
|
93
|
+
edges.push({
|
|
94
|
+
source: filePath,
|
|
95
|
+
target: specifier,
|
|
96
|
+
specifier,
|
|
97
|
+
typeOnly: decl.isTypeOnly(),
|
|
98
|
+
dynamic: false,
|
|
99
|
+
line: decl.getStartLineNumber()
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
103
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
104
|
+
const args = call.getArguments();
|
|
105
|
+
if (args.length === 0) continue;
|
|
106
|
+
const arg = args[0];
|
|
107
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
108
|
+
const specifier = arg.getText().slice(1, -1);
|
|
109
|
+
if (shouldSkip(specifier)) continue;
|
|
110
|
+
edges.push({
|
|
111
|
+
source: filePath,
|
|
112
|
+
target: specifier,
|
|
113
|
+
specifier,
|
|
114
|
+
typeOnly: false,
|
|
115
|
+
dynamic: true,
|
|
116
|
+
line: call.getStartLineNumber()
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return edges;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/resolve-import.ts
|
|
123
|
+
import { builtinModules } from "module";
|
|
124
|
+
import { dirname, resolve } from "path";
|
|
125
|
+
var BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);
|
|
126
|
+
function resolveImport(specifier, fromFile, project, workspacePackages) {
|
|
127
|
+
if (BUILTINS.has(specifier)) {
|
|
128
|
+
return { kind: "builtin" };
|
|
129
|
+
}
|
|
130
|
+
const wsMatch = workspacePackages.find(
|
|
131
|
+
(pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)
|
|
132
|
+
);
|
|
133
|
+
if (wsMatch) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "workspace",
|
|
136
|
+
resolvedPath: wsMatch.path,
|
|
137
|
+
packageName: wsMatch.name
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
141
|
+
const resolved = tryResolve(specifier, fromFile, project);
|
|
142
|
+
if (resolved) {
|
|
143
|
+
return { kind: "internal", resolvedPath: resolved };
|
|
144
|
+
}
|
|
145
|
+
return { kind: "unresolved" };
|
|
146
|
+
}
|
|
147
|
+
const aliasResolved = tryResolve(specifier, fromFile, project);
|
|
148
|
+
if (aliasResolved) {
|
|
149
|
+
return { kind: "internal", resolvedPath: aliasResolved };
|
|
150
|
+
}
|
|
151
|
+
return { kind: "external", packageName: specifier.split("/")[0] };
|
|
152
|
+
}
|
|
153
|
+
function tryResolve(specifier, fromFile, project) {
|
|
154
|
+
const sourceFile = project.getSourceFile(fromFile);
|
|
155
|
+
if (sourceFile) {
|
|
156
|
+
const dir = dirname(fromFile);
|
|
157
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
158
|
+
for (const ext of extensions) {
|
|
159
|
+
const candidate = specifier.startsWith(".") ? resolve(dir, specifier + ext) : specifier + ext;
|
|
160
|
+
const found = project.getSourceFile(candidate);
|
|
161
|
+
if (found) return found.getFilePath();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/build-graph.ts
|
|
168
|
+
import { relative } from "path";
|
|
169
|
+
import { Worker } from "worker_threads";
|
|
170
|
+
import { Project } from "ts-morph";
|
|
171
|
+
var DEFAULT_IGNORE = [
|
|
172
|
+
"**/node_modules/**",
|
|
173
|
+
"**/dist/**",
|
|
174
|
+
"**/build/**",
|
|
175
|
+
"**/.next/**",
|
|
176
|
+
"**/.nuxt/**",
|
|
177
|
+
"**/coverage/**"
|
|
178
|
+
];
|
|
179
|
+
async function buildImportGraph(projectRoot, options) {
|
|
180
|
+
try {
|
|
181
|
+
return await buildImportGraphInWorker(projectRoot, options);
|
|
182
|
+
} catch {
|
|
183
|
+
return buildImportGraphSync(projectRoot, options);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function buildImportGraphInWorker(projectRoot, options) {
|
|
187
|
+
return new Promise((resolve2, reject) => {
|
|
188
|
+
const workerPath = new URL("./build-graph-worker.js", import.meta.url);
|
|
189
|
+
const worker = new Worker(workerPath, {
|
|
190
|
+
workerData: { projectRoot, options }
|
|
191
|
+
});
|
|
192
|
+
worker.on("message", (result) => {
|
|
193
|
+
resolve2(result);
|
|
194
|
+
void worker.terminate();
|
|
195
|
+
});
|
|
196
|
+
worker.on("error", reject);
|
|
197
|
+
worker.on("exit", (code) => {
|
|
198
|
+
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function buildImportGraphSync(projectRoot, options) {
|
|
203
|
+
const packages = options?.packages ?? [];
|
|
204
|
+
const shouldDetectCycles = options?.detectCycles !== false;
|
|
205
|
+
const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;
|
|
206
|
+
const project = new Project({
|
|
207
|
+
tsConfigFilePath: options?.tsconfigPath,
|
|
208
|
+
skipAddingFilesFromTsConfig: true
|
|
209
|
+
});
|
|
210
|
+
const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);
|
|
211
|
+
project.addSourceFilesAtPaths(sourceGlobs);
|
|
212
|
+
const nodes = [];
|
|
213
|
+
const allEdges = [];
|
|
214
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
215
|
+
const filePath = sourceFile.getFilePath();
|
|
216
|
+
const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));
|
|
217
|
+
nodes.push({
|
|
218
|
+
filePath,
|
|
219
|
+
relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),
|
|
220
|
+
packageName: ownerPkg?.name
|
|
221
|
+
});
|
|
222
|
+
const rawEdges = parseImports(sourceFile);
|
|
223
|
+
for (const edge of rawEdges) {
|
|
224
|
+
const resolved = resolveImport(edge.target, filePath, project, packages);
|
|
225
|
+
if (resolved.resolvedPath) {
|
|
226
|
+
allEdges.push({
|
|
227
|
+
...edge,
|
|
228
|
+
target: resolved.resolvedPath
|
|
229
|
+
});
|
|
230
|
+
} else if (resolved.kind === "external" || resolved.kind === "builtin") {
|
|
231
|
+
allEdges.push(edge);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
let cycles = [];
|
|
236
|
+
if (shouldDetectCycles) {
|
|
237
|
+
const internalEdges = allEdges.filter(
|
|
238
|
+
(e) => e.target.startsWith("/") && !e.target.includes("node_modules")
|
|
239
|
+
);
|
|
240
|
+
cycles = detectCycles(internalEdges);
|
|
241
|
+
}
|
|
242
|
+
return { nodes, edges: allEdges, packages, cycles };
|
|
243
|
+
}
|
|
244
|
+
function buildSourceGlobs(projectRoot, packages, ignore) {
|
|
245
|
+
const globs = [];
|
|
246
|
+
if (packages.length > 0) {
|
|
247
|
+
for (const pkg of packages) {
|
|
248
|
+
globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);
|
|
252
|
+
globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);
|
|
253
|
+
}
|
|
254
|
+
globs.push(
|
|
255
|
+
"!**/node_modules/**",
|
|
256
|
+
"!**/dist/**",
|
|
257
|
+
"!**/build/**",
|
|
258
|
+
"!**/.next/**",
|
|
259
|
+
"!**/coverage/**"
|
|
260
|
+
);
|
|
261
|
+
for (const pattern of ignore) {
|
|
262
|
+
globs.push(`!${pattern}`);
|
|
263
|
+
}
|
|
264
|
+
return globs;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export {
|
|
268
|
+
detectCycles,
|
|
269
|
+
parseImports,
|
|
270
|
+
resolveImport,
|
|
271
|
+
buildImportGraph,
|
|
272
|
+
buildImportGraphSync
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=chunk-DQX7OULF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/detect-cycles.ts","../src/parse-imports.ts","../src/resolve-import.ts","../src/build-graph.ts"],"sourcesContent":["/**\n * Detects import cycles in a directed graph of file dependencies.\n * Uses DFS with three-color marking (white/gray/black) to find back edges.\n *\n * @param edges - Array of directed edges with source and target file paths.\n * @returns Array of cycles, each represented as a list of file paths.\n */\nexport function detectCycles(edges: Array<{ source: string; target: string }>): string[][] {\n // Build adjacency list\n const graph = new Map<string, string[]>();\n const nodes = new Set<string>();\n\n for (const { source, target } of edges) {\n nodes.add(source);\n nodes.add(target);\n const neighbors = graph.get(source);\n if (neighbors) {\n neighbors.push(target);\n } else {\n graph.set(source, [target]);\n }\n }\n\n const WHITE = 0; // unvisited\n const GRAY = 1; // in current DFS path\n const BLACK = 2; // fully processed\n\n const color = new Map<string, number>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n const cycles: string[][] = [];\n const path: string[] = [];\n\n function dfs(node: string): void {\n color.set(node, GRAY);\n path.push(node);\n\n const neighbors = graph.get(node) ?? [];\n for (const neighbor of neighbors) {\n const c = color.get(neighbor);\n\n if (c === GRAY) {\n // Found a cycle — extract it from the path\n const cycleStart = path.indexOf(neighbor);\n if (cycleStart !== -1) {\n cycles.push(path.slice(cycleStart));\n }\n } else if (c === WHITE) {\n dfs(neighbor);\n }\n }\n\n path.pop();\n color.set(node, BLACK);\n }\n\n for (const node of nodes) {\n if (color.get(node) === WHITE) {\n dfs(node);\n }\n }\n\n return cycles;\n}\n","import type { ImportEdge } from '@viberails/types';\nimport { type SourceFile, SyntaxKind } from 'ts-morph';\n\n/** File extensions to skip (non-JS assets). */\nconst SKIP_EXTENSIONS = new Set([\n '.css',\n '.scss',\n '.less',\n '.sass',\n '.png',\n '.svg',\n '.jpg',\n '.jpeg',\n '.gif',\n '.ico',\n '.webp',\n '.json',\n '.woff',\n '.woff2',\n '.ttf',\n '.eot',\n]);\n\n/**\n * Checks whether an import specifier should be skipped (non-JS asset).\n */\nfunction shouldSkip(specifier: string): boolean {\n const dotIndex = specifier.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Parses all import statements from a ts-morph SourceFile and returns\n * them as ImportEdge objects.\n *\n * Handles static imports, default imports, namespace imports, type-only\n * imports, side-effect imports, dynamic imports, and re-exports.\n *\n * @param sourceFile - A ts-morph SourceFile to extract imports from.\n * @returns Array of ImportEdge objects for each import found.\n */\nexport function parseImports(sourceFile: SourceFile): ImportEdge[] {\n const edges: ImportEdge[] = [];\n const filePath = sourceFile.getFilePath();\n\n // Static imports (including type-only, default, namespace, side-effect)\n for (const decl of sourceFile.getImportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Re-exports: export { x } from './foo' and export * from './foo'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (!specifier || shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;\n\n const args = call.getArguments();\n if (args.length === 0) continue;\n\n const arg = args[0];\n if (arg.getKind() !== SyntaxKind.StringLiteral) continue;\n\n const specifier = arg.getText().slice(1, -1); // Remove quotes\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: false,\n dynamic: true,\n line: call.getStartLineNumber(),\n });\n }\n\n return edges;\n}\n","import { builtinModules } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type { ImportKind, WorkspacePackage } from '@viberails/types';\nimport type { Project } from 'ts-morph';\n\n/** Result of resolving an import specifier. */\nexport interface ResolvedImport {\n /** Classification of the import. */\n kind: ImportKind;\n /** Absolute path for internal/workspace imports. */\n resolvedPath?: string;\n /** Package name for workspace/external imports. */\n packageName?: string;\n}\n\n/** Set of Node.js builtin module names (with and without node: prefix). */\nconst BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);\n\n/**\n * Resolves an import specifier and classifies it.\n *\n * Classification order:\n * 1. `node:` prefix or known builtin → `builtin`\n * 2. Matches a workspace package name → `workspace`\n * 3. Relative path (`.` or `/`) → resolve via ts-morph → `internal`\n * 4. Otherwise → `external`\n * 5. If resolution fails → `unresolved`\n *\n * @param specifier - The raw import specifier as written in source.\n * @param fromFile - Absolute path of the file containing the import.\n * @param project - ts-morph Project for resolution.\n * @param workspacePackages - Known workspace packages for monorepo resolution.\n * @returns Classification and resolved path information.\n */\nexport function resolveImport(\n specifier: string,\n fromFile: string,\n project: Project,\n workspacePackages: WorkspacePackage[],\n): ResolvedImport {\n // 1. Node.js builtins\n if (BUILTINS.has(specifier)) {\n return { kind: 'builtin' };\n }\n\n // 2. Workspace packages\n const wsMatch = workspacePackages.find(\n (pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`),\n );\n if (wsMatch) {\n return {\n kind: 'workspace',\n resolvedPath: wsMatch.path,\n packageName: wsMatch.name,\n };\n }\n\n // 3. Relative or absolute imports → internal\n if (specifier.startsWith('.') || specifier.startsWith('/')) {\n const resolved = tryResolve(specifier, fromFile, project);\n if (resolved) {\n return { kind: 'internal', resolvedPath: resolved };\n }\n return { kind: 'unresolved' };\n }\n\n // 4. Check if ts-morph can resolve it (e.g. path aliases)\n const aliasResolved = tryResolve(specifier, fromFile, project);\n if (aliasResolved) {\n return { kind: 'internal', resolvedPath: aliasResolved };\n }\n\n // 5. External package\n return { kind: 'external', packageName: specifier.split('/')[0] };\n}\n\n/**\n * Attempts to resolve a specifier using ts-morph's module resolution.\n * Returns the absolute path if resolved, undefined otherwise.\n */\nfunction tryResolve(specifier: string, fromFile: string, project: Project): string | undefined {\n // Try ts-morph resolution first\n const sourceFile = project.getSourceFile(fromFile);\n if (sourceFile) {\n // Try common TypeScript extensions\n const dir = dirname(fromFile);\n const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];\n\n for (const ext of extensions) {\n const candidate = specifier.startsWith('.') ? resolve(dir, specifier + ext) : specifier + ext;\n const found = project.getSourceFile(candidate);\n if (found) return found.getFilePath();\n }\n }\n\n return undefined;\n}\n","import { relative } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport type { ImportGraph, ImportGraphNode, WorkspacePackage } from '@viberails/types';\nimport { Project } from 'ts-morph';\nimport { detectCycles } from './detect-cycles.js';\nimport { parseImports } from './parse-imports.js';\nimport { resolveImport } from './resolve-import.js';\n\n/** Options for building an import graph. */\nexport interface GraphOptions {\n /** Workspace packages to include in resolution. */\n packages?: WorkspacePackage[];\n /** Glob patterns for files to ignore. */\n ignore?: string[];\n /** Whether to detect import cycles. @default true */\n detectCycles?: boolean;\n /** Path to tsconfig.json. Auto-detected if not provided. */\n tsconfigPath?: string;\n}\n\n/** Default glob patterns for files to ignore when building the graph. */\nconst DEFAULT_IGNORE = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/coverage/**',\n];\n\n/**\n * Builds a complete import graph for a project.\n *\n * Runs in a worker thread to keep the main thread responsive\n * (e.g. for CLI spinner animations). Falls back to in-process\n * execution if the worker cannot be spawned.\n *\n * @param projectRoot - Absolute path to the project root.\n * @param options - Configuration options.\n * @returns The complete import graph.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n try {\n return await buildImportGraphInWorker(projectRoot, options);\n } catch {\n // Worker failed (e.g. bundler issue, test environment) — run in-process\n return buildImportGraphSync(projectRoot, options);\n }\n}\n\n/**\n * Spawns a worker thread to run the graph build off the main thread.\n */\nfunction buildImportGraphInWorker(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n return new Promise((resolve, reject) => {\n // Resolve worker script path relative to this file's compiled location\n const workerPath = new URL('./build-graph-worker.js', import.meta.url);\n const worker = new Worker(workerPath, {\n workerData: { projectRoot, options },\n });\n worker.on('message', (result: ImportGraph) => {\n resolve(result);\n void worker.terminate();\n });\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker exited with code ${code}`));\n });\n });\n}\n\n/**\n * Core import graph building logic. Runs synchronously on whichever\n * thread calls it (main thread as fallback, or worker thread).\n */\nexport function buildImportGraphSync(projectRoot: string, options?: GraphOptions): ImportGraph {\n const packages = options?.packages ?? [];\n const shouldDetectCycles = options?.detectCycles !== false;\n const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;\n\n // Create ts-morph project\n const project = new Project({\n tsConfigFilePath: options?.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // Add source files from project root and workspace packages\n const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);\n project.addSourceFilesAtPaths(sourceGlobs);\n\n // Build nodes and edges\n const nodes: ImportGraphNode[] = [];\n const allEdges: ImportGraph['edges'] = [];\n\n for (const sourceFile of project.getSourceFiles()) {\n const filePath = sourceFile.getFilePath();\n\n // Determine which package this file belongs to\n const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));\n\n nodes.push({\n filePath,\n relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),\n packageName: ownerPkg?.name,\n });\n\n // Parse and resolve imports\n const rawEdges = parseImports(sourceFile);\n for (const edge of rawEdges) {\n const resolved = resolveImport(edge.target, filePath, project, packages);\n\n // Only include edges with resolved file paths (skip externals/builtins)\n if (resolved.resolvedPath) {\n allEdges.push({\n ...edge,\n target: resolved.resolvedPath,\n });\n } else if (resolved.kind === 'external' || resolved.kind === 'builtin') {\n // Keep external/builtin edges with the specifier as target\n allEdges.push(edge);\n }\n }\n }\n\n // Detect cycles among internal file edges only\n let cycles: string[][] = [];\n if (shouldDetectCycles) {\n const internalEdges = allEdges.filter(\n (e) => e.target.startsWith('/') && !e.target.includes('node_modules'),\n );\n cycles = detectCycles(internalEdges);\n }\n\n return { nodes, edges: allEdges, packages, cycles };\n}\n\n/**\n * Builds glob patterns for adding source files to the ts-morph project.\n */\nfunction buildSourceGlobs(\n projectRoot: string,\n packages: WorkspacePackage[],\n ignore: string[],\n): string[] {\n const globs: string[] = [];\n\n if (packages.length > 0) {\n // Add source files from each workspace package (src/ and other dirs like app/, pages/)\n for (const pkg of packages) {\n globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);\n }\n } else {\n // Single-package project\n globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);\n globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);\n }\n\n // Exclude build outputs and dependencies\n globs.push(\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/.next/**',\n '!**/coverage/**',\n );\n\n // Add negation patterns for ignored paths\n for (const pattern of ignore) {\n globs.push(`!${pattern}`);\n }\n\n return globs;\n}\n"],"mappings":"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","names":["resolve"]}
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
32
32
|
|
|
33
33
|
// src/build-graph.ts
|
|
34
34
|
var import_node_path2 = require("path");
|
|
35
|
+
var import_node_worker_threads = require("worker_threads");
|
|
35
36
|
var import_ts_morph2 = require("ts-morph");
|
|
36
37
|
|
|
37
38
|
// src/detect-cycles.ts
|
|
@@ -201,6 +202,7 @@ function tryResolve(specifier, fromFile, project) {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
// src/build-graph.ts
|
|
205
|
+
var import_meta = {};
|
|
204
206
|
var DEFAULT_IGNORE = [
|
|
205
207
|
"**/node_modules/**",
|
|
206
208
|
"**/dist/**",
|
|
@@ -210,6 +212,29 @@ var DEFAULT_IGNORE = [
|
|
|
210
212
|
"**/coverage/**"
|
|
211
213
|
];
|
|
212
214
|
async function buildImportGraph(projectRoot, options) {
|
|
215
|
+
try {
|
|
216
|
+
return await buildImportGraphInWorker(projectRoot, options);
|
|
217
|
+
} catch {
|
|
218
|
+
return buildImportGraphSync(projectRoot, options);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function buildImportGraphInWorker(projectRoot, options) {
|
|
222
|
+
return new Promise((resolve2, reject) => {
|
|
223
|
+
const workerPath = new URL("./build-graph-worker.js", import_meta.url);
|
|
224
|
+
const worker = new import_node_worker_threads.Worker(workerPath, {
|
|
225
|
+
workerData: { projectRoot, options }
|
|
226
|
+
});
|
|
227
|
+
worker.on("message", (result) => {
|
|
228
|
+
resolve2(result);
|
|
229
|
+
void worker.terminate();
|
|
230
|
+
});
|
|
231
|
+
worker.on("error", reject);
|
|
232
|
+
worker.on("exit", (code) => {
|
|
233
|
+
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function buildImportGraphSync(projectRoot, options) {
|
|
213
238
|
const packages = options?.packages ?? [];
|
|
214
239
|
const shouldDetectCycles = options?.detectCycles !== false;
|
|
215
240
|
const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;
|
|
@@ -416,7 +441,7 @@ function inferSinglePackageBoundaries(graph) {
|
|
|
416
441
|
}
|
|
417
442
|
|
|
418
443
|
// src/index.ts
|
|
419
|
-
var VERSION = "0.6.
|
|
444
|
+
var VERSION = "0.6.4";
|
|
420
445
|
// Annotate the CommonJS export names for ESM import in node:
|
|
421
446
|
0 && (module.exports = {
|
|
422
447
|
VERSION,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/build-graph.ts","../src/detect-cycles.ts","../src/parse-imports.ts","../src/resolve-import.ts","../src/check-boundaries.ts","../src/infer-boundaries.ts"],"sourcesContent":["declare const __PACKAGE_VERSION__: string;\nexport const VERSION: string = __PACKAGE_VERSION__;\n\nexport { buildImportGraph, type GraphOptions } from './build-graph.js';\nexport { checkBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries } from './infer-boundaries.js';\nexport { parseImports } from './parse-imports.js';\nexport { type ResolvedImport, resolveImport } from './resolve-import.js';\n","import { relative } from 'node:path';\nimport type { ImportGraph, ImportGraphNode, WorkspacePackage } from '@viberails/types';\nimport { Project } from 'ts-morph';\nimport { detectCycles } from './detect-cycles.js';\nimport { parseImports } from './parse-imports.js';\nimport { resolveImport } from './resolve-import.js';\n\n/** Options for building an import graph. */\nexport interface GraphOptions {\n /** Workspace packages to include in resolution. */\n packages?: WorkspacePackage[];\n /** Glob patterns for files to ignore. */\n ignore?: string[];\n /** Whether to detect import cycles. @default true */\n detectCycles?: boolean;\n /** Path to tsconfig.json. Auto-detected if not provided. */\n tsconfigPath?: string;\n}\n\n/** Default glob patterns for files to ignore when building the graph. */\nconst DEFAULT_IGNORE = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/coverage/**',\n];\n\n/**\n * Builds a complete import graph for a project.\n *\n * Creates a ts-morph Project, adds all source files, parses imports,\n * resolves specifiers, and optionally detects cycles.\n *\n * @param projectRoot - Absolute path to the project root.\n * @param options - Configuration options.\n * @returns The complete import graph.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n const packages = options?.packages ?? [];\n const shouldDetectCycles = options?.detectCycles !== false;\n const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;\n\n // Create ts-morph project\n const project = new Project({\n tsConfigFilePath: options?.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // Add source files from project root and workspace packages\n const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);\n project.addSourceFilesAtPaths(sourceGlobs);\n\n // Build nodes and edges\n const nodes: ImportGraphNode[] = [];\n const allEdges: ImportGraph['edges'] = [];\n\n for (const sourceFile of project.getSourceFiles()) {\n const filePath = sourceFile.getFilePath();\n\n // Determine which package this file belongs to\n const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));\n\n nodes.push({\n filePath,\n relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),\n packageName: ownerPkg?.name,\n });\n\n // Parse and resolve imports\n const rawEdges = parseImports(sourceFile);\n for (const edge of rawEdges) {\n const resolved = resolveImport(edge.target, filePath, project, packages);\n\n // Only include edges with resolved file paths (skip externals/builtins)\n if (resolved.resolvedPath) {\n allEdges.push({\n ...edge,\n target: resolved.resolvedPath,\n });\n } else if (resolved.kind === 'external' || resolved.kind === 'builtin') {\n // Keep external/builtin edges with the specifier as target\n allEdges.push(edge);\n }\n }\n }\n\n // Detect cycles among internal file edges only\n let cycles: string[][] = [];\n if (shouldDetectCycles) {\n const internalEdges = allEdges.filter(\n (e) => e.target.startsWith('/') && !e.target.includes('node_modules'),\n );\n cycles = detectCycles(internalEdges);\n }\n\n return { nodes, edges: allEdges, packages, cycles };\n}\n\n/**\n * Builds glob patterns for adding source files to the ts-morph project.\n */\nfunction buildSourceGlobs(\n projectRoot: string,\n packages: WorkspacePackage[],\n ignore: string[],\n): string[] {\n const globs: string[] = [];\n\n if (packages.length > 0) {\n // Add source files from each workspace package (src/ and other dirs like app/, pages/)\n for (const pkg of packages) {\n globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);\n }\n } else {\n // Single-package project\n globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);\n globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);\n }\n\n // Exclude build outputs and dependencies\n globs.push(\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/.next/**',\n '!**/coverage/**',\n );\n\n // Add negation patterns for ignored paths\n for (const pattern of ignore) {\n globs.push(`!${pattern}`);\n }\n\n return globs;\n}\n","/**\n * Detects import cycles in a directed graph of file dependencies.\n * Uses DFS with three-color marking (white/gray/black) to find back edges.\n *\n * @param edges - Array of directed edges with source and target file paths.\n * @returns Array of cycles, each represented as a list of file paths.\n */\nexport function detectCycles(edges: Array<{ source: string; target: string }>): string[][] {\n // Build adjacency list\n const graph = new Map<string, string[]>();\n const nodes = new Set<string>();\n\n for (const { source, target } of edges) {\n nodes.add(source);\n nodes.add(target);\n const neighbors = graph.get(source);\n if (neighbors) {\n neighbors.push(target);\n } else {\n graph.set(source, [target]);\n }\n }\n\n const WHITE = 0; // unvisited\n const GRAY = 1; // in current DFS path\n const BLACK = 2; // fully processed\n\n const color = new Map<string, number>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n const cycles: string[][] = [];\n const path: string[] = [];\n\n function dfs(node: string): void {\n color.set(node, GRAY);\n path.push(node);\n\n const neighbors = graph.get(node) ?? [];\n for (const neighbor of neighbors) {\n const c = color.get(neighbor);\n\n if (c === GRAY) {\n // Found a cycle — extract it from the path\n const cycleStart = path.indexOf(neighbor);\n if (cycleStart !== -1) {\n cycles.push(path.slice(cycleStart));\n }\n } else if (c === WHITE) {\n dfs(neighbor);\n }\n }\n\n path.pop();\n color.set(node, BLACK);\n }\n\n for (const node of nodes) {\n if (color.get(node) === WHITE) {\n dfs(node);\n }\n }\n\n return cycles;\n}\n","import type { ImportEdge } from '@viberails/types';\nimport { type SourceFile, SyntaxKind } from 'ts-morph';\n\n/** File extensions to skip (non-JS assets). */\nconst SKIP_EXTENSIONS = new Set([\n '.css',\n '.scss',\n '.less',\n '.sass',\n '.png',\n '.svg',\n '.jpg',\n '.jpeg',\n '.gif',\n '.ico',\n '.webp',\n '.json',\n '.woff',\n '.woff2',\n '.ttf',\n '.eot',\n]);\n\n/**\n * Checks whether an import specifier should be skipped (non-JS asset).\n */\nfunction shouldSkip(specifier: string): boolean {\n const dotIndex = specifier.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Parses all import statements from a ts-morph SourceFile and returns\n * them as ImportEdge objects.\n *\n * Handles static imports, default imports, namespace imports, type-only\n * imports, side-effect imports, dynamic imports, and re-exports.\n *\n * @param sourceFile - A ts-morph SourceFile to extract imports from.\n * @returns Array of ImportEdge objects for each import found.\n */\nexport function parseImports(sourceFile: SourceFile): ImportEdge[] {\n const edges: ImportEdge[] = [];\n const filePath = sourceFile.getFilePath();\n\n // Static imports (including type-only, default, namespace, side-effect)\n for (const decl of sourceFile.getImportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Re-exports: export { x } from './foo' and export * from './foo'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (!specifier || shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;\n\n const args = call.getArguments();\n if (args.length === 0) continue;\n\n const arg = args[0];\n if (arg.getKind() !== SyntaxKind.StringLiteral) continue;\n\n const specifier = arg.getText().slice(1, -1); // Remove quotes\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: false,\n dynamic: true,\n line: call.getStartLineNumber(),\n });\n }\n\n return edges;\n}\n","import { builtinModules } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type { ImportKind, WorkspacePackage } from '@viberails/types';\nimport type { Project } from 'ts-morph';\n\n/** Result of resolving an import specifier. */\nexport interface ResolvedImport {\n /** Classification of the import. */\n kind: ImportKind;\n /** Absolute path for internal/workspace imports. */\n resolvedPath?: string;\n /** Package name for workspace/external imports. */\n packageName?: string;\n}\n\n/** Set of Node.js builtin module names (with and without node: prefix). */\nconst BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);\n\n/**\n * Resolves an import specifier and classifies it.\n *\n * Classification order:\n * 1. `node:` prefix or known builtin → `builtin`\n * 2. Matches a workspace package name → `workspace`\n * 3. Relative path (`.` or `/`) → resolve via ts-morph → `internal`\n * 4. Otherwise → `external`\n * 5. If resolution fails → `unresolved`\n *\n * @param specifier - The raw import specifier as written in source.\n * @param fromFile - Absolute path of the file containing the import.\n * @param project - ts-morph Project for resolution.\n * @param workspacePackages - Known workspace packages for monorepo resolution.\n * @returns Classification and resolved path information.\n */\nexport function resolveImport(\n specifier: string,\n fromFile: string,\n project: Project,\n workspacePackages: WorkspacePackage[],\n): ResolvedImport {\n // 1. Node.js builtins\n if (BUILTINS.has(specifier)) {\n return { kind: 'builtin' };\n }\n\n // 2. Workspace packages\n const wsMatch = workspacePackages.find(\n (pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`),\n );\n if (wsMatch) {\n return {\n kind: 'workspace',\n resolvedPath: wsMatch.path,\n packageName: wsMatch.name,\n };\n }\n\n // 3. Relative or absolute imports → internal\n if (specifier.startsWith('.') || specifier.startsWith('/')) {\n const resolved = tryResolve(specifier, fromFile, project);\n if (resolved) {\n return { kind: 'internal', resolvedPath: resolved };\n }\n return { kind: 'unresolved' };\n }\n\n // 4. Check if ts-morph can resolve it (e.g. path aliases)\n const aliasResolved = tryResolve(specifier, fromFile, project);\n if (aliasResolved) {\n return { kind: 'internal', resolvedPath: aliasResolved };\n }\n\n // 5. External package\n return { kind: 'external', packageName: specifier.split('/')[0] };\n}\n\n/**\n * Attempts to resolve a specifier using ts-morph's module resolution.\n * Returns the absolute path if resolved, undefined otherwise.\n */\nfunction tryResolve(specifier: string, fromFile: string, project: Project): string | undefined {\n // Try ts-morph resolution first\n const sourceFile = project.getSourceFile(fromFile);\n if (sourceFile) {\n // Try common TypeScript extensions\n const dir = dirname(fromFile);\n const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];\n\n for (const ext of extensions) {\n const candidate = specifier.startsWith('.') ? resolve(dir, specifier + ext) : specifier + ext;\n const found = project.getSourceFile(candidate);\n if (found) return found.getFilePath();\n }\n }\n\n return undefined;\n}\n","import type {\n BoundaryConfig,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Checks import edges against boundary rules and returns violations.\n *\n * For each edge in the graph, determines the source and target\n * package/directory and checks if any deny rule matches.\n * Skips external/builtin imports, same-package/directory edges,\n * and files listed in the ignore list.\n *\n * @param graph - The complete import graph for a project.\n * @param boundaries - Boundary config with deny map and optional ignore list.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n boundaries: BoundaryConfig,\n): BoundaryViolation[] {\n const denyMap = boundaries.deny;\n if (Object.keys(denyMap).length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build ignored file set for quick lookup\n const ignoredFiles = new Set(boundaries.ignore ?? []);\n\n // Build package path → package name lookup for workspace imports\n const packagePathIndex = new Map<string, string>();\n for (const pkg of graph.packages) {\n packagePathIndex.set(pkg.path, pkg.name);\n }\n\n const violations: BoundaryViolation[] = [];\n\n for (const edge of graph.edges) {\n // Skip external/builtin targets (not absolute paths)\n if (!edge.target.startsWith('/')) continue;\n\n const sourceNode = nodeIndex.get(edge.source);\n if (!sourceNode) continue;\n\n // Skip files in the ignore list\n if (ignoredFiles.has(sourceNode.relativePath)) continue;\n\n // Determine target zone: try node lookup first, then package path lookup\n const targetNode = nodeIndex.get(edge.target);\n let targetZone: string | undefined;\n if (isMonorepo) {\n targetZone = targetNode?.packageName ?? packagePathIndex.get(edge.target);\n } else {\n if (targetNode) {\n targetZone = getTopLevelDirectory(targetNode.relativePath);\n }\n }\n\n const sourceZone = isMonorepo\n ? sourceNode.packageName\n : getTopLevelDirectory(sourceNode.relativePath);\n\n // Skip if we can't determine zones or they're the same\n if (!sourceZone || !targetZone || sourceZone === targetZone) continue;\n\n // Check deny map\n const deniedTargets = denyMap[sourceZone];\n if (deniedTargets?.includes(targetZone)) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: { from: sourceZone, to: targetZone },\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * Same logic as infer-boundaries — strips src/ prefix.\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n","import type { BoundaryConfig, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Infers boundary rules from existing import patterns in the graph.\n *\n * For monorepos, creates package-level rules based on which packages\n * import from each other. For single-package projects, creates\n * directory-level rules based on top-level directory imports.\n *\n * Only creates deny rules where the codebase already follows\n * the pattern (zero imports in that direction), so inferred rules never\n * produce immediate violations.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryConfig with inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryConfig {\n if (graph.packages.length > 0) {\n return inferMonorepoBoundaries(graph);\n }\n return inferSinglePackageBoundaries(graph);\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Infer boundary rules for a monorepo based on package-to-package imports.\n */\nfunction inferMonorepoBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n const packageNames = graph.packages.map((p) => p.name);\n\n // Build a set of declared internal dependencies per package\n const declaredDeps = new Map<string, Set<string>>();\n for (const pkg of graph.packages) {\n declaredDeps.set(pkg.name, new Set(pkg.internalDeps));\n }\n\n // Count imports from package A to package B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode?.packageName || !targetNode?.packageName) continue;\n if (sourceNode.packageName === targetNode.packageName) continue;\n\n const k = key(sourceNode.packageName, targetNode.packageName);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n\n for (const from of packageNames) {\n for (const to of packageNames) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;\n\n if (count === 0 && !isDeclaredDep) {\n // No imports and not a declared dependency — disallow\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n // If imports exist and it's a declared dependency — implicitly allowed (no rule needed)\n // If imports exist but NOT declared → skip (would produce immediate violation)\n }\n }\n\n return { deny };\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * e.g. \"src/components/Button.tsx\" → \"components\" (strips src/ prefix)\n * \"components/Button.tsx\" → \"components\"\n * \"index.ts\" → undefined (root-level file, no directory)\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n // Strip leading src/ prefix if present\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n\n/**\n * Infer boundary rules for a single-package project based on directory imports.\n */\nfunction inferSinglePackageBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Collect all top-level directories\n const directories = new Set<string>();\n for (const node of graph.nodes) {\n const dir = getTopLevelDirectory(node.relativePath);\n if (dir) directories.add(dir);\n }\n\n // Need at least 2 directories to form boundaries\n if (directories.size < 2) return { deny: {} };\n\n // Count imports from directory A to directory B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode || !targetNode) continue;\n\n const sourceDir = getTopLevelDirectory(sourceNode.relativePath);\n const targetDir = getTopLevelDirectory(targetNode.relativePath);\n if (!sourceDir || !targetDir || sourceDir === targetDir) continue;\n\n const k = key(sourceDir, targetDir);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n const dirList = [...directories].sort();\n\n for (const from of dirList) {\n for (const to of dirList) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n if (count === 0) {\n // No imports exist in this direction — safe to create a boundary\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n }\n }\n\n return { deny };\n}\n"],"mappings":"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","names":["import_node_path","import_ts_morph","buildNodeIndex","getTopLevelDirectory"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/build-graph.ts","../src/detect-cycles.ts","../src/parse-imports.ts","../src/resolve-import.ts","../src/check-boundaries.ts","../src/infer-boundaries.ts"],"sourcesContent":["declare const __PACKAGE_VERSION__: string;\nexport const VERSION: string = __PACKAGE_VERSION__;\n\nexport { buildImportGraph, type GraphOptions } from './build-graph.js';\nexport { checkBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries } from './infer-boundaries.js';\nexport { parseImports } from './parse-imports.js';\nexport { type ResolvedImport, resolveImport } from './resolve-import.js';\n","import { relative } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport type { ImportGraph, ImportGraphNode, WorkspacePackage } from '@viberails/types';\nimport { Project } from 'ts-morph';\nimport { detectCycles } from './detect-cycles.js';\nimport { parseImports } from './parse-imports.js';\nimport { resolveImport } from './resolve-import.js';\n\n/** Options for building an import graph. */\nexport interface GraphOptions {\n /** Workspace packages to include in resolution. */\n packages?: WorkspacePackage[];\n /** Glob patterns for files to ignore. */\n ignore?: string[];\n /** Whether to detect import cycles. @default true */\n detectCycles?: boolean;\n /** Path to tsconfig.json. Auto-detected if not provided. */\n tsconfigPath?: string;\n}\n\n/** Default glob patterns for files to ignore when building the graph. */\nconst DEFAULT_IGNORE = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/coverage/**',\n];\n\n/**\n * Builds a complete import graph for a project.\n *\n * Runs in a worker thread to keep the main thread responsive\n * (e.g. for CLI spinner animations). Falls back to in-process\n * execution if the worker cannot be spawned.\n *\n * @param projectRoot - Absolute path to the project root.\n * @param options - Configuration options.\n * @returns The complete import graph.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n try {\n return await buildImportGraphInWorker(projectRoot, options);\n } catch {\n // Worker failed (e.g. bundler issue, test environment) — run in-process\n return buildImportGraphSync(projectRoot, options);\n }\n}\n\n/**\n * Spawns a worker thread to run the graph build off the main thread.\n */\nfunction buildImportGraphInWorker(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n return new Promise((resolve, reject) => {\n // Resolve worker script path relative to this file's compiled location\n const workerPath = new URL('./build-graph-worker.js', import.meta.url);\n const worker = new Worker(workerPath, {\n workerData: { projectRoot, options },\n });\n worker.on('message', (result: ImportGraph) => {\n resolve(result);\n void worker.terminate();\n });\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker exited with code ${code}`));\n });\n });\n}\n\n/**\n * Core import graph building logic. Runs synchronously on whichever\n * thread calls it (main thread as fallback, or worker thread).\n */\nexport function buildImportGraphSync(projectRoot: string, options?: GraphOptions): ImportGraph {\n const packages = options?.packages ?? [];\n const shouldDetectCycles = options?.detectCycles !== false;\n const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;\n\n // Create ts-morph project\n const project = new Project({\n tsConfigFilePath: options?.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // Add source files from project root and workspace packages\n const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);\n project.addSourceFilesAtPaths(sourceGlobs);\n\n // Build nodes and edges\n const nodes: ImportGraphNode[] = [];\n const allEdges: ImportGraph['edges'] = [];\n\n for (const sourceFile of project.getSourceFiles()) {\n const filePath = sourceFile.getFilePath();\n\n // Determine which package this file belongs to\n const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));\n\n nodes.push({\n filePath,\n relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),\n packageName: ownerPkg?.name,\n });\n\n // Parse and resolve imports\n const rawEdges = parseImports(sourceFile);\n for (const edge of rawEdges) {\n const resolved = resolveImport(edge.target, filePath, project, packages);\n\n // Only include edges with resolved file paths (skip externals/builtins)\n if (resolved.resolvedPath) {\n allEdges.push({\n ...edge,\n target: resolved.resolvedPath,\n });\n } else if (resolved.kind === 'external' || resolved.kind === 'builtin') {\n // Keep external/builtin edges with the specifier as target\n allEdges.push(edge);\n }\n }\n }\n\n // Detect cycles among internal file edges only\n let cycles: string[][] = [];\n if (shouldDetectCycles) {\n const internalEdges = allEdges.filter(\n (e) => e.target.startsWith('/') && !e.target.includes('node_modules'),\n );\n cycles = detectCycles(internalEdges);\n }\n\n return { nodes, edges: allEdges, packages, cycles };\n}\n\n/**\n * Builds glob patterns for adding source files to the ts-morph project.\n */\nfunction buildSourceGlobs(\n projectRoot: string,\n packages: WorkspacePackage[],\n ignore: string[],\n): string[] {\n const globs: string[] = [];\n\n if (packages.length > 0) {\n // Add source files from each workspace package (src/ and other dirs like app/, pages/)\n for (const pkg of packages) {\n globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);\n }\n } else {\n // Single-package project\n globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);\n globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);\n }\n\n // Exclude build outputs and dependencies\n globs.push(\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/.next/**',\n '!**/coverage/**',\n );\n\n // Add negation patterns for ignored paths\n for (const pattern of ignore) {\n globs.push(`!${pattern}`);\n }\n\n return globs;\n}\n","/**\n * Detects import cycles in a directed graph of file dependencies.\n * Uses DFS with three-color marking (white/gray/black) to find back edges.\n *\n * @param edges - Array of directed edges with source and target file paths.\n * @returns Array of cycles, each represented as a list of file paths.\n */\nexport function detectCycles(edges: Array<{ source: string; target: string }>): string[][] {\n // Build adjacency list\n const graph = new Map<string, string[]>();\n const nodes = new Set<string>();\n\n for (const { source, target } of edges) {\n nodes.add(source);\n nodes.add(target);\n const neighbors = graph.get(source);\n if (neighbors) {\n neighbors.push(target);\n } else {\n graph.set(source, [target]);\n }\n }\n\n const WHITE = 0; // unvisited\n const GRAY = 1; // in current DFS path\n const BLACK = 2; // fully processed\n\n const color = new Map<string, number>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n const cycles: string[][] = [];\n const path: string[] = [];\n\n function dfs(node: string): void {\n color.set(node, GRAY);\n path.push(node);\n\n const neighbors = graph.get(node) ?? [];\n for (const neighbor of neighbors) {\n const c = color.get(neighbor);\n\n if (c === GRAY) {\n // Found a cycle — extract it from the path\n const cycleStart = path.indexOf(neighbor);\n if (cycleStart !== -1) {\n cycles.push(path.slice(cycleStart));\n }\n } else if (c === WHITE) {\n dfs(neighbor);\n }\n }\n\n path.pop();\n color.set(node, BLACK);\n }\n\n for (const node of nodes) {\n if (color.get(node) === WHITE) {\n dfs(node);\n }\n }\n\n return cycles;\n}\n","import type { ImportEdge } from '@viberails/types';\nimport { type SourceFile, SyntaxKind } from 'ts-morph';\n\n/** File extensions to skip (non-JS assets). */\nconst SKIP_EXTENSIONS = new Set([\n '.css',\n '.scss',\n '.less',\n '.sass',\n '.png',\n '.svg',\n '.jpg',\n '.jpeg',\n '.gif',\n '.ico',\n '.webp',\n '.json',\n '.woff',\n '.woff2',\n '.ttf',\n '.eot',\n]);\n\n/**\n * Checks whether an import specifier should be skipped (non-JS asset).\n */\nfunction shouldSkip(specifier: string): boolean {\n const dotIndex = specifier.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Parses all import statements from a ts-morph SourceFile and returns\n * them as ImportEdge objects.\n *\n * Handles static imports, default imports, namespace imports, type-only\n * imports, side-effect imports, dynamic imports, and re-exports.\n *\n * @param sourceFile - A ts-morph SourceFile to extract imports from.\n * @returns Array of ImportEdge objects for each import found.\n */\nexport function parseImports(sourceFile: SourceFile): ImportEdge[] {\n const edges: ImportEdge[] = [];\n const filePath = sourceFile.getFilePath();\n\n // Static imports (including type-only, default, namespace, side-effect)\n for (const decl of sourceFile.getImportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Re-exports: export { x } from './foo' and export * from './foo'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (!specifier || shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;\n\n const args = call.getArguments();\n if (args.length === 0) continue;\n\n const arg = args[0];\n if (arg.getKind() !== SyntaxKind.StringLiteral) continue;\n\n const specifier = arg.getText().slice(1, -1); // Remove quotes\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: false,\n dynamic: true,\n line: call.getStartLineNumber(),\n });\n }\n\n return edges;\n}\n","import { builtinModules } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type { ImportKind, WorkspacePackage } from '@viberails/types';\nimport type { Project } from 'ts-morph';\n\n/** Result of resolving an import specifier. */\nexport interface ResolvedImport {\n /** Classification of the import. */\n kind: ImportKind;\n /** Absolute path for internal/workspace imports. */\n resolvedPath?: string;\n /** Package name for workspace/external imports. */\n packageName?: string;\n}\n\n/** Set of Node.js builtin module names (with and without node: prefix). */\nconst BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);\n\n/**\n * Resolves an import specifier and classifies it.\n *\n * Classification order:\n * 1. `node:` prefix or known builtin → `builtin`\n * 2. Matches a workspace package name → `workspace`\n * 3. Relative path (`.` or `/`) → resolve via ts-morph → `internal`\n * 4. Otherwise → `external`\n * 5. If resolution fails → `unresolved`\n *\n * @param specifier - The raw import specifier as written in source.\n * @param fromFile - Absolute path of the file containing the import.\n * @param project - ts-morph Project for resolution.\n * @param workspacePackages - Known workspace packages for monorepo resolution.\n * @returns Classification and resolved path information.\n */\nexport function resolveImport(\n specifier: string,\n fromFile: string,\n project: Project,\n workspacePackages: WorkspacePackage[],\n): ResolvedImport {\n // 1. Node.js builtins\n if (BUILTINS.has(specifier)) {\n return { kind: 'builtin' };\n }\n\n // 2. Workspace packages\n const wsMatch = workspacePackages.find(\n (pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`),\n );\n if (wsMatch) {\n return {\n kind: 'workspace',\n resolvedPath: wsMatch.path,\n packageName: wsMatch.name,\n };\n }\n\n // 3. Relative or absolute imports → internal\n if (specifier.startsWith('.') || specifier.startsWith('/')) {\n const resolved = tryResolve(specifier, fromFile, project);\n if (resolved) {\n return { kind: 'internal', resolvedPath: resolved };\n }\n return { kind: 'unresolved' };\n }\n\n // 4. Check if ts-morph can resolve it (e.g. path aliases)\n const aliasResolved = tryResolve(specifier, fromFile, project);\n if (aliasResolved) {\n return { kind: 'internal', resolvedPath: aliasResolved };\n }\n\n // 5. External package\n return { kind: 'external', packageName: specifier.split('/')[0] };\n}\n\n/**\n * Attempts to resolve a specifier using ts-morph's module resolution.\n * Returns the absolute path if resolved, undefined otherwise.\n */\nfunction tryResolve(specifier: string, fromFile: string, project: Project): string | undefined {\n // Try ts-morph resolution first\n const sourceFile = project.getSourceFile(fromFile);\n if (sourceFile) {\n // Try common TypeScript extensions\n const dir = dirname(fromFile);\n const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];\n\n for (const ext of extensions) {\n const candidate = specifier.startsWith('.') ? resolve(dir, specifier + ext) : specifier + ext;\n const found = project.getSourceFile(candidate);\n if (found) return found.getFilePath();\n }\n }\n\n return undefined;\n}\n","import type {\n BoundaryConfig,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Checks import edges against boundary rules and returns violations.\n *\n * For each edge in the graph, determines the source and target\n * package/directory and checks if any deny rule matches.\n * Skips external/builtin imports, same-package/directory edges,\n * and files listed in the ignore list.\n *\n * @param graph - The complete import graph for a project.\n * @param boundaries - Boundary config with deny map and optional ignore list.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n boundaries: BoundaryConfig,\n): BoundaryViolation[] {\n const denyMap = boundaries.deny;\n if (Object.keys(denyMap).length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build ignored file set for quick lookup\n const ignoredFiles = new Set(boundaries.ignore ?? []);\n\n // Build package path → package name lookup for workspace imports\n const packagePathIndex = new Map<string, string>();\n for (const pkg of graph.packages) {\n packagePathIndex.set(pkg.path, pkg.name);\n }\n\n const violations: BoundaryViolation[] = [];\n\n for (const edge of graph.edges) {\n // Skip external/builtin targets (not absolute paths)\n if (!edge.target.startsWith('/')) continue;\n\n const sourceNode = nodeIndex.get(edge.source);\n if (!sourceNode) continue;\n\n // Skip files in the ignore list\n if (ignoredFiles.has(sourceNode.relativePath)) continue;\n\n // Determine target zone: try node lookup first, then package path lookup\n const targetNode = nodeIndex.get(edge.target);\n let targetZone: string | undefined;\n if (isMonorepo) {\n targetZone = targetNode?.packageName ?? packagePathIndex.get(edge.target);\n } else {\n if (targetNode) {\n targetZone = getTopLevelDirectory(targetNode.relativePath);\n }\n }\n\n const sourceZone = isMonorepo\n ? sourceNode.packageName\n : getTopLevelDirectory(sourceNode.relativePath);\n\n // Skip if we can't determine zones or they're the same\n if (!sourceZone || !targetZone || sourceZone === targetZone) continue;\n\n // Check deny map\n const deniedTargets = denyMap[sourceZone];\n if (deniedTargets?.includes(targetZone)) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: { from: sourceZone, to: targetZone },\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * Same logic as infer-boundaries — strips src/ prefix.\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n","import type { BoundaryConfig, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Infers boundary rules from existing import patterns in the graph.\n *\n * For monorepos, creates package-level rules based on which packages\n * import from each other. For single-package projects, creates\n * directory-level rules based on top-level directory imports.\n *\n * Only creates deny rules where the codebase already follows\n * the pattern (zero imports in that direction), so inferred rules never\n * produce immediate violations.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryConfig with inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryConfig {\n if (graph.packages.length > 0) {\n return inferMonorepoBoundaries(graph);\n }\n return inferSinglePackageBoundaries(graph);\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Infer boundary rules for a monorepo based on package-to-package imports.\n */\nfunction inferMonorepoBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n const packageNames = graph.packages.map((p) => p.name);\n\n // Build a set of declared internal dependencies per package\n const declaredDeps = new Map<string, Set<string>>();\n for (const pkg of graph.packages) {\n declaredDeps.set(pkg.name, new Set(pkg.internalDeps));\n }\n\n // Count imports from package A to package B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode?.packageName || !targetNode?.packageName) continue;\n if (sourceNode.packageName === targetNode.packageName) continue;\n\n const k = key(sourceNode.packageName, targetNode.packageName);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n\n for (const from of packageNames) {\n for (const to of packageNames) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;\n\n if (count === 0 && !isDeclaredDep) {\n // No imports and not a declared dependency — disallow\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n // If imports exist and it's a declared dependency — implicitly allowed (no rule needed)\n // If imports exist but NOT declared → skip (would produce immediate violation)\n }\n }\n\n return { deny };\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * e.g. \"src/components/Button.tsx\" → \"components\" (strips src/ prefix)\n * \"components/Button.tsx\" → \"components\"\n * \"index.ts\" → undefined (root-level file, no directory)\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n // Strip leading src/ prefix if present\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n\n/**\n * Infer boundary rules for a single-package project based on directory imports.\n */\nfunction inferSinglePackageBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Collect all top-level directories\n const directories = new Set<string>();\n for (const node of graph.nodes) {\n const dir = getTopLevelDirectory(node.relativePath);\n if (dir) directories.add(dir);\n }\n\n // Need at least 2 directories to form boundaries\n if (directories.size < 2) return { deny: {} };\n\n // Count imports from directory A to directory B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode || !targetNode) continue;\n\n const sourceDir = getTopLevelDirectory(sourceNode.relativePath);\n const targetDir = getTopLevelDirectory(targetNode.relativePath);\n if (!sourceDir || !targetDir || sourceDir === targetDir) continue;\n\n const k = key(sourceDir, targetDir);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n const dirList = [...directories].sort();\n\n for (const from of dirList) {\n for (const to of dirList) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n if (count === 0) {\n // No imports exist in this direction — safe to create a boundary\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n }\n }\n\n return { deny };\n}\n"],"mappings":"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","names":["import_node_path","import_ts_morph","resolve","buildNodeIndex","getTopLevelDirectory"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -15,8 +15,9 @@ interface GraphOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Builds a complete import graph for a project.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Runs in a worker thread to keep the main thread responsive
|
|
19
|
+
* (e.g. for CLI spinner animations). Falls back to in-process
|
|
20
|
+
* execution if the worker cannot be spawned.
|
|
20
21
|
*
|
|
21
22
|
* @param projectRoot - Absolute path to the project root.
|
|
22
23
|
* @param options - Configuration options.
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ interface GraphOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Builds a complete import graph for a project.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Runs in a worker thread to keep the main thread responsive
|
|
19
|
+
* (e.g. for CLI spinner animations). Falls back to in-process
|
|
20
|
+
* execution if the worker cannot be spawned.
|
|
20
21
|
*
|
|
21
22
|
* @param projectRoot - Absolute path to the project root.
|
|
22
23
|
* @param options - Configuration options.
|
package/dist/index.js
CHANGED
|
@@ -1,246 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const graph = /* @__PURE__ */ new Map();
|
|
8
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
9
|
-
for (const { source, target } of edges) {
|
|
10
|
-
nodes.add(source);
|
|
11
|
-
nodes.add(target);
|
|
12
|
-
const neighbors = graph.get(source);
|
|
13
|
-
if (neighbors) {
|
|
14
|
-
neighbors.push(target);
|
|
15
|
-
} else {
|
|
16
|
-
graph.set(source, [target]);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const WHITE = 0;
|
|
20
|
-
const GRAY = 1;
|
|
21
|
-
const BLACK = 2;
|
|
22
|
-
const color = /* @__PURE__ */ new Map();
|
|
23
|
-
for (const node of nodes) {
|
|
24
|
-
color.set(node, WHITE);
|
|
25
|
-
}
|
|
26
|
-
const cycles = [];
|
|
27
|
-
const path = [];
|
|
28
|
-
function dfs(node) {
|
|
29
|
-
color.set(node, GRAY);
|
|
30
|
-
path.push(node);
|
|
31
|
-
const neighbors = graph.get(node) ?? [];
|
|
32
|
-
for (const neighbor of neighbors) {
|
|
33
|
-
const c = color.get(neighbor);
|
|
34
|
-
if (c === GRAY) {
|
|
35
|
-
const cycleStart = path.indexOf(neighbor);
|
|
36
|
-
if (cycleStart !== -1) {
|
|
37
|
-
cycles.push(path.slice(cycleStart));
|
|
38
|
-
}
|
|
39
|
-
} else if (c === WHITE) {
|
|
40
|
-
dfs(neighbor);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
path.pop();
|
|
44
|
-
color.set(node, BLACK);
|
|
45
|
-
}
|
|
46
|
-
for (const node of nodes) {
|
|
47
|
-
if (color.get(node) === WHITE) {
|
|
48
|
-
dfs(node);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return cycles;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/parse-imports.ts
|
|
55
|
-
import { SyntaxKind } from "ts-morph";
|
|
56
|
-
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
57
|
-
".css",
|
|
58
|
-
".scss",
|
|
59
|
-
".less",
|
|
60
|
-
".sass",
|
|
61
|
-
".png",
|
|
62
|
-
".svg",
|
|
63
|
-
".jpg",
|
|
64
|
-
".jpeg",
|
|
65
|
-
".gif",
|
|
66
|
-
".ico",
|
|
67
|
-
".webp",
|
|
68
|
-
".json",
|
|
69
|
-
".woff",
|
|
70
|
-
".woff2",
|
|
71
|
-
".ttf",
|
|
72
|
-
".eot"
|
|
73
|
-
]);
|
|
74
|
-
function shouldSkip(specifier) {
|
|
75
|
-
const dotIndex = specifier.lastIndexOf(".");
|
|
76
|
-
if (dotIndex === -1) return false;
|
|
77
|
-
return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());
|
|
78
|
-
}
|
|
79
|
-
function parseImports(sourceFile) {
|
|
80
|
-
const edges = [];
|
|
81
|
-
const filePath = sourceFile.getFilePath();
|
|
82
|
-
for (const decl of sourceFile.getImportDeclarations()) {
|
|
83
|
-
const specifier = decl.getModuleSpecifierValue();
|
|
84
|
-
if (shouldSkip(specifier)) continue;
|
|
85
|
-
edges.push({
|
|
86
|
-
source: filePath,
|
|
87
|
-
target: specifier,
|
|
88
|
-
specifier,
|
|
89
|
-
typeOnly: decl.isTypeOnly(),
|
|
90
|
-
dynamic: false,
|
|
91
|
-
line: decl.getStartLineNumber()
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
for (const decl of sourceFile.getExportDeclarations()) {
|
|
95
|
-
const specifier = decl.getModuleSpecifierValue();
|
|
96
|
-
if (!specifier || shouldSkip(specifier)) continue;
|
|
97
|
-
edges.push({
|
|
98
|
-
source: filePath,
|
|
99
|
-
target: specifier,
|
|
100
|
-
specifier,
|
|
101
|
-
typeOnly: decl.isTypeOnly(),
|
|
102
|
-
dynamic: false,
|
|
103
|
-
line: decl.getStartLineNumber()
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
107
|
-
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
108
|
-
const args = call.getArguments();
|
|
109
|
-
if (args.length === 0) continue;
|
|
110
|
-
const arg = args[0];
|
|
111
|
-
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
112
|
-
const specifier = arg.getText().slice(1, -1);
|
|
113
|
-
if (shouldSkip(specifier)) continue;
|
|
114
|
-
edges.push({
|
|
115
|
-
source: filePath,
|
|
116
|
-
target: specifier,
|
|
117
|
-
specifier,
|
|
118
|
-
typeOnly: false,
|
|
119
|
-
dynamic: true,
|
|
120
|
-
line: call.getStartLineNumber()
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return edges;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// src/resolve-import.ts
|
|
127
|
-
import { builtinModules } from "module";
|
|
128
|
-
import { dirname, resolve } from "path";
|
|
129
|
-
var BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);
|
|
130
|
-
function resolveImport(specifier, fromFile, project, workspacePackages) {
|
|
131
|
-
if (BUILTINS.has(specifier)) {
|
|
132
|
-
return { kind: "builtin" };
|
|
133
|
-
}
|
|
134
|
-
const wsMatch = workspacePackages.find(
|
|
135
|
-
(pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)
|
|
136
|
-
);
|
|
137
|
-
if (wsMatch) {
|
|
138
|
-
return {
|
|
139
|
-
kind: "workspace",
|
|
140
|
-
resolvedPath: wsMatch.path,
|
|
141
|
-
packageName: wsMatch.name
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
145
|
-
const resolved = tryResolve(specifier, fromFile, project);
|
|
146
|
-
if (resolved) {
|
|
147
|
-
return { kind: "internal", resolvedPath: resolved };
|
|
148
|
-
}
|
|
149
|
-
return { kind: "unresolved" };
|
|
150
|
-
}
|
|
151
|
-
const aliasResolved = tryResolve(specifier, fromFile, project);
|
|
152
|
-
if (aliasResolved) {
|
|
153
|
-
return { kind: "internal", resolvedPath: aliasResolved };
|
|
154
|
-
}
|
|
155
|
-
return { kind: "external", packageName: specifier.split("/")[0] };
|
|
156
|
-
}
|
|
157
|
-
function tryResolve(specifier, fromFile, project) {
|
|
158
|
-
const sourceFile = project.getSourceFile(fromFile);
|
|
159
|
-
if (sourceFile) {
|
|
160
|
-
const dir = dirname(fromFile);
|
|
161
|
-
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
162
|
-
for (const ext of extensions) {
|
|
163
|
-
const candidate = specifier.startsWith(".") ? resolve(dir, specifier + ext) : specifier + ext;
|
|
164
|
-
const found = project.getSourceFile(candidate);
|
|
165
|
-
if (found) return found.getFilePath();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return void 0;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// src/build-graph.ts
|
|
172
|
-
var DEFAULT_IGNORE = [
|
|
173
|
-
"**/node_modules/**",
|
|
174
|
-
"**/dist/**",
|
|
175
|
-
"**/build/**",
|
|
176
|
-
"**/.next/**",
|
|
177
|
-
"**/.nuxt/**",
|
|
178
|
-
"**/coverage/**"
|
|
179
|
-
];
|
|
180
|
-
async function buildImportGraph(projectRoot, options) {
|
|
181
|
-
const packages = options?.packages ?? [];
|
|
182
|
-
const shouldDetectCycles = options?.detectCycles !== false;
|
|
183
|
-
const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;
|
|
184
|
-
const project = new Project({
|
|
185
|
-
tsConfigFilePath: options?.tsconfigPath,
|
|
186
|
-
skipAddingFilesFromTsConfig: true
|
|
187
|
-
});
|
|
188
|
-
const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);
|
|
189
|
-
project.addSourceFilesAtPaths(sourceGlobs);
|
|
190
|
-
const nodes = [];
|
|
191
|
-
const allEdges = [];
|
|
192
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
193
|
-
const filePath = sourceFile.getFilePath();
|
|
194
|
-
const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));
|
|
195
|
-
nodes.push({
|
|
196
|
-
filePath,
|
|
197
|
-
relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),
|
|
198
|
-
packageName: ownerPkg?.name
|
|
199
|
-
});
|
|
200
|
-
const rawEdges = parseImports(sourceFile);
|
|
201
|
-
for (const edge of rawEdges) {
|
|
202
|
-
const resolved = resolveImport(edge.target, filePath, project, packages);
|
|
203
|
-
if (resolved.resolvedPath) {
|
|
204
|
-
allEdges.push({
|
|
205
|
-
...edge,
|
|
206
|
-
target: resolved.resolvedPath
|
|
207
|
-
});
|
|
208
|
-
} else if (resolved.kind === "external" || resolved.kind === "builtin") {
|
|
209
|
-
allEdges.push(edge);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
let cycles = [];
|
|
214
|
-
if (shouldDetectCycles) {
|
|
215
|
-
const internalEdges = allEdges.filter(
|
|
216
|
-
(e) => e.target.startsWith("/") && !e.target.includes("node_modules")
|
|
217
|
-
);
|
|
218
|
-
cycles = detectCycles(internalEdges);
|
|
219
|
-
}
|
|
220
|
-
return { nodes, edges: allEdges, packages, cycles };
|
|
221
|
-
}
|
|
222
|
-
function buildSourceGlobs(projectRoot, packages, ignore) {
|
|
223
|
-
const globs = [];
|
|
224
|
-
if (packages.length > 0) {
|
|
225
|
-
for (const pkg of packages) {
|
|
226
|
-
globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);
|
|
230
|
-
globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);
|
|
231
|
-
}
|
|
232
|
-
globs.push(
|
|
233
|
-
"!**/node_modules/**",
|
|
234
|
-
"!**/dist/**",
|
|
235
|
-
"!**/build/**",
|
|
236
|
-
"!**/.next/**",
|
|
237
|
-
"!**/coverage/**"
|
|
238
|
-
);
|
|
239
|
-
for (const pattern of ignore) {
|
|
240
|
-
globs.push(`!${pattern}`);
|
|
241
|
-
}
|
|
242
|
-
return globs;
|
|
243
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
buildImportGraph,
|
|
3
|
+
detectCycles,
|
|
4
|
+
parseImports,
|
|
5
|
+
resolveImport
|
|
6
|
+
} from "./chunk-DQX7OULF.js";
|
|
244
7
|
|
|
245
8
|
// src/check-boundaries.ts
|
|
246
9
|
function checkBoundaries(graph, boundaries) {
|
|
@@ -384,7 +147,7 @@ function inferSinglePackageBoundaries(graph) {
|
|
|
384
147
|
}
|
|
385
148
|
|
|
386
149
|
// src/index.ts
|
|
387
|
-
var VERSION = "0.6.
|
|
150
|
+
var VERSION = "0.6.4";
|
|
388
151
|
export {
|
|
389
152
|
VERSION,
|
|
390
153
|
buildImportGraph,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/build-graph.ts","../src/detect-cycles.ts","../src/parse-imports.ts","../src/resolve-import.ts","../src/check-boundaries.ts","../src/infer-boundaries.ts","../src/index.ts"],"sourcesContent":["import { relative } from 'node:path';\nimport type { ImportGraph, ImportGraphNode, WorkspacePackage } from '@viberails/types';\nimport { Project } from 'ts-morph';\nimport { detectCycles } from './detect-cycles.js';\nimport { parseImports } from './parse-imports.js';\nimport { resolveImport } from './resolve-import.js';\n\n/** Options for building an import graph. */\nexport interface GraphOptions {\n /** Workspace packages to include in resolution. */\n packages?: WorkspacePackage[];\n /** Glob patterns for files to ignore. */\n ignore?: string[];\n /** Whether to detect import cycles. @default true */\n detectCycles?: boolean;\n /** Path to tsconfig.json. Auto-detected if not provided. */\n tsconfigPath?: string;\n}\n\n/** Default glob patterns for files to ignore when building the graph. */\nconst DEFAULT_IGNORE = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/coverage/**',\n];\n\n/**\n * Builds a complete import graph for a project.\n *\n * Creates a ts-morph Project, adds all source files, parses imports,\n * resolves specifiers, and optionally detects cycles.\n *\n * @param projectRoot - Absolute path to the project root.\n * @param options - Configuration options.\n * @returns The complete import graph.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n options?: GraphOptions,\n): Promise<ImportGraph> {\n const packages = options?.packages ?? [];\n const shouldDetectCycles = options?.detectCycles !== false;\n const ignorePatterns = options?.ignore ?? DEFAULT_IGNORE;\n\n // Create ts-morph project\n const project = new Project({\n tsConfigFilePath: options?.tsconfigPath,\n skipAddingFilesFromTsConfig: true,\n });\n\n // Add source files from project root and workspace packages\n const sourceGlobs = buildSourceGlobs(projectRoot, packages, ignorePatterns);\n project.addSourceFilesAtPaths(sourceGlobs);\n\n // Build nodes and edges\n const nodes: ImportGraphNode[] = [];\n const allEdges: ImportGraph['edges'] = [];\n\n for (const sourceFile of project.getSourceFiles()) {\n const filePath = sourceFile.getFilePath();\n\n // Determine which package this file belongs to\n const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));\n\n nodes.push({\n filePath,\n relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),\n packageName: ownerPkg?.name,\n });\n\n // Parse and resolve imports\n const rawEdges = parseImports(sourceFile);\n for (const edge of rawEdges) {\n const resolved = resolveImport(edge.target, filePath, project, packages);\n\n // Only include edges with resolved file paths (skip externals/builtins)\n if (resolved.resolvedPath) {\n allEdges.push({\n ...edge,\n target: resolved.resolvedPath,\n });\n } else if (resolved.kind === 'external' || resolved.kind === 'builtin') {\n // Keep external/builtin edges with the specifier as target\n allEdges.push(edge);\n }\n }\n }\n\n // Detect cycles among internal file edges only\n let cycles: string[][] = [];\n if (shouldDetectCycles) {\n const internalEdges = allEdges.filter(\n (e) => e.target.startsWith('/') && !e.target.includes('node_modules'),\n );\n cycles = detectCycles(internalEdges);\n }\n\n return { nodes, edges: allEdges, packages, cycles };\n}\n\n/**\n * Builds glob patterns for adding source files to the ts-morph project.\n */\nfunction buildSourceGlobs(\n projectRoot: string,\n packages: WorkspacePackage[],\n ignore: string[],\n): string[] {\n const globs: string[] = [];\n\n if (packages.length > 0) {\n // Add source files from each workspace package (src/ and other dirs like app/, pages/)\n for (const pkg of packages) {\n globs.push(`${pkg.path}/**/*.{ts,tsx,js,jsx}`);\n }\n } else {\n // Single-package project\n globs.push(`${projectRoot}/src/**/*.{ts,tsx,js,jsx}`);\n globs.push(`${projectRoot}/**/*.{ts,tsx,js,jsx}`);\n }\n\n // Exclude build outputs and dependencies\n globs.push(\n '!**/node_modules/**',\n '!**/dist/**',\n '!**/build/**',\n '!**/.next/**',\n '!**/coverage/**',\n );\n\n // Add negation patterns for ignored paths\n for (const pattern of ignore) {\n globs.push(`!${pattern}`);\n }\n\n return globs;\n}\n","/**\n * Detects import cycles in a directed graph of file dependencies.\n * Uses DFS with three-color marking (white/gray/black) to find back edges.\n *\n * @param edges - Array of directed edges with source and target file paths.\n * @returns Array of cycles, each represented as a list of file paths.\n */\nexport function detectCycles(edges: Array<{ source: string; target: string }>): string[][] {\n // Build adjacency list\n const graph = new Map<string, string[]>();\n const nodes = new Set<string>();\n\n for (const { source, target } of edges) {\n nodes.add(source);\n nodes.add(target);\n const neighbors = graph.get(source);\n if (neighbors) {\n neighbors.push(target);\n } else {\n graph.set(source, [target]);\n }\n }\n\n const WHITE = 0; // unvisited\n const GRAY = 1; // in current DFS path\n const BLACK = 2; // fully processed\n\n const color = new Map<string, number>();\n for (const node of nodes) {\n color.set(node, WHITE);\n }\n\n const cycles: string[][] = [];\n const path: string[] = [];\n\n function dfs(node: string): void {\n color.set(node, GRAY);\n path.push(node);\n\n const neighbors = graph.get(node) ?? [];\n for (const neighbor of neighbors) {\n const c = color.get(neighbor);\n\n if (c === GRAY) {\n // Found a cycle — extract it from the path\n const cycleStart = path.indexOf(neighbor);\n if (cycleStart !== -1) {\n cycles.push(path.slice(cycleStart));\n }\n } else if (c === WHITE) {\n dfs(neighbor);\n }\n }\n\n path.pop();\n color.set(node, BLACK);\n }\n\n for (const node of nodes) {\n if (color.get(node) === WHITE) {\n dfs(node);\n }\n }\n\n return cycles;\n}\n","import type { ImportEdge } from '@viberails/types';\nimport { type SourceFile, SyntaxKind } from 'ts-morph';\n\n/** File extensions to skip (non-JS assets). */\nconst SKIP_EXTENSIONS = new Set([\n '.css',\n '.scss',\n '.less',\n '.sass',\n '.png',\n '.svg',\n '.jpg',\n '.jpeg',\n '.gif',\n '.ico',\n '.webp',\n '.json',\n '.woff',\n '.woff2',\n '.ttf',\n '.eot',\n]);\n\n/**\n * Checks whether an import specifier should be skipped (non-JS asset).\n */\nfunction shouldSkip(specifier: string): boolean {\n const dotIndex = specifier.lastIndexOf('.');\n if (dotIndex === -1) return false;\n return SKIP_EXTENSIONS.has(specifier.slice(dotIndex).toLowerCase());\n}\n\n/**\n * Parses all import statements from a ts-morph SourceFile and returns\n * them as ImportEdge objects.\n *\n * Handles static imports, default imports, namespace imports, type-only\n * imports, side-effect imports, dynamic imports, and re-exports.\n *\n * @param sourceFile - A ts-morph SourceFile to extract imports from.\n * @returns Array of ImportEdge objects for each import found.\n */\nexport function parseImports(sourceFile: SourceFile): ImportEdge[] {\n const edges: ImportEdge[] = [];\n const filePath = sourceFile.getFilePath();\n\n // Static imports (including type-only, default, namespace, side-effect)\n for (const decl of sourceFile.getImportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Re-exports: export { x } from './foo' and export * from './foo'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue();\n if (!specifier || shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: decl.isTypeOnly(),\n dynamic: false,\n line: decl.getStartLineNumber(),\n });\n }\n\n // Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;\n\n const args = call.getArguments();\n if (args.length === 0) continue;\n\n const arg = args[0];\n if (arg.getKind() !== SyntaxKind.StringLiteral) continue;\n\n const specifier = arg.getText().slice(1, -1); // Remove quotes\n if (shouldSkip(specifier)) continue;\n\n edges.push({\n source: filePath,\n target: specifier,\n specifier,\n typeOnly: false,\n dynamic: true,\n line: call.getStartLineNumber(),\n });\n }\n\n return edges;\n}\n","import { builtinModules } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type { ImportKind, WorkspacePackage } from '@viberails/types';\nimport type { Project } from 'ts-morph';\n\n/** Result of resolving an import specifier. */\nexport interface ResolvedImport {\n /** Classification of the import. */\n kind: ImportKind;\n /** Absolute path for internal/workspace imports. */\n resolvedPath?: string;\n /** Package name for workspace/external imports. */\n packageName?: string;\n}\n\n/** Set of Node.js builtin module names (with and without node: prefix). */\nconst BUILTINS = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);\n\n/**\n * Resolves an import specifier and classifies it.\n *\n * Classification order:\n * 1. `node:` prefix or known builtin → `builtin`\n * 2. Matches a workspace package name → `workspace`\n * 3. Relative path (`.` or `/`) → resolve via ts-morph → `internal`\n * 4. Otherwise → `external`\n * 5. If resolution fails → `unresolved`\n *\n * @param specifier - The raw import specifier as written in source.\n * @param fromFile - Absolute path of the file containing the import.\n * @param project - ts-morph Project for resolution.\n * @param workspacePackages - Known workspace packages for monorepo resolution.\n * @returns Classification and resolved path information.\n */\nexport function resolveImport(\n specifier: string,\n fromFile: string,\n project: Project,\n workspacePackages: WorkspacePackage[],\n): ResolvedImport {\n // 1. Node.js builtins\n if (BUILTINS.has(specifier)) {\n return { kind: 'builtin' };\n }\n\n // 2. Workspace packages\n const wsMatch = workspacePackages.find(\n (pkg) => specifier === pkg.name || specifier.startsWith(`${pkg.name}/`),\n );\n if (wsMatch) {\n return {\n kind: 'workspace',\n resolvedPath: wsMatch.path,\n packageName: wsMatch.name,\n };\n }\n\n // 3. Relative or absolute imports → internal\n if (specifier.startsWith('.') || specifier.startsWith('/')) {\n const resolved = tryResolve(specifier, fromFile, project);\n if (resolved) {\n return { kind: 'internal', resolvedPath: resolved };\n }\n return { kind: 'unresolved' };\n }\n\n // 4. Check if ts-morph can resolve it (e.g. path aliases)\n const aliasResolved = tryResolve(specifier, fromFile, project);\n if (aliasResolved) {\n return { kind: 'internal', resolvedPath: aliasResolved };\n }\n\n // 5. External package\n return { kind: 'external', packageName: specifier.split('/')[0] };\n}\n\n/**\n * Attempts to resolve a specifier using ts-morph's module resolution.\n * Returns the absolute path if resolved, undefined otherwise.\n */\nfunction tryResolve(specifier: string, fromFile: string, project: Project): string | undefined {\n // Try ts-morph resolution first\n const sourceFile = project.getSourceFile(fromFile);\n if (sourceFile) {\n // Try common TypeScript extensions\n const dir = dirname(fromFile);\n const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];\n\n for (const ext of extensions) {\n const candidate = specifier.startsWith('.') ? resolve(dir, specifier + ext) : specifier + ext;\n const found = project.getSourceFile(candidate);\n if (found) return found.getFilePath();\n }\n }\n\n return undefined;\n}\n","import type {\n BoundaryConfig,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Checks import edges against boundary rules and returns violations.\n *\n * For each edge in the graph, determines the source and target\n * package/directory and checks if any deny rule matches.\n * Skips external/builtin imports, same-package/directory edges,\n * and files listed in the ignore list.\n *\n * @param graph - The complete import graph for a project.\n * @param boundaries - Boundary config with deny map and optional ignore list.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n boundaries: BoundaryConfig,\n): BoundaryViolation[] {\n const denyMap = boundaries.deny;\n if (Object.keys(denyMap).length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build ignored file set for quick lookup\n const ignoredFiles = new Set(boundaries.ignore ?? []);\n\n // Build package path → package name lookup for workspace imports\n const packagePathIndex = new Map<string, string>();\n for (const pkg of graph.packages) {\n packagePathIndex.set(pkg.path, pkg.name);\n }\n\n const violations: BoundaryViolation[] = [];\n\n for (const edge of graph.edges) {\n // Skip external/builtin targets (not absolute paths)\n if (!edge.target.startsWith('/')) continue;\n\n const sourceNode = nodeIndex.get(edge.source);\n if (!sourceNode) continue;\n\n // Skip files in the ignore list\n if (ignoredFiles.has(sourceNode.relativePath)) continue;\n\n // Determine target zone: try node lookup first, then package path lookup\n const targetNode = nodeIndex.get(edge.target);\n let targetZone: string | undefined;\n if (isMonorepo) {\n targetZone = targetNode?.packageName ?? packagePathIndex.get(edge.target);\n } else {\n if (targetNode) {\n targetZone = getTopLevelDirectory(targetNode.relativePath);\n }\n }\n\n const sourceZone = isMonorepo\n ? sourceNode.packageName\n : getTopLevelDirectory(sourceNode.relativePath);\n\n // Skip if we can't determine zones or they're the same\n if (!sourceZone || !targetZone || sourceZone === targetZone) continue;\n\n // Check deny map\n const deniedTargets = denyMap[sourceZone];\n if (deniedTargets?.includes(targetZone)) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: { from: sourceZone, to: targetZone },\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * Same logic as infer-boundaries — strips src/ prefix.\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n","import type { BoundaryConfig, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Infers boundary rules from existing import patterns in the graph.\n *\n * For monorepos, creates package-level rules based on which packages\n * import from each other. For single-package projects, creates\n * directory-level rules based on top-level directory imports.\n *\n * Only creates deny rules where the codebase already follows\n * the pattern (zero imports in that direction), so inferred rules never\n * produce immediate violations.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryConfig with inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryConfig {\n if (graph.packages.length > 0) {\n return inferMonorepoBoundaries(graph);\n }\n return inferSinglePackageBoundaries(graph);\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Infer boundary rules for a monorepo based on package-to-package imports.\n */\nfunction inferMonorepoBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n const packageNames = graph.packages.map((p) => p.name);\n\n // Build a set of declared internal dependencies per package\n const declaredDeps = new Map<string, Set<string>>();\n for (const pkg of graph.packages) {\n declaredDeps.set(pkg.name, new Set(pkg.internalDeps));\n }\n\n // Count imports from package A to package B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode?.packageName || !targetNode?.packageName) continue;\n if (sourceNode.packageName === targetNode.packageName) continue;\n\n const k = key(sourceNode.packageName, targetNode.packageName);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n\n for (const from of packageNames) {\n for (const to of packageNames) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;\n\n if (count === 0 && !isDeclaredDep) {\n // No imports and not a declared dependency — disallow\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n // If imports exist and it's a declared dependency — implicitly allowed (no rule needed)\n // If imports exist but NOT declared → skip (would produce immediate violation)\n }\n }\n\n return { deny };\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * e.g. \"src/components/Button.tsx\" → \"components\" (strips src/ prefix)\n * \"components/Button.tsx\" → \"components\"\n * \"index.ts\" → undefined (root-level file, no directory)\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n // Strip leading src/ prefix if present\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n\n/**\n * Infer boundary rules for a single-package project based on directory imports.\n */\nfunction inferSinglePackageBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Collect all top-level directories\n const directories = new Set<string>();\n for (const node of graph.nodes) {\n const dir = getTopLevelDirectory(node.relativePath);\n if (dir) directories.add(dir);\n }\n\n // Need at least 2 directories to form boundaries\n if (directories.size < 2) return { deny: {} };\n\n // Count imports from directory A to directory B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode || !targetNode) continue;\n\n const sourceDir = getTopLevelDirectory(sourceNode.relativePath);\n const targetDir = getTopLevelDirectory(targetNode.relativePath);\n if (!sourceDir || !targetDir || sourceDir === targetDir) continue;\n\n const k = key(sourceDir, targetDir);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n const dirList = [...directories].sort();\n\n for (const from of dirList) {\n for (const to of dirList) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n if (count === 0) {\n // No imports exist in this direction — safe to create a boundary\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n }\n }\n\n return { deny };\n}\n","declare const __PACKAGE_VERSION__: string;\nexport const VERSION: string = __PACKAGE_VERSION__;\n\nexport { buildImportGraph, type GraphOptions } from './build-graph.js';\nexport { checkBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries } from './infer-boundaries.js';\nexport { parseImports } from './parse-imports.js';\nexport { type ResolvedImport, resolveImport } from './resolve-import.js';\n"],"mappings":"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","names":["buildNodeIndex","getTopLevelDirectory"]}
|
|
1
|
+
{"version":3,"sources":["../src/check-boundaries.ts","../src/infer-boundaries.ts","../src/index.ts"],"sourcesContent":["import type {\n BoundaryConfig,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Checks import edges against boundary rules and returns violations.\n *\n * For each edge in the graph, determines the source and target\n * package/directory and checks if any deny rule matches.\n * Skips external/builtin imports, same-package/directory edges,\n * and files listed in the ignore list.\n *\n * @param graph - The complete import graph for a project.\n * @param boundaries - Boundary config with deny map and optional ignore list.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n boundaries: BoundaryConfig,\n): BoundaryViolation[] {\n const denyMap = boundaries.deny;\n if (Object.keys(denyMap).length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build ignored file set for quick lookup\n const ignoredFiles = new Set(boundaries.ignore ?? []);\n\n // Build package path → package name lookup for workspace imports\n const packagePathIndex = new Map<string, string>();\n for (const pkg of graph.packages) {\n packagePathIndex.set(pkg.path, pkg.name);\n }\n\n const violations: BoundaryViolation[] = [];\n\n for (const edge of graph.edges) {\n // Skip external/builtin targets (not absolute paths)\n if (!edge.target.startsWith('/')) continue;\n\n const sourceNode = nodeIndex.get(edge.source);\n if (!sourceNode) continue;\n\n // Skip files in the ignore list\n if (ignoredFiles.has(sourceNode.relativePath)) continue;\n\n // Determine target zone: try node lookup first, then package path lookup\n const targetNode = nodeIndex.get(edge.target);\n let targetZone: string | undefined;\n if (isMonorepo) {\n targetZone = targetNode?.packageName ?? packagePathIndex.get(edge.target);\n } else {\n if (targetNode) {\n targetZone = getTopLevelDirectory(targetNode.relativePath);\n }\n }\n\n const sourceZone = isMonorepo\n ? sourceNode.packageName\n : getTopLevelDirectory(sourceNode.relativePath);\n\n // Skip if we can't determine zones or they're the same\n if (!sourceZone || !targetZone || sourceZone === targetZone) continue;\n\n // Check deny map\n const deniedTargets = denyMap[sourceZone];\n if (deniedTargets?.includes(targetZone)) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: { from: sourceZone, to: targetZone },\n });\n }\n }\n\n return violations;\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * Same logic as infer-boundaries — strips src/ prefix.\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n","import type { BoundaryConfig, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Infers boundary rules from existing import patterns in the graph.\n *\n * For monorepos, creates package-level rules based on which packages\n * import from each other. For single-package projects, creates\n * directory-level rules based on top-level directory imports.\n *\n * Only creates deny rules where the codebase already follows\n * the pattern (zero imports in that direction), so inferred rules never\n * produce immediate violations.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryConfig with inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryConfig {\n if (graph.packages.length > 0) {\n return inferMonorepoBoundaries(graph);\n }\n return inferSinglePackageBoundaries(graph);\n}\n\n/**\n * Build a lookup from absolute file path to its graph node.\n */\nfunction buildNodeIndex(nodes: ImportGraphNode[]): Map<string, ImportGraphNode> {\n const index = new Map<string, ImportGraphNode>();\n for (const node of nodes) {\n index.set(node.filePath, node);\n }\n return index;\n}\n\n/**\n * Infer boundary rules for a monorepo based on package-to-package imports.\n */\nfunction inferMonorepoBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n const packageNames = graph.packages.map((p) => p.name);\n\n // Build a set of declared internal dependencies per package\n const declaredDeps = new Map<string, Set<string>>();\n for (const pkg of graph.packages) {\n declaredDeps.set(pkg.name, new Set(pkg.internalDeps));\n }\n\n // Count imports from package A to package B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode?.packageName || !targetNode?.packageName) continue;\n if (sourceNode.packageName === targetNode.packageName) continue;\n\n const k = key(sourceNode.packageName, targetNode.packageName);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n\n for (const from of packageNames) {\n for (const to of packageNames) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;\n\n if (count === 0 && !isDeclaredDep) {\n // No imports and not a declared dependency — disallow\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n // If imports exist and it's a declared dependency — implicitly allowed (no rule needed)\n // If imports exist but NOT declared → skip (would produce immediate violation)\n }\n }\n\n return { deny };\n}\n\n/**\n * Extract the top-level directory for a file's relative path.\n * e.g. \"src/components/Button.tsx\" → \"components\" (strips src/ prefix)\n * \"components/Button.tsx\" → \"components\"\n * \"index.ts\" → undefined (root-level file, no directory)\n */\nfunction getTopLevelDirectory(relativePath: string): string | undefined {\n // Strip leading src/ prefix if present\n const normalized = relativePath.startsWith('src/') ? relativePath.slice(4) : relativePath;\n\n const slashIndex = normalized.indexOf('/');\n if (slashIndex === -1) return undefined;\n return normalized.slice(0, slashIndex);\n}\n\n/**\n * Infer boundary rules for a single-package project based on directory imports.\n */\nfunction inferSinglePackageBoundaries(graph: ImportGraph): BoundaryConfig {\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Collect all top-level directories\n const directories = new Set<string>();\n for (const node of graph.nodes) {\n const dir = getTopLevelDirectory(node.relativePath);\n if (dir) directories.add(dir);\n }\n\n // Need at least 2 directories to form boundaries\n if (directories.size < 2) return { deny: {} };\n\n // Count imports from directory A to directory B\n const importCounts = new Map<string, number>();\n const key = (from: string, to: string) => `${from} -> ${to}`;\n\n for (const edge of graph.edges) {\n const sourceNode = nodeIndex.get(edge.source);\n const targetNode = nodeIndex.get(edge.target);\n if (!sourceNode || !targetNode) continue;\n\n const sourceDir = getTopLevelDirectory(sourceNode.relativePath);\n const targetDir = getTopLevelDirectory(targetNode.relativePath);\n if (!sourceDir || !targetDir || sourceDir === targetDir) continue;\n\n const k = key(sourceDir, targetDir);\n importCounts.set(k, (importCounts.get(k) ?? 0) + 1);\n }\n\n const deny: Record<string, string[]> = {};\n const dirList = [...directories].sort();\n\n for (const from of dirList) {\n for (const to of dirList) {\n if (from === to) continue;\n\n const count = importCounts.get(key(from, to)) ?? 0;\n if (count === 0) {\n // No imports exist in this direction — safe to create a boundary\n if (!deny[from]) deny[from] = [];\n deny[from].push(to);\n }\n }\n }\n\n return { deny };\n}\n","declare const __PACKAGE_VERSION__: string;\nexport const VERSION: string = __PACKAGE_VERSION__;\n\nexport { buildImportGraph, type GraphOptions } from './build-graph.js';\nexport { checkBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries } from './infer-boundaries.js';\nexport { parseImports } from './parse-imports.js';\nexport { type ResolvedImport, resolveImport } from './resolve-import.js';\n"],"mappings":"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","names":["buildNodeIndex","getTopLevelDirectory"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viberails/graph",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Import graph analysis and boundary checking for viberails",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"ts-morph": "^27.0.2",
|
|
29
|
-
"@viberails/types": "0.6.
|
|
29
|
+
"@viberails/types": "0.6.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^25.3.5"
|