Skip to content

Commit 363535c

Browse files
authored
feat: warn on obvious legacy component instantiation (#12648)
Adds a compiler warning that warns about legacy component instantiation (i.e. using `new Component(..)`). This won't catch all cases, but the most obvious ones which probably make up ~80%
1 parent 85f2f16 commit 363535c

File tree

5 files changed

+67
-1
lines changed

5 files changed

+67
-1
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
88
9+
## legacy_component_creation
10+
11+
> Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.
12+
913
## non_reactive_update
1014

1115
> `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { AssignmentExpression, CallExpression, Expression, Identifier, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
1+
/** @import { AssignmentExpression, CallExpression, Expression, ImportDeclaration, Identifier, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
22
/** @import { Attribute, Component, ElementLike, Fragment, RegularElement, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TransitionDirective } from '#compiler' */
33
/** @import { NodeLike } from '../../errors.js' */
44
/** @import { AnalysisState, Context, Visitors } from './types.js' */
@@ -362,6 +362,32 @@ function validate_block_not_empty(node, context) {
362362
* @type {Visitors}
363363
*/
364364
const validation = {
365+
ExpressionStatement(node, { state }) {
366+
if (
367+
node.expression.type === 'NewExpression' &&
368+
node.expression.callee.type === 'Identifier' &&
369+
node.expression.arguments.length === 1 &&
370+
node.expression.arguments[0].type === 'ObjectExpression' &&
371+
node.expression.arguments[0].properties.some(
372+
(p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'target'
373+
)
374+
) {
375+
const binding = state.scope.get(node.expression.callee.name);
376+
if (binding?.kind === 'normal' && binding.declaration_kind === 'import') {
377+
const declaration = /** @type {ImportDeclaration} */ (binding.initial);
378+
379+
// Theoretically someone could import a class from a `.svelte.js` module, but that's too rare to worry about
380+
if (
381+
/** @type {string} */ (declaration.source.value)?.endsWith('.svelte') &&
382+
declaration.specifiers.find(
383+
(s) => s.local.name === binding.node.name && s.type === 'ImportDefaultSpecifier'
384+
)
385+
) {
386+
w.legacy_component_creation(node.expression);
387+
}
388+
}
389+
}
390+
},
365391
MemberExpression(node, context) {
366392
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
367393
const binding = context.state.scope.get(node.object.name);

packages/svelte/src/compiler/warnings.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const codes = [
9696
"options_renamed_ssr_dom",
9797
"derived_iife",
9898
"export_let_unused",
99+
"legacy_component_creation",
99100
"non_reactive_update",
100101
"perf_avoid_inline_class",
101102
"perf_avoid_nested_class",
@@ -586,6 +587,14 @@ export function export_let_unused(node, name) {
586587
w(node, "export_let_unused", `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``);
587588
}
588589

590+
/**
591+
* Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.
592+
* @param {null | NodeLike} node
593+
*/
594+
export function legacy_component_creation(node) {
595+
w(node, "legacy_component_creation", "Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.");
596+
}
597+
589598
/**
590599
* `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates
591600
* @param {null | NodeLike} node
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import Foo, { Bar } from './Somewhere.svelte';
3+
import Baz from './somewhereelse';
4+
5+
let Buzz;
6+
7+
new Foo({ target: null });
8+
new Foo({}); // also a false negative to be really sure we don't get false positives
9+
new Foo();
10+
new Bar();
11+
new Baz();
12+
new Buzz();
13+
</script>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"code": "legacy_component_creation",
4+
"message": "Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.",
5+
"start": {
6+
"column": 1,
7+
"line": 7
8+
},
9+
"end": {
10+
"column": 26,
11+
"line": 7
12+
}
13+
}
14+
]

0 commit comments

Comments
 (0)