Skip to content

Vue3: Only reload the preview when docgen changes on hot update - #35705

Merged
huang-julien merged 3 commits into
storybookjs:nextfrom
alliasgher:vue3-hmr-skip-reload-when-docgen-unchanged
Sep 21, 2026
Merged

huang-julien merged 3 commits into
storybookjs:nextfrom
alliasgher:vue3-hmr-skip-reload-when-docgen-unchanged

Conversation

@alliasgher

Copy link
Copy Markdown
Contributor

Closes #35653

What I did

The vue-component-meta plugin's handleHotUpdate sent a Vite full-reload on every file change. The reload only exists to get fresh docgen into the preview, but it fired even when the generated meta was identical, so editing a template, a style block or any component internal reloaded the whole preview iframe and lost transient story state (open dialogs, form values, focus, scroll position).

This PR makes the reload conditional:

  • The transform hook now caches the docgen it emits for each module (emittedMeta).
  • handleHotUpdate still refreshes the checker with the new content (so the checker never goes stale), then recomputes the docgen and compares it to what was last emitted.
  • If the docgen is unchanged, it returns undefined and Vue's own HMR handles the edit, preserving story state.
  • If the docgen changed — or the file has no docgen yet — the previous invalidate + full-reload behaviour is kept unchanged.

The meta extraction that transform already performed is pulled out into a computeMetaSources helper so both hooks share one implementation. Most of the diff is that move; the behavioural change is confined to handleHotUpdate.

This addresses the first half of the issue (preserve Vue HMR when docgen is unchanged). Updating the metadata without a reload when it does change is a bigger change and I left it out of scope here.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Three cases were added to vue-component-meta.test.ts: docgen unchanged (no reload, and the checker is still updated), docgen changed (reload), and a file with no docgen yet (reload). I confirmed the first test fails without the production change.

Manual testing

  1. Clone the reproduction from the issue: https://github.com/Ericlm/storybook-vue-hmr/
  2. Point it at this branch and start Storybook, then open App.stories.ts.
  3. Open the dialog in the story.
  4. Edit the background colour of DialogContent (a style-only change) and save. Expected: the style updates via Vue HMR and the dialog stays open. On next the preview full-reloads and the dialog closes.
  5. Now add or rename a prop on the component and save. Expected: the preview still full-reloads and the Controls panel shows the new prop, i.e. docgen changes are not missed.

Note

I was not able to run the repo's full Vitest suite locally (the monorepo install did not complete in my environment), so I ran vue-component-meta.test.ts against the real plugin source in an isolated Vitest project with storybook/internal/common and storybook/internal/oxc-parser stubbed. All 15 tests pass there (12 existing + 3 new). Please let CI confirm on the real harness.

The vue-component-meta plugin's handleHotUpdate sent a Vite full-reload on
every file change, so editing a template, style or any component internal
reloaded the whole preview iframe and lost transient story state such as
open dialogs, form values, focus and scroll position.

Cache the docgen emitted for each module in the transform hook and, on hot
update, recompute it after refreshing the checker. When the docgen is
unchanged, return undefined so Vue's own HMR handles the edit. The reload
is kept for changes that actually alter the generated docs, and for files
that have no docgen yet.
@github-actions

github-actions Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor
Fails
🚫 This PR needs an approving review from a Storybook Core or Developer Experience team member before it can be merged. No approvals found.

Generated by 🚫 dangerJS against a69a14c

@alliasgher

Copy link
Copy Markdown
Contributor Author

Danger is failing on the required labels (bug, ci:*, qa:*). I can't add those as an outside contributor, so this needs a maintainer to apply them.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: baf1afbf-4ddc-43c0-8b77-535a6e464ee2

📥 Commits

Reviewing files that changed from the base of the PR and between d48adc8 and c91a886.

📒 Files selected for processing (2)
  • code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts
  • code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

The Vue component metadata plugin caches serialized metadata during transforms and recomputes metadata for dependent modules during hot updates. Unchanged metadata preserves normal Vue HMR. Changed or unavailable metadata triggers a full reload.

Changes

Vue component metadata flow

Layer / File(s) Summary
Metadata tracking and dependency discovery
code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts
The transform handler stores serialized metadata. The plugin traverses importer relationships to find dependent modules with cached metadata.
Hot-update metadata reconciliation
code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts
The hot-update handler skips undocumented files, synchronizes the checker, and recomputes dependent metadata. Unchanged metadata returns undefined; changed or unavailable metadata sends a full reload and returns an empty module list.
Typed handler test coverage
code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts
Tests add typed handler loading, a minimal HMR context, checker synchronization, normal HMR for unchanged metadata, and reload behavior for changed or missing metadata.

Sequence Diagram(s)

sequenceDiagram
  participant Vite
  participant vueComponentMeta
  participant Checker
  participant WebSocket
  Vite->>vueComponentMeta: process hot update
  vueComponentMeta->>Checker: synchronize checker
  vueComponentMeta->>vueComponentMeta: recompute dependent metadata
  alt Metadata unchanged
    vueComponentMeta-->>Vite: return undefined
  else Metadata changed or unavailable
    vueComponentMeta->>WebSocket: send full-reload
    vueComponentMeta-->>Vite: return empty module list
  end
Loading

Priority: ➖ Normal — Impact reflects medium issue severity.

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to c91a8

Vue metadata hot updates now preserve story state when generated metadata is unchanged while retaining full reloads when metadata changes or has no cached baseline. No actionable current-head merge risk remains.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts (2)

249-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Configure the changed-docgen mock in beforeEach.

Line 253 changes mock behavior inside the test case and accesses the mock without vi.mocked(). Move this setup into a nested describe block with a beforeEach, and configure it through vi.mocked(mockChecker.getComponentMeta).

As per coding guidelines, “Use vi.mocked() to type and access the mocked functions in Vitest tests” and “Implement mock behaviors in beforeEach blocks in Vitest tests.”

🤖 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/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts` around
lines 249 - 256, Move the changed-docgen setup from the test body into a nested
describe block’s beforeEach, configuring mockChecker.getComponentMeta through
vi.mocked(). Keep the test focused on reload behavior while preserving the
existing returned metadata with the newProp entry.

Source: Coding guidelines


39-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve the Vite hook contract in the test helper.

Line 41 casts handleHotUpdate to accept unknown. This prevents TypeScript from validating that makeHotUpdateContext matches the Vite HMR context. Type the handler and ctx from the plugin hook signature so changes to required fields, such as modules or server, fail at compile time.

As per coding guidelines, “Encode invariants with TypeScript types and existing lint rules first.”

🤖 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/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts` around
lines 39 - 65, Update makeHotUpdateContext and the plugin wrapper to derive the
handleHotUpdate parameter type from the Vite hook signature instead of using
unknown and a manually shaped context. Type the returned ctx and helper callback
against that inferred hook parameter so required fields such as modules and
server are checked by TypeScript.

Source: Coding guidelines

code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts (1)

36-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: computeMetaSources mixes several concerns in one function.

The function gathers export names, calls applyTempFixForEventDescriptions, filters empty/unknown meta, removes nested schemas, and de-duplicates exposed entries, all inline in one forEach. Extracting the per-component normalization (lines 59-113) into a small helper (for example normalizeComponentMeta(meta, exportName, id)) would let each concern be unit-tested and read independently, without changing behavior.

This is optional. The current extraction from the old inline transform logic is already a net improvement.

🤖 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/frameworks/vue3-vite/src/plugins/vue-component-meta.ts` around lines 36
- 117, Optionally refactor computeMetaSources by extracting the per-component
normalization logic into a helper such as normalizeComponentMeta(meta,
exportName, id). Keep export collection, applyTempFixForEventDescriptions,
empty/unknown filtering, nested-schema removal, exposed-entry de-duplication,
and generated MetaSource values behaviorally unchanged.
🤖 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/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 129-130: In the transform flow around computeMetaSources, check
metaSources.length before caching the result in emittedMeta. Only call
emittedMeta.set for non-empty metadata, while preserving the existing undefined
return for empty docgen so handleHotUpdate retains full-reload behavior.

---

Nitpick comments:
In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts`:
- Around line 249-256: Move the changed-docgen setup from the test body into a
nested describe block’s beforeEach, configuring mockChecker.getComponentMeta
through vi.mocked(). Keep the test focused on reload behavior while preserving
the existing returned metadata with the newProp entry.
- Around line 39-65: Update makeHotUpdateContext and the plugin wrapper to
derive the handleHotUpdate parameter type from the Vite hook signature instead
of using unknown and a manually shaped context. Type the returned ctx and helper
callback against that inferred hook parameter so required fields such as modules
and server are checked by TypeScript.

In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 36-117: Optionally refactor computeMetaSources by extracting the
per-component normalization logic into a helper such as
normalizeComponentMeta(meta, exportName, id). Keep export collection,
applyTempFixForEventDescriptions, empty/unknown filtering, nested-schema
removal, exposed-entry de-duplication, and generated MetaSource values
behaviorally unchanged.
🪄 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 Plus

Run ID: 754a8f8e-235b-450d-8328-6038e7bf5692

📥 Commits

Reviewing files that changed from the base of the PR and between 3e12dfc and 927706f.

📒 Files selected for processing (2)
  • code/frameworks/vue3-vite/src/plugins/vue-component-meta.test.ts
  • code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts

Comment thread code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts Outdated

@storybook-contribution-review storybook-contribution-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 I am the Storybook contribution review bot; a human maintainer approved this message before it was posted.
Review phase: Architecture review (stage 1 of 3)

We are trying out something new: a three-stage automated review - architecture, then code quality, then runtime verification - that runs on a PR before a human maintainer looks at it closely. A maintainer still reads this and still makes the call; the aim is that by the time they do, the reachability and evidence work is already done. Tell us how it lands for you, that feedback is genuinely useful to us right now.

Hi @alliasgher,

Thank you for tackling the Vue HMR state-loss problem. Keeping this decision inside the @storybook/vue3-vite plugin is the right ownership boundary, and preserving open dialogs, form state, focus, and scroll position during style-only edits is a valuable improvement.

I am requesting changes because the cache currently compares only the file passed to handleHotUpdate. Vue component metadata is whole-program output, so a dependency can change an already-emitted component's docgen even when that dependency has no docgen of its own.

There is a concrete path in the existing Vue fixture: component.vue imports the Severity enum from severity.ts. After both files have been transformed, severity.ts is cached as []. If that enum changes, checker.updateFile refreshes the program, but the hook recomputes only severity.ts, compares [] with [], and returns undefined. That suppresses the full reload even though component.vue's injected prop metadata changed, which can leave Controls and docs stale. The three new tests cover same-file changes and an uncached file, but not this dependency case.

To make this acceptable:

  • Rebase onto current next and keep normalized extraction in the existing collectComponentMetaSources path exposed through experimental_vueDocgenEngine; please do not restore a second computeMetaSources implementation in vue3-vite.
  • Cache only component module IDs whose transform actually emitted __docgenInfo.
  • After checker.updateFile, recompute every potentially affected cached component. Recomputing all cached emitted component IDs is a safe first version; a reverse-dependency index is also fine if it includes type-only imports that Vite may erase.
  • When a change is detected, reload without advancing the cached baseline first. Let the subsequent transform store the metadata that was actually emitted.
  • Add a regression test where a cached component imports a type or enum from another .ts file and editing that dependency changes the component metadata and triggers a reload. Please keep the unchanged style/template case too.

This review is against head 927706f90c35311717406869cedadcabfb1cd5f4. Once the dependency-aware invalidation path is covered, the architecture gate can run again.

@huang-julien
huang-julien self-requested a review September 7, 2026 07:57
@huang-julien huang-julien self-assigned this Sep 7, 2026
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@storybook-contribution-review storybook-contribution-review Bot added the review:changes-requested Review paused: changes requested; the review comment names the failing stage label Sep 8, 2026
@huang-julien huang-julien added the ci:normal Run our default set of CI jobs (choose this for most PRs). label Sep 9, 2026
@storybook-app-bot

storybook-app-bot Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Package Benchmarks

Commit: a69a14c, ran on 21 September 2026 at 07:39:22 UTC

No significant changes detected, all good. 👏

@storybook-contribution-review storybook-contribution-review Bot added sev:S3 Medium priority. Fix within months if possible. and removed sev:S2 High priority. Fix within weeks. labels Sep 11, 2026
@huang-julien huang-julien added the qa:needed Pull Requests that will need manual QA prior to release. label Sep 21, 2026
@huang-julien
huang-julien merged commit 124f51f into storybookjs:next Sep 21, 2026
128 of 140 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-scan:human bug ci:normal Run our default set of CI jobs (choose this for most PRs). docgen hmr needs triage qa:needed Pull Requests that will need manual QA prior to release. review:changes-requested Review paused: changes requested; the review comment names the failing stage sev:S3 Medium priority. Fix within months if possible. vue vue3

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: vue-component-meta triggers a full preview reload on every file change

3 participants