Skip to content

Commit

Permalink
feat: Svelte 5 fixes and improvements (#2217)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
dummdidumm authored Nov 24, 2023
1 parent 2ff3a7c commit 364ac7d
Show file tree
Hide file tree
Showing 66 changed files with 1,118 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class SveltePlugin
async getCompiledResult(document: Document): Promise<SvelteCompileResult | null> {
try {
const svelteDoc = await this.getSvelteDoc(document);
// @ts-ignore is 'client' in Svelte 5
return svelteDoc.getCompiledWith({ generate: 'dom' });
} catch (error) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot, DocumentMapper {
*/
export interface SvelteSnapshotOptions {
parse: typeof import('svelte/compiler').parse | undefined;
version: string | undefined;
transformOnTemplateError: boolean;
typingsNamespace: string;
}
Expand Down Expand Up @@ -199,6 +200,7 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
try {
const tsx = svelte2tsx(text, {
parse: options.parse,
version: options.version,
filename: document.getFilePath() ?? undefined,
isTsFile: scriptKind === ts.ScriptKind.TS,
mode: 'ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export enum DiagnosticCode {
MISSING_PROP = 2741, // "Property '..' is missing in type '..' but required in type '..'."
NO_OVERLOAD_MATCHES_CALL = 2769, // "No overload matches this call"
CANNOT_FIND_NAME = 2304, // "Cannot find name 'xxx'"
EXPECTED_N_ARGUMENTS = 2554 // Expected {0} arguments, but got {1}.
EXPECTED_N_ARGUMENTS = 2554, // Expected {0} arguments, but got {1}.
DEPRECATED_SIGNATURE = 6387 // The signature '..' of '..' is deprecated
}

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

const generatedVarRegex = /'\$\$_\w+(\.\$on)?'/;

function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
const text = document.getText();
const usesPug = document.getLanguageAttribute('template') === 'pug';
Expand All @@ -238,6 +241,14 @@ function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
}
}

if (
diagnostic.code === DiagnosticCode.DEPRECATED_SIGNATURE &&
generatedVarRegex.test(diagnostic.message)
) {
// Svelte 5: $on and constructor is deprecated, but we don't want to show this warning for generated code
return false;
}

return (
isNoUsedBeforeAssigned(diagnostic, text, tsDoc) &&
(!usesPug || isNoPugFalsePositive(diagnostic, document))
Expand Down
1 change: 1 addition & 0 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ async function createLanguageService(
let languageService = ts.createLanguageService(host);
const transformationConfig: SvelteSnapshotOptions = {
parse: svelteCompiler?.parse,
version: svelteCompiler?.VERSION,
transformOnTemplateError: docContext.transformOnTemplateError,
typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML'
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ export class Works2 extends SvelteComponentTyped<
};
}
> {}
export class Works3 extends SvelteComponentTyped<any, { [evt: string]: CustomEvent<any> }, never> {}
export class Works3 extends SvelteComponentTyped<
any,
{ [evt: string]: CustomEvent<any> },
Record<string, never>
> {}
export class DoesntWork {}
5 changes: 5 additions & 0 deletions packages/svelte2tsx/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export function svelte2tsx(
* The Svelte parser to use. Defaults to the one bundled with `svelte2tsx`.
*/
parse?: typeof import('svelte/compiler').parse;
/**
* The VERSION from 'svelte/compiler'. Defaults to the one bundled with `svelte2tsx`.
* Transpiled output may vary between versions.
*/
version?: string;
}
): SvelteCompiledToTsx

Expand Down
5 changes: 4 additions & 1 deletion packages/svelte2tsx/repl/debug.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import fs from 'fs';
import { svelte2tsx } from '../src/svelte2tsx/index';
import { VERSION } from 'svelte/compiler';

const content = fs.readFileSync(`${__dirname}/index.svelte`, 'utf-8');
console.log(svelte2tsx(content).code);

console.log(svelte2tsx(content, {version: VERSION}).code);
/**
* To enable the REPL, simply run the "dev" package script.
*
Expand Down
12 changes: 10 additions & 2 deletions packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { handleSpread } from './nodes/Spread';
import { handleStyleDirective } from './nodes/StyleDirective';
import { handleText } from './nodes/Text';
import { handleTransitionDirective } from './nodes/Transition';
import { handleSnippet } from './nodes/SnippetBlock';
import { handleImplicitChildren, handleSnippet } from './nodes/SnippetBlock';
import { handleRenderTag } from './nodes/RenderTag';

type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void;
Expand All @@ -48,7 +48,11 @@ export function convertHtmlxToJsx(
ast: TemplateNode,
onWalk: Walker = null,
onLeave: Walker = null,
options: { preserveAttributeCase?: boolean; typingsNamespace?: string } = {}
options: {
svelte5Plus: boolean;
preserveAttributeCase?: boolean;
typingsNamespace?: string;
} = { svelte5Plus: false }
) {
const htmlx = str.original;
options = { preserveAttributeCase: false, ...options };
Expand Down Expand Up @@ -114,6 +118,9 @@ export function convertHtmlxToJsx(
} else {
element = new InlineComponent(str, node);
}
if (options.svelte5Plus) {
handleImplicitChildren(node, element as InlineComponent);
}
break;
case 'Element':
case 'Options':
Expand Down Expand Up @@ -248,6 +255,7 @@ export function htmlx2jsx(
emitOnTemplateError?: boolean;
preserveAttributeCase: boolean;
typingsNamespace: string;
svelte5Plus: boolean;
}
) {
const ast = parseHtmlx(htmlx, parse, { ...options }).htmlxAst;
Expand Down
51 changes: 48 additions & 3 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { surroundWithIgnoreComments } from '../../utils/ignore';
export function handleSnippet(
str: MagicString,
snippetBlock: BaseNode,
element?: InlineComponent
component?: InlineComponent
): void {
const endSnippet = str.original.lastIndexOf('{', snippetBlock.end - 1);
// Return something to silence the "snippet type not assignable to return type void" error
Expand All @@ -39,7 +39,7 @@ export function handleSnippet(
const startEnd =
str.original.indexOf('}', snippetBlock.context?.end || snippetBlock.expression.end) + 1;

if (element !== undefined) {
if (component !== undefined) {
str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true });
const transforms: TransformationArray = ['('];
if (snippetBlock.context) {
Expand All @@ -53,7 +53,10 @@ export function handleSnippet(
}
transforms.push(') => {');
transforms.push([startEnd, snippetBlock.end]);
element.addProp([[snippetBlock.expression.start, snippetBlock.expression.end]], transforms);
component.addProp(
[[snippetBlock.expression.start, snippetBlock.expression.end]],
transforms
);
} else {
const generic = snippetBlock.context
? snippetBlock.context.typeAnnotation
Expand All @@ -79,3 +82,45 @@ export function handleSnippet(
transform(str, snippetBlock.start, startEnd, startEnd, transforms);
}
}

export function handleImplicitChildren(componentNode: BaseNode, component: InlineComponent): void {
if (componentNode.children?.length === 0) {
return;
}

let hasSlot = false;

for (const child of componentNode.children) {
if (
child.type === 'SvelteSelf' ||
child.type === 'InlineComponent' ||
child.type === 'Element' ||
child.type === 'SlotTemplate'
) {
if (
child.attributes.some(
(a) =>
a.type === 'Attribute' &&
a.name === 'slot' &&
a.value[0]?.data !== 'default'
)
) {
continue;
}
}
if (child.type === 'Text' && child.data.trim() === '') {
continue;
}
if (child.type !== 'SnippetBlock') {
hasSlot = true;
break;
}
}

if (!hasSlot) {
return;
}

// it's enough to fake a children prop, we don't need to actually move the content inside (which would also reset control flow)
component.addProp(['children'], ['() => { return __sveltets_2_any(0); }']);
}
35 changes: 27 additions & 8 deletions packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
slots: Map<string, Map<string, string>>;
events: ComponentEvents;
uses$$SlotsInterface: boolean;
svelte5Plus: boolean;
mode?: 'ts' | 'dts';
}

Expand All @@ -28,6 +29,7 @@ export function createRenderFunction({
uses$$slots,
uses$$SlotsInterface,
generics,
svelte5Plus,
mode
}: CreateRenderFunctionPara) {
const htmlx = str.original;
Expand Down Expand Up @@ -105,19 +107,28 @@ export function createRenderFunction({
: '{' +
Array.from(slots.entries())
.map(([name, attrs]) => {
const attrsAsString = Array.from(attrs.entries())
.map(([exportName, expr]) =>
exportName.startsWith('__spread__')
? `...${expr}`
: `${exportName}:${expr}`
)
.join(', ');
return `'${name}': {${attrsAsString}}`;
return `'${name}': {${slotAttributesToString(attrs)}}`;
})
.join(', ') +
'}';

const needsImplicitChildrenProp =
svelte5Plus &&
!exportedNames.uses$propsRune() &&
slots.has('default') &&
!exportedNames.getExportsMap().has('default');
if (needsImplicitChildrenProp) {
exportedNames.addImplicitChildrenExport(slots.get('default')!.size > 0);
}

const returnString =
`${
needsImplicitChildrenProp && slots.get('default')!.size > 0
? `\nlet $$implicit_children = __sveltets_2_snippet({${slotAttributesToString(
slots.get('default')!
)}});`
: ''
}` +
`\nreturn { props: ${exportedNames.createPropsStr(uses$$props || uses$$restProps)}` +
`, slots: ${slotsAsDef}` +
`, events: ${events.toDefString()} }}`;
Expand All @@ -127,3 +138,11 @@ export function createRenderFunction({

str.append(returnString);
}

function slotAttributesToString(attrs: Map<string, string>) {
return Array.from(attrs.entries())
.map(([exportName, expr]) =>
exportName.startsWith('__spread__') ? `...${expr}` : `${exportName}:${expr}`
)
.join(', ');
}
15 changes: 12 additions & 3 deletions packages/svelte2tsx/src/svelte2tsx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { createRenderFunction } from './createRenderFunction';
// @ts-ignore
import { TemplateNode } from 'svelte/types/compiler/interfaces';
import path from 'path';
import { parse } from 'svelte/compiler';
import { VERSION, parse } from 'svelte/compiler';

type TemplateProcessResult = {
/**
Expand Down Expand Up @@ -55,6 +55,7 @@ function processSvelteTemplate(
accessors?: boolean;
mode?: 'ts' | 'dts';
typingsNamespace?: string;
svelte5Plus: boolean;
}
): TemplateProcessResult {
const { htmlxAst, tags } = parseHtmlx(str.original, parse, options);
Expand Down Expand Up @@ -273,7 +274,8 @@ function processSvelteTemplate(

const rootSnippets = convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, {
preserveAttributeCase: options?.namespace == 'foreign',
typingsNamespace: options.typingsNamespace
typingsNamespace: options.typingsNamespace,
svelte5Plus: options.svelte5Plus
});

// resolve scripts
Expand Down Expand Up @@ -309,6 +311,7 @@ export function svelte2tsx(
svelte: string,
options: {
parse?: typeof import('svelte/compiler').parse;
version?: string;
filename?: string;
isTsFile?: boolean;
emitOnTemplateError?: boolean;
Expand All @@ -320,9 +323,11 @@ export function svelte2tsx(
} = { parse }
) {
options.mode = options.mode || 'ts';
options.version = options.version || VERSION;

const str = new MagicString(svelte);
const basename = path.basename(options.filename || '');
const svelte5Plus = Number(options.version![0]) > 4;
// process the htmlx as a svelte template
let {
htmlAst,
Expand All @@ -337,7 +342,10 @@ export function svelte2tsx(
componentDocumentation,
resolvedStores,
usesAccessors
} = processSvelteTemplate(str, options.parse || parse, options);
} = processSvelteTemplate(str, options.parse || parse, {
...options,
svelte5Plus
});

/* Rearrange the script tags so that module is first, and instance second followed finally by the template
* This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough
Expand Down Expand Up @@ -400,6 +408,7 @@ export function svelte2tsx(
uses$$slots,
uses$$SlotsInterface,
generics,
svelte5Plus,
mode: options.mode
});

Expand Down
Loading

0 comments on commit 364ac7d

Please sign in to comment.