Core: Implement component to stories lookup#34972
Conversation
📝 WalkthroughWalkthroughThe PR establishes an in-memory registry for the active ChangesChange Detection Service Registry and Inspection
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts`:
- Around line 516-520: getStoryIdsByFile currently returns references into the
internal cache (storyIdsByFileCache) allowing callers to mutate internal state;
change getStoryIdsByFile (in ChangeDetectionService) to return a defensive copy:
after obtaining the Map from getStoryIdsByAbsolutePath, create and return a new
Map with cloned Sets for each key (i.e., for each [k,v] produce [k, new
Set(v)]), so callers receive independent structures and cannot mutate
storyIdsByFileCache or its Sets.
In `@code/core/src/core-server/dev-server.ts`:
- Around line 60-63: The active service is being registered unconditionally via
setActiveChangeDetectionService(changeDetectionService) before any start() path
runs, exposing a non-started instance; change this to only register the service
when change detection is actually startable/active (e.g. after verifying the
feature flags like ignorePreview and adapter availability or after a successful
start() / startability check on changeDetectionService) so consumers cannot
retrieve a non-started service; update the logic around
setActiveChangeDetectionService, gating it by the changeDetectionService's
startability (or post-start success) and remove the unconditional early
registration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cad1e290-5a29-4745-b87b-10633de241f9
📒 Files selected for processing (5)
code/core/src/core-server/change-detection/ChangeDetectionService.tscode/core/src/core-server/change-detection/active-service-registry.tscode/core/src/core-server/change-detection/index.tscode/core/src/core-server/dev-server.tscode/core/src/core-server/index.ts
| async getStoryIdsByFile(): Promise<Map<string, Set<string>>> { | ||
| const generator = await this.options.storyIndexGeneratorPromise; | ||
| const storyIndex = await generator.getIndex(); | ||
| return getStoryIdsByAbsolutePath(this.storyIdsByFileCache, storyIndex, this.workingDir); | ||
| } |
There was a problem hiding this comment.
Avoid returning the mutable internal cache map directly.
This method exposes cached Map<string, Set<string>> references to callers. External mutation can corrupt internal state used later by change-detection scans.
💡 Proposed fix
async getStoryIdsByFile(): Promise<Map<string, Set<string>>> {
const generator = await this.options.storyIndexGeneratorPromise;
const storyIndex = await generator.getIndex();
- return getStoryIdsByAbsolutePath(this.storyIdsByFileCache, storyIndex, this.workingDir);
+ const storyIdsByFile = getStoryIdsByAbsolutePath(
+ this.storyIdsByFileCache,
+ storyIndex,
+ this.workingDir
+ );
+ return new Map(
+ Array.from(storyIdsByFile.entries(), ([filePath, storyIds]) => [filePath, new Set(storyIds)])
+ );
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts` around
lines 516 - 520, getStoryIdsByFile currently returns references into the
internal cache (storyIdsByFileCache) allowing callers to mutate internal state;
change getStoryIdsByFile (in ChangeDetectionService) to return a defensive copy:
after obtaining the Map from getStoryIdsByAbsolutePath, create and return a new
Map with cloned Sets for each key (i.e., for each [k,v] produce [k, new
Set(v)]), so callers receive independent structures and cannot mutate
storyIdsByFileCache or its Sets.
| // Expose to in-process consumers (addon presets) via the active-service registry. | ||
| // Dev server is single-instance, so only one service is ever active. | ||
| setActiveChangeDetectionService(changeDetectionService); | ||
|
|
There was a problem hiding this comment.
Register the active service only when change detection is actually active.
The registry is set before any start() path runs. In cases like ignorePreview, disabled feature, or unavailable adapter, consumers can still retrieve a non-started service instance, which breaks the API contract.
💡 Proposed fix (gate registration by startability)
- // Expose to in-process consumers (addon presets) via the active-service registry.
- // Dev server is single-instance, so only one service is ever active.
- setActiveChangeDetectionService(changeDetectionService); if (!options.ignorePreview) {
if (!features.changeDetection) {
+ setActiveChangeDetectionService(undefined);
changeDetectionService.start(undefined, false);
}
@@
if (features.changeDetection) {
let adapter: ChangeDetectionAdapter | undefined;
@@
- changeDetectionService.start(adapter, true);
+ if (adapter) {
+ setActiveChangeDetectionService(changeDetectionService);
+ } else {
+ setActiveChangeDetectionService(undefined);
+ }
+ changeDetectionService.start(adapter, true);
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@code/core/src/core-server/dev-server.ts` around lines 60 - 63, The active
service is being registered unconditionally via
setActiveChangeDetectionService(changeDetectionService) before any start() path
runs, exposing a non-started instance; change this to only register the service
when change detection is actually startable/active (e.g. after verifying the
feature flags like ignorePreview and adapter availability or after a successful
start() / startability check on changeDetectionService) so consumers cannot
retrieve a non-started service; update the logic around
setActiveChangeDetectionService, gating it by the changeDetectionService's
startability (or post-start success) and remove the unconditional early
registration.
Closes #
What I did
This PR opens the change-detection module's reverse-dependency index to external consumers (e.g. @storybook/addon-mcp) so they can answer "which stories relate to this file?" for any arbitrary list of paths.
The MCP addon needs this for its
get-stories-by-componentandget-changed-storiestools, so that it won't need to reinvent the wheel in order to detect the correlation of a change to stories. This is needed because the change detection is not always accurate (e.g. if you make a change in preview file, it won't detect any change), and with access to the reverse index the agent can decide on what to drill in.This is done through a new preset
experimental_getActiveChangeDetectionServiceand three accessor methods on ChangeDetectionService:getReverseIndex(): ReverseIndexImpl | undefined— live reverse-dep map; same reference patched by IncrementalPatcher.getStoryIdsByFile(): Promise<Map<string, Set<string>>>— join helper, absolute story-file path → declared story IDs.getWorkingDir(): string— for resolving relative paths.Usage from an addon side:
Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
Caution
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsMake sure this PR contains one of the labels below:
Available labels
bug: Internal changes that fixes incorrect behavior.maintenance: User-facing maintenance tasks.dependencies: Upgrading (sometimes downgrading) dependencies.build: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup: Minor cleanup style change. Will not show up in release changelog.documentation: Documentation only changes. Will not show up in release changelog.feature request: Introducing a new feature.BREAKING CHANGE: Changes that break compatibility in some way with current major version.other: Changes that don't fit in the above categories.🦋 Canary release
This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the
@storybookjs/coreteam here.core team members can create a canary release here or locally with
gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>Summary by CodeRabbit