From 7dcbf7932d4eed9cac9e8eae91ad04acd588a140 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 19 Dec 2025 21:12:06 -0500 Subject: [PATCH 1/3] fix: prevent infinite loop when HMRing a component with an `await` --- .changeset/giant-gifts-mate.md | 5 ++++ .../3-transform/client/transform-client.js | 10 ++----- .../svelte/src/internal/client/dev/hmr.js | 29 +++++++++---------- playgrounds/sandbox/run.js | 1 + 4 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 .changeset/giant-gifts-mate.md diff --git a/.changeset/giant-gifts-mate.md b/.changeset/giant-gifts-mate.md new file mode 100644 index 000000000000..dc2486b69816 --- /dev/null +++ b/.changeset/giant-gifts-mate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite loop when HMRing a component with an `await` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index f51042eb7c62..d16b910f714d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -519,14 +519,9 @@ export function client_component(analysis, options) { if (options.hmr) { const id = b.id(analysis.name); - const HMR = b.id('$.HMR'); - - const existing = b.member(id, HMR, true); - const incoming = b.member(b.id('module.default'), HMR, true); const accept_fn_body = [ - b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))), - b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original'))) + b.stmt(b.call(b.member(id, b.id('$.HMR'), true), b.id('module.default'))) ]; if (analysis.css.hash) { @@ -535,8 +530,7 @@ export function client_component(analysis, options) { } const hmr = b.block([ - b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))), - + b.stmt(b.assignment('=', id, b.call('$.hmr', id))), b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body)))) ]); diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 709a1b272220..c44d929bb2e9 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -1,18 +1,20 @@ -/** @import { Source, Effect, TemplateNode } from '#client' */ +/** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HMR } from '../../../constants.js'; import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_node, hydrating } from '../dom/hydration.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js'; -import { source } from '../reactivity/sources.js'; +import { source, update } from '../reactivity/sources.js'; import { set_should_intro } from '../render.js'; import { get } from '../runtime.js'; /** * @template {(anchor: Comment, props: any) => any} Component - * @param {Component} original - * @param {() => Source} get_source + * @param {Component} component */ -export function hmr(original, get_source) { +export function hmr(component) { + let v = -1; + let s = source(0); + /** * @param {TemplateNode} anchor * @param {any} props @@ -26,8 +28,9 @@ export function hmr(original, get_source) { let ran = false; block(() => { - const source = get_source(); - const component = get(source); + if (v === (v = get(s))) { + return; + } if (effect) { // @ts-ignore @@ -62,16 +65,12 @@ export function hmr(original, get_source) { } // @ts-expect-error - wrapper[FILENAME] = original[FILENAME]; + wrapper[FILENAME] = component[FILENAME]; // @ts-ignore - wrapper[HMR] = { - // When we accept an update, we set the original source to the new component - original, - // The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept` - // function we always replace it with `previous[HMR].source`, which in practice - // means we only ever update the original - source: source(original) + wrapper[HMR] = (c) => { + component = c; + update(s); }; return wrapper; diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 35bffb67a22d..f79243d4e7de 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -95,6 +95,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { if (generate === 'server' || FROM_HTML) { from_html = compile(source, { dev: DEV, + hmr: DEV, filename: input, generate, runes: argv.values.runes, From 8fc4f25839fb116e11549035bd973ea1b6e20a0b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 19 Dec 2025 21:26:02 -0500 Subject: [PATCH 2/3] update test --- .../snapshot/samples/hmr/_expected/client/index.svelte.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 1fac1338c5f9..5878c51aaed5 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -11,11 +11,10 @@ function Hmr($$anchor) { } if (import.meta.hot) { - Hmr = $.hmr(Hmr, () => Hmr[$.HMR].source); + Hmr = $.hmr(Hmr); import.meta.hot.accept((module) => { - module.default[$.HMR].source = Hmr[$.HMR].source; - $.set(Hmr[$.HMR].source, module.default[$.HMR].original); + Hmr[$.HMR](module.default); }); } From 0625ec8ffaedc794e3db7c282f3e2b5eb4f51207 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 19 Dec 2025 21:32:54 -0500 Subject: [PATCH 3/3] fix --- packages/svelte/src/internal/client/dev/hmr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index c44d929bb2e9..2303336ef664 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -12,7 +12,6 @@ import { get } from '../runtime.js'; * @param {Component} component */ export function hmr(component) { - let v = -1; let s = source(0); /** @@ -20,6 +19,7 @@ export function hmr(component) { * @param {any} props */ function wrapper(anchor, props) { + let v = -1; let instance = {}; /** @type {Effect} */