-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Svelte: Repaint PreviewRender on non-Vite builder HMR #34810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
25ccad5
f60dccb
0b878d9
fdc33df
1c2a718
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,50 @@ | ||
| <script module> | ||
| import { writable } from 'svelte/store'; | ||
|
|
||
| /** | ||
| * HMR cycle counter. Increments on every hot-update so any `$derived` | ||
| * that reads it is forced to re-evaluate, and any `{#key}` block keyed | ||
| * on it tears down and re-creates its children. | ||
| * | ||
| * Why this is necessary: Svelte 5's `$.hmr(Component)` returns an | ||
| * identity-stable proxy across updates. `$derived.by(() => storyFn())` | ||
| * compares the returned `{ Component, props }` by identity, sees the | ||
| * same proxy reference, and never re-fires — so the renderer freezes | ||
| * on the pre-HMR component code on any non-Vite builder. | ||
| * | ||
| * This is scoped to webpack/Rspack on purpose. Under Vite, | ||
| * `vite-plugin-svelte` papers over the proxy issue with its own | ||
| * private coordination channels and performs fine-grained HMR that | ||
| * preserves component state. Forcing a full `{#key}` remount there | ||
| * would defeat that state preservation, so we deliberately do NOT | ||
| * listen to `vite:afterUpdate` and let Vite's native HMR handle it. | ||
| * | ||
| * The webpack/Rspack status-handler API is feature-detected so a | ||
| * single PreviewRender module works on every builder without | ||
| * import-time errors. | ||
| */ | ||
| const hmrTick = writable(0); | ||
|
|
||
| if (typeof import.meta !== 'undefined') { | ||
| const hot = import.meta.webpackHot ?? import.meta.hot; | ||
| // webpack / Rspack: status transitions to 'idle' once apply finishes. | ||
| // Vite is intentionally excluded (see comment above). | ||
| if (hot && typeof hot.addStatusHandler === 'function') { | ||
| let inCycle = false; | ||
| hot.addStatusHandler((status) => { | ||
| if (status === 'check' || status === 'prepare') { | ||
| inCycle = true; | ||
| return; | ||
| } | ||
| if (status === 'idle' && inCycle) { | ||
| inCycle = false; | ||
| hmrTick.update((n) => n + 1); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <script> | ||
| /* | ||
| ! DO NOT change this DecoratorHandler import to a relative path, it will break it. | ||
|
|
@@ -8,11 +55,18 @@ | |
|
|
||
| const { name, title, storyFn, showError } = $props(); | ||
|
|
||
| // Touch `hmrTick` so the derivation re-runs on every HMR cycle. Without | ||
| // this, `$derived.by` short-circuits when `storyFn()` returns the same | ||
| // (identity-stable) Svelte HMR proxy reference, and new component code | ||
| // never reaches the DOM. | ||
| const tick = $derived($hmrTick); | ||
|
|
||
| let { | ||
| /** @type {import('svelte').SvelteComponent} */ | ||
| Component, | ||
| props = {}, | ||
| } = $derived.by(() => { | ||
| void tick; | ||
| return storyFn(); | ||
| }); | ||
|
|
||
|
|
@@ -34,5 +88,7 @@ | |
| {#snippet pending()} | ||
| <div id="sb-pending-async-component-notice">Pending async component...</div> | ||
| {/snippet} | ||
| <DecoratorHandler {Component} {props} /> | ||
| {#key tick} | ||
| <DecoratorHandler {Component} {props} /> | ||
| {/key} | ||
|
Comment on lines
+91
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't this make Svelte re-render the whole component tree, losing the state in the process?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — split into two cases. {#key tick} only re-creates the subtree when tick changes, and tick only changes on an HMR cycle, so normal runtime state is unaffected. For webpack/Rspack, a remount on hot-update is the intended trade-off: Svelte 5's $.hmr() returns an identity-stable proxy, $derived.by short-circuits on the unchanged reference, and the renderer otherwise freezes on the pre-HMR code. A full repaint beats a stale render there. You're right that for Vite it would be a regression — vite-plugin-svelte already does fine-grained, state-preserving HMR. I've scoped the tick to the webpack/Rspack addStatusHandler path and dropped the vite:afterUpdate listener, so Vite keeps its native HMR untouched. Pushed as a follow-up commit (f60dccb). |
||
| </svelte:boundary> | ||
Uh oh!
There was an error while loading. Please reload this page.