Skip to content

Commit 364ac7d

Browse files
authored
feat: Svelte 5 fixes and improvements (#2217)
- pass children to zero types Svelte 5: +layout.svelte children not included in zero-effort type safety #2212 - add possibility to pass in version to svelte2tsx to differentiate transpiler targets - add implicit children prop in Svelte 5 mode Svelte 5: Implicit children not detected correctly #2211 - add best-effort fallback typings to $props() rune - hide deprecation warnings in generated code Svelte 5: Typescript generics in components are marked as deprecated svelte#9586
1 parent 2ff3a7c commit 364ac7d

File tree

66 files changed

+1118
-58
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1118
-58
lines changed

packages/language-server/src/plugins/svelte/SveltePlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class SveltePlugin
6868
async getCompiledResult(document: Document): Promise<SvelteCompileResult | null> {
6969
try {
7070
const svelteDoc = await this.getSvelteDoc(document);
71+
// @ts-ignore is 'client' in Svelte 5
7172
return svelteDoc.getCompiledWith({ generate: 'dom' });
7273
} catch (error) {
7374
return null;

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot, DocumentMapper {
6868
*/
6969
export interface SvelteSnapshotOptions {
7070
parse: typeof import('svelte/compiler').parse | undefined;
71+
version: string | undefined;
7172
transformOnTemplateError: boolean;
7273
typingsNamespace: string;
7374
}
@@ -199,6 +200,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
199200
try {
200201
const tsx = svelte2tsx(text, {
201202
parse: options.parse,
203+
version: options.version,
202204
filename: document.getFilePath() ?? undefined,
203205
isTsFile: scriptKind === ts.ScriptKind.TS,
204206
mode: 'ts',

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export enum DiagnosticCode {
4242
MISSING_PROP = 2741, // "Property '..' is missing in type '..' but required in type '..'."
4343
NO_OVERLOAD_MATCHES_CALL = 2769, // "No overload matches this call"
4444
CANNOT_FIND_NAME = 2304, // "Cannot find name 'xxx'"
45-
EXPECTED_N_ARGUMENTS = 2554 // Expected {0} arguments, but got {1}.
45+
EXPECTED_N_ARGUMENTS = 2554, // Expected {0} arguments, but got {1}.
46+
DEPRECATED_SIGNATURE = 6387 // The signature '..' of '..' is deprecated
4647
}
4748

4849
export class DiagnosticsProviderImpl implements DiagnosticsProvider {
@@ -222,6 +223,8 @@ function hasNoNegativeLines(diagnostic: Diagnostic): boolean {
222223
return diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0;
223224
}
224225

226+
const generatedVarRegex = /'\$\$_\w+(\.\$on)?'/;
227+
225228
function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
226229
const text = document.getText();
227230
const usesPug = document.getLanguageAttribute('template') === 'pug';
@@ -238,6 +241,14 @@ function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
238241
}
239242
}
240243

244+
if (
245+
diagnostic.code === DiagnosticCode.DEPRECATED_SIGNATURE &&
246+
generatedVarRegex.test(diagnostic.message)
247+
) {
248+
// Svelte 5: $on and constructor is deprecated, but we don't want to show this warning for generated code
249+
return false;
250+
}
251+
241252
return (
242253
isNoUsedBeforeAssigned(diagnostic, text, tsDoc) &&
243254
(!usesPug || isNoPugFalsePositive(diagnostic, document))

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ async function createLanguageService(
298298
let languageService = ts.createLanguageService(host);
299299
const transformationConfig: SvelteSnapshotOptions = {
300300
parse: svelteCompiler?.parse,
301+
version: svelteCompiler?.VERSION,
301302
transformOnTemplateError: docContext.transformOnTemplateError,
302303
typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML'
303304
};

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/component-invalid/components.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ export class Works2 extends SvelteComponentTyped<
1010
};
1111
}
1212
> {}
13-
export class Works3 extends SvelteComponentTyped<any, { [evt: string]: CustomEvent<any> }, never> {}
13+
export class Works3 extends SvelteComponentTyped<
14+
any,
15+
{ [evt: string]: CustomEvent<any> },
16+
Record<string, never>
17+
> {}
1418
export class DoesntWork {}

packages/svelte2tsx/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export function svelte2tsx(
7979
* The Svelte parser to use. Defaults to the one bundled with `svelte2tsx`.
8080
*/
8181
parse?: typeof import('svelte/compiler').parse;
82+
/**
83+
* The VERSION from 'svelte/compiler'. Defaults to the one bundled with `svelte2tsx`.
84+
* Transpiled output may vary between versions.
85+
*/
86+
version?: string;
8287
}
8388
): SvelteCompiledToTsx
8489

packages/svelte2tsx/repl/debug.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import fs from 'fs';
22
import { svelte2tsx } from '../src/svelte2tsx/index';
3+
import { VERSION } from 'svelte/compiler';
4+
35
const content = fs.readFileSync(`${__dirname}/index.svelte`, 'utf-8');
4-
console.log(svelte2tsx(content).code);
6+
7+
console.log(svelte2tsx(content, {version: VERSION}).code);
58
/**
69
* To enable the REPL, simply run the "dev" package script.
710
*

packages/svelte2tsx/src/htmlxtojsx_v2/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { handleSpread } from './nodes/Spread';
2626
import { handleStyleDirective } from './nodes/StyleDirective';
2727
import { handleText } from './nodes/Text';
2828
import { handleTransitionDirective } from './nodes/Transition';
29-
import { handleSnippet } from './nodes/SnippetBlock';
29+
import { handleImplicitChildren, handleSnippet } from './nodes/SnippetBlock';
3030
import { handleRenderTag } from './nodes/RenderTag';
3131

3232
type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void;
@@ -48,7 +48,11 @@ export function convertHtmlxToJsx(
4848
ast: TemplateNode,
4949
onWalk: Walker = null,
5050
onLeave: Walker = null,
51-
options: { preserveAttributeCase?: boolean; typingsNamespace?: string } = {}
51+
options: {
52+
svelte5Plus: boolean;
53+
preserveAttributeCase?: boolean;
54+
typingsNamespace?: string;
55+
} = { svelte5Plus: false }
5256
) {
5357
const htmlx = str.original;
5458
options = { preserveAttributeCase: false, ...options };
@@ -114,6 +118,9 @@ export function convertHtmlxToJsx(
114118
} else {
115119
element = new InlineComponent(str, node);
116120
}
121+
if (options.svelte5Plus) {
122+
handleImplicitChildren(node, element as InlineComponent);
123+
}
117124
break;
118125
case 'Element':
119126
case 'Options':
@@ -248,6 +255,7 @@ export function htmlx2jsx(
248255
emitOnTemplateError?: boolean;
249256
preserveAttributeCase: boolean;
250257
typingsNamespace: string;
258+
svelte5Plus: boolean;
251259
}
252260
) {
253261
const ast = parseHtmlx(htmlx, parse, { ...options }).htmlxAst;

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { surroundWithIgnoreComments } from '../../utils/ignore';
2828
export function handleSnippet(
2929
str: MagicString,
3030
snippetBlock: BaseNode,
31-
element?: InlineComponent
31+
component?: InlineComponent
3232
): void {
3333
const endSnippet = str.original.lastIndexOf('{', snippetBlock.end - 1);
3434
// Return something to silence the "snippet type not assignable to return type void" error
@@ -39,7 +39,7 @@ export function handleSnippet(
3939
const startEnd =
4040
str.original.indexOf('}', snippetBlock.context?.end || snippetBlock.expression.end) + 1;
4141

42-
if (element !== undefined) {
42+
if (component !== undefined) {
4343
str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true });
4444
const transforms: TransformationArray = ['('];
4545
if (snippetBlock.context) {
@@ -53,7 +53,10 @@ export function handleSnippet(
5353
}
5454
transforms.push(') => {');
5555
transforms.push([startEnd, snippetBlock.end]);
56-
element.addProp([[snippetBlock.expression.start, snippetBlock.expression.end]], transforms);
56+
component.addProp(
57+
[[snippetBlock.expression.start, snippetBlock.expression.end]],
58+
transforms
59+
);
5760
} else {
5861
const generic = snippetBlock.context
5962
? snippetBlock.context.typeAnnotation
@@ -79,3 +82,45 @@ export function handleSnippet(
7982
transform(str, snippetBlock.start, startEnd, startEnd, transforms);
8083
}
8184
}
85+
86+
export function handleImplicitChildren(componentNode: BaseNode, component: InlineComponent): void {
87+
if (componentNode.children?.length === 0) {
88+
return;
89+
}
90+
91+
let hasSlot = false;
92+
93+
for (const child of componentNode.children) {
94+
if (
95+
child.type === 'SvelteSelf' ||
96+
child.type === 'InlineComponent' ||
97+
child.type === 'Element' ||
98+
child.type === 'SlotTemplate'
99+
) {
100+
if (
101+
child.attributes.some(
102+
(a) =>
103+
a.type === 'Attribute' &&
104+
a.name === 'slot' &&
105+
a.value[0]?.data !== 'default'
106+
)
107+
) {
108+
continue;
109+
}
110+
}
111+
if (child.type === 'Text' && child.data.trim() === '') {
112+
continue;
113+
}
114+
if (child.type !== 'SnippetBlock') {
115+
hasSlot = true;
116+
break;
117+
}
118+
}
119+
120+
if (!hasSlot) {
121+
return;
122+
}
123+
124+
// it's enough to fake a children prop, we don't need to actually move the content inside (which would also reset control flow)
125+
component.addProp(['children'], ['() => { return __sveltets_2_any(0); }']);
126+
}

packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
1212
slots: Map<string, Map<string, string>>;
1313
events: ComponentEvents;
1414
uses$$SlotsInterface: boolean;
15+
svelte5Plus: boolean;
1516
mode?: 'ts' | 'dts';
1617
}
1718

@@ -28,6 +29,7 @@ export function createRenderFunction({
2829
uses$$slots,
2930
uses$$SlotsInterface,
3031
generics,
32+
svelte5Plus,
3133
mode
3234
}: CreateRenderFunctionPara) {
3335
const htmlx = str.original;
@@ -105,19 +107,28 @@ export function createRenderFunction({
105107
: '{' +
106108
Array.from(slots.entries())
107109
.map(([name, attrs]) => {
108-
const attrsAsString = Array.from(attrs.entries())
109-
.map(([exportName, expr]) =>
110-
exportName.startsWith('__spread__')
111-
? `...${expr}`
112-
: `${exportName}:${expr}`
113-
)
114-
.join(', ');
115-
return `'${name}': {${attrsAsString}}`;
110+
return `'${name}': {${slotAttributesToString(attrs)}}`;
116111
})
117112
.join(', ') +
118113
'}';
119114

115+
const needsImplicitChildrenProp =
116+
svelte5Plus &&
117+
!exportedNames.uses$propsRune() &&
118+
slots.has('default') &&
119+
!exportedNames.getExportsMap().has('default');
120+
if (needsImplicitChildrenProp) {
121+
exportedNames.addImplicitChildrenExport(slots.get('default')!.size > 0);
122+
}
123+
120124
const returnString =
125+
`${
126+
needsImplicitChildrenProp && slots.get('default')!.size > 0
127+
? `\nlet $$implicit_children = __sveltets_2_snippet({${slotAttributesToString(
128+
slots.get('default')!
129+
)}});`
130+
: ''
131+
}` +
121132
`\nreturn { props: ${exportedNames.createPropsStr(uses$$props || uses$$restProps)}` +
122133
`, slots: ${slotsAsDef}` +
123134
`, events: ${events.toDefString()} }}`;
@@ -127,3 +138,11 @@ export function createRenderFunction({
127138

128139
str.append(returnString);
129140
}
141+
142+
function slotAttributesToString(attrs: Map<string, string>) {
143+
return Array.from(attrs.entries())
144+
.map(([exportName, expr]) =>
145+
exportName.startsWith('__spread__') ? `...${expr}` : `${exportName}:${expr}`
146+
)
147+
.join(', ');
148+
}

0 commit comments

Comments
 (0)