Skip to content

Commit d288735

Browse files
Rich-Harrisjrmajor
andauthored
breaking: disallow string literal values in <svelte:element this="..."> (#11454)
* breaking: disallow string literal values in `<svelte:element this="...">` * note breaking change * Update sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md Co-authored-by: Jeremiasz Major <jrh.mjr@gmail.com> * prettier * make invalid `<svelte:element this>` a warning instead of an error (#11641) * make it a warning instead of an error * format --------- Co-authored-by: Jeremiasz Major <jrh.mjr@gmail.com>
1 parent ade6b6e commit d288735

File tree

26 files changed

+90
-93
lines changed

26 files changed

+90
-93
lines changed

.changeset/dull-worms-relax.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+
breaking: disallow string literal values in `<svelte:element this="...">`

packages/svelte/messages/compile-errors/template.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,9 @@
260260

261261
> `<svelte:component>` must have a 'this' attribute
262262
263-
## svelte_element_invalid_this
264-
265-
> Invalid element definition — must be an `{expression}`
266-
267263
## svelte_element_missing_this
268264

269-
> `<svelte:element>` must have a 'this' attribute
265+
> `<svelte:element>` must have a 'this' attribute with a value
270266
271267
## svelte_fragment_invalid_attribute
272268

packages/svelte/messages/compile-warnings/template.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@
3737
## slot_element_deprecated
3838

3939
> Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead
40+
41+
## svelte_element_invalid_this
42+
43+
> `this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,21 +1138,12 @@ export function svelte_component_missing_this(node) {
11381138
}
11391139

11401140
/**
1141-
* Invalid element definition — must be an `{expression}`
1142-
* @param {null | number | NodeLike} node
1143-
* @returns {never}
1144-
*/
1145-
export function svelte_element_invalid_this(node) {
1146-
e(node, "svelte_element_invalid_this", "Invalid element definition — must be an `{expression}`");
1147-
}
1148-
1149-
/**
1150-
* `<svelte:element>` must have a 'this' attribute
1141+
* `<svelte:element>` must have a 'this' attribute with a value
11511142
* @param {null | number | NodeLike} node
11521143
* @returns {never}
11531144
*/
11541145
export function svelte_element_missing_this(node) {
1155-
e(node, "svelte_element_missing_this", "`<svelte:element>` must have a 'this' attribute");
1146+
e(node, "svelte_element_missing_this", "`<svelte:element>` must have a 'this' attribute with a value");
11561147
}
11571148

11581149
/**

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { read_script } from '../read/script.js';
44
import read_style from '../read/style.js';
55
import { closing_tag_omitted, decode_character_references } from '../utils/html.js';
66
import * as e from '../../../errors.js';
7+
import * as w from '../../../warnings.js';
78
import { create_fragment } from '../utils/create.js';
89
import { create_attribute } from '../../nodes.js';
910

@@ -262,14 +263,28 @@ export default function tag(parser) {
262263
const definition = /** @type {import('#compiler').Attribute} */ (
263264
element.attributes.splice(index, 1)[0]
264265
);
265-
if (definition.value === true || definition.value.length !== 1) {
266-
e.svelte_element_invalid_this(definition.start);
266+
267+
if (definition.value === true) {
268+
e.svelte_element_missing_this(definition);
267269
}
270+
268271
const chunk = definition.value[0];
269-
element.tag =
270-
chunk.type === 'Text'
271-
? { type: 'Literal', value: chunk.data, raw: `'${chunk.raw}'` }
272-
: chunk.expression;
272+
273+
if (definition.value.length !== 1 || chunk.type !== 'ExpressionTag') {
274+
chunk.type;
275+
w.svelte_element_invalid_this(definition);
276+
277+
// note that this is wrong, in the case of e.g. `this="h{n}"` — it will result in `<h>`.
278+
// it would be much better to just error here, but we are preserving the existing buggy
279+
// Svelte 4 behaviour out of an overabundance of caution regarding breaking changes.
280+
// TODO in 6.0, error
281+
element.tag =
282+
chunk.type === 'Text'
283+
? { type: 'Literal', value: chunk.data, raw: `'${chunk.raw}'` }
284+
: chunk.expression;
285+
} else {
286+
element.tag = chunk.expression;
287+
}
273288
}
274289

275290
if (is_top_level_script_or_style) {

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,24 +1449,6 @@ const common_visitors = {
14491449
SvelteElement(node, context) {
14501450
context.state.analysis.elements.push(node);
14511451

1452-
// TODO why are we handling the `<svelte:element this="x" />` case? there is no
1453-
// reason for someone to use a static value with `<svelte:element>`
1454-
if (
1455-
context.state.options.namespace !== 'foreign' &&
1456-
node.tag.type === 'Literal' &&
1457-
typeof node.tag.value === 'string'
1458-
) {
1459-
if (SVGElements.includes(node.tag.value)) {
1460-
node.metadata.svg = true;
1461-
return;
1462-
}
1463-
1464-
if (MathMLElements.includes(node.tag.value)) {
1465-
node.metadata.mathml = true;
1466-
return;
1467-
}
1468-
}
1469-
14701452
for (const attribute of node.attributes) {
14711453
if (attribute.type === 'Attribute') {
14721454
if (attribute.name === 'xmlns' && is_text_attribute(attribute)) {

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

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -586,22 +586,6 @@ const validation = {
586586
) {
587587
e.render_tag_invalid_call_expression(node);
588588
}
589-
590-
const is_inside_textarea = context.path.find((n) => {
591-
return (
592-
n.type === 'SvelteElement' &&
593-
n.name === 'svelte:element' &&
594-
n.tag.type === 'Literal' &&
595-
n.tag.value === 'textarea'
596-
);
597-
});
598-
if (is_inside_textarea) {
599-
e.tag_invalid_placement(
600-
node,
601-
'inside <textarea> or <svelte:element this="textarea">',
602-
'render'
603-
);
604-
}
605589
},
606590
IfBlock(node, context) {
607591
validate_block_not_empty(node.consequent, context);

packages/svelte/src/compiler/warnings.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ export const codes = [
9090
"component_name_lowercase",
9191
"element_invalid_self_closing_tag",
9292
"event_directive_deprecated",
93-
"slot_element_deprecated"
93+
"slot_element_deprecated",
94+
"svelte_element_invalid_this"
9495
];
9596

9697
/**
@@ -712,4 +713,12 @@ export function event_directive_deprecated(node, name) {
712713
*/
713714
export function slot_element_deprecated(node) {
714715
w(node, "slot_element_deprecated", "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead");
716+
}
717+
718+
/**
719+
* `this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte
720+
* @param {null | NodeLike} node
721+
*/
722+
export function svelte_element_invalid_this(node) {
723+
w(node, "svelte_element_invalid_this", "`this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte");
715724
}

packages/svelte/tests/css/samples/dynamic-element/_config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ export default test({
66
code: 'css_unused_selector',
77
message: 'Unused CSS selector ".unused"',
88
start: {
9-
character: 79,
9+
character: 81,
1010
column: 1,
1111
line: 7
1212
},
1313
end: {
14-
character: 86,
14+
character: 88,
1515
column: 8,
1616
line: 7
1717
}

packages/svelte/tests/css/samples/dynamic-element/input.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<svelte:element this="div" class="used" />
1+
<svelte:element this={"div"} class="used" />
22

33
<style>
44
.used {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
<svelte:element this="div"></svelte:element>
2-
<svelte:element this="div" class="foo"></svelte:element>
1+
<svelte:element this={"div"}></svelte:element>
2+
<svelte:element this={"div"} class="foo"></svelte:element>

packages/svelte/tests/parser-legacy/samples/dynamic-element-string/output.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,40 @@
22
"html": {
33
"type": "Fragment",
44
"start": 0,
5-
"end": 101,
5+
"end": 105,
66
"children": [
77
{
88
"type": "Element",
99
"name": "svelte:element",
1010
"start": 0,
11-
"end": 44,
11+
"end": 46,
1212
"tag": "div",
1313
"attributes": [],
1414
"children": []
1515
},
1616
{
1717
"type": "Text",
18-
"start": 44,
19-
"end": 45,
18+
"start": 46,
19+
"end": 47,
2020
"raw": "\n",
2121
"data": "\n"
2222
},
2323
{
2424
"type": "Element",
2525
"name": "svelte:element",
26-
"start": 45,
27-
"end": 101,
26+
"start": 47,
27+
"end": 105,
2828
"tag": "div",
2929
"attributes": [
3030
{
3131
"type": "Attribute",
32-
"start": 72,
33-
"end": 83,
32+
"start": 76,
33+
"end": 87,
3434
"name": "class",
3535
"value": [
3636
{
37-
"start": 79,
38-
"end": 82,
37+
"start": 83,
38+
"end": 86,
3939
"type": "Text",
4040
"raw": "foo",
4141
"data": "foo"

packages/svelte/tests/runtime-legacy/samples/dynamic-element-attribute-boolean/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
export let disabled = false;
33
</script>
44

5-
<svelte:element {disabled} this="button">Click me</svelte:element>
5+
<svelte:element {disabled} this={"button"}>Click me</svelte:element>

packages/svelte/tests/runtime-legacy/samples/dynamic-element-attribute-spread/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
};
77
</script>
88

9-
<svelte:element this="button" {...props}>Click me</svelte:element>
9+
<svelte:element this={"button"} {...props}>Click me</svelte:element>

packages/svelte/tests/runtime-legacy/samples/dynamic-element-class-directive/Link.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export let item;
33
</script>
44

5-
<svelte:element this="div" class:active={true}>
5+
<svelte:element this={"div"} class:active={true}>
66
{item.text}
77
</svelte:element>
88

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<svelte:element this="div">Foo</svelte:element>
1+
<svelte:element this={"div"}>Foo</svelte:element>

packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace/main.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
</svg>
1010

1111
<svg>
12-
<svelte:element this="path">
12+
<svelte:element this={"path"}>
1313
<foreignObject>
14-
<svelte:element this="span">ok</svelte:element>
14+
<svelte:element this={"span"}>ok</svelte:element>
1515
</foreignObject>
1616
<foreignObject>
1717
{#if true}
18-
<svelte:element this="span">ok</svelte:element>
18+
<svelte:element this={"span"}>ok</svelte:element>
1919
{/if}
2020
</foreignObject>
2121
</svelte:element>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<svelte:element this="svg" xmlns="http://www.w3.org/2000/svg">
2-
<svelte:element this="path" xmlns="http://www.w3.org/2000/svg"></svelte:element>
3-
</svelte:element>
1+
<svelte:element this={"svg"} xmlns="http://www.w3.org/2000/svg">
2+
<svelte:element this={"path"} xmlns="http://www.w3.org/2000/svg"></svelte:element>
3+
</svelte:element>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<svelte:element this="textarea"></svelte:element>
1+
<svelte:element this={"textarea"}></svelte:element>

packages/svelte/tests/runtime-legacy/samples/dynamic-element-void-tag/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<h1></h1>
88
<svelte:element this="{'foo'}"></svelte:element>
9-
<svelte:element this="img"></svelte:element>
9+
<svelte:element this={"img"}></svelte:element>
1010
<svelte:element this="{propTag}"></svelte:element>
1111
<svelte:element this="{static_tag}"></svelte:element>
1212
<svelte:element this="{func_tag()}"></svelte:element>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<div>
2-
<svelte:element this="p" />
2+
<svelte:element this={"p"} />
33
</div>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<svelte:element this="div">Foo</svelte:element>
1+
<svelte:element this={"div"}>Foo</svelte:element>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<svelte:element this="title">lorem</svelte:element>
2-
<svelte:element this="style">{'.ipsum { display: block; }'}</svelte:element>
3-
<svelte:element this="script">{'console.log(true);'}</svelte:element>
1+
<svelte:element this={"title"}>lorem</svelte:element>
2+
<svelte:element this={"style"}>{'.ipsum { display: block; }'}</svelte:element>
3+
<svelte:element this={"script"}>{'console.log(true);'}</svelte:element>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[
22
{
3-
"code": "svelte_element_invalid_this",
4-
"message": "Invalid element definition — must be an `{expression}`",
3+
"code": "svelte_element_missing_this",
4+
"message": "`<svelte:element>` must have a 'this' attribute with a value",
55
"start": {
66
"line": 2,
77
"column": 17
88
},
99
"end": {
1010
"line": 2,
11-
"column": 17
11+
"column": 21
1212
}
1313
}
1414
]

packages/svelte/tests/validator/samples/dynamic-element-missing-tag/errors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"code": "svelte_element_missing_this",
4-
"message": "`<svelte:element>` must have a 'this' attribute",
4+
"message": "`<svelte:element>` must have a 'this' attribute with a value",
55
"start": {
66
"line": 2,
77
"column": 1

sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,14 @@ In Svelte 4 you could have content inside a `<svelte:options />` tag. It was ign
230230
### `<slot>` elements in declarative shadow roots are preserved
231231

232232
Svelte 4 replaced the `<slot />` tag in all places with its own version of slots. Svelte 5 preserves them in the case they are a child of a `<template shadowrootmode="...">` element.
233+
234+
### `<svelte:element>` tag must be an expression
235+
236+
In Svelte 4, `<svelte:element this="div">` is valid code. This makes little sense — you should just do `<div>`. In the vanishingly rare case that you _do_ need to use a literal value for some reason, you can do this:
237+
238+
```diff
239+
- <svelte:element this="div">
240+
+ <svelte:element this={"div"}>
241+
```
242+
243+
Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for example) identically to `<input>` for the purposes of determining which `bind:` directives could be applied, Svelte 5 does not.

0 commit comments

Comments
 (0)