Skip to content

Commit d51075c

Browse files
authored
breaking: warn/error on old syntax in runes mode (#11203)
* breaking: warn/error on old syntax in runes mode - warn on slots and event handlers in runes mode - error on `<slot>` + `{@render ...}` tag usage in same component closes #9416 * render tag + slot could occur in legacy mode as well, error there, too
1 parent 4ef6454 commit d51075c

File tree

11 files changed

+106
-6
lines changed

11 files changed

+106
-6
lines changed

.changeset/four-pugs-listen.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: warn on slots and event handlers in runes mode, error on `<slot>` + `{@render ...}` tag usage in same component

packages/svelte/src/compiler/errors.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ const special_elements = {
163163
* @param {string | null} match
164164
*/
165165
'invalid-svelte-tag': (tags, match) =>
166-
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`
166+
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`,
167+
'conflicting-slot-usage': () =>
168+
`Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.`
167169
};
168170

169171
/** @satisfies {Errors} */

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export function analyze_component(root, source, options) {
379379
uses_rest_props: false,
380380
uses_slots: false,
381381
uses_component_bindings: false,
382+
uses_render_tags: false,
382383
custom_element: options.customElementOptions ?? options.customElement,
383384
inject_styles: options.css === 'injected' || options.customElement,
384385
accessors: options.customElement
@@ -388,7 +389,7 @@ export function analyze_component(root, source, options) {
388389
!!options.legacy?.componentApi,
389390
reactive_statements: new Map(),
390391
binding_groups: new Map(),
391-
slot_names: new Set(),
392+
slot_names: new Map(),
392393
warnings,
393394
css: {
394395
ast: root.css,
@@ -502,6 +503,10 @@ export function analyze_component(root, source, options) {
502503
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
503504
}
504505

506+
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
507+
error(analysis.slot_names.values().next().value, 'conflicting-slot-usage');
508+
}
509+
505510
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
506511
for (const scope of [module.scope, instance.scope]) {
507512
outer: for (const [name, binding] of scope.declarations) {
@@ -1087,7 +1092,7 @@ const common_visitors = {
10871092
break;
10881093
}
10891094
}
1090-
context.state.analysis.slot_names.add(name);
1095+
context.state.analysis.slot_names.set(name, node);
10911096
},
10921097
StyleDirective(node, context) {
10931098
if (node.value === true) {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,8 @@ const validation = {
578578
});
579579
},
580580
RenderTag(node, context) {
581+
context.state.analysis.uses_render_tags = true;
582+
581583
const raw_args = unwrap_optional(node.expression).arguments;
582584
for (const arg of raw_args) {
583585
if (arg.type === 'SpreadElement') {
@@ -1183,6 +1185,18 @@ export const validation_runes = merge(validation, a11y_validators, {
11831185
warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration');
11841186
}
11851187
},
1188+
SlotElement(node, { state, path }) {
1189+
if (!state.analysis.custom_element) {
1190+
warn(state.analysis.warnings, node, path, 'deprecated-slot-element');
1191+
}
1192+
},
1193+
OnDirective(node, { state, path }) {
1194+
const parent_type = path.at(-1)?.type;
1195+
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
1196+
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
1197+
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name);
1198+
}
1199+
},
11861200
// TODO this is a code smell. need to refactor this stuff
11871201
ClassBody: validation_runes_js.ClassBody,
11881202
ClassDeclaration: validation_runes_js.ClassDeclaration,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
Css,
44
Fragment,
55
RegularElement,
6+
SlotElement,
67
SvelteElement,
78
SvelteNode,
89
SvelteOptions
@@ -61,13 +62,14 @@ export interface ComponentAnalysis extends Analysis {
6162
/** Whether the component uses `$$slots` */
6263
uses_slots: boolean;
6364
uses_component_bindings: boolean;
65+
uses_render_tags: boolean;
6466
custom_element: boolean | SvelteOptions['customElement'];
6567
/** If `true`, should append styles through JavaScript */
6668
inject_styles: boolean;
6769
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
6870
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
6971
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
70-
slot_names: Set<string>;
72+
slot_names: Map<string, SlotElement>;
7173
css: {
7274
ast: Css.StyleSheet | null;
7375
hash: string;

packages/svelte/src/compiler/warnings.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,12 @@ const legacy = {
233233
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
234234
/** @param {string} name */
235235
'unused-export-let': (name) =>
236-
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
236+
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``,
237+
'deprecated-slot-element': () =>
238+
`Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.`,
239+
/** @param {string} name */
240+
'deprecated-event-handler': (name) =>
241+
`Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`
237242
};
238243

239244
const block = {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'conflicting-slot-usage',
6+
message:
7+
'Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.',
8+
position: [71, 84]
9+
}
10+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
5+
{@render children()}
6+
<slot></slot>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let { foo } = $props();
3+
</script>
4+
5+
<!-- ok -->
6+
<button onclick={foo}>click me</button>
7+
<Button onclick={foo}>click me</Button>
8+
<Button on:click={foo}>click me</Button>
9+
10+
<!-- warn -->
11+
<slot></slot>
12+
<slot name="foo"></slot>
13+
<button on:click={foo}>click me</button>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"code": "deprecated-slot-element",
4+
"end": {
5+
"column": 13,
6+
"line": 11
7+
},
8+
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
9+
"start": {
10+
"column": 0,
11+
"line": 11
12+
}
13+
},
14+
{
15+
"code": "deprecated-slot-element",
16+
"end": {
17+
"column": 24,
18+
"line": 12
19+
},
20+
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
21+
"start": {
22+
"column": 0,
23+
"line": 12
24+
}
25+
},
26+
{
27+
"code": "deprecated-event-handler",
28+
"end": {
29+
"column": 22,
30+
"line": 13
31+
},
32+
"message": "Using on:click to listen to the click event is is deprecated. Use the event attribute onclick instead.",
33+
"start": {
34+
"column": 8,
35+
"line": 13
36+
}
37+
}
38+
]

packages/svelte/tests/validator/samples/static-state-reference/input.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
console.log(doubled);
99
</script>
1010

11-
<button on:click={() => count += 1}>
11+
<button onclick={() => count += 1}>
1212
clicks: {count}
1313
</button>

0 commit comments

Comments
 (0)