Skip to content

Commit 837b61f

Browse files
authored
breaking(svelte5): only generate function component shape in runes mode (#2517)
When a component is in runes mode and not using slots or events, adjust the output to only create the function type that mimics the underlying real shape of components in Svelte 5. This is a breaking change because previously the type was enhanced such that it also had the legacy class shape. As a result, users now may need to switch to `typeof Component` when using the component inside types. Sadly, due to a combination of requirements and TypeScript limitations, we need to always create both a legacy class component and function component type. - Constraints: Need to support Svelte 4 class component types, therefore we need to use __sveltets_2_ensureComponent to transform function components to classes - Limitations: TypeScript is not able to preserve generics during said transformation (i.e. there's no way to express keeping the generic etc) TODO Svelte 6/7: Switch this around and not use new Component in svelte2tsx anymore, which means we can remove the legacy class component. We need something like _ensureFnComponent then.
1 parent 35af691 commit 837b61f

File tree

30 files changed

+285
-148
lines changed

30 files changed

+285
-148
lines changed

packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,23 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
6666
continue;
6767
}
6868

69-
const originalPosition = this.mapToOrigin(
69+
const original = this.map(
7070
textDocument,
7171
tsDoc,
7272
generatedOffset,
7373
generatedLength,
74-
encodedClassification
74+
encodedClassification,
75+
classificationType
7576
);
76-
if (!originalPosition) {
77-
continue;
78-
}
79-
80-
const [line, character, length] = originalPosition;
8177

8278
// remove identifiers whose start and end mapped to the same location,
8379
// like the svelte2tsx inserted render function,
8480
// or reversed like Component.$on
85-
if (length <= 0) {
81+
if (!original || original[2] <= 0) {
8682
continue;
8783
}
8884

89-
const modifier = this.getTokenModifierFromClassification(encodedClassification);
90-
91-
data.push([line, character, length, classificationType, modifier]);
85+
data.push(original);
9286
}
9387

9488
const sorted = data.sort((a, b) => {
@@ -103,17 +97,20 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
10397
return builder.build();
10498
}
10599

106-
private mapToOrigin(
100+
private map(
107101
document: Document,
108102
snapshot: SvelteDocumentSnapshot,
109103
generatedOffset: number,
110104
generatedLength: number,
111-
token: number
112-
): [line: number, character: number, length: number, start: number] | undefined {
105+
encodedClassification: number,
106+
classificationType: number
107+
):
108+
| [line: number, character: number, length: number, token: number, modifier: number]
109+
| undefined {
113110
const text = snapshot.getFullText();
114111
if (
115112
isInGeneratedCode(text, generatedOffset, generatedOffset + generatedLength) ||
116-
(token === 2817 /* top level function */ &&
113+
(encodedClassification === 2817 /* top level function */ &&
117114
text.substring(generatedOffset, generatedOffset + generatedLength) === 'render')
118115
) {
119116
return;
@@ -132,7 +129,26 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
132129
const startOffset = document.offsetAt(startPosition);
133130
const endOffset = document.offsetAt(endPosition);
134131

135-
return [startPosition.line, startPosition.character, endOffset - startOffset, startOffset];
132+
// Ensure components in the template get no semantic highlighting
133+
if (
134+
(classificationType === 0 ||
135+
classificationType === 5 ||
136+
classificationType === 7 ||
137+
classificationType === 10) &&
138+
snapshot.svelteNodeAt(startOffset)?.type === 'InlineComponent' &&
139+
(document.getText().charCodeAt(startOffset - 1) === /* < */ 60 ||
140+
document.getText().charCodeAt(startOffset - 1) === /* / */ 47)
141+
) {
142+
return;
143+
}
144+
145+
return [
146+
startPosition.line,
147+
startPosition.character,
148+
endOffset - startOffset,
149+
classificationType,
150+
this.getTokenModifierFromClassification(encodedClassification)
151+
];
136152
}
137153

138154
/**

packages/language-server/test/plugins/typescript/features/SemanticTokensProvider.test.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,6 @@ describe('SemanticTokensProvider', function () {
191191
type: TokenType.variable,
192192
modifiers: [TokenModifier.declaration, TokenModifier.local, TokenModifier.readonly]
193193
},
194-
{
195-
line: 12,
196-
character: 5,
197-
length: 'Imported'.length,
198-
type: isSvelte5Plus ? TokenType.type : TokenType.class,
199-
modifiers: isSvelte5Plus ? [TokenModifier.readonly] : []
200-
},
201194
{
202195
line: 12,
203196
character: 23,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts" generics="T">
2+
let { readonly, can_bind = $bindable() }: { readonly?: T; can_bind?: T } = $props();
3+
4+
export function only_bind() {
5+
return true;
6+
}
7+
</script>

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/bindings/expected_svelte_5.json

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
"range": {
66
"end": {
77
"character": 20,
8-
"line": 25
8+
"line": 26
99
},
1010
"start": {
1111
"character": 7,
12-
"line": 25
12+
"line": 26
1313
}
1414
},
1515
"severity": 1,
@@ -22,11 +22,11 @@
2222
"range": {
2323
"end": {
2424
"character": 21,
25-
"line": 26
25+
"line": 27
2626
},
2727
"start": {
2828
"character": 12,
29-
"line": 26
29+
"line": 27
3030
}
3131
},
3232
"severity": 1,
@@ -39,11 +39,11 @@
3939
"range": {
4040
"end": {
4141
"character": 21,
42-
"line": 26
42+
"line": 27
4343
},
4444
"start": {
4545
"character": 7,
46-
"line": 26
46+
"line": 27
4747
}
4848
},
4949
"severity": 1,
@@ -56,11 +56,79 @@
5656
"range": {
5757
"end": {
5858
"character": 17,
59-
"line": 27
59+
"line": 28
6060
},
6161
"start": {
6262
"character": 8,
63-
"line": 27
63+
"line": 28
64+
}
65+
},
66+
"severity": 1,
67+
"source": "ts",
68+
"tags": []
69+
},
70+
{
71+
"code": 2322,
72+
"message": "Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\nTo mark a property as bindable: 'let { readonly = $bindable() } = $props()'",
73+
"range": {
74+
"end": {
75+
"character": 27,
76+
"line": 30
77+
},
78+
"start": {
79+
"character": 14,
80+
"line": 30
81+
}
82+
},
83+
"severity": 1,
84+
"source": "ts",
85+
"tags": []
86+
},
87+
{
88+
"code": 2353,
89+
"message": "Object literal may only specify known properties, and 'only_bind' does not exist in type '$$ComponentProps'.",
90+
"range": {
91+
"end": {
92+
"character": 28,
93+
"line": 31
94+
},
95+
"start": {
96+
"character": 19,
97+
"line": 31
98+
}
99+
},
100+
"severity": 1,
101+
"source": "ts",
102+
"tags": []
103+
},
104+
{
105+
"code": 2322,
106+
"message": "Cannot use 'bind:' with this property. It is declared as non-bindable inside the component.\nTo mark a property as bindable: 'let { only_bind = $bindable() } = $props()'",
107+
"range": {
108+
"end": {
109+
"character": 28,
110+
"line": 31
111+
},
112+
"start": {
113+
"character": 14,
114+
"line": 31
115+
}
116+
},
117+
"severity": 1,
118+
"source": "ts",
119+
"tags": []
120+
},
121+
{
122+
"code": 2353,
123+
"message": "Object literal may only specify known properties, and 'only_bind' does not exist in type '$$ComponentProps'.",
124+
"range": {
125+
"end": {
126+
"character": 24,
127+
"line": 32
128+
},
129+
"start": {
130+
"character": 15,
131+
"line": 32
64132
}
65133
},
66134
"severity": 1,

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/bindings/expectedv2.json

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
[
2+
{
3+
"code": 2344,
4+
"message": "Type 'typeof Runes__SvelteComponent_' does not satisfy the constraint '(...args: any) => any'.\n Type 'typeof Runes__SvelteComponent_' provides no match for the signature '(...args: any): any'.",
5+
"range": {
6+
"end": {
7+
"character": 41,
8+
"line": 12
9+
},
10+
"start": {
11+
"character": 29,
12+
"line": 12
13+
}
14+
},
15+
"severity": 1,
16+
"source": "ts",
17+
"tags": []
18+
},
219
{
320
"code": 2353,
421
"message": "Object literal may only specify known properties, and 'can_bind' does not exist in type '{ only_bind?: (() => boolean) | undefined; }'.",
522
"range": {
623
"end": {
724
"character": 20,
8-
"line": 20
25+
"line": 21
926
},
1027
"start": {
1128
"character": 12,
12-
"line": 20
29+
"line": 21
1330
}
1431
},
1532
"severity": 1,
@@ -22,11 +39,11 @@
2239
"range": {
2340
"end": {
2441
"character": 16,
25-
"line": 21
42+
"line": 22
2643
},
2744
"start": {
2845
"character": 8,
29-
"line": 21
46+
"line": 22
3047
}
3148
},
3249
"severity": 1,
@@ -39,11 +56,11 @@
3956
"range": {
4057
"end": {
4158
"character": 16,
42-
"line": 22
59+
"line": 23
4360
},
4461
"start": {
4562
"character": 8,
46-
"line": 22
63+
"line": 23
4764
}
4865
},
4966
"severity": 1,
@@ -56,11 +73,28 @@
5673
"range": {
5774
"end": {
5875
"character": 20,
59-
"line": 25
76+
"line": 26
6077
},
6178
"start": {
6279
"character": 12,
63-
"line": 25
80+
"line": 26
81+
}
82+
},
83+
"severity": 1,
84+
"source": "ts",
85+
"tags": []
86+
},
87+
{
88+
"code": 2353,
89+
"message": "Object literal may only specify known properties, and 'readonly' does not exist in type '{ only_bind?: (() => boolean) | undefined; }'.",
90+
"range": {
91+
"end": {
92+
"character": 27,
93+
"line": 30
94+
},
95+
"start": {
96+
"character": 19,
97+
"line": 30
6498
}
6599
},
66100
"severity": 1,

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/bindings/input.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import Legacy from './Legacy.svelte';
33
import Runes from './Runes.svelte';
4+
import RunesGeneric from './RunesGeneric.svelte';
45
56
let bind_and_prop: () => boolean;
67
let value = '';
@@ -9,7 +10,7 @@
910
let can_bind = '';
1011
let readonly = ''
1112
12-
let instance: Runes;
13+
let instance: ReturnType<typeof Runes>;
1314
instance!.only_bind() === true;
1415
</script>
1516

@@ -26,3 +27,7 @@
2627
<Runes bind:readonly />
2728
<Runes bind:only_bind />
2829
<Runes {only_bind} />
30+
31+
<RunesGeneric bind:readonly />
32+
<RunesGeneric bind:only_bind />
33+
<RunesGeneric {only_bind} />

0 commit comments

Comments
 (0)