Keep this file, AGENTS.md, up to date when Storybook's architecture, tooling, workflows, or contributor guidance changes.
This file is the canonical instruction source for coding agents. Files like CLAUDE.md should point here instead of duplicating instructions.
Storybook is a large TypeScript monorepo. The git root is the repo root, the main code lives in code/, and build tooling lives in scripts/. The default branch is next.
- Base branch:
next(all PRs should targetnext, notmain) - Node.js:
22.22.3(see.nvmrc) — supports.tsnatively via type stripping (no loader needed) - Package Manager: Yarn Berry
- Task orchestration: NX plus the custom
yarn taskrunner - Linting: oxlint (root
.oxlintrc.json, extended bycode/.oxlintrc.jsonandscripts/.oxlintrc.json; custom rules load viajsPlugins). ESLint is no longer used for repo linting —code/lib/eslint-pluginremains as the publishedeslint-plugin-storybookpackage. - Formatting: oxfmt (root
.oxfmtrc.json) - CI environment: Linux and Windows
- TS execution: Migrating from
jitito nativenodefor running.tsfiles. New scripts should usenode ./path/file.tswith explicit.tsimport extensions (enabled byallowImportingTsExtensionsin tsconfig). Legacy scripts still usejitibut should be migrated over time. - Type checking: Per-package checks (
yarn task check,scripts/check/check-package.ts) run on the TypeScript 7 native compiler (thetypescript-nativenpm alias); diagnostics are filtered to the checked package.@storybook/vue3,@storybook/docgen-harness(for its.vuefixtures), and@storybook/svelteusevue-tsc/svelte-check(TS 6 based). The workspacetypescriptdependency stays on TS 6 for IDEs and API consumers, so tsconfigs must remain valid for both (e.g. nobaseUrl).
storybook/
├── .github/ # GitHub configs and workflows
├── .nx/ # NX workflow state
├── code/ # Main codebase
│ ├── .storybook/ # Internal Storybook UI config
│ ├── core/ # Core package published as "storybook"
│ ├── addons/ # Core addons
│ ├── builders/ # Builder integrations
│ ├── renderers/ # Renderer integrations
│ ├── frameworks/ # Framework integrations
│ ├── lib/ # Supporting libraries
│ ├── presets/ # Webpack-oriented presets
│ └── sandbox/ # Internal build artifacts
├── scripts/ # Build and development scripts
├── docs/ # Documentation
├── test-storybooks/ # Test repos
└── ../storybook-sandboxes/ # Generated sandboxes outside repo
| Concept | Role | Example |
|---|---|---|
| Renderer | Mounts UI framework to the DOM | @storybook/react |
| Builder | Bundles and serves Storybook | @storybook/builder-vite |
| Framework | Renderer + builder + framework config | @storybook/react-vite |
The main package is code/core/src/. The most important areas are:
core-server/for dev server, static build, and presetsmanager/andmanager-api/for the Storybook UIpreview/andpreview-api/for story renderingchannels/for manager <-> preview communicationcsf-tools/for AST-based story indexingcommon/for shared Node.js utilitiestest/andinstrumenter/for testing support
Public exports include:
storybook/actionsstorybook/preview-apistorybook/manager-apistorybook/themingstorybook/test
Internal exports include:
storybook/internal/core-serverstorybook/internal/csf-toolsstorybook/internal/commonstorybook/internal/channels
.storybook/main.tsis loaded at startup.storybook/preview.tsis bundled into preview (TSX for React-based frameworks).storybook/manager.tsis bundled into manager*.stories.*files are indexed by AST before runtime- Story selection loads the module, prepares the story, and renders it
AST indexing keeps the sidebar fast and prevents one broken story file from breaking the whole UI.
- OSA hosts two sibling constructs behind the
storybook/open-serviceentry: services (defineService/registerService) own internal state, synchronization, queries, commands, and loading; toolsets (defineToolset/registerToolset) are the public agent surface for CLI/MCP. They live in mirrored trees:open-service/services/andopen-service/toolsets/. - All core OSA services are
internal: trueand may change without a public semver bump. Resolve internal services withgetService(id, { internal: true }). A plaingetService(id)throws when the service is internal. - A toolset has an
id, description, and methods with onlyschema,description, andhandler. - Toolsets register imperatively via
registerToolset, called from the same place the paired service registers (theservicespreset hook for core and addons; the mechanism itself does not depend on the Node preset system). Feature gating is shared: a disabled feature registers neither the service nor its toolset. Adapters read the set viagetRegisteredToolsets(); nothing consumes it before Milestone 4. - Handlers receive
(input, ctx)withconsumer('cli' | 'mcp'), optionalorigin, requiredformat('markdown' | 'json'), andgetService. Methods never declare the output format; adapters own the mapping (CLI--jsonflag, MCPjsontool input). - The docs toolset's Markdown is a verbatim port of the
@storybook/mcpmanifest formatter (toolsets/docs/manifest-formatter/); the two copies must not drift until Milestone 4 deletes the original. MCP consumer + Markdown is the parity-tested cell. - The toolset surface remains experimental. Production MCP migration is Milestone 4. CLI generation
and production
storybook toolswiring are Milestone 5. MCP tools remain hand-authored inaddon-mcpuntil Milestone 4.
Run commands from the repository root unless stated otherwise.
For routine agent work, prefer the faster non-production commands first. Add -c production only when you need sandbox-related NX tasks or you are explicitly matching CI behavior.
yarn
yarn task compile
yarn nx run-many -t compile
yarn nx compile <nx-project-name>yarn lint
yarn --cwd code lint:js:cmd <file-relative-to-code-folder> --fix
yarn task check
yarn nx run-many -t checkcd code && yarn storybook:ui
cd code && yarn storybook:ui:build
yarn test
yarn test:watch
yarn storybook:vitest| Scenario | Command |
|---|---|
| Compile everything quickly | yarn nx run-many -t compile |
| Compile one project | yarn nx compile <nx-project-name> |
| Check TypeScript errors quickly | yarn nx run-many -t check |
| Start the internal Storybook UI | cd code && yarn storybook:ui |
| Build the internal Storybook UI | cd code && yarn storybook:ui:build |
| Run unit tests | yarn test |
| Run Storybook Vitest tests | yarn storybook:vitest |
| Generate a sandbox | yarn task sandbox --template react-vite/default-ts --start-from auto |
| Run sandbox E2E tests | yarn task e2e-tests-dev --template react-vite/default-ts --start-from auto |
| Run sandbox test-runner tests | yarn task test-runner-dev --template react-vite/default-ts --start-from auto |
| Run the docgen perf bench | yarn workspace @storybook/docgen-harness bench:docgen-perf |
| Run the docgen memory gate | yarn workspace @storybook/docgen-harness bench:docgen-memory |
Use NX when you want better caching and dependency tracking. Prefer these faster defaults first, and only add -c production or --no-link when you specifically need sandbox parity or CI-like behavior.
# Compile all packages
yarn task compile
yarn nx run-many -t compile
# Check all packages
yarn task check
yarn nx run-many -t check
# Run E2E tests for a template
yarn task e2e-tests-dev --template react-vite/default-ts --start-from auto
yarn nx e2e-tests-dev react-vite/default-ts -c production
# Jump to a later step
yarn task e2e-tests-dev --start-from e2e-tests --template react-vite/default-ts
yarn nx e2e-tests-dev -c production --exclude-task-dependenciesKey points:
-c productionis required for sandbox-related NX commands and CI-parity runsreact-vite/default-tsis the default sandbox template--no-linkis opt-in, not the default- NX handles task dependencies via
nx.json - NX target commands use Nx project names (from
project.json/ Nx graph), notpackage.jsonnames - Example:
yarn nx compile core(projectcoreis published as packagestorybook) - NX Cloud remote-cache auth failures (e.g. HTTP 401 "insufficient access") degrade to the local cache, so they are expected on local runs where
NX_CLOUD_ACCESS_TOKENis unset. CI always sets that token, so a 401 there means an invalid or expired token and should be investigated rather than ignored. A read-only token enables cache reads but cannot store artifacts, so the "wasn't able to store" warning is still expected with one
Sandboxes are generated outside the repository at ../storybook-sandboxes/ by default.
STORYBOOK_SANDBOX_ROOT=./sandboxforces local output, but is usually not preferred./sandboxinside the repo mainly exists for NX outputs, not CI sandboxes- If sandbox generation fails, fall back to
cd code && yarn storybook:ui
Generate and use a sandbox with the same sandbox command shape used elsewhere in this file:
yarn task sandbox --template react-vite/default-ts --start-from auto
# Same sandbox step via NX
yarn nx sandbox react-vite/default-ts -c production
cd ../storybook-sandboxes/react-vite-default-ts
yarn install
yarn storybookCommon templates:
react-vite/default-tsreact-webpack/default-tsangular-cli/default-tssvelte-vite/default-tsvue3-vite/default-tsnextjs/default-ts
- Install if needed:
yarn - Compile with NX:
yarn nx run-many -t compile - Make changes
- Recompile affected packages
- Validate there are no TypeScript errors with
yarn nx run-many -t check - Run relevant lint and tests
- Validate behavior in the internal Storybook UI first, then switch to sandbox or
-c productionflows only if you need template or CI parity
- Edit the relevant package under
code/addons/,code/frameworks/, orcode/renderers/ - Recompile with NX, starting without
-c production - Generate a matching sandbox
- Run the relevant test-runner, E2E, or Storybook UI validation flow
Important
For React components, write Storybook stories with play functions — do NOT write *.test.tsx unit tests. Behavior, accessibility, and interaction assertions belong in *.stories.tsx co-located with the component, executed via the Storybook Vitest project (yarn storybook:vitest or vitest run --config code/vitest.config.storybook.ts). Unit tests (*.test.ts(x)) are reserved for pure utilities, hooks, and non-React modules where rendering is not involved.
- Use
yarn storybook:vitestto run Storybook story tests (the primary test path for components) - Use
yarn testfor unit tests of utilities, hooks, and non-React modules - Prefer focused unit-test runs during iteration — the full suite is large:
yarn test <pattern>(e.g.yarn test csf-tools) - Use Storybook UI or Chromatic for visual validation
- Use
yarn task e2e-tests --start-from autooryarn task e2e-tests-dev --start-from autofor E2E coverage - Use
yarn task test-runner --start-from autooryarn task test-runner-dev --start-from autofor test-runner scenarios - Use
yarn task smoke-test --start-from autofor smoke checks
Watch-mode commands:
yarn test:watch
yarn storybook:vitestWhen writing tests for components:
- Add or update
<Component>.stories.tsxwith stories covering each behavior; useplayfunctions withexpect,userEvent,withinfromstorybook/test - Mock external context (e.g.
ManagerContext.Provider) inside story decorators orbeforeEach - Run
vitest --config code/vitest.config.storybook.ts <story-file>to verify play assertions
When writing unit tests (utilities, hooks, non-React modules):
- Export functions that need direct tests
- Test real behavior, not just syntax patterns
- Use coverage when useful:
yarn vitest run --coverage <test-file> - Mock external dependencies like file system access and loggers
- Use Node's path.resolve to wrap expected FS paths when writing path-related tests, so they work on Windows
For unit tests that touch node:fs / node:fs/promises, use memfs instead of real temp directories or wholesale node:fs mocks:
- Import
volfrommemfsand callvol.reset()inbeforeEach - Seed virtual files with
vol.fromNestedJSON({ '/absolute/path/file.json': '...' })or memfswriteFileafter redirecting spies - Use
vi.mock('node:fs/promises', { spy: true })and, inbeforeEach, pointmkdir/writeFile/readFileatmemfs.fs.promises(seecode/core/src/shared/open-service/server.test.ts) - Assert disk state with
vol.toJSON()when helpful
Do not use /tmp paths or replace node:fs/promises with a full async factory mock unless a test file already standardizes on the spy redirect pattern above.
Important
Under no circumstances may a test mutate a global by assigning it directly (e.g. globalThis.FEATURES = {...}, globalThis.window = ..., global.fetch = ...). Direct assignment leaks across tests and files — Vitest does not restore it — so it silently changes behavior in unrelated tests and creates order-dependent flakiness.
Use Vitest's global stubbing instead, which is tracked and restorable:
- Set a global with
vi.stubGlobal('FEATURES', { experimentalDocgenServer: true }). - Restore in
afterEach(() => vi.unstubAllGlobals())(or enableunstubGlobals: truein the Vitest config so it resets before each test automatically). - For a value used by every test in a file, stub it in
beforeEachand unstub inafterEach; for a one-off override, callvi.stubGlobalinside that single test. - Never capture-and-restore by hand (
const original = globalThis.X; ... globalThis.X = original);vi.stubGlobal+vi.unstubAllGlobals()does this correctly, including deleting keys that did not previously exist.
This applies to all ambient globals, not just FEATURES (e.g. window, document, navigator, fetch, IS_REACT_ACT_ENVIRONMENT).
After changing files:
- Always format with
yarn fmt:write, run from thecode/directory (cd code && yarn fmt:write), once you are done editing. The repo usesoxfmt, so hand-written formatting will frequently be wrong — do not skip this step. - Lint with
yarn --cwd code lint:js:cmd <file-relative-to-code-folder> --fixorcd code && yarn lint:js:cmd <file-relative-to-code-folder> - Run relevant tests before submitting a PR
Use Storybook loggers instead of raw console.* in normal code paths:
- Server-side:
storybook/internal/node-logger - Client-side:
storybook/internal/client-logger
For TypeScript source in the repo, prefer explicit file extensions for relative code imports and exports such as ./foo.ts or ./bar.tsx when the target is another TS/JS module in this repository. Keep framework-specific component imports like .vue and .svelte in the form already expected by their package tooling.
The pre-commit hook automatically detects AI agents (via std-env) and switches from check-only to write mode, so formatting is auto-fixed when agents commit.
Avoid console.log, console.warn, and console.error unless the file is isolated enough that importing the logger is not reasonable.
- Build failures are often fixed by rerunning
yarnandyarn nx run-many -t compile - Storybook UI uses port
6006by default - Large compiles may require more Node.js memory
- Sandbox paths are
../storybook-sandboxes/, not./sandboxorcode/sandbox/ - Use
--debugfor verbose CLI output - Check generated sandbox directories and
.cache/for build artifacts
| Variable | Purpose |
|---|---|
IN_STORYBOOK_SANDBOX |
Set during sandbox creation |
STORYBOOK_DISABLE_TELEMETRY |
Disable telemetry |
STORYBOOK_TELEMETRY_DEBUG |
Log telemetry events |
DEBUG |
Enable debug logging |
FIX_ON_COMMIT |
Force autofix for fmt & lint in pre-commit hook |
NX_CLOUD_ACCESS_TOKEN |
Authenticate the NX Cloud remote cache |
- DO NOT RUN
yarn task devwithout an explicit sandbox template - DO NOT RUN
yarn start
These usually start long-running development servers and are the wrong default for agents.
These are recurring failure modes in agent-authored changes to this repo. Apply them when writing or reviewing code, not just when asked.
- Comments are maintenance docs, not an investigation transcript. Explain why for the next maintainer. Do not commit internal ticket / acceptance-criteria codes (
AC-X2,Probe B,R6), the narrative of how you figured something out, "verified byte-identical" provenance prose, or cross-file line references (L125→L131) — they are noise and they rot. One or two sentences of rationale beats a paragraph of evidence. - Verify environment assumptions empirically before encoding them. If a design rests on "the bundler strips X" or "this metadata is empty here", prove it with a throwaway probe before building on it (and before writing it into a comment as fact). A 10-line experiment is cheaper than a wrong architecture.
- Encode assumptions with static checks first. If an assumption is expected to always hold, prefer making it impossible via TypeScript types and existing lint rules. When static checks are not practical, add a cheap runtime assertion close to the boundary so violations fail loudly at the source.
- Avoid redundant tests already covered elsewhere. Do not add tests for code patterns already guaranteed by TypeScript or linting, and do not duplicate coverage that already exists in Storybook
playfunctions or Playwright tests. - Test contracts (including side effects), not private implementation details. It is valid to assert side effects when they are part of the public contract. Avoid assertions about internals that are not part of an exported contract, user-visible DOM output, or externally observable behavior.
- Bias toward broader coverage for security and migrations. For security-sensitive code paths and legacy data migration logic, prefer handling more edge cases and documenting evidence for the chosen safeguards. Migration compatibility code should be explicitly version-scoped so it can be removed once the support window ends.
- Prefer deletion and simplicity over speculative generality. No abstraction, fallback, or "flexibility" for a consumer or scenario that does not exist in this codebase today. If a change adds many lines, check whether the right change removes them.
- Don't commit accidental overrides to generated code. Files like
code/core/src/manager/globals/exports.tsare auto-generated, as stated in their JSDoc header. Only commit changes if they match changes you made on your PR, otherwise leave them untouched and flag flaky generated files in the PR description.
- Use this file as the canonical instruction source
- Update
AGENTS.mdwhen architecture, commands, versions, release flows, or contributor guidance changes - Keep
CLAUDE.mdand other agent entrypoints as thin references toAGENTS.md - Do not reintroduce duplicated instruction files when a reference will do