fix: prevent infinite loop when HMRing a component with an `await` (#17380)

* fix: prevent infinite loop when HMRing a component with an `await`

* update test

* fix

* reintroduce old logic to fix ever-growing stack of block effects

* update snapshot

* tweak names for clarity, simplify `update` slightly

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/17389/head
Rich Harris 9 months ago committed by GitHub
parent 435da13fdd
commit 95a895a9d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent infinite loop when HMRing a component with an `await`

@ -519,14 +519,9 @@ export function client_component(analysis, options) {
if (options.hmr) { if (options.hmr) {
const id = b.id(analysis.name); 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 = [ const accept_fn_body = [
b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))), b.stmt(b.call(b.member(b.member(id, b.id('$.HMR'), true), 'update'), b.id('module.default')))
b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original')))
]; ];
if (analysis.css.hash) { if (analysis.css.hash) {
@ -535,8 +530,7 @@ export function client_component(analysis, options) {
} }
const hmr = b.block([ 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)))) b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
]); ]);

@ -1,23 +1,25 @@
/** @import { Source, Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js'; import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '#client/constants'; import { EFFECT_TRANSPARENT } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js'; import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js'; import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js'; import { set_should_intro } from '../render.js';
import { get } from '../runtime.js'; import { get } from '../runtime.js';
/** /**
* @template {(anchor: Comment, props: any) => any} Component * @template {(anchor: Comment, props: any) => any} Component
* @param {Component} original * @param {Component} fn
* @param {() => Source<Component>} get_source
*/ */
export function hmr(original, get_source) { export function hmr(fn) {
const current = source(fn);
/** /**
* @param {TemplateNode} anchor * @param {TemplateNode} anchor
* @param {any} props * @param {any} props
*/ */
function wrapper(anchor, props) { function wrapper(anchor, props) {
let component = {};
let instance = {}; let instance = {};
/** @type {Effect} */ /** @type {Effect} */
@ -26,8 +28,9 @@ export function hmr(original, get_source) {
let ran = false; let ran = false;
block(() => { block(() => {
const source = get_source(); if (component === (component = get(current))) {
const component = get(source); return;
}
if (effect) { if (effect) {
// @ts-ignore // @ts-ignore
@ -62,16 +65,24 @@ export function hmr(original, get_source) {
} }
// @ts-expect-error // @ts-expect-error
wrapper[FILENAME] = original[FILENAME]; wrapper[FILENAME] = fn[FILENAME];
// @ts-ignore // @ts-ignore
wrapper[HMR] = { wrapper[HMR] = {
// When we accept an update, we set the original source to the new component fn,
original, current,
// The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept` update: (/** @type {any} */ incoming) => {
// function we always replace it with `previous[HMR].source`, which in practice // This logic ensures that the first version of the component is the one
// means we only ever update the original // whose update function and therefore block effect is preserved across updates.
source: source(original) // If we don't do this dance and instead just use `incoming` as the new component
// and then update, we'll create an ever-growing stack of block effects.
// Trigger the original block effect
set(wrapper[HMR].current, incoming[HMR].fn);
// Replace the incoming source with the original one
incoming[HMR].current = wrapper[HMR].current;
}
}; };
return wrapper; return wrapper;

@ -11,11 +11,10 @@ function Hmr($$anchor) {
} }
if (import.meta.hot) { if (import.meta.hot) {
Hmr = $.hmr(Hmr, () => Hmr[$.HMR].source); Hmr = $.hmr(Hmr);
import.meta.hot.accept((module) => { import.meta.hot.accept((module) => {
module.default[$.HMR].source = Hmr[$.HMR].source; Hmr[$.HMR].update(module.default);
$.set(Hmr[$.HMR].source, module.default[$.HMR].original);
}); });
} }

@ -95,6 +95,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
if (generate === 'server' || FROM_HTML) { if (generate === 'server' || FROM_HTML) {
from_html = compile(source, { from_html = compile(source, {
dev: DEV, dev: DEV,
hmr: DEV,
filename: input, filename: input,
generate, generate,
runes: argv.values.runes, runes: argv.values.runes,

@ -1,6 +1,6 @@
export default { export default {
compilerOptions: { compilerOptions: {
hmr: false, hmr: true,
experimental: { experimental: {
async: true async: true

Loading…
Cancel
Save