Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Svelte 5 fixes and improvements #2217

Merged
merged 8 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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