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

@ -1,11 +1,13 @@
import { DEV } from 'esm-env';
import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { empty } from '../operations.js';
/** /**
* @template P * @template P
* @template {(props: P) => void} C * @template {(props: P) => void} C
* @param {import('#client').TemplateNode} anchor * @param {import('#client').TemplateNode} anchor
* @param {() => C} get_component * @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} * @returns {void}
*/ */
export function component(anchor, get_component, render_fn) { 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} */ /** @type {import('#client').Effect | null} */
let effect; 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, () => { block(anchor, 0, () => {
if (component === (component = get_component())) return; if (component === (component = get_component())) return;
@ -24,7 +31,8 @@ export function component(anchor, get_component, render_fn) {
} }
if (component) { 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