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 4 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) {
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(b.member(id, b.id('$.HMR'), true), 'update'), 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))))
]);

@ -1,23 +1,25 @@
/** @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 { set, source } 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<Component>} get_source
* @param {Component} fn
*/
export function hmr(original, get_source) {
export function hmr(fn) {
const current = source(fn);
/**
* @param {TemplateNode} anchor
* @param {any} props
*/
function wrapper(anchor, props) {
let component = {};
let instance = {};
/** @type {Effect} */
@ -26,8 +28,9 @@ export function hmr(original, get_source) {
let ran = false;
block(() => {
const source = get_source();
const component = get(source);
if (component === (component = get(current))) {
return;
}
if (effect) {
// @ts-ignore
@ -62,16 +65,24 @@ export function hmr(original, get_source) {
}
// @ts-expect-error
wrapper[FILENAME] = original[FILENAME];
wrapper[FILENAME] = fn[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)
fn,
current,
update: (/** @type {any} */ incoming) => {
// This logic ensures that the first version of the component is the one
// whose update function and therefore block effect is preserved across updates.
// 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;

@ -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].update(module.default);
});
}

@ -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,

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

Loading…
Cancel
Save