Skip to content

Commit 2c807ad

Browse files
authored
fix: store DOM boundaries on effects (#12215)
* WIP * progress * fix * add comment * update tests * mostly fix dynamic elements * delete unused code * remove unused code * more * tidy up * fix * more * relink effects inside each block * simpler to just leave these in (without children) and ignore them * fix head stuff * tidy up * fix some type errors * simplify * use hydration marker as effect boundary where possible * all tests passing * tidy up * tidy up a bit * tidy up * beat typescript into submission * bring tests over from fix-each-dom-bug * tweaks * simplify a tad * tidy up * simplify * reduce indirection * belt and braces * tidy * revert config change * missed a spot * regenerate types * cleaner separation between EachState and EachItem - precursor to efficient relinking * HMR fix * set effects * FINALLY
1 parent 7c95c7b commit 2c807ad

File tree

37 files changed

+534
-343
lines changed

37 files changed

+534
-343
lines changed

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,10 @@ function special(parser) {
594594
type: 'RenderTag',
595595
start,
596596
end: parser.index,
597-
expression: expression
597+
expression: expression,
598+
metadata: {
599+
dynamic: false
600+
}
598601
});
599602
}
600603
}

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,13 @@ const common_visitors = {
15201520
return;
15211521
}
15221522
}
1523+
},
1524+
Component(node, context) {
1525+
const binding = context.state.scope.get(
1526+
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
1527+
);
1528+
1529+
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
15231530
}
15241531
};
15251532

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,11 @@ const validation = {
633633
});
634634
},
635635
RenderTag(node, context) {
636+
const callee = unwrap_optional(node.expression).callee;
637+
638+
node.metadata.dynamic =
639+
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
640+
636641
context.state.analysis.uses_render_tags = true;
637642

638643
const raw_args = unwrap_optional(node.expression).arguments;
@@ -642,7 +647,6 @@ const validation = {
642647
}
643648
}
644649

645-
const callee = unwrap_optional(node.expression).callee;
646650
if (
647651
callee.type === 'MemberExpression' &&
648652
callee.property.type === 'Identifier' &&

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
EACH_KEYED,
3737
is_capture_event,
3838
TEMPLATE_FRAGMENT,
39+
TEMPLATE_UNSET_START,
3940
TEMPLATE_USE_IMPORT_NODE,
4041
TRANSITION_GLOBAL,
4142
TRANSITION_IN,
@@ -942,6 +943,7 @@ function serialize_inline_component(node, component_name, context) {
942943
fn = (node_id) => {
943944
return b.call(
944945
'$.component',
946+
node_id,
945947
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
946948
b.arrow(
947949
[b.id(component_name)],
@@ -1680,14 +1682,35 @@ export const template_visitors = {
16801682

16811683
process_children(trimmed, expression, false, { ...context, state });
16821684

1685+
var first = trimmed[0];
1686+
1687+
/**
1688+
* If the first item in an effect is a static slot or render tag, it will clone
1689+
* a template but without creating a child effect. In these cases, we need to keep
1690+
* the current `effect.nodes.start` undefined, so that it can be populated by
1691+
* the item in question
1692+
* TODO come up with a better name than `unset`
1693+
*/
1694+
var unset = false;
1695+
1696+
if (first.type === 'SlotElement') unset = true;
1697+
if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true;
1698+
if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) {
1699+
unset = true;
1700+
}
1701+
16831702
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
16841703

16851704
if (use_comment_template) {
16861705
// special case — we can use `$.comment` instead of creating a unique template
1687-
body.push(b.var(id, b.call('$.comment')));
1706+
body.push(b.var(id, b.call('$.comment', unset && b.literal(unset))));
16881707
} else {
16891708
let flags = TEMPLATE_FRAGMENT;
16901709

1710+
if (unset) {
1711+
flags |= TEMPLATE_UNSET_START;
1712+
}
1713+
16911714
if (state.metadata.context.template_needs_import_node) {
16921715
flags |= TEMPLATE_USE_IMPORT_NODE;
16931716
}
@@ -1832,27 +1855,26 @@ export const template_visitors = {
18321855
context.state.template.push('<!>');
18331856
const callee = unwrap_optional(node.expression).callee;
18341857
const raw_args = unwrap_optional(node.expression).arguments;
1835-
const is_reactive =
1836-
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
18371858

1838-
/** @type {import('estree').Expression[]} */
1839-
const args = [context.state.node];
1840-
for (const arg of raw_args) {
1841-
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
1842-
}
1859+
const args = raw_args.map((arg) =>
1860+
b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))
1861+
);
18431862

18441863
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
18451864
if (context.state.options.dev) {
18461865
snippet_function = b.call('$.validate_snippet', snippet_function);
18471866
}
18481867

1849-
if (is_reactive) {
1850-
context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args)));
1868+
if (node.metadata.dynamic) {
1869+
context.state.init.push(
1870+
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
1871+
);
18511872
} else {
18521873
context.state.init.push(
18531874
b.stmt(
18541875
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
18551876
snippet_function,
1877+
context.state.node,
18561878
...args
18571879
)
18581880
)
@@ -1915,7 +1937,7 @@ export const template_visitors = {
19151937
}
19161938

19171939
if (node.name === 'noscript') {
1918-
context.state.template.push('<!>');
1940+
context.state.template.push('<noscript></noscript>');
19191941
return;
19201942
}
19211943
if (node.name === 'script') {
@@ -2985,16 +3007,14 @@ export const template_visitors = {
29853007
}
29863008
},
29873009
Component(node, context) {
2988-
const binding = context.state.scope.get(
2989-
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
2990-
);
2991-
if (binding !== null && binding.kind !== 'normal') {
3010+
if (node.metadata.dynamic) {
29923011
// Handle dynamic references to what seems like static inline components
29933012
const component = serialize_inline_component(node, '$$component', context);
29943013
context.state.init.push(
29953014
b.stmt(
29963015
b.call(
29973016
'$.component',
3017+
context.state.node,
29983018
// TODO use untrack here to not update when binding changes?
29993019
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
30003020
b.thunk(
@@ -3006,6 +3026,7 @@ export const template_visitors = {
30063026
);
30073027
return;
30083028
}
3029+
30093030
const component = serialize_inline_component(node, node.name, context);
30103031
context.state.init.push(component);
30113032
},

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ export interface DebugTag extends BaseNode {
152152
export interface RenderTag extends BaseNode {
153153
type: 'RenderTag';
154154
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
155+
metadata: {
156+
dynamic: boolean;
157+
};
155158
}
156159

157160
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;
@@ -271,6 +274,9 @@ interface BaseElement extends BaseNode {
271274

272275
export interface Component extends BaseElement {
273276
type: 'Component';
277+
metadata: {
278+
dynamic: boolean;
279+
};
274280
}
275281

276282
interface TitleElement extends BaseElement {

packages/svelte/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const TRANSITION_GLOBAL = 1 << 2;
1818

1919
export const TEMPLATE_FRAGMENT = 1;
2020
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
21+
export const TEMPLATE_UNSET_START = 1 << 2;
2122

2223
export const HYDRATION_START = '[';
2324
export const HYDRATION_END = ']';

packages/svelte/src/internal/client/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const EFFECT_TRANSPARENT = 1 << 15;
1717
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
1818
export const LEGACY_DERIVED_PROP = 1 << 16;
1919
export const INSPECT_EFFECT = 1 << 17;
20+
export const HEAD_EFFECT = 1 << 18;
2021

2122
export const STATE_SYMBOL = Symbol('$state');
2223
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');

packages/svelte/src/internal/client/dev/hmr.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function hmr(source) {
1818
/** @type {import("#client").Effect} */
1919
let effect;
2020

21-
block(() => {
21+
block(anchor, 0, () => {
2222
const component = get(source);
2323

2424
if (effect) {

packages/svelte/src/internal/client/dom/blocks/await.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
105105
}
106106
}
107107

108-
var effect = block(() => {
108+
var effect = block(anchor, 0, () => {
109109
if (input === (input = get_input())) return;
110110

111111
if (is_promise(input)) {

0 commit comments

Comments
 (0)