scripts/circular-deps-check/ANALYSIS.md
This document provides detailed analysis of circular dependencies in the Bit repository for future reference when tackling circular dependency issues.
main.runtime.ts has a provider method that receives dependency instances via dependency injectionCRITICAL FINDING: Analysis of actual cycles reveals a misleading pattern:
Key Discovery Pattern:
// 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:
bit tag on 150+ dependent components--skip-auto-tag (but this prevents dependents from getting latest versions)import type { SomeType } from '@teambit/other-component'Cycle Path:
workspace → component/graph → workspace
Root Cause Analysis Needed:
component/graph imports types from workspaceworkspace likely imports graph types for its getGraph() methodsSpecific 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 typesimport type statements between these componentsREALISTIC Solution Analysis:
Problem with Type Extraction: Workspace has ~100 public methods. Creating a separate interface would be:
Practical Approaches:
// Instead of importing Workspace types, check if you really need them
// Many type imports might be removable with better local typing
// 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
}
}
// Instead of importing Workspace, use generic patterns
type ComponentHost = {
getGraph(ids?: ComponentID[]): Promise<ComponentGraph>;
// Only the methods you actually need
};
# 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
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)workspace importing types/functionality FROM workspace-config-filesActual 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:
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
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, EnvsMainscopes/dependencies/dependencies/dependencies.main.runtime.ts imports and uses DependencyResolverAspect extensivelyProblematic Usage in dependencies component:
workspace.addSpecificComponentConfig(compId, DependencyResolverAspect.id, config)Proposed Solution (HIGH IMPACT, HIGH EFFORT): Split dependency management concerns:
// 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
Cycle Path:
envs → compiler → dependency-resolver → envs
Root Cause:
envs depends on compilation services (compiler, bundler, linter, etc.)dependency-resolver for package resolutiondependency-resolver imports envs types: EnvsAspect, EnvDefinition, EnvsMainSpecific 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:
// 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
Before implementing solutions, we need to identify which cycles are:
import type) - Lower risk, can use type extraction# 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
Example 1: component/graph (Easy Fix)
// 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)
// 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)
// 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
CRITICAL CORRECTION: The circular dependency problem is NOT about aspects importing workspace types. It's about components that:
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.
Immediate Actions (Low Risk, High Impact):
import type { Workspace } → use anyimport type { Workspace } if only used for typingimport type { Workspace } if minimal usageWorkspaceComponentLoadOptions type is necessaryExpected reduction: ~400+ cycles (20%+ improvement)
Medium Effort Actions:
Workspace type with any or minimal interfaceany or genericsExpected reduction: ~400 cycles (systematic type import removal)
Higher Effort (only if needed):
Expected reduction: ~400 cycles (if required)
Use the scripts in this directory to track progress:
# 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
Immediate Priority:
scopes/workspace/workspace-config-files/workspace-config-files.main.runtime.tsscopes/component/graph/graph-builder.tsscopes/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
When you run bit tag on a single aspect, it currently tags 150+ components due to circular dependencies making everything appear as dependents.
# 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")'
Breaking key circular dependencies should dramatically reduce auto-tagging:
Run current measurement: node check-circular-deps.js --verbose
Phase 0 - Deep Investigation (CRITICAL):
# 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.
Start with "Low Hanging Fruit":
any type imports that don't add valueTest one small fix and measure:
node check-circular-deps.jsbit tag some-component --dry-runScale successful patterns to similar cases
Measure impact on both cycles and auto-tagging after each change
Update baseline when improvements are stable
Run current measurement: node check-circular-deps.js --verbose
Start with Easiest Win - component/graph:
# 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 {
Test the impact:
node check-circular-deps.js --verbose
# Should see cycle count reduction
# Test auto-tagging impact:
bit tag component/graph --dry-run --verbose
Apply same pattern to other easy wins:
import type { Workspace } from docs/docs, git/ci, etc.any or remove type annotationsMeasure after each change and scale successful patterns
any - the problem is much more solvable than initially thought!Expected Outcome: 2,056 → 1,600 cycles (20%+ reduction) with low-risk changes.