fix: add dummy anchor for dynamic component HMR wrappers (#12252)

* add failing test

* add dummy anchor for HMR wrappers

* lint
pull/12246/head
Rich Harris 6 months ago committed by GitHub
parent 9f51760dc6
commit 9a293ea8c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -653,9 +653,10 @@ function collect_parent_each_blocks(context) {
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
* @param {import('../types.js').ComponentContext} context
* @param {import('estree').Expression} anchor
* @returns {import('estree').Statement}
*/
function serialize_inline_component(node, component_name, context) {
function serialize_inline_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<import('estree').Property[] | import('estree').Expression>} */
const props_and_spreads = [];
@ -946,13 +947,13 @@ function serialize_inline_component(node, component_name, context) {
node_id,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id(component_name)],
[b.id('$$anchor'), b.id(component_name)],
b.block([
...binding_initializers,
b.stmt(
context.state.options.dev
? b.call('$.validate_dynamic_component', b.thunk(prev(node_id)))
: prev(node_id)
? b.call('$.validate_dynamic_component', b.thunk(prev(b.id('$$anchor'))))
: prev(b.id('$$anchor'))
)
])
)
@ -970,12 +971,12 @@ function serialize_inline_component(node, component_name, context) {
);
statements.push(
b.stmt(b.call('$.css_props', context.state.node, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(context.state.node, b.id('lastChild'))))
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(anchor, b.id('lastChild'))))
);
} else {
context.state.template.push('<!>');
statements.push(b.stmt(fn(context.state.node)));
statements.push(b.stmt(fn(anchor)));
}
return statements.length > 1 ? b.block(statements) : statements[0];
@ -3025,7 +3026,7 @@ export const template_visitors = {
Component(node, context) {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context);
const component = serialize_inline_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
b.stmt(
b.call(
@ -3036,7 +3037,7 @@ export const template_visitors = {
b.thunk(
/** @type {import('estree').Expression} */ (context.visit(b.member_id(node.name)))
),
b.arrow([b.id('$$component')], b.block([component]))
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
)
)
);

@ -18,7 +18,7 @@ export function hmr(source) {
/** @type {import("#client").Effect} */
let effect;
block(null, 0, () => {
block(anchor, 0, () => {
const component = get(source);
if (effect) {

@ -1,11 +1,13 @@
import { DEV } from 'esm-env';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { empty } from '../operations.js';
/**
* @template P
* @template {(props: P) => void} C
* @param {import('#client').TemplateNode} anchor
* @param {() => C} get_component
* @param {(component: C) => import('#client').Dom | void} render_fn
* @param {(anchor: import('#client').TemplateNode, component: C) => import('#client').Dom | void} render_fn
* @returns {void}
*/
export function component(anchor, get_component, render_fn) {
@ -15,6 +17,11 @@ export function component(anchor, get_component, render_fn) {
/** @type {import('#client').Effect | null} */
let effect;
var component_anchor = anchor;
// create a dummy anchor for the HMR wrapper, if such there be
if (DEV) component_anchor = empty();
block(anchor, 0, () => {
if (component === (component = get_component())) return;
@ -24,7 +31,8 @@ export function component(anchor, get_component, render_fn) {
}
if (component) {
effect = branch(() => render_fn(component));
if (DEV) anchor.before(component_anchor);
effect = branch(() => render_fn(component_anchor, component));
}
});
}

@ -0,0 +1,21 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
hmr: true
},
html: `<button>toggle</button>`,
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>toggle</button><p>hello</p>`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>toggle</button>`);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
let open = $state(false);
</script>
<button onclick={() => (open = !open)}>toggle</button>
{#if open}
<Child />
{/if}
Loading…
Cancel
Save