@viberails/graph 0.3.2 → 0.3.3

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/index.cjs CHANGED
@@ -270,21 +270,22 @@ function buildSourceGlobs(projectRoot, packages, ignore) {
270
270
  }
271
271
 
272
272
  // src/check-boundaries.ts
273
- function checkBoundaries(graph, rules) {
274
- if (rules.length === 0) return [];
273
+ function checkBoundaries(graph, boundaries) {
274
+ const denyMap = boundaries.deny;
275
+ if (Object.keys(denyMap).length === 0) return [];
275
276
  const isMonorepo = graph.packages.length > 0;
276
277
  const nodeIndex = buildNodeIndex(graph.nodes);
278
+ const ignoredFiles = new Set(boundaries.ignore ?? []);
277
279
  const packagePathIndex = /* @__PURE__ */ new Map();
278
280
  for (const pkg of graph.packages) {
279
281
  packagePathIndex.set(pkg.path, pkg.name);
280
282
  }
281
- const denyRules = rules.filter((r) => !r.allow);
282
- const allowRules = rules.filter((r) => r.allow);
283
283
  const violations = [];
284
284
  for (const edge of graph.edges) {
285
285
  if (!edge.target.startsWith("/")) continue;
286
286
  const sourceNode = nodeIndex.get(edge.source);
287
287
  if (!sourceNode) continue;
288
+ if (ignoredFiles.has(sourceNode.relativePath)) continue;
288
289
  const targetNode = nodeIndex.get(edge.target);
289
290
  let targetZone;
290
291
  if (isMonorepo) {
@@ -296,16 +297,14 @@ function checkBoundaries(graph, rules) {
296
297
  }
297
298
  const sourceZone = isMonorepo ? sourceNode.packageName : getTopLevelDirectory(sourceNode.relativePath);
298
299
  if (!sourceZone || !targetZone || sourceZone === targetZone) continue;
299
- const isAllowed = allowRules.some((r) => r.from === sourceZone && r.to === targetZone);
300
- if (isAllowed) continue;
301
- const matchedRule = denyRules.find((r) => r.from === sourceZone && r.to === targetZone);
302
- if (matchedRule) {
300
+ const deniedTargets = denyMap[sourceZone];
301
+ if (deniedTargets?.includes(targetZone)) {
303
302
  violations.push({
304
303
  file: edge.source,
305
304
  line: edge.line,
306
305
  specifier: edge.specifier,
307
306
  resolvedTo: edge.target,
308
- rule: matchedRule
307
+ rule: { from: sourceZone, to: targetZone }
309
308
  });
310
309
  }
311
310
  }
@@ -356,25 +355,19 @@ function inferMonorepoBoundaries(graph) {
356
355
  const k = key(sourceNode.packageName, targetNode.packageName);
357
356
  importCounts.set(k, (importCounts.get(k) ?? 0) + 1);
358
357
  }
359
- const rules = [];
358
+ const deny = {};
360
359
  for (const from of packageNames) {
361
360
  for (const to of packageNames) {
362
361
  if (from === to) continue;
363
362
  const count = importCounts.get(key(from, to)) ?? 0;
364
363
  const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;
365
364
  if (count === 0 && !isDeclaredDep) {
366
- rules.push({
367
- from,
368
- to,
369
- allow: false,
370
- reason: `${from} should not depend on ${to}`
371
- });
372
- } else if (count > 0 && isDeclaredDep) {
373
- rules.push({ from, to, allow: true });
365
+ if (!deny[from]) deny[from] = [];
366
+ deny[from].push(to);
374
367
  }
375
368
  }
376
369
  }
377
- return rules;
370
+ return { deny };
378
371
  }
379
372
  function getTopLevelDirectory2(relativePath) {
380
373
  const normalized = relativePath.startsWith("src/") ? relativePath.slice(4) : relativePath;
@@ -389,7 +382,7 @@ function inferSinglePackageBoundaries(graph) {
389
382
  const dir = getTopLevelDirectory2(node.relativePath);
390
383
  if (dir) directories.add(dir);
391
384
  }
392
- if (directories.size < 2) return [];
385
+ if (directories.size < 2) return { deny: {} };
393
386
  const importCounts = /* @__PURE__ */ new Map();
394
387
  const key = (from, to) => `${from} -> ${to}`;
395
388
  for (const edge of graph.edges) {
@@ -402,27 +395,23 @@ function inferSinglePackageBoundaries(graph) {
402
395
  const k = key(sourceDir, targetDir);
403
396
  importCounts.set(k, (importCounts.get(k) ?? 0) + 1);
404
397
  }
405
- const rules = [];
398
+ const deny = {};
406
399
  const dirList = [...directories].sort();
407
400
  for (const from of dirList) {
408
401
  for (const to of dirList) {
409
402
  if (from === to) continue;
410
403
  const count = importCounts.get(key(from, to)) ?? 0;
411
404
  if (count === 0) {
412
- rules.push({
413
- from,
414
- to,
415
- allow: false,
416
- reason: `${from} should not depend on ${to}`
417
- });
405
+ if (!deny[from]) deny[from] = [];
406
+ deny[from].push(to);
418
407
  }
419
408
  }
420
409
  }
421
- return rules;
410
+ return { deny };
422
411
  }
423
412
 
424
413
  // src/index.ts
425
- var VERSION = "0.3.2";
414
+ var VERSION = "0.3.3";
426
415
  // Annotate the CommonJS export names for ESM import in node:
427
416
  0 && (module.exports = {
428
417
  VERSION,
@@ -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 for (const glob of sourceGlobs) {\n project.addSourceFilesAtPaths(glob);\n }\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\n for (const pkg of packages) {\n globs.push(`${pkg.path}/src/**/*.{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 // 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 BoundaryRule,\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 `allow: false` rule matches.\n * Skips external/builtin imports and same-package/directory edges.\n *\n * @param graph - The complete import graph for a project.\n * @param rules - Boundary rules to check against.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[] {\n if (rules.length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build package path → package name lookup for workspace imports\n // (workspace imports resolve to the package root path, not a file)\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 denyRules = rules.filter((r) => !r.allow);\n const allowRules = rules.filter((r) => r.allow);\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 // 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 if explicitly allowed\n const isAllowed = allowRules.some((r) => r.from === sourceZone && r.to === targetZone);\n if (isAllowed) continue;\n\n // Check deny rules\n const matchedRule = denyRules.find((r) => r.from === sourceZone && r.to === targetZone);\n if (matchedRule) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: matchedRule,\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 { BoundaryRule, 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 `allow: false` 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 An array of inferred boundary rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryRule[] {\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): BoundaryRule[] {\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 rules: BoundaryRule[] = [];\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 rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n } else if (count > 0 && isDeclaredDep) {\n // Imports exist and it's a declared dependency — allow\n rules.push({ from, to, allow: true });\n }\n // If imports exist but NOT declared → skip rule creation\n // (would produce immediate violation, defeats auto-detection purpose)\n }\n }\n\n return rules;\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): BoundaryRule[] {\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 [];\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 rules: BoundaryRule[] = [];\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 rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n }\n }\n }\n\n return rules;\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 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 for (const glob of sourceGlobs) {\n project.addSourceFilesAtPaths(glob);\n }\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\n for (const pkg of packages) {\n globs.push(`${pkg.path}/src/**/*.{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 // 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"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkspacePackage, ImportGraph, BoundaryRule, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
1
+ import { WorkspacePackage, ImportGraph, BoundaryConfig, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
2
2
  import { SourceFile, Project } from 'ts-morph';
3
3
 
4
4
  /** Options for building an import graph. */
@@ -28,14 +28,15 @@ declare function buildImportGraph(projectRoot: string, options?: GraphOptions):
28
28
  * Checks import edges against boundary rules and returns violations.
29
29
  *
30
30
  * For each edge in the graph, determines the source and target
31
- * package/directory and checks if any `allow: false` rule matches.
32
- * Skips external/builtin imports and same-package/directory edges.
31
+ * package/directory and checks if any deny rule matches.
32
+ * Skips external/builtin imports, same-package/directory edges,
33
+ * and files listed in the ignore list.
33
34
  *
34
35
  * @param graph - The complete import graph for a project.
35
- * @param rules - Boundary rules to check against.
36
+ * @param boundaries - Boundary config with deny map and optional ignore list.
36
37
  * @returns An array of boundary violations.
37
38
  */
38
- declare function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[];
39
+ declare function checkBoundaries(graph: ImportGraph, boundaries: BoundaryConfig): BoundaryViolation[];
39
40
 
40
41
  /**
41
42
  * Detects import cycles in a directed graph of file dependencies.
@@ -56,14 +57,14 @@ declare function detectCycles(edges: Array<{
56
57
  * import from each other. For single-package projects, creates
57
58
  * directory-level rules based on top-level directory imports.
58
59
  *
59
- * Only creates `allow: false` rules where the codebase already follows
60
+ * Only creates deny rules where the codebase already follows
60
61
  * the pattern (zero imports in that direction), so inferred rules never
61
62
  * produce immediate violations.
62
63
  *
63
64
  * @param graph - The complete import graph for a project.
64
- * @returns An array of inferred boundary rules.
65
+ * @returns A BoundaryConfig with inferred deny rules.
65
66
  */
66
- declare function inferBoundaries(graph: ImportGraph): BoundaryRule[];
67
+ declare function inferBoundaries(graph: ImportGraph): BoundaryConfig;
67
68
 
68
69
  /**
69
70
  * Parses all import statements from a ts-morph SourceFile and returns
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkspacePackage, ImportGraph, BoundaryRule, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
1
+ import { WorkspacePackage, ImportGraph, BoundaryConfig, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
2
2
  import { SourceFile, Project } from 'ts-morph';
3
3
 
4
4
  /** Options for building an import graph. */
@@ -28,14 +28,15 @@ declare function buildImportGraph(projectRoot: string, options?: GraphOptions):
28
28
  * Checks import edges against boundary rules and returns violations.
29
29
  *
30
30
  * For each edge in the graph, determines the source and target
31
- * package/directory and checks if any `allow: false` rule matches.
32
- * Skips external/builtin imports and same-package/directory edges.
31
+ * package/directory and checks if any deny rule matches.
32
+ * Skips external/builtin imports, same-package/directory edges,
33
+ * and files listed in the ignore list.
33
34
  *
34
35
  * @param graph - The complete import graph for a project.
35
- * @param rules - Boundary rules to check against.
36
+ * @param boundaries - Boundary config with deny map and optional ignore list.
36
37
  * @returns An array of boundary violations.
37
38
  */
38
- declare function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[];
39
+ declare function checkBoundaries(graph: ImportGraph, boundaries: BoundaryConfig): BoundaryViolation[];
39
40
 
40
41
  /**
41
42
  * Detects import cycles in a directed graph of file dependencies.
@@ -56,14 +57,14 @@ declare function detectCycles(edges: Array<{
56
57
  * import from each other. For single-package projects, creates
57
58
  * directory-level rules based on top-level directory imports.
58
59
  *
59
- * Only creates `allow: false` rules where the codebase already follows
60
+ * Only creates deny rules where the codebase already follows
60
61
  * the pattern (zero imports in that direction), so inferred rules never
61
62
  * produce immediate violations.
62
63
  *
63
64
  * @param graph - The complete import graph for a project.
64
- * @returns An array of inferred boundary rules.
65
+ * @returns A BoundaryConfig with inferred deny rules.
65
66
  */
66
- declare function inferBoundaries(graph: ImportGraph): BoundaryRule[];
67
+ declare function inferBoundaries(graph: ImportGraph): BoundaryConfig;
67
68
 
68
69
  /**
69
70
  * Parses all import statements from a ts-morph SourceFile and returns
package/dist/index.js CHANGED
@@ -238,21 +238,22 @@ function buildSourceGlobs(projectRoot, packages, ignore) {
238
238
  }
239
239
 
240
240
  // src/check-boundaries.ts
241
- function checkBoundaries(graph, rules) {
242
- if (rules.length === 0) return [];
241
+ function checkBoundaries(graph, boundaries) {
242
+ const denyMap = boundaries.deny;
243
+ if (Object.keys(denyMap).length === 0) return [];
243
244
  const isMonorepo = graph.packages.length > 0;
244
245
  const nodeIndex = buildNodeIndex(graph.nodes);
246
+ const ignoredFiles = new Set(boundaries.ignore ?? []);
245
247
  const packagePathIndex = /* @__PURE__ */ new Map();
246
248
  for (const pkg of graph.packages) {
247
249
  packagePathIndex.set(pkg.path, pkg.name);
248
250
  }
249
- const denyRules = rules.filter((r) => !r.allow);
250
- const allowRules = rules.filter((r) => r.allow);
251
251
  const violations = [];
252
252
  for (const edge of graph.edges) {
253
253
  if (!edge.target.startsWith("/")) continue;
254
254
  const sourceNode = nodeIndex.get(edge.source);
255
255
  if (!sourceNode) continue;
256
+ if (ignoredFiles.has(sourceNode.relativePath)) continue;
256
257
  const targetNode = nodeIndex.get(edge.target);
257
258
  let targetZone;
258
259
  if (isMonorepo) {
@@ -264,16 +265,14 @@ function checkBoundaries(graph, rules) {
264
265
  }
265
266
  const sourceZone = isMonorepo ? sourceNode.packageName : getTopLevelDirectory(sourceNode.relativePath);
266
267
  if (!sourceZone || !targetZone || sourceZone === targetZone) continue;
267
- const isAllowed = allowRules.some((r) => r.from === sourceZone && r.to === targetZone);
268
- if (isAllowed) continue;
269
- const matchedRule = denyRules.find((r) => r.from === sourceZone && r.to === targetZone);
270
- if (matchedRule) {
268
+ const deniedTargets = denyMap[sourceZone];
269
+ if (deniedTargets?.includes(targetZone)) {
271
270
  violations.push({
272
271
  file: edge.source,
273
272
  line: edge.line,
274
273
  specifier: edge.specifier,
275
274
  resolvedTo: edge.target,
276
- rule: matchedRule
275
+ rule: { from: sourceZone, to: targetZone }
277
276
  });
278
277
  }
279
278
  }
@@ -324,25 +323,19 @@ function inferMonorepoBoundaries(graph) {
324
323
  const k = key(sourceNode.packageName, targetNode.packageName);
325
324
  importCounts.set(k, (importCounts.get(k) ?? 0) + 1);
326
325
  }
327
- const rules = [];
326
+ const deny = {};
328
327
  for (const from of packageNames) {
329
328
  for (const to of packageNames) {
330
329
  if (from === to) continue;
331
330
  const count = importCounts.get(key(from, to)) ?? 0;
332
331
  const isDeclaredDep = declaredDeps.get(from)?.has(to) ?? false;
333
332
  if (count === 0 && !isDeclaredDep) {
334
- rules.push({
335
- from,
336
- to,
337
- allow: false,
338
- reason: `${from} should not depend on ${to}`
339
- });
340
- } else if (count > 0 && isDeclaredDep) {
341
- rules.push({ from, to, allow: true });
333
+ if (!deny[from]) deny[from] = [];
334
+ deny[from].push(to);
342
335
  }
343
336
  }
344
337
  }
345
- return rules;
338
+ return { deny };
346
339
  }
347
340
  function getTopLevelDirectory2(relativePath) {
348
341
  const normalized = relativePath.startsWith("src/") ? relativePath.slice(4) : relativePath;
@@ -357,7 +350,7 @@ function inferSinglePackageBoundaries(graph) {
357
350
  const dir = getTopLevelDirectory2(node.relativePath);
358
351
  if (dir) directories.add(dir);
359
352
  }
360
- if (directories.size < 2) return [];
353
+ if (directories.size < 2) return { deny: {} };
361
354
  const importCounts = /* @__PURE__ */ new Map();
362
355
  const key = (from, to) => `${from} -> ${to}`;
363
356
  for (const edge of graph.edges) {
@@ -370,27 +363,23 @@ function inferSinglePackageBoundaries(graph) {
370
363
  const k = key(sourceDir, targetDir);
371
364
  importCounts.set(k, (importCounts.get(k) ?? 0) + 1);
372
365
  }
373
- const rules = [];
366
+ const deny = {};
374
367
  const dirList = [...directories].sort();
375
368
  for (const from of dirList) {
376
369
  for (const to of dirList) {
377
370
  if (from === to) continue;
378
371
  const count = importCounts.get(key(from, to)) ?? 0;
379
372
  if (count === 0) {
380
- rules.push({
381
- from,
382
- to,
383
- allow: false,
384
- reason: `${from} should not depend on ${to}`
385
- });
373
+ if (!deny[from]) deny[from] = [];
374
+ deny[from].push(to);
386
375
  }
387
376
  }
388
377
  }
389
- return rules;
378
+ return { deny };
390
379
  }
391
380
 
392
381
  // src/index.ts
393
- var VERSION = "0.3.2";
382
+ var VERSION = "0.3.3";
394
383
  export {
395
384
  VERSION,
396
385
  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 for (const glob of sourceGlobs) {\n project.addSourceFilesAtPaths(glob);\n }\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\n for (const pkg of packages) {\n globs.push(`${pkg.path}/src/**/*.{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 // 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 BoundaryRule,\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 `allow: false` rule matches.\n * Skips external/builtin imports and same-package/directory edges.\n *\n * @param graph - The complete import graph for a project.\n * @param rules - Boundary rules to check against.\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[] {\n if (rules.length === 0) return [];\n\n const isMonorepo = graph.packages.length > 0;\n const nodeIndex = buildNodeIndex(graph.nodes);\n\n // Build package path → package name lookup for workspace imports\n // (workspace imports resolve to the package root path, not a file)\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 denyRules = rules.filter((r) => !r.allow);\n const allowRules = rules.filter((r) => r.allow);\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 // 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 if explicitly allowed\n const isAllowed = allowRules.some((r) => r.from === sourceZone && r.to === targetZone);\n if (isAllowed) continue;\n\n // Check deny rules\n const matchedRule = denyRules.find((r) => r.from === sourceZone && r.to === targetZone);\n if (matchedRule) {\n violations.push({\n file: edge.source,\n line: edge.line,\n specifier: edge.specifier,\n resolvedTo: edge.target,\n rule: matchedRule,\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 { BoundaryRule, 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 `allow: false` 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 An array of inferred boundary rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryRule[] {\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): BoundaryRule[] {\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 rules: BoundaryRule[] = [];\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 rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n } else if (count > 0 && isDeclaredDep) {\n // Imports exist and it's a declared dependency — allow\n rules.push({ from, to, allow: true });\n }\n // If imports exist but NOT declared → skip rule creation\n // (would produce immediate violation, defeats auto-detection purpose)\n }\n }\n\n return rules;\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): BoundaryRule[] {\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 [];\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 rules: BoundaryRule[] = [];\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 rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n }\n }\n }\n\n return rules;\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/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 for (const glob of sourceGlobs) {\n project.addSourceFilesAtPaths(glob);\n }\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\n for (const pkg of packages) {\n globs.push(`${pkg.path}/src/**/*.{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 // 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viberails/graph",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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.3.2"
29
+ "@viberails/types": "0.3.3"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^25.3.5"