Skip to content

Commit 7c97a85

Browse files
committed
chore: remove template expression inlining
1 parent 4c98c2e commit 7c97a85

File tree

25 files changed

+227
-318
lines changed

25 files changed

+227
-318
lines changed

.changeset/calm-mice-perform.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+
chore: remove template expression inlining

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

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
22
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
33
/** @import { Context } from '../types' */
4-
import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js';
4+
import { is_capture_event, is_delegated } from '../../../../utils.js';
55
import {
66
get_attribute_chunks,
77
get_attribute_expression,
@@ -16,28 +16,14 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
1616
export function Attribute(node, context) {
1717
context.next();
1818

19-
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
20-
21-
if (parent.type === 'RegularElement') {
22-
// special case <option value="" />
23-
if (node.name === 'value' && parent.name === 'option') {
24-
mark_subtree_dynamic(context.path);
25-
}
26-
27-
// special case <img loading="lazy" />
28-
if (node.name === 'loading' && parent.name === 'img') {
19+
// special case
20+
if (node.name === 'value') {
21+
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
22+
if (parent.type === 'RegularElement' && parent.name === 'option') {
2923
mark_subtree_dynamic(context.path);
3024
}
3125
}
3226

33-
if (node.name.startsWith('on')) {
34-
mark_subtree_dynamic(context.path);
35-
}
36-
37-
if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) {
38-
node.metadata.expression.can_inline = false;
39-
}
40-
4127
if (node.value !== true) {
4228
for (const chunk of get_attribute_chunks(node.value)) {
4329
if (chunk.type !== 'ExpressionTag') continue;
@@ -51,7 +37,6 @@ export function Attribute(node, context) {
5137

5238
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
5339
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
54-
node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline;
5540
}
5641

5742
if (is_event_attribute(node)) {

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,6 @@ export function CallExpression(node, context) {
179179
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
180180
context.state.expression.has_call = true;
181181
context.state.expression.has_state = true;
182-
context.state.expression.can_inline = false;
183-
mark_subtree_dynamic(context.path);
184182
}
185183
}
186184
}

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

Lines changed: 5 additions & 0 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 { mark_subtree_dynamic } from './shared/fragment.js';
56

67
/**
78
* @param {AST.ExpressionTag} node
@@ -14,5 +15,9 @@ export function ExpressionTag(node, context) {
1415
}
1516
}
1617

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);
21+
1722
context.next({ ...context.state, expression: node.metadata.expression });
1823
}

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/** @import { Expression, Identifier } from 'estree' */
2+
/** @import { EachBlock } from '#compiler' */
23
/** @import { Context } from '../types' */
34
import is_reference from 'is-reference';
45
import { should_proxy } from '../../3-transform/client/utils.js';
@@ -19,6 +20,8 @@ export function Identifier(node, context) {
1920
return;
2021
}
2122

23+
mark_subtree_dynamic(context.path);
24+
2225
// If we are using arguments outside of a function, then throw an error
2326
if (
2427
node.name === 'arguments' &&
@@ -84,12 +87,6 @@ export function Identifier(node, context) {
8487
}
8588
}
8689

87-
// no binding means global, and we can't inline e.g. `<span>{location}</span>`
88-
// because it could change between component renders. if there _is_ a
89-
// binding and it is outside module scope, the expression cannot
90-
// be inlined (TODO allow inlining in more cases - e.g. primitive consts)
91-
let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal';
92-
9390
if (binding) {
9491
if (context.state.expression) {
9592
context.state.expression.dependencies.add(binding);
@@ -125,12 +122,4 @@ export function Identifier(node, context) {
125122
w.reactive_declaration_module_script_dependency(node);
126123
}
127124
}
128-
129-
if (!can_inline) {
130-
if (context.state.expression) {
131-
context.state.expression.can_inline = false;
132-
}
133-
134-
mark_subtree_dynamic(context.path);
135-
}
136125
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export function MemberExpression(node, context) {
2020

2121
if (context.state.expression && !is_pure(node, context)) {
2222
context.state.expression.has_state = true;
23-
context.state.expression.can_inline = false;
24-
25-
mark_subtree_dynamic(context.path);
2623
}
2724

2825
if (!is_safe_identifier(node, context.state.scope)) {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export function TaggedTemplateExpression(node, context) {
1010
if (context.state.expression && !is_pure(node.tag, context)) {
1111
context.state.expression.has_call = true;
1212
context.state.expression.has_state = true;
13-
context.state.expression.can_inline = false;
1413
}
1514

1615
if (node.tag.type === 'Identifier') {

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

Lines changed: 46 additions & 6 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 { 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' */
6+
import * as b from '../../../utils/builders.js';
7+
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
68
import {
7-
PROPS_IS_BINDABLE,
8-
PROPS_IS_IMMUTABLE,
99
PROPS_IS_LAZY_INITIAL,
10+
PROPS_IS_IMMUTABLE,
1011
PROPS_IS_RUNES,
11-
PROPS_IS_UPDATED
12+
PROPS_IS_UPDATED,
13+
PROPS_IS_BINDABLE
1214
} from '../../../../constants.js';
1315
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,3 +311,43 @@ 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/Fragment.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
1+
/** @import { Expression, Identifier, Statement } from 'estree' */
22
/** @import { AST, Namespace } from '#compiler' */
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -141,14 +141,14 @@ export function Fragment(node, context) {
141141
const id = b.id(context.state.scope.generate('fragment'));
142142

143143
const use_space_template =
144-
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
145-
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
144+
trimmed.some((node) => node.type === 'ExpressionTag') &&
145+
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
146146

147147
if (use_space_template) {
148148
// special case — we can use `$.text` instead of creating a unique template
149149
const id = b.id(context.state.scope.generate('text'));
150150

151-
process_children(trimmed, () => id, null, {
151+
process_children(trimmed, () => id, false, {
152152
...context,
153153
state
154154
});
@@ -158,12 +158,12 @@ export function Fragment(node, context) {
158158
} else {
159159
if (is_standalone) {
160160
// no need to create a template, we can just use the existing block's anchor
161-
process_children(trimmed, () => b.id('$$anchor'), null, { ...context, state });
161+
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
162162
} else {
163163
/** @type {(is_text: boolean) => Expression} */
164164
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
165165

166-
process_children(trimmed, expression, null, { ...context, state });
166+
process_children(trimmed, expression, false, { ...context, state });
167167

168168
let flags = TEMPLATE_FRAGMENT;
169169

@@ -212,34 +212,12 @@ function join_template(items) {
212212
let quasi = b.quasi('');
213213
const template = b.template([quasi], []);
214214

215-
/**
216-
* @param {Expression} expression
217-
*/
218-
function push(expression) {
219-
if (expression.type === 'TemplateLiteral') {
220-
for (let i = 0; i < expression.expressions.length; i += 1) {
221-
const q = expression.quasis[i];
222-
const e = expression.expressions[i];
223-
224-
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
225-
push(e);
226-
}
227-
228-
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
229-
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
230-
} else if (expression.type === 'Literal') {
231-
/** @type {string} */ (quasi.value.cooked) += expression.value;
232-
} else {
233-
template.expressions.push(expression);
234-
template.quasis.push((quasi = b.quasi('')));
235-
}
236-
}
237-
238215
for (const item of items) {
239216
if (typeof item === 'string') {
240217
quasi.value.cooked += item;
241218
} else {
242-
push(item);
219+
template.expressions.push(item);
220+
template.quasis.push((quasi = b.quasi('')));
243221
}
244222
}
245223

0 commit comments

Comments
 (0)