@viberails/graph 0.2.2 → 0.3.0

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
@@ -25,8 +25,10 @@ __export(index_exports, {
25
25
  checkBoundaries: () => checkBoundaries,
26
26
  detectCycles: () => detectCycles,
27
27
  inferBoundaries: () => inferBoundaries,
28
+ normalizeBoundaries: () => normalizeBoundaries,
28
29
  parseImports: () => parseImports,
29
- resolveImport: () => resolveImport
30
+ resolveImport: () => resolveImport,
31
+ toBoundaryMap: () => toBoundaryMap
30
32
  });
31
33
  module.exports = __toCommonJS(index_exports);
32
34
 
@@ -225,7 +227,7 @@ async function buildImportGraph(projectRoot, options) {
225
227
  const allEdges = [];
226
228
  for (const sourceFile of project.getSourceFiles()) {
227
229
  const filePath = sourceFile.getFilePath();
228
- const ownerPkg = packages.find((pkg) => filePath.startsWith(pkg.path + "/"));
230
+ const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));
229
231
  nodes.push({
230
232
  filePath,
231
233
  relativePath: (0, import_node_path2.relative)(ownerPkg?.path ?? projectRoot, filePath),
@@ -267,16 +269,32 @@ function buildSourceGlobs(projectRoot, packages, _ignore) {
267
269
  }
268
270
 
269
271
  // src/check-boundaries.ts
272
+ function normalizeBoundaries(boundaries) {
273
+ if (Array.isArray(boundaries)) return boundaries;
274
+ const rules = [];
275
+ for (const [from, denied] of Object.entries(boundaries)) {
276
+ for (const to of denied) {
277
+ rules.push({
278
+ from,
279
+ to,
280
+ allow: false,
281
+ reason: `${from} should not depend on ${to}`
282
+ });
283
+ }
284
+ }
285
+ return rules;
286
+ }
270
287
  function checkBoundaries(graph, rules) {
271
- if (rules.length === 0) return [];
288
+ const normalizedRules = normalizeBoundaries(rules);
289
+ if (normalizedRules.length === 0) return [];
272
290
  const isMonorepo = graph.packages.length > 0;
273
291
  const nodeIndex = buildNodeIndex(graph.nodes);
274
292
  const packagePathIndex = /* @__PURE__ */ new Map();
275
293
  for (const pkg of graph.packages) {
276
294
  packagePathIndex.set(pkg.path, pkg.name);
277
295
  }
278
- const denyRules = rules.filter((r) => !r.allow);
279
- const allowRules = rules.filter((r) => r.allow);
296
+ const denyRules = normalizedRules.filter((r) => !r.allow);
297
+ const allowRules = normalizedRules.filter((r) => r.allow);
280
298
  const violations = [];
281
299
  for (const edge of graph.edges) {
282
300
  if (!edge.target.startsWith("/")) continue;
@@ -323,11 +341,18 @@ function getTopLevelDirectory(relativePath) {
323
341
  }
324
342
 
325
343
  // src/infer-boundaries.ts
326
- function inferBoundaries(graph) {
327
- if (graph.packages.length > 0) {
328
- return inferMonorepoBoundaries(graph);
344
+ function toBoundaryMap(rules) {
345
+ const map = {};
346
+ for (const rule of rules) {
347
+ if (rule.allow) continue;
348
+ if (!map[rule.from]) map[rule.from] = [];
349
+ map[rule.from].push(rule.to);
329
350
  }
330
- return inferSinglePackageBoundaries(graph);
351
+ return map;
352
+ }
353
+ function inferBoundaries(graph) {
354
+ const rules = graph.packages.length > 0 ? inferMonorepoBoundaries(graph) : inferSinglePackageBoundaries(graph);
355
+ return toBoundaryMap(rules);
331
356
  }
332
357
  function buildNodeIndex2(nodes) {
333
358
  const index = /* @__PURE__ */ new Map();
@@ -419,7 +444,7 @@ function inferSinglePackageBoundaries(graph) {
419
444
  }
420
445
 
421
446
  // src/index.ts
422
- var VERSION = "0.2.2";
447
+ var VERSION = "0.3.0";
423
448
  // Annotate the CommonJS export names for ESM import in node:
424
449
  0 && (module.exports = {
425
450
  VERSION,
@@ -427,7 +452,9 @@ var VERSION = "0.2.2";
427
452
  checkBoundaries,
428
453
  detectCycles,
429
454
  inferBoundaries,
455
+ normalizeBoundaries,
430
456
  parseImports,
431
- resolveImport
457
+ resolveImport,
458
+ toBoundaryMap
432
459
  });
433
460
  //# sourceMappingURL=index.cjs.map
@@ -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 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, normalizeBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries, toBoundaryMap } 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 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 BoundaryMap,\n BoundaryRule,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Normalize boundaries from either compact map format or legacy array format\n * into a flat array of BoundaryRule objects.\n *\n * Map format: `{ \"@app/types\": [\"@app/scanner\", \"@app/config\"] }`\n * Array format: `[{ from: \"@app/types\", to: \"@app/scanner\", allow: false }]`\n *\n * @param boundaries - Boundaries in either format\n * @returns A flat array of BoundaryRule objects\n */\nexport function normalizeBoundaries(boundaries: BoundaryMap | BoundaryRule[]): BoundaryRule[] {\n if (Array.isArray(boundaries)) return boundaries;\n\n const rules: BoundaryRule[] = [];\n for (const [from, denied] of Object.entries(boundaries)) {\n for (const to of denied) {\n rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n }\n }\n return rules;\n}\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 (either format).\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n rules: BoundaryMap | BoundaryRule[],\n): BoundaryViolation[] {\n const normalizedRules = normalizeBoundaries(rules);\n if (normalizedRules.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 = normalizedRules.filter((r) => !r.allow);\n const allowRules = normalizedRules.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 { BoundaryMap, BoundaryRule, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Convert an array of deny BoundaryRules into a compact BoundaryMap.\n * Only includes deny rules (allow: false). Allow rules are discarded\n * since the map format only represents denied imports.\n */\nexport function toBoundaryMap(rules: BoundaryRule[]): BoundaryMap {\n const map: BoundaryMap = {};\n for (const rule of rules) {\n if (rule.allow) continue;\n if (!map[rule.from]) map[rule.from] = [];\n map[rule.from].push(rule.to);\n }\n return map;\n}\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 * Returns a compact BoundaryMap: `{ package: [denied...] }`.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryMap of inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryMap {\n const rules =\n graph.packages.length > 0\n ? inferMonorepoBoundaries(graph)\n : inferSinglePackageBoundaries(graph);\n return toBoundaryMap(rules);\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"]}
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, BoundaryMap, BoundaryRule, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
2
2
  import { SourceFile, Project } from 'ts-morph';
3
3
 
4
4
  /** Options for building an import graph. */
@@ -24,6 +24,17 @@ interface GraphOptions {
24
24
  */
25
25
  declare function buildImportGraph(projectRoot: string, options?: GraphOptions): Promise<ImportGraph>;
26
26
 
27
+ /**
28
+ * Normalize boundaries from either compact map format or legacy array format
29
+ * into a flat array of BoundaryRule objects.
30
+ *
31
+ * Map format: `{ "@app/types": ["@app/scanner", "@app/config"] }`
32
+ * Array format: `[{ from: "@app/types", to: "@app/scanner", allow: false }]`
33
+ *
34
+ * @param boundaries - Boundaries in either format
35
+ * @returns A flat array of BoundaryRule objects
36
+ */
37
+ declare function normalizeBoundaries(boundaries: BoundaryMap | BoundaryRule[]): BoundaryRule[];
27
38
  /**
28
39
  * Checks import edges against boundary rules and returns violations.
29
40
  *
@@ -32,10 +43,10 @@ declare function buildImportGraph(projectRoot: string, options?: GraphOptions):
32
43
  * Skips external/builtin imports and same-package/directory edges.
33
44
  *
34
45
  * @param graph - The complete import graph for a project.
35
- * @param rules - Boundary rules to check against.
46
+ * @param rules - Boundary rules to check against (either format).
36
47
  * @returns An array of boundary violations.
37
48
  */
38
- declare function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[];
49
+ declare function checkBoundaries(graph: ImportGraph, rules: BoundaryMap | BoundaryRule[]): BoundaryViolation[];
39
50
 
40
51
  /**
41
52
  * Detects import cycles in a directed graph of file dependencies.
@@ -49,6 +60,12 @@ declare function detectCycles(edges: Array<{
49
60
  target: string;
50
61
  }>): string[][];
51
62
 
63
+ /**
64
+ * Convert an array of deny BoundaryRules into a compact BoundaryMap.
65
+ * Only includes deny rules (allow: false). Allow rules are discarded
66
+ * since the map format only represents denied imports.
67
+ */
68
+ declare function toBoundaryMap(rules: BoundaryRule[]): BoundaryMap;
52
69
  /**
53
70
  * Infers boundary rules from existing import patterns in the graph.
54
71
  *
@@ -56,14 +73,16 @@ declare function detectCycles(edges: Array<{
56
73
  * import from each other. For single-package projects, creates
57
74
  * directory-level rules based on top-level directory imports.
58
75
  *
59
- * Only creates `allow: false` rules where the codebase already follows
76
+ * Only creates deny rules where the codebase already follows
60
77
  * the pattern (zero imports in that direction), so inferred rules never
61
78
  * produce immediate violations.
62
79
  *
80
+ * Returns a compact BoundaryMap: `{ package: [denied...] }`.
81
+ *
63
82
  * @param graph - The complete import graph for a project.
64
- * @returns An array of inferred boundary rules.
83
+ * @returns A BoundaryMap of inferred deny rules.
65
84
  */
66
- declare function inferBoundaries(graph: ImportGraph): BoundaryRule[];
85
+ declare function inferBoundaries(graph: ImportGraph): BoundaryMap;
67
86
 
68
87
  /**
69
88
  * Parses all import statements from a ts-morph SourceFile and returns
@@ -106,4 +125,4 @@ declare function resolveImport(specifier: string, fromFile: string, project: Pro
106
125
 
107
126
  declare const VERSION: string;
108
127
 
109
- export { type GraphOptions, type ResolvedImport, VERSION, buildImportGraph, checkBoundaries, detectCycles, inferBoundaries, parseImports, resolveImport };
128
+ export { type GraphOptions, type ResolvedImport, VERSION, buildImportGraph, checkBoundaries, detectCycles, inferBoundaries, normalizeBoundaries, parseImports, resolveImport, toBoundaryMap };
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, BoundaryMap, BoundaryRule, BoundaryViolation, ImportEdge, ImportKind } from '@viberails/types';
2
2
  import { SourceFile, Project } from 'ts-morph';
3
3
 
4
4
  /** Options for building an import graph. */
@@ -24,6 +24,17 @@ interface GraphOptions {
24
24
  */
25
25
  declare function buildImportGraph(projectRoot: string, options?: GraphOptions): Promise<ImportGraph>;
26
26
 
27
+ /**
28
+ * Normalize boundaries from either compact map format or legacy array format
29
+ * into a flat array of BoundaryRule objects.
30
+ *
31
+ * Map format: `{ "@app/types": ["@app/scanner", "@app/config"] }`
32
+ * Array format: `[{ from: "@app/types", to: "@app/scanner", allow: false }]`
33
+ *
34
+ * @param boundaries - Boundaries in either format
35
+ * @returns A flat array of BoundaryRule objects
36
+ */
37
+ declare function normalizeBoundaries(boundaries: BoundaryMap | BoundaryRule[]): BoundaryRule[];
27
38
  /**
28
39
  * Checks import edges against boundary rules and returns violations.
29
40
  *
@@ -32,10 +43,10 @@ declare function buildImportGraph(projectRoot: string, options?: GraphOptions):
32
43
  * Skips external/builtin imports and same-package/directory edges.
33
44
  *
34
45
  * @param graph - The complete import graph for a project.
35
- * @param rules - Boundary rules to check against.
46
+ * @param rules - Boundary rules to check against (either format).
36
47
  * @returns An array of boundary violations.
37
48
  */
38
- declare function checkBoundaries(graph: ImportGraph, rules: BoundaryRule[]): BoundaryViolation[];
49
+ declare function checkBoundaries(graph: ImportGraph, rules: BoundaryMap | BoundaryRule[]): BoundaryViolation[];
39
50
 
40
51
  /**
41
52
  * Detects import cycles in a directed graph of file dependencies.
@@ -49,6 +60,12 @@ declare function detectCycles(edges: Array<{
49
60
  target: string;
50
61
  }>): string[][];
51
62
 
63
+ /**
64
+ * Convert an array of deny BoundaryRules into a compact BoundaryMap.
65
+ * Only includes deny rules (allow: false). Allow rules are discarded
66
+ * since the map format only represents denied imports.
67
+ */
68
+ declare function toBoundaryMap(rules: BoundaryRule[]): BoundaryMap;
52
69
  /**
53
70
  * Infers boundary rules from existing import patterns in the graph.
54
71
  *
@@ -56,14 +73,16 @@ declare function detectCycles(edges: Array<{
56
73
  * import from each other. For single-package projects, creates
57
74
  * directory-level rules based on top-level directory imports.
58
75
  *
59
- * Only creates `allow: false` rules where the codebase already follows
76
+ * Only creates deny rules where the codebase already follows
60
77
  * the pattern (zero imports in that direction), so inferred rules never
61
78
  * produce immediate violations.
62
79
  *
80
+ * Returns a compact BoundaryMap: `{ package: [denied...] }`.
81
+ *
63
82
  * @param graph - The complete import graph for a project.
64
- * @returns An array of inferred boundary rules.
83
+ * @returns A BoundaryMap of inferred deny rules.
65
84
  */
66
- declare function inferBoundaries(graph: ImportGraph): BoundaryRule[];
85
+ declare function inferBoundaries(graph: ImportGraph): BoundaryMap;
67
86
 
68
87
  /**
69
88
  * Parses all import statements from a ts-morph SourceFile and returns
@@ -106,4 +125,4 @@ declare function resolveImport(specifier: string, fromFile: string, project: Pro
106
125
 
107
126
  declare const VERSION: string;
108
127
 
109
- export { type GraphOptions, type ResolvedImport, VERSION, buildImportGraph, checkBoundaries, detectCycles, inferBoundaries, parseImports, resolveImport };
128
+ export { type GraphOptions, type ResolvedImport, VERSION, buildImportGraph, checkBoundaries, detectCycles, inferBoundaries, normalizeBoundaries, parseImports, resolveImport, toBoundaryMap };
package/dist/index.js CHANGED
@@ -193,7 +193,7 @@ async function buildImportGraph(projectRoot, options) {
193
193
  const allEdges = [];
194
194
  for (const sourceFile of project.getSourceFiles()) {
195
195
  const filePath = sourceFile.getFilePath();
196
- const ownerPkg = packages.find((pkg) => filePath.startsWith(pkg.path + "/"));
196
+ const ownerPkg = packages.find((pkg) => filePath.startsWith(`${pkg.path}/`));
197
197
  nodes.push({
198
198
  filePath,
199
199
  relativePath: relative(ownerPkg?.path ?? projectRoot, filePath),
@@ -235,16 +235,32 @@ function buildSourceGlobs(projectRoot, packages, _ignore) {
235
235
  }
236
236
 
237
237
  // src/check-boundaries.ts
238
+ function normalizeBoundaries(boundaries) {
239
+ if (Array.isArray(boundaries)) return boundaries;
240
+ const rules = [];
241
+ for (const [from, denied] of Object.entries(boundaries)) {
242
+ for (const to of denied) {
243
+ rules.push({
244
+ from,
245
+ to,
246
+ allow: false,
247
+ reason: `${from} should not depend on ${to}`
248
+ });
249
+ }
250
+ }
251
+ return rules;
252
+ }
238
253
  function checkBoundaries(graph, rules) {
239
- if (rules.length === 0) return [];
254
+ const normalizedRules = normalizeBoundaries(rules);
255
+ if (normalizedRules.length === 0) return [];
240
256
  const isMonorepo = graph.packages.length > 0;
241
257
  const nodeIndex = buildNodeIndex(graph.nodes);
242
258
  const packagePathIndex = /* @__PURE__ */ new Map();
243
259
  for (const pkg of graph.packages) {
244
260
  packagePathIndex.set(pkg.path, pkg.name);
245
261
  }
246
- const denyRules = rules.filter((r) => !r.allow);
247
- const allowRules = rules.filter((r) => r.allow);
262
+ const denyRules = normalizedRules.filter((r) => !r.allow);
263
+ const allowRules = normalizedRules.filter((r) => r.allow);
248
264
  const violations = [];
249
265
  for (const edge of graph.edges) {
250
266
  if (!edge.target.startsWith("/")) continue;
@@ -291,11 +307,18 @@ function getTopLevelDirectory(relativePath) {
291
307
  }
292
308
 
293
309
  // src/infer-boundaries.ts
294
- function inferBoundaries(graph) {
295
- if (graph.packages.length > 0) {
296
- return inferMonorepoBoundaries(graph);
310
+ function toBoundaryMap(rules) {
311
+ const map = {};
312
+ for (const rule of rules) {
313
+ if (rule.allow) continue;
314
+ if (!map[rule.from]) map[rule.from] = [];
315
+ map[rule.from].push(rule.to);
297
316
  }
298
- return inferSinglePackageBoundaries(graph);
317
+ return map;
318
+ }
319
+ function inferBoundaries(graph) {
320
+ const rules = graph.packages.length > 0 ? inferMonorepoBoundaries(graph) : inferSinglePackageBoundaries(graph);
321
+ return toBoundaryMap(rules);
299
322
  }
300
323
  function buildNodeIndex2(nodes) {
301
324
  const index = /* @__PURE__ */ new Map();
@@ -387,14 +410,16 @@ function inferSinglePackageBoundaries(graph) {
387
410
  }
388
411
 
389
412
  // src/index.ts
390
- var VERSION = "0.2.2";
413
+ var VERSION = "0.3.0";
391
414
  export {
392
415
  VERSION,
393
416
  buildImportGraph,
394
417
  checkBoundaries,
395
418
  detectCycles,
396
419
  inferBoundaries,
420
+ normalizeBoundaries,
397
421
  parseImports,
398
- resolveImport
422
+ resolveImport,
423
+ toBoundaryMap
399
424
  };
400
425
  //# sourceMappingURL=index.js.map
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 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 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 BoundaryMap,\n BoundaryRule,\n BoundaryViolation,\n ImportGraph,\n ImportGraphNode,\n} from '@viberails/types';\n\n/**\n * Normalize boundaries from either compact map format or legacy array format\n * into a flat array of BoundaryRule objects.\n *\n * Map format: `{ \"@app/types\": [\"@app/scanner\", \"@app/config\"] }`\n * Array format: `[{ from: \"@app/types\", to: \"@app/scanner\", allow: false }]`\n *\n * @param boundaries - Boundaries in either format\n * @returns A flat array of BoundaryRule objects\n */\nexport function normalizeBoundaries(boundaries: BoundaryMap | BoundaryRule[]): BoundaryRule[] {\n if (Array.isArray(boundaries)) return boundaries;\n\n const rules: BoundaryRule[] = [];\n for (const [from, denied] of Object.entries(boundaries)) {\n for (const to of denied) {\n rules.push({\n from,\n to,\n allow: false,\n reason: `${from} should not depend on ${to}`,\n });\n }\n }\n return rules;\n}\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 (either format).\n * @returns An array of boundary violations.\n */\nexport function checkBoundaries(\n graph: ImportGraph,\n rules: BoundaryMap | BoundaryRule[],\n): BoundaryViolation[] {\n const normalizedRules = normalizeBoundaries(rules);\n if (normalizedRules.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 = normalizedRules.filter((r) => !r.allow);\n const allowRules = normalizedRules.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 { BoundaryMap, BoundaryRule, ImportGraph, ImportGraphNode } from '@viberails/types';\n\n/**\n * Convert an array of deny BoundaryRules into a compact BoundaryMap.\n * Only includes deny rules (allow: false). Allow rules are discarded\n * since the map format only represents denied imports.\n */\nexport function toBoundaryMap(rules: BoundaryRule[]): BoundaryMap {\n const map: BoundaryMap = {};\n for (const rule of rules) {\n if (rule.allow) continue;\n if (!map[rule.from]) map[rule.from] = [];\n map[rule.from].push(rule.to);\n }\n return map;\n}\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 * Returns a compact BoundaryMap: `{ package: [denied...] }`.\n *\n * @param graph - The complete import graph for a project.\n * @returns A BoundaryMap of inferred deny rules.\n */\nexport function inferBoundaries(graph: ImportGraph): BoundaryMap {\n const rules =\n graph.packages.length > 0\n ? inferMonorepoBoundaries(graph)\n : inferSinglePackageBoundaries(graph);\n return toBoundaryMap(rules);\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, normalizeBoundaries } from './check-boundaries.js';\nexport { detectCycles } from './detect-cycles.js';\nexport { inferBoundaries, toBoundaryMap } 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.2.2",
3
+ "version": "0.3.0",
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.2.2"
29
+ "@viberails/types": "0.3.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^25.3.5"