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
pull/11209/head
Simon H 6 months ago committed by GitHub
parent 4ef64541dd
commit d51075c154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: warn on slots and event handlers in runes mode, error on `<slot>` + `{@render ...}` tag usage in same component

@ -163,7 +163,9 @@ const special_elements = {
* @param {string | null} match * @param {string | null} match
*/ */
'invalid-svelte-tag': (tags, match) => 'invalid-svelte-tag': (tags, match) =>
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}` `Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`,
'conflicting-slot-usage': () =>
`Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.`
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */

@ -379,6 +379,7 @@ export function analyze_component(root, source, options) {
uses_rest_props: false, uses_rest_props: false,
uses_slots: false, uses_slots: false,
uses_component_bindings: false, uses_component_bindings: false,
uses_render_tags: false,
custom_element: options.customElementOptions ?? options.customElement, custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement, inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement accessors: options.customElement
@ -388,7 +389,7 @@ export function analyze_component(root, source, options) {
!!options.legacy?.componentApi, !!options.legacy?.componentApi,
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Set(), slot_names: new Map(),
warnings, warnings,
css: { css: {
ast: root.css, ast: root.css,
@ -502,6 +503,10 @@ export function analyze_component(root, source, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
error(analysis.slot_names.values().next().value, 'conflicting-slot-usage');
}
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template // warn on any nonstate declarations that are a) reassigned and b) referenced in the template
for (const scope of [module.scope, instance.scope]) { for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) { outer: for (const [name, binding] of scope.declarations) {
@ -1087,7 +1092,7 @@ const common_visitors = {
break; break;
} }
} }
context.state.analysis.slot_names.add(name); context.state.analysis.slot_names.set(name, node);
}, },
StyleDirective(node, context) { StyleDirective(node, context) {
if (node.value === true) { if (node.value === true) {

@ -578,6 +578,8 @@ const validation = {
}); });
}, },
RenderTag(node, context) { RenderTag(node, context) {
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) { for (const arg of raw_args) {
if (arg.type === 'SpreadElement') { if (arg.type === 'SpreadElement') {
@ -1183,6 +1185,18 @@ export const validation_runes = merge(validation, a11y_validators, {
warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration'); warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration');
} }
}, },
SlotElement(node, { state, path }) {
if (!state.analysis.custom_element) {
warn(state.analysis.warnings, node, path, 'deprecated-slot-element');
}
},
OnDirective(node, { state, path }) {
const parent_type = path.at(-1)?.type;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name);
}
},
// TODO this is a code smell. need to refactor this stuff // TODO this is a code smell. need to refactor this stuff
ClassBody: validation_runes_js.ClassBody, ClassBody: validation_runes_js.ClassBody,
ClassDeclaration: validation_runes_js.ClassDeclaration, ClassDeclaration: validation_runes_js.ClassDeclaration,

@ -3,6 +3,7 @@ import type {
Css, Css,
Fragment, Fragment,
RegularElement, RegularElement,
SlotElement,
SvelteElement, SvelteElement,
SvelteNode, SvelteNode,
SvelteOptions SvelteOptions
@ -61,13 +62,14 @@ export interface ComponentAnalysis extends Analysis {
/** Whether the component uses `$$slots` */ /** Whether the component uses `$$slots` */
uses_slots: boolean; uses_slots: boolean;
uses_component_bindings: boolean; uses_component_bindings: boolean;
uses_render_tags: boolean;
custom_element: boolean | SvelteOptions['customElement']; custom_element: boolean | SvelteOptions['customElement'];
/** If `true`, should append styles through JavaScript */ /** If `true`, should append styles through JavaScript */
inject_styles: boolean; inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>; reactive_statements: Map<LabeledStatement, ReactiveStatement>;
/** Identifiers that make up the `bind:group` expression -> internal group binding name */ /** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>; binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Set<string>; slot_names: Map<string, SlotElement>;
css: { css: {
ast: Css.StyleSheet | null; ast: Css.StyleSheet | null;
hash: string; hash: string;

@ -233,7 +233,12 @@ const legacy = {
'All dependencies of the reactive declaration are declared in a module script and will not be reactive', 'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */ /** @param {string} name */
'unused-export-let': (name) => 'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\`` `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``,
'deprecated-slot-element': () =>
`Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.`,
/** @param {string} name */
'deprecated-event-handler': (name) =>
`Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`
}; };
const block = { const block = {

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'conflicting-slot-usage',
message:
'Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.',
position: [71, 84]
}
});

@ -0,0 +1,6 @@
<script>
let { children } = $props();
</script>
{@render children()}
<slot></slot>

@ -0,0 +1,13 @@
<script>
let { foo } = $props();
</script>
<!-- ok -->
<button onclick={foo}>click me</button>
<Button onclick={foo}>click me</Button>
<Button on:click={foo}>click me</Button>
<!-- warn -->
<slot></slot>
<slot name="foo"></slot>
<button on:click={foo}>click me</button>

@ -0,0 +1,38 @@
[
{
"code": "deprecated-slot-element",
"end": {
"column": 13,
"line": 11
},
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
"start": {
"column": 0,
"line": 11
}
},
{
"code": "deprecated-slot-element",
"end": {
"column": 24,
"line": 12
},
"message": "Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.",
"start": {
"column": 0,
"line": 12
}
},
{
"code": "deprecated-event-handler",
"end": {
"column": 22,
"line": 13
},
"message": "Using on:click to listen to the click event is is deprecated. Use the event attribute onclick instead.",
"start": {
"column": 8,
"line": 13
}
}
]

@ -8,6 +8,6 @@
console.log(doubled); console.log(doubled);
</script> </script>
<button on:click={() => count += 1}> <button onclick={() => count += 1}>
clicks: {count} clicks: {count}
</button> </button>

Loading…
Cancel
Save