Back to Bit

Circular Dependencies Analysis for Future Development

scripts/circular-deps-check/ANALYSIS.md

14.8.9-server.119.0 KB
Original Source

Circular Dependencies Analysis for Future Development

This document provides detailed analysis of circular dependencies in the Bit repository for future reference when tackling circular dependency issues.

Current State (Baseline)

  • Total circular dependency edges: 2,056
  • Unique components involved: 324
  • Analysis date: July 25, 2025

Important Context About Bit Aspects & Circular Dependencies

Aspect Architecture & Runtime Dependencies

  • Aspects can't have runtime circular dependencies - Bit's aspect system prevents this at the provider level
  • Each aspect's main.runtime.ts has a provider method that receives dependency instances via dependency injection
  • Runtime dependencies are enforced to be acyclic by the aspect system

The Real Problem: Pseudo-Runtime Dependencies (Actually Type Usage)

CRITICAL FINDING: Analysis of actual cycles reveals a misleading pattern:

  • 14,147 cycles marked as "prod" dependencies in bit graph
  • Only 315 cycles marked as "dev" dependencies
  • BUT: Many "prod" dependencies are actually used primarily for types!

Key Discovery Pattern:

typescript
// Common pattern causing "prod" circular dependencies:
import { WorkspaceAspect } from '@teambit/workspace';  // For DI - runtime
import type { Workspace } from '@teambit/workspace';   // For typing - the actual usage

// Actual usage is often minimal:
private getWorkspaceIfExist(): Workspace | undefined {  // Only for typing!
  return this.componentAspect.getHost('...') as Workspace;
}

Reality: Most "prod" circular dependencies are:

  1. Aspect imports for dependency injection (minimal runtime)
  2. Type imports for method signatures (the primary usage)
  3. Easily breakable by removing type dependencies or using minimal interfaces

Business Impact of Circular Dependencies

1. TypeScript Project References Optimization

  • Goal: Enable TypeScript project references for build optimization
  • Blocker: TypeScript requires acyclic dependency graph
  • Impact: Can't use incremental TypeScript compilation with project references

2. Massive Auto-Tagging Problem ⚠️ CRITICAL

  • Current behavior: Changing one aspect triggers bit tag on 150+ dependent components
  • Root cause: Circular dependencies make everything appear as dependents
  • Desired behavior: Only actual dependents should be auto-tagged
  • Workaround: --skip-auto-tag (but this prevents dependents from getting latest versions)
  • Business value: Precise dependency tracking and cleaner versioning

Types of Circular Dependencies Identified

TypeScript Type Import Circulars (Most Common)

  • Pattern: import type { SomeType } from '@teambit/other-component'
  • Technical impact: Prevents TypeScript project references
  • Business impact: Massive auto-tagging when components change
  • Solution approach: Extract shared type definitions or use module augmentation

Runtime Circular Dependencies (Less Common, More Critical)

  • Pattern: Actual runtime imports that create circular execution
  • Technical impact: Can cause runtime initialization issues
  • Business impact: Same auto-tagging problem plus potential runtime bugs

Key Circular Dependency Patterns Identified

1. CRITICAL: Workspace ↔ Component Graph (Likely Type Imports)

Cycle Path:

workspace → component/graph → workspace

Root Cause Analysis Needed:

  • Check if component/graph imports types from workspace
  • workspace likely imports graph types for its getGraph() methods
  • May be solvable by extracting shared graph types to separate component

Specific Code Locations to Investigate:

  • scopes/workspace/workspace/workspace.ts:575-590 - Graph methods (likely imports graph types)
  • scopes/component/graph/graph-builder.ts:18-32 - May import workspace types
  • Look for import type statements between these components

REALISTIC Solution Analysis:

Problem with Type Extraction: Workspace has ~100 public methods. Creating a separate interface would be:

  • Extremely time-consuming to create
  • High maintenance burden (every Workspace change needs interface update)
  • Prone to interface/implementation drift
  • Not scalable for other large components

Practical Approaches:

  1. Remove Unnecessary Type Imports (EASIEST):
typescript
// Instead of importing Workspace types, check if you really need them
// Many type imports might be removable with better local typing
  1. Module Augmentation (MEDIUM EFFORT):
typescript
// In component/graph, instead of importing workspace types:
declare module '@teambit/workspace' {
  interface Workspace {
    getGraph(ids?: ComponentID[]): Promise<ComponentGraph>;
    // Only declare the specific methods you actually use
  }
}
  1. Use Generic/Utility Types (LOW EFFORT):
typescript
// Instead of importing Workspace, use generic patterns
type ComponentHost = {
  getGraph(ids?: ComponentID[]): Promise<ComponentGraph>;
  // Only the methods you actually need
};
  1. Investigate Actual Usage (CRITICAL FIRST STEP):
bash
# Find what types are actually being imported from workspace
grep -r "import type.*workspace" scopes/
grep -r "import.*Workspace" scopes/ | grep -v "from.*workspace"

Estimated Impact: 20-30 components removed from cycle


2. CRITICAL INSIGHT: Understanding DI Direction vs Circular Dependencies

IMPORTANT: The DI (Dependency Injection) direction is CORRECT and should NOT be changed:

✅ CORRECT (Keep): workspace-config-files → workspace (via DI provider)
❌ PROBLEM (Fix): workspace → workspace-config-files (likely type import)

Root Cause:

  • workspace-config-files aspect legitimately depends on workspace via DI (this is architectural and correct)
  • The circular dependency is created by workspace importing types/functionality FROM workspace-config-files
  • Focus should be on removing imports FROM workspace TO workspace-config-files, not the other way around

Actual Usage (from scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.ts):

  • workspace.path (multiple lines for file operations)
  • workspace.defaultDirectory (line 337 - config path resolution)
  • workspace.list() (line 358 - get all components)
  • workspace.componentDir() (line 371 - component directory resolution)

Proposed Solution (HIGH IMPACT, MEDIUM EFFORT): Create minimal workspace interface:

typescript
interface WorkspaceMetadata {
  readonly path: string;
  readonly defaultDirectory?: string;
  list(): Promise<Component[]>;
  componentDir(id: ComponentID, options?: { relative?: boolean }): string;
}

// Update constructor to use interface instead of full workspace
class WorkspaceConfigFilesMain {
  constructor(
    private workspaceMetadata: WorkspaceMetadata, // Instead of full workspace
    private envs: EnvsMain,
    private logger: Logger,
    private config: WorkspaceConfigFilesAspectConfig
  ) {}
}

Files to modify:

  • scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.ts:119-124 (constructor)
  • scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.ts:470-486 (provider method)

Estimated Impact: 10-15 components removed from cycle


3. CRITICAL: Dependencies ↔ Dependency-Resolver Direct Cycle

Cycle Path:

dependency-resolver → dependencies → dependency-resolver

Root Cause: Direct bi-directional imports between these components:

  • scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts:13-14 imports EnvsAspect, EnvDefinition, EnvsMain
  • scopes/dependencies/dependencies/dependencies.main.runtime.ts imports and uses DependencyResolverAspect extensively

Problematic Usage in dependencies component:

  • Lines 74-81: workspace.addSpecificComponentConfig(compId, DependencyResolverAspect.id, config)
  • Lines 87, 130, 167, 201: Multiple direct aspect ID references
  • Deep integration with workspace configuration and component management

Proposed Solution (HIGH IMPACT, HIGH EFFORT): Split dependency management concerns:

typescript
// Extract common dependency interfaces
interface DependencyPolicyManager {
  setPeerDependency(componentId: string, dependencies: Record<string, string>): Promise<void>;
  unsetPeerDependency(componentId: string, dependencies: string[]): Promise<void>;
  setDependencies(pattern: string, dependencies: Record<string, string>): Promise<void>;
  removeDependencies(pattern: string, dependencies: string[]): Promise<void>;
}

// Dependencies component implements policy management
class DependenciesMain implements DependencyPolicyManager {
  // Implementation without importing dependency-resolver
}

// Dependency resolver uses policy manager interface
class DependencyResolverMain {
  constructor(private policyManager: DependencyPolicyManager) {}
}

Estimated Impact: 30-40 components removed from cycle


4. MEDIUM PRIORITY: Envs → Dependency-Resolver Indirect Cycle

Cycle Path:

envs → compiler → dependency-resolver → envs

Root Cause:

  • envs depends on compilation services (compiler, bundler, linter, etc.)
  • These services depend on dependency-resolver for package resolution
  • dependency-resolver imports envs types: EnvsAspect, EnvDefinition, EnvsMain

Specific Dependencies (from cycles analysis):

envs → compiler → dependency-resolver
envs → bundler → dependency-resolver
envs → builder → dependency-resolver

Proposed Solution (MEDIUM IMPACT, MEDIUM EFFORT): Move env-related dependency resolver logic to envs:

typescript
// Remove envs imports from dependency-resolver
// Move env-specific dependency logic to envs component
// Use dependency injection/events for env-specific functionality

Estimated Impact: 15-20 components removed from cycle


Critical Investigation Needed: Type vs Runtime Imports

FIRST STEP: Categorize Circular Dependencies by Type

Before implementing solutions, we need to identify which cycles are:

  1. Type-only imports (import type) - Lower risk, can use type extraction
  2. Runtime imports - Higher risk, need architectural changes
  3. Mixed imports - Need careful analysis

Investigation Scripts Needed

bash
# 1. Find all type imports in circular dependencies
grep -r "import type.*@teambit" scopes/ | grep -f <(bit graph --cycles --json | jq -r '.edges[].sourceId' | cut -d'@' -f1)

# 2. Find runtime imports in circular dependencies
grep -r "import.*@teambit" scopes/ | grep -v "import type" | grep -f <(bit graph --cycles --json | jq -r '.edges[].sourceId' | cut -d'@' -f1)

# 3. Find what specific types are imported from workspace
grep -r "import.*{.*}.*@teambit/workspace" scopes/ | head -20

# 4. Find components that might not need workspace types at all
grep -r "import type.*Workspace" scopes/ | wc -l

Actual Examples Found in Codebase

Example 1: component/graph (Easy Fix)

typescript
// Current: Only used for typing return value
import type { Workspace } from '@teambit/workspace';

private getWorkspaceIfExist(): Workspace | undefined {
  return this.componentAspect.getHost('teambit.workspace/workspace') as Workspace;
}

// Fix: Remove type import entirely
private getWorkspaceIfExist(): any {
  return this.componentAspect.getHost('teambit.workspace/workspace');
}

Example 2: typescript/typescript (Mixed Usage)

typescript
// Has both runtime usage AND type usage
import { WorkspaceAspect } from '@teambit/workspace';  // Keep - needed for DI
import type { Workspace } from '@teambit/workspace';   // Remove - replace with any/generic

// Runtime usage (keep):
workspace.registerOnComponentChange(tsMain.onComponentChange.bind(tsMain));

// Type usage (removable):
readonly workspace: Workspace, // Change to: readonly workspace: any,

Example 3: workspace-config-files (Refactorable)

typescript
// Imports full workspace but only uses minimal interface
import type { Workspace } from '@teambit/workspace';

// Only uses: workspace.path, workspace.list(), workspace.componentDir()
// Solution: Create minimal interface or use generics

REALISTIC Implementation Strategy (CORRECTED Understanding)

Key Insight: Focus on DI Direction Understanding

CRITICAL CORRECTION: The circular dependency problem is NOT about aspects importing workspace types. It's about components that:

  1. Legitimately depend on workspace-config-files, install, etc. via DI (correct, keep these)
  2. But also import types FROM workspace creating the circular path

Example Circular Path:

workspace → (type import) → some-aspect → (DI dependency) → workspace-config-files → (DI dependency) → workspace

Real Strategy: Find and remove unnecessary type imports FROM workspace TO other aspects, not the other way around.

Phase 1: Easy Wins - Remove Unnecessary Type Imports (Target: 2,056 → 1,600 cycles)

Immediate Actions (Low Risk, High Impact):

  1. component/graph: Remove import type { Workspace } → use any
  2. docs/docs: Remove import type { Workspace } if only used for typing
  3. git/ci: Remove import type { Workspace } if minimal usage
  4. compilation/compiler: Check if WorkspaceComponentLoadOptions type is necessary

Expected reduction: ~400+ cycles (20%+ improvement)

Phase 2: Refactor Minimal Runtime Usages (Target: 1,600 → 1,200 cycles)

Medium Effort Actions:

  1. typescript/typescript: Replace Workspace type with any or minimal interface
  2. workspace-config-files: Create minimal interface for the 4 methods it actually uses
  3. Other components: Apply same pattern - remove type imports, use any or generics

Expected reduction: ~400 cycles (systematic type import removal)

Phase 3: Architectural Changes (Target: 1,200 → 800 cycles)

Higher Effort (only if needed):

  1. Address remaining legitimate circular dependencies using dependency injection
  2. Focus on the most impactful cycles for auto-tagging reduction

Expected reduction: ~400 cycles (if required)

Architecture Insights

Root Causes of Circular Dependencies

  1. Convenience Imports: Components import full interfaces when they only need subsets
  2. Tight Coupling: Core components (workspace, envs, dependency-resolver) are too interdependent
  3. Shared State: Multiple components manage overlapping concerns (configuration, component metadata)
  4. Service Registration: Components register with each other creating circular relationships

Architectural Principles for Prevention

  1. Interface Segregation: Create minimal interfaces for specific use cases
  2. Dependency Injection: Use DI to break direct import cycles
  3. Event-Driven Architecture: Use events/callbacks instead of direct method calls
  4. Service Abstractions: Create service layers that don't know about their consumers

Measurement & Monitoring

Use the scripts in this directory to track progress:

bash
# Check current state
node check-circular-deps.js --verbose

# After improvements, update baseline
node check-circular-deps.js --baseline --verbose

# Set improvement goals
node check-circular-deps.js --max-cycles=1800  # ~12% improvement target

Files Most Critical to Address

Immediate Priority:

  1. scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.ts
  2. scopes/component/graph/graph-builder.ts
  3. scopes/workspace/workspace/workspace.ts (lines 575-590)

Secondary Priority: 4. scopes/dependencies/dependencies/dependencies.main.runtime.ts 5. scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts 6. scopes/envs/envs/environments.main.runtime.ts

Auto-Tagging Impact Investigation

Understanding the Current Problem

When you run bit tag on a single aspect, it currently tags 150+ components due to circular dependencies making everything appear as dependents.

Investigation Commands

bash
# Find which components get auto-tagged when modifying workspace
bit status --verbose
bit tag workspace --dry-run --verbose

# Analyze dependency graph for specific component
bit graph --json | jq '.edges[] | select(.sourceId | contains("workspace"))'

# Find shortest paths between components (to understand why they're considered dependents)
bit graph --json --filter="workspace" | jq '.edges[] | select(.type != "devDependency")'

Expected Improvement

Breaking key circular dependencies should dramatically reduce auto-tagging:

  • Current: Change 1 aspect → 150+ components tagged
  • Target: Change 1 aspect → 5-20 actual dependents tagged
  • Business value: Cleaner git history, faster CI, more precise versioning

Next Steps When Tackling This Issue

  1. Run current measurement: node check-circular-deps.js --verbose

  2. Phase 0 - Deep Investigation (CRITICAL):

    bash
    # Find actual type import patterns
    grep -r "import type.*Workspace" scopes/ | head -10
    grep -r "import.*{.*}.*@teambit/workspace" scopes/ | head -10
    
    # Understand what's actually being used
    # Look for patterns like: workspace: Workspace, comp: Component, etc.
    
  3. Start with "Low Hanging Fruit":

    • Remove any type imports that don't add value
    • Replace simple parameter types with inline types or generics
    • Use module augmentation for method signatures you can't avoid
  4. Test one small fix and measure:

    • Pick one obvious unnecessary type import
    • Remove it and run: node check-circular-deps.js
    • Test auto-tagging: bit tag some-component --dry-run
  5. Scale successful patterns to similar cases

  6. Measure impact on both cycles and auto-tagging after each change

  7. Update baseline when improvements are stable

Most Important: Focus on removing/avoiding type imports rather than creating complex type extraction systems

Success Metrics

  • Primary: Circular dependency count reduction (2,056 → target)
  • Secondary: Auto-tagging reduction (150+ → ~10-20 components)
  • Tertiary: TypeScript project references enablement

UPDATED NEXT STEPS (Based on Analysis)

  1. Run current measurement: node check-circular-deps.js --verbose

  2. Start with Easiest Win - component/graph:

    bash
    # Edit: scopes/component/graph/graph-cmd.ts
    # Change: import type { Workspace } from '@teambit/workspace';
    # To: Remove the import entirely
    # Change: private getWorkspaceIfExist(): Workspace | undefined {
    # To: private getWorkspaceIfExist(): any {
    
  3. Test the impact:

    bash
    node check-circular-deps.js --verbose
    # Should see cycle count reduction
    
    # Test auto-tagging impact:
    bit tag component/graph --dry-run --verbose
    
  4. Apply same pattern to other easy wins:

    • Remove import type { Workspace } from docs/docs, git/ci, etc.
    • Replace return types with any or remove type annotations
  5. Measure after each change and scale successful patterns

Key Strategy: Replace unnecessary type imports with any - the problem is much more solvable than initially thought!

Expected Outcome: 2,056 → 1,600 cycles (20%+ reduction) with low-risk changes.