Read OSS

Build Infrastructure, Testing, and the Developer Workflow

Intermediate

Prerequisites

  • Article 1: Architecture Overview
  • Familiarity with build tools (esbuild, Vite, webpack concepts)
  • Understanding of monorepo tooling (Nx, Yarn workspaces)

Build Infrastructure, Testing, and the Developer Workflow

We've spent five articles exploring Storybook's runtime architecture — the three environments, the preset system, channels, the Preview rendering pipeline, and the Manager UI. But none of that code ships without a sophisticated build and development infrastructure. Storybook's monorepo contains 50+ packages that must be compiled in dependency order, tested across every supported framework combination, and shipped with consistent error handling.

This final article covers the machinery that makes it all work: Nx task orchestration, the build-config.ts entry classification we introduced in Part 1, the StoryIndexGenerator that powers the sidebar, the ChangeDetectionService for smart rebuilds, the sandbox system for integration testing, and the structured error system.

Nx Task Orchestration

The Storybook monorepo uses Nx for task orchestration. The nx.json file defines the task graph:

nx.json#L24-L187

flowchart TD
    Compile["compile"] -->|"^compile (dependencies first)"| Check["check"]
    Compile --> Publish["publish"]
    Publish --> RunRegistry["run-registry (verdaccio)"]
    RunRegistry --> Sandbox["sandbox"]
    Sandbox --> Build["build"]
    Sandbox --> Dev["dev"]
    Build --> Serve["serve"]
    Serve --> E2E["e2e-tests"]
    Serve --> TestRunner["test-runner"]
    Build --> Chromatic["chromatic"]

    Compile --> Jest["jest"]
    Compile --> Vitest["vitest"]
    Compile --> PlaywrightCT["playwright-ct"]

The key tasks are:

Task Depends On Purpose
compile ^compile (package dependencies) Build a single package with esbuild
check All packages compiled TypeScript type checking
publish All packages compiled Publish to local Verdaccio registry
sandbox Local registry running Generate a test project for a framework
build Sandbox created Build the sandbox's Storybook
e2e-tests Sandbox served Run Playwright tests against the sandbox

The compile task (line 25–35) is the foundation. It runs build-package.ts for the current project, uses ^compile as a dependency (meaning all upstream packages must be compiled first), and caches results based on production inputs. The parallel: 8 setting at the root allows up to 8 packages to compile simultaneously.

Tip: When contributing to Storybook, run yarn nx compile storybook to build just the core package. The ^compile dependency ensures all upstream packages are built first. Use yarn nx affected -t compile to build only what's changed since the last commit.

Package Compilation Pipeline

Each package's build is driven by build-package.ts:

scripts/build/build-package.ts#L1-L80

flowchart TD
    Start["build-package.ts"] --> ReadPkg["Read package.json"]
    ReadPkg --> FindConfig["Find build-config.ts"]
    FindConfig --> Prebuild{"Has prebuild?"}
    Prebuild -->|Yes| RunPrebuild["Run prebuild script"]
    Prebuild -->|No| Skip
    RunPrebuild --> GenBundle["generateBundle()"]
    Skip --> GenBundle
    GenBundle --> |"For each entry category"| ESBuild["esbuild with platform-specific config"]
    GenBundle --> GenTypes["generateTypesFiles()"]
    GenBundle --> GenPkgJson["generatePackageJsonFile()"]

The script reads the package's build-config.ts (which we covered in Part 1) and processes each entry category with different esbuild configurations:

  • Node entries: platform: 'node', allows Node.js built-ins
  • Browser entries: platform: 'browser', targets Chrome 100+/Safari 15+/Firefox 91+
  • Runtime entries: Like browser but with code-splitting disabled (must be self-contained scripts)
  • Globalized runtime entries: Wrapped to expose exports as window.__STORYBOOK_* globals

The core package has a prebuild step (line 8–21 of build-config.ts) that runs generate-source-files.ts — this generates version files, type re-exports, and other derived source code before the main build runs.

As we saw in Part 1, the core's build-config.ts classifies entries:

code/core/build-config.ts#L22-L210

The 12 node entries, 23 browser entries, 3 runtime entries, and 1 globalized runtime entry are each compiled with the appropriate platform settings. This classification is what prevents accidental cross-environment imports — a browser entry that tries to import fs will fail at build time, not at runtime.

The StoryIndexGenerator

The StoryIndexGenerator is the server-side component that discovers and indexes all stories. It's what populates index.json and ultimately drives the sidebar:

code/core/src/core-server/utils/StoryIndexGenerator.ts#L101-L127

flowchart TD
    Specifiers["stories: ['../src/**/*.stories.tsx']"] --> Glob["Glob filesystem"]
    Glob --> Files["Matched files"]
    Files --> Indexer{"Match indexer?"}
    Indexer -->|"CSF indexer"| Parse["Parse with loadCsf()"]
    Indexer -->|"MDX indexer"| MDXParse["Parse MDX"]
    Parse --> Entries["Story entries + docs entries"]
    MDXParse --> DocsEntry["Docs entry"]
    Entries --> Cache["SpecifierStoriesCache"]
    DocsEntry --> Cache
    Cache --> Sort["Sort stories (storySortParameter)"]
    Sort --> Index["StoryIndex (index.json)"]

The generator maintains a two-level cache:

  1. Specifier → Files cache: Maps each stories glob pattern to the set of matched files
  2. File → Entries cache: Maps each file to its parsed story/docs entries

When a file changes, only that file's cache entry is invalidated. The generator then recomputes the full index by combining all cached entries, deduplicating (stories preferred over docs), and sorting according to the user's storySortParameter.

The CSF indexer (defined in common-preset, as we saw in Part 2) uses loadCsf() from storybook/internal/csf-tools to parse story files. This is an AST-based parser that extracts story metadata without executing the file — it can determine story names, tags, and parameters from static analysis alone.

Change Detection and Smart Rebuilds

The ChangeDetectionService is a newer addition that provides intelligent rebuild behavior during development:

code/core/src/core-server/change-detection/ChangeDetectionService.ts#L87-L153

flowchart TD
    Builder["Preview Builder (Vite)"] -->|"onModuleGraphChange"| CDS["ChangeDetectionService"]
    CDS --> Debounce["Debounce 200ms"]
    Debounce --> Git["GitDiffProvider.getChangedFiles()"]
    Git --> Trace["findAffectedStoryFiles(moduleGraph, changedFiles)"]
    Trace --> Status["Compute story statuses:\n    - new\n    - modified\n    - affected"]
    Status --> Store["StatusStore.set()"]
    Store --> UI["Manager sidebar shows change indicators"]

The service coordinates three inputs:

  1. Module graph updates from the builder (Vite provides dependency information)
  2. Git diff from GitDiffProvider (which files have changed relative to the base branch)
  3. Story index from the StoryIndexGenerator (which stories map to which files)

By tracing the builder's module graph, the service can determine not just directly changed story files, but transitively affected ones — if a shared utility changes, all stories that import it are marked as "affected." Statuses are published to the StatusStore, which the Manager reads to show change indicators in the sidebar.

The service is feature-flagged via features.changeDetection (disabled by default in the common preset) and requires builder support for module graph reporting.

The Sandbox System

Integration testing across Storybook's supported frameworks uses a sandbox generation system. The Nx task graph makes this explicit:

compile → publish → run-registry → sandbox → build → serve → e2e-tests

Each sandbox is a real project generated for a specific framework combination (e.g., react-vite/default-ts, angular/default-ts, svelte-vite/default-ts). The process:

  1. Publish: All packages are published to a local Verdaccio npm registry
  2. Sandbox: A template project is generated, installs packages from the local registry, and configures Storybook
  3. Build: The sandbox's Storybook is built to verify the build pipeline
  4. E2E: Playwright tests run against the served sandbox

This approach catches integration issues that unit tests miss — framework-specific Vite plugins, preset composition order, HMR behavior, and renderer compatibility are all exercised against real projects.

The nx.json defines these as cacheable targets with ^production inputs, meaning the cache is invalidated when any production source file in any dependency changes.

Structured Error System

Storybook uses a structured error hierarchy based on the StorybookError abstract class:

code/core/src/storybook-error.ts#L25-L135

classDiagram
    class StorybookError {
        <<abstract>>
        +category: string
        +code: number
        +documentation: boolean|string|string[]
        +fromStorybook: true
        +isHandledError: boolean
        +subErrors: StorybookError[]
        +fullErrorCode: string
        +name: string
    }
    class ServerError {
        category = "SERVER"
    }
    class PreviewError {
        category = "PREVIEW_API"
    }
    class ManagerError {
        category = "MANAGER_UI"
    }
    StorybookError <|-- ServerError
    StorybookError <|-- PreviewError
    StorybookError <|-- ManagerError

Each error has:

  • A category (e.g., SERVER, PREVIEW_API, MANAGER_UI) that classifies the environment
  • A code (a number, zero-padded to 4 digits) that uniquely identifies the error
  • An optional documentation link

The fullErrorCode getter (line 59) generates strings like SB_SERVER_0001 or SB_PREVIEW_API_0003. The name getter formats as SB_SERVER_0001 (MissingBuilderError).

Error definitions are split across three files matching the three environments:

  • server-errors.ts — Node.js server errors
  • preview-errors.ts — Preview iframe errors
  • manager-errors.ts — Manager UI errors

The fromStorybook: true flag (line 51) enables the preview runtime's error handler (which we saw in Part 1) to distinguish Storybook errors from user code errors and route them to telemetry.

The subErrors array (line 92) supports error aggregation — a parent error can carry multiple related child errors. When sent to telemetry, both the parent and each sub-error are reported as separate events.

Tip: When creating new errors in Storybook contributions, always extend from the appropriate environment-specific error file and assign a unique code number. The structured format enables automated error tracking and makes it easy to link users to documentation.

Wrapping Up the Series

Over these six articles, we've traced Storybook's architecture from the outermost shell to the innermost implementation detail:

  1. Architecture Overview: Three environments, unified package, CLI dispatch
  2. Preset System: Configuration composition via reduce chains
  3. Channels and Events: Cross-environment communication protocol
  4. Preview Rendering: CSF processing, StoryRender lifecycle, framework renderers
  5. Manager UI: Modular state management, addon integration, composition
  6. Build Infrastructure: Nx orchestration, story indexing, change detection, structured errors

The recurring theme is composition: presets compose configuration, channels compose communication, modules compose state, and the build tool composes packages. Storybook's power — and its complexity — comes from the same source: making every piece independently composable while maintaining a coherent whole.