This document describes the allowed dependency structure within the CDC Open Viz monorepo.
The CDC Open Viz monorepo follows a specific dependency architecture to maintain clean separation of concerns and prevent circular dependencies. The standalone build tests enforce these rules to ensure packages can be built independently.
Quick Reference:
- Core (
@cdc/core) → Depends on nothing (foundation) - Visualizations (
chart,map,data-table, etc.) → Depend only on Core - Orchestrators (
dashboard,editor) → Can depend on everything
-
Core Package (
@cdc/core)- Cannot depend on any other internal packages (foundation layer)
- Any package can depend on
@cdc/core - Core contains shared utilities, types, components, and helpers used across the monorepo
- Examples:
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
-
Dashboard Package (
@cdc/dashboard)- Can depend on ANY other package
- Dashboard serves as an orchestrator that combines multiple visualization components
- Examples:
import CdcChart from '@cdc/chart/src/CdcChartComponent' import CdcMap from '@cdc/map/src/CdcMapComponent'
-
Editor Package (
@cdc/editor)- Can depend on ANY other package
- Editor needs access to all components for configuration and preview
- Examples:
import CdcChart from '@cdc/chart/src/CdcChart' import { stripConfig } from '@cdc/dashboard/src/helpers/formatConfigBeforeSave'
All other packages should NOT have cross-package dependencies (except to @cdc/core).
This includes both:
- Explicit package imports: Using
@cdc/package-namesyntax - Relative path imports: Using
../or similar paths to access other packages through the monorepo directory structure
Individual visualization packages should be self-contained:
@cdc/chart@cdc/map@cdc/data-table@cdc/data-bite@cdc/waffle-chart@cdc/filtered-text@cdc/markup-include
┌─────────────────────────────────────────────────────────┐
│ Tier 3: Orchestrators (can import from all packages) │
├─────────────────────────────────────────────────────────┤
│ @cdc/dashboard ← Combines multiple visualizations │
│ @cdc/editor ← Configuration tool for all packages │
└─────────────────────────────────────────────────────────┘
│ │
↓ (depends on) │
┌─────────────────────────────────────────┐ │
│ Tier 2: Standalone Visualizations │ │
│ (depend only on core) │ │
├─────────────────────────────────────────┤ │
│ @cdc/chart │ │
│ @cdc/map │ │
│ @cdc/data-table │ │
│ @cdc/data-bite │ │
│ @cdc/waffle-chart │ │
│ @cdc/filtered-text │ │
│ @cdc/markup-include │ │
└─────────────────────────────────────────┘ │
│ │
↓ (depends on) ↓ (also depends on)
┌─────────────────────────────────────────────────────────┐
│ Tier 1: Foundation (no internal package dependencies) │
├─────────────────────────────────────────────────────────┤
│ @cdc/core ← Shared utilities, types, components │
└─────────────────────────────────────────────────────────┘
- Standalone Build Testing: Each visualization package should be buildable independently
- Clean Architecture: Prevents tangled dependencies and circular imports
- Reusability: Individual packages can be consumed separately without pulling in unnecessary dependencies
- Maintainability: Clear separation makes the codebase easier to understand and modify
Files within **/_stories/ or *.stories.* are exempt from dependency rules:
- Storybook stories are for development and documentation purposes
- They are not included in the actual package builds
- Stories may import from any package to demonstrate component interactions
- Example:
packages/core/components/_stories/DataTable.stories.tsxcan import from@cdc/map
The dependency rules are enforced by:
- Standalone Build Tests: Each package has a test that verifies it can be built in isolation
- Code Reviews: Manual verification during pull request reviews
- Monitoring Commands: Grep-based commands to detect cross-package imports (see Monitoring section)
TypeScript type-only imports (using the type keyword) can slip through standalone build tests:
// This violates architecture but may not fail builds
import { type DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'Why builds still succeed:
- Type-only imports are stripped during compilation and create no runtime dependency
- TypeScript may not fail when the imported package isn't installed
- The package can build successfully in isolation
Important: These are still architectural violations even if they don't break builds. Type-only imports:
- Create conceptual coupling between packages
- Can evolve into runtime imports over time
- Violate the intended dependency hierarchy
Solution: Move shared types to @cdc/core so both packages can reference them without violating the architecture.
When cross-package dependencies are needed:
Move shared types, utilities, or components to @cdc/core:
// Before (violation - explicit package import)
import { SomeType } from '@cdc/other-package/src/types'
// Before (violation - relative path import)
import { SomeType } from '../../other-package/src/types'
// After (valid)
import { SomeType } from '@cdc/core/types'Sometimes it's better to duplicate small pieces of code rather than create dependencies:
// Instead of importing a small utility, duplicate it locally
const localUtility = input => {
/* implementation */
}Pass dependencies through props or configuration:
// Instead of direct import, accept as prop
interface ComponentProps {
colorDistribution?: (data: any) => Colors
}// Any package can import from core
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
import { DataTableConfig } from '@cdc/core/types/DataTable'
// Dashboard can import from anywhere (visualizations + core)
import CdcChart from '@cdc/chart/src/CdcChartComponent'
import CdcMap from '@cdc/map/src/CdcMapComponent'
import { useReduceData } from '@cdc/core/helpers/useReduceData'
// Editor can import from anywhere (visualizations + core + dashboard)
import CdcChart from '@cdc/chart/src/CdcChart'
import { stripConfig } from '@cdc/dashboard/src/helpers/formatConfigBeforeSave'
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'Example: In the @cdc/map package (or any Tier 2/1 visualization package)
// Explicit package imports (PROHIBITED)
import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
import { v2ColorDistribution } from '@cdc/chart/src/helpers/chartColorDistributions'
import { ViewportSize } from '@cdc/chart/src/types/ChartConfig'
// Relative path imports across packages (ALSO PROHIBITED)
import { SomeUtility } from '../../chart/src/helpers/someUtility'
import { ChartTypes } from '../../../chart/src/types/ChartTypes'
import DashboardComponent from '../../../../dashboard/src/components/SomeComponent'
// Any path that goes up and into a sibling package directory is forbidden
import { Helper } from '../chart/...' // ❌ NO
import { Type } from '../../other-package/...' // ❌ NOTo check for dependency violations:
# Run standalone build tests
npx lerna run test
# Search for explicit cross-package imports (excluding core, stories, dashboard, and editor)
grep -r "from '@cdc/" packages/ --exclude="*.stories.*" --exclude-dir=node_modules | grep -v "/_stories/" | grep -v "@cdc/core" | grep -v "packages/dashboard/" | grep -v "packages/editor/" | grep -v "/types/" | grep -v "README.md"
# Search for relative imports that explicitly reference other package names
# These patterns catch imports like: from '../../chart/...' or from '../../../map/src/...'
grep -rE "from ['\"]\.\..*/(chart|map|data-table|data-bite|waffle-chart|filtered-text|markup-include|dashboard|editor)/" packages/ --exclude="*.stories.*" --exclude-dir=node_modules | grep -v "/_stories/" | grep -v "packages/dashboard/" | grep -v "packages/editor/"