Skip to content

Commit 0fa43e2

Browse files
fix: consider static attributes that are inlined in the template (#14249)
* fix: consider static attributes that are inlined in the template * fix: use `is_inlinable_expression` * fix: move check for inlinable expression as last * fix: simplify and correct * chore: accept single node in `is_inlinable_expression` * chore: update comment * chore: add snapshots for non static nodes
1 parent 832499f commit 0fa43e2

File tree

8 files changed

+91
-44
lines changed

8 files changed

+91
-44
lines changed

.changeset/mighty-ads-appear.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: consider static attributes that are inlined in the template

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
2-
/** @import { Binding, SvelteNode } from '#compiler' */
2+
/** @import { AST, Binding, SvelteNode } from '#compiler' */
33
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
44
/** @import { Analysis } from '../../types.js' */
55
/** @import { Scope } from '../../scope.js' */
@@ -326,3 +326,28 @@ export function can_inline_variable(binding) {
326326
binding.initial?.type === 'Literal'
327327
);
328328
}
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: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import {
1919
import * as b from '../../../../utils/builders.js';
2020
import { is_custom_element_node } from '../../../nodes.js';
2121
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
22-
import { build_getter, can_inline_variable, create_derived } from '../utils.js';
22+
import {
23+
build_getter,
24+
can_inline_variable,
25+
create_derived,
26+
is_inlinable_expression
27+
} from '../utils.js';
2328
import {
2429
get_attribute_name,
2530
build_attribute_value,
@@ -584,10 +589,7 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
584589
const inlinable_expression =
585590
attribute.value === true
586591
? false // not an expression
587-
: is_inlinable_expression(
588-
Array.isArray(attribute.value) ? attribute.value : [attribute.value],
589-
context.state
590-
);
592+
: is_inlinable_expression(attribute.value, context.state);
591593
if (attribute.metadata.expression.has_state) {
592594
if (has_call) {
593595
state.init.push(build_update(update));
@@ -605,30 +607,6 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
605607
}
606608
}
607609

608-
/**
609-
* @param {(AST.Text | AST.ExpressionTag)[]} nodes
610-
* @param {import('../types.js').ComponentClientTransformState} state
611-
*/
612-
function is_inlinable_expression(nodes, state) {
613-
let has_expression_tag = false;
614-
for (let value of nodes) {
615-
if (value.type === 'ExpressionTag') {
616-
if (value.expression.type === 'Identifier') {
617-
const binding = state.scope
618-
.owner(value.expression.name)
619-
?.declarations.get(value.expression.name);
620-
if (!can_inline_variable(binding)) {
621-
return false;
622-
}
623-
} else {
624-
return false;
625-
}
626-
has_expression_tag = true;
627-
}
628-
}
629-
return has_expression_tag;
630-
}
631-
632610
/**
633611
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.
634612
* @param {Identifier} node_id

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +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';
67
import { build_template_literal, build_update } from './utils.js';
78

89
/**
@@ -97,7 +98,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
9798

9899
let child_state = state;
99100

100-
if (is_static_element(node)) {
101+
if (is_static_element(node, state)) {
101102
skipped += 1;
102103
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
103104
node.metadata.is_controlled = true;
@@ -124,10 +125,12 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
124125

125126
/**
126127
* @param {SvelteNode} node
128+
* @param {ComponentContext["state"]} state
127129
*/
128-
function is_static_element(node) {
130+
function is_static_element(node, state) {
129131
if (node.type !== 'RegularElement') return false;
130132
if (node.fragment.metadata.dynamic) return false;
133+
if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties
131134

132135
for (const attribute of node.attributes) {
133136
if (attribute.type !== 'Attribute') {
@@ -138,10 +141,6 @@ function is_static_element(node) {
138141
return false;
139142
}
140143

141-
if (attribute.value !== true && !is_text_attribute(attribute)) {
142-
return false;
143-
}
144-
145144
if (attribute.name === 'autofocus' || attribute.name === 'muted') {
146145
return false;
147146
}
@@ -155,8 +154,14 @@ function is_static_element(node) {
155154
return false;
156155
}
157156

158-
if (node.name.includes('-')) {
159-
return false; // we're setting all attributes on custom elements through properties
157+
if (
158+
attribute.value !== true &&
159+
!is_text_attribute(attribute) &&
160+
// If the attribute is not a text attribute but is inlinable we will directly inline it in the
161+
// the template so before returning false we need to check that the attribute is not inlinable
162+
!is_inlinable_expression(attribute.value, state)
163+
) {
164+
return false;
160165
}
161166
}
162167

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ var root = $.template(`<picture><source srcset="${__DECLARED_ASSET_0__}" type="i
99

1010
export default function Inline_module_vars($$anchor) {
1111
var picture = root();
12-
var source = $.child(picture);
13-
var source_1 = $.sibling(source, 2);
14-
var source_2 = $.sibling(source_1, 2);
15-
var img = $.sibling(source_2, 2);
1612

13+
$.next(6);
1714
$.reset(picture);
1815
$.append($$anchor, picture);
1916
}

packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "svelte/internal/disclose-version";
22
import * as $ from "svelte/internal/client";
33

4-
var root = $.template(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1> </h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> <!> <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements></custom-elements></cant-skip>`, 3);
4+
var root = $.template(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1> </h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> <!> <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements></custom-elements></cant-skip> <div><input></div> <div><source></div> <select><option>a</option></select> <img src="..." alt="" loading="lazy">`, 3);
55

66
export default function Skip_static_subtree($$anchor, $$props) {
77
var fragment = root();
@@ -22,6 +22,28 @@ export default function Skip_static_subtree($$anchor, $$props) {
2222

2323
$.set_custom_element_data(custom_elements, "with", "attributes");
2424
$.reset(cant_skip);
25+
26+
var div = $.sibling(cant_skip, 2);
27+
var input = $.child(div);
28+
29+
$.autofocus(input, true);
30+
$.reset(div);
31+
32+
var div_1 = $.sibling(div, 2);
33+
var source = $.child(div_1);
34+
35+
source.muted = true;
36+
$.reset(div_1);
37+
38+
var select = $.sibling(div_1, 2);
39+
var option = $.child(select);
40+
41+
option.value = null == (option.__value = "a") ? "" : "a";
42+
$.reset(select);
43+
44+
var img = $.sibling(select, 2);
45+
2546
$.template_effect(() => $.set_text(text, $$props.title));
47+
$.handle_lazy_img(img);
2648
$.append($$anchor, fragment);
2749
}

packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import * as $ from "svelte/internal/server";
33
export default function Skip_static_subtree($$payload, $$props) {
44
let { title, content } = $$props;
55

6-
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip>`;
6+
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus></div> <div><source muted></div> <select><option value="a">a</option></select> <img src="..." alt="" loading="lazy">`;
77
}

packages/svelte/tests/snapshot/samples/skip-static-subtree/index.svelte

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,18 @@
3030
<cant-skip>
3131
<custom-elements with="attributes"></custom-elements>
3232
</cant-skip>
33+
34+
<div>
35+
<!-- svelte-ignore a11y_autofocus -->
36+
<input autofocus />
37+
</div>
38+
39+
<div>
40+
<source muted />
41+
</div>
42+
43+
<select>
44+
<option value="a">a</option>
45+
</select>
46+
47+
<img src="..." alt="" loading="lazy" />

0 commit comments

Comments
 (0)