Skip to content

Commit 3ce5ee7

Browse files
committed
fix: avoid marking subtree as dynamic for inlined attributes
1 parent 4a85c41 commit 3ce5ee7

File tree

8 files changed

+88
-70
lines changed

8 files changed

+88
-70
lines changed

.changeset/spotty-sheep-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: avoid marking subtree as dynamic for inlined attributes

packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/** @import { Context } from '../types' */
33
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
44
import * as e from '../../../errors.js';
5+
import { is_inlinable_expression } from '../../utils.js';
56
import { mark_subtree_dynamic } from './shared/fragment.js';
67

78
/**
@@ -15,9 +16,17 @@ export function ExpressionTag(node, context) {
1516
}
1617
}
1718

18-
// TODO ideally we wouldn't do this here, we'd just do it on encountering
19-
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
20-
mark_subtree_dynamic(context.path);
19+
const attribute_parent = context.path.find((parent) => parent.type === 'Attribute');
20+
/**
21+
* if the expression tag is part of an attribute we want to check if it's inlinable before marking
22+
* the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
23+
* directly making the whole thing actually static.
24+
*/
25+
if (attribute_parent && !is_inlinable_expression(node, context.state.scope)) {
26+
// TODO ideally we wouldn't do this here, we'd just do it on encountering
27+
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
28+
mark_subtree_dynamic(context.path);
29+
}
2130

2231
context.next({ ...context.state, expression: node.metadata.expression });
2332
}

packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
77
import * as w from '../../../warnings.js';
88
import { is_rune } from '../../../../utils.js';
99
import { mark_subtree_dynamic } from './shared/fragment.js';
10+
import { is_inlinable_expression } from '../../utils.js';
1011

1112
/**
1213
* @param {Identifier} node
@@ -20,7 +21,21 @@ export function Identifier(node, context) {
2021
return;
2122
}
2223

23-
mark_subtree_dynamic(context.path);
24+
const expression_tag_parent = context.path.find((parent) => parent.type === 'ExpressionTag');
25+
const attribute_parent = context.path.find((parent) => parent.type === 'Attribute');
26+
27+
/**
28+
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
29+
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
30+
* directly making the whole thing actually static.
31+
*/
32+
if (
33+
!attribute_parent ||
34+
!expression_tag_parent ||
35+
!is_inlinable_expression(expression_tag_parent, context.state.scope)
36+
) {
37+
mark_subtree_dynamic(context.path);
38+
}
2439

2540
// If we are using arguments outside of a function, then throw an error
2641
if (

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

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
2-
/** @import { AST, Binding, SvelteNode } from '#compiler' */
2+
/** @import { Binding, SvelteNode } from '#compiler' */
33
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
44
/** @import { Analysis } from '../../types.js' */
55
/** @import { Scope } from '../../scope.js' */
6-
import * as b from '../../../utils/builders.js';
7-
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
86
import {
9-
PROPS_IS_LAZY_INITIAL,
7+
PROPS_IS_BINDABLE,
108
PROPS_IS_IMMUTABLE,
9+
PROPS_IS_LAZY_INITIAL,
1110
PROPS_IS_RUNES,
12-
PROPS_IS_UPDATED,
13-
PROPS_IS_BINDABLE
11+
PROPS_IS_UPDATED
1412
} from '../../../../constants.js';
1513
import { dev } from '../../../state.js';
14+
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
15+
import * as b from '../../../utils/builders.js';
1616
import { get_value } from './visitors/shared/declarations.js';
1717

1818
/**
@@ -311,43 +311,3 @@ export function create_derived_block_argument(node, context) {
311311
export function create_derived(state, arg) {
312312
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
313313
}
314-
315-
/**
316-
* Whether a variable can be referenced directly from template string.
317-
* @param {import('#compiler').Binding | undefined} binding
318-
* @returns {boolean}
319-
*/
320-
export function can_inline_variable(binding) {
321-
return (
322-
!!binding &&
323-
// in a `<script module>` block
324-
!binding.scope.parent &&
325-
// to prevent the need for escaping
326-
binding.initial?.type === 'Literal'
327-
);
328-
}
329-
330-
/**
331-
* @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes
332-
* @param {import('./types.js').ComponentClientTransformState} state
333-
*/
334-
export function is_inlinable_expression(node_or_nodes, state) {
335-
let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes];
336-
let has_expression_tag = false;
337-
for (let value of nodes) {
338-
if (value.type === 'ExpressionTag') {
339-
if (value.expression.type === 'Identifier') {
340-
const binding = state.scope
341-
.owner(value.expression.name)
342-
?.declarations.get(value.expression.name);
343-
if (!can_inline_variable(binding)) {
344-
return false;
345-
}
346-
} else {
347-
return false;
348-
}
349-
has_expression_tag = true;
350-
}
351-
}
352-
return has_expression_tag;
353-
}

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

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,28 @@
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
55
/** @import { Scope } from '../../../scope' */
6+
import { escape_html } from '../../../../../escaping.js';
67
import {
78
is_boolean_attribute,
89
is_dom_property,
910
is_load_error_element,
1011
is_void
1112
} from '../../../../../utils.js';
12-
import { escape_html } from '../../../../../escaping.js';
1313
import { dev, is_ignored, locator } from '../../../../state.js';
14-
import {
15-
get_attribute_expression,
16-
is_event_attribute,
17-
is_text_attribute
18-
} from '../../../../utils/ast.js';
14+
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
1915
import * as b from '../../../../utils/builders.js';
2016
import { is_custom_element_node } from '../../../nodes.js';
17+
import { is_inlinable_expression } from '../../../utils.js';
2118
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
19+
import { build_getter, create_derived } from '../utils.js';
2220
import {
23-
build_getter,
24-
can_inline_variable,
25-
create_derived,
26-
is_inlinable_expression
27-
} from '../utils.js';
28-
import {
29-
get_attribute_name,
3021
build_attribute_value,
3122
build_class_directives,
23+
build_set_attributes,
3224
build_style_directives,
33-
build_set_attributes
25+
get_attribute_name
3426
} from './shared/element.js';
27+
import { visit_event_attribute } from './shared/events.js';
3528
import { process_children } from './shared/fragment.js';
3629
import {
3730
build_render_statement,
@@ -40,7 +33,6 @@ import {
4033
build_update_assignment,
4134
get_states_and_calls
4235
} from './shared/utils.js';
43-
import { visit_event_attribute } from './shared/events.js';
4436

4537
/**
4638
* @param {AST.RegularElement} node
@@ -589,7 +581,7 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
589581
const inlinable_expression =
590582
attribute.value === true
591583
? false // not an expression
592-
: is_inlinable_expression(attribute.value, context.state);
584+
: is_inlinable_expression(attribute.value, context.state.scope);
593585
if (attribute.metadata.expression.has_state) {
594586
if (has_call) {
595587
state.init.push(build_update(update));

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/** @import { ComponentContext } from '../../types' */
44
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
55
import * as b from '../../../../../utils/builders.js';
6-
import { is_inlinable_expression } from '../../utils.js';
6+
import { is_inlinable_expression } from '../../../../utils.js';
77
import { build_template_literal, build_update } from './utils.js';
88

99
/**
@@ -159,7 +159,7 @@ function is_static_element(node, state) {
159159
!is_text_attribute(attribute) &&
160160
// If the attribute is not a text attribute but is inlinable we will directly inline it in the
161161
// the template so before returning false we need to check that the attribute is not inlinable
162-
!is_inlinable_expression(attribute.value, state)
162+
!is_inlinable_expression(attribute.value, state.scope)
163163
) {
164164
return false;
165165
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/** @import { AST, Binding } from '#compiler' */
2+
3+
/**
4+
* Whether a variable can be referenced directly from template string.
5+
* @param {Binding | undefined} binding
6+
* @returns {boolean}
7+
*/
8+
function can_inline_variable(binding) {
9+
return (
10+
!!binding &&
11+
// in a `<script module>` block
12+
!binding.scope.parent &&
13+
// to prevent the need for escaping
14+
binding.initial?.type === 'Literal'
15+
);
16+
}
17+
18+
/**
19+
* @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes
20+
* @param {import('./scope.js').Scope} scope
21+
*/
22+
export function is_inlinable_expression(node_or_nodes, scope) {
23+
let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes];
24+
let has_expression_tag = false;
25+
for (let value of nodes) {
26+
if (value.type === 'ExpressionTag') {
27+
if (value.expression.type === 'Identifier') {
28+
const binding = scope.owner(value.expression.name)?.declarations.get(value.expression.name);
29+
if (!can_inline_variable(binding)) {
30+
return false;
31+
}
32+
} else {
33+
return false;
34+
}
35+
has_expression_tag = true;
36+
}
37+
}
38+
return has_expression_tag;
39+
}

packages/svelte/tests/snapshot/samples/inline-module-vars/_expected/client/index.svelte.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,5 @@ var root = $.template(`<picture><source srcset="${__DECLARED_ASSET_0__}" type="i
1010
export default function Inline_module_vars($$anchor) {
1111
var picture = root();
1212

13-
$.next(6);
14-
$.reset(picture);
1513
$.append($$anchor, picture);
1614
}

0 commit comments

Comments
 (0)