Repository: sveltejs/language-tools
Pull request #1789 (2022-12-19 22:41:03)

Enhance Quickfixes to include Svelte Stores
Fixes https://github.com/sveltejs/language-tools/issues/1583



Patch:
diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
index 285c30c4..02e33b9c 100644
--- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -26,7 +26,7 @@ import {
 } from './utils';
 
 /**
- * An error which occured while trying to parse/preprocess the svelte file contents.
+ * An error which occurred while trying to parse/preprocess the svelte file contents.
  */
 export interface ParserError {
     message: string;
@@ -311,14 +311,14 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
         return this.exportedNames.has(name);
     }
 
-    svelteNodeAt(postionOrOffset: number | Position): SvelteNode | null {
+    svelteNodeAt(positionOrOffset: number | Position): SvelteNode | null {
         if (!this.htmlAst) {
             return null;
         }
         const offset =
-            typeof postionOrOffset === 'number'
-                ? postionOrOffset
-                : this.parent.offsetAt(postionOrOffset);
+            typeof positionOrOffset === 'number'
+                ? positionOrOffset
+                : this.parent.offsetAt(positionOrOffset);
 
         let foundNode: SvelteNode | null = null;
         walk(this.htmlAst, {
diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
index 8ef76ce1..c8007444 100644
--- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
+++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts
@@ -298,7 +298,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
                     userPreferences
                 )
                 .concat(
-                    this.createElementEventHandlerQuickFix(
+                    await this.getSvelteQuickFixes(
                         lang,
                         document,
                         cannotFoundNameDiagnostic,
@@ -513,18 +513,14 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
         );
     }
 
-    /**
-     * Workaround for TypesScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
-     * We can remove this once TypesScript doesn't have this limitation.
-     */
-    private createElementEventHandlerQuickFix(
+    private async getSvelteQuickFixes(
         lang: ts.LanguageService,
         document: Document,
         diagnostics: Diagnostic[],
         tsDoc: DocumentSnapshot,
         formatCodeBasis: FormatCodeBasis,
         userPreferences: ts.UserPreferences
-    ): ts.CodeFixAction[] {
+    ): Promise<ts.CodeFixAction[]> {
         const program = lang.getProgram();
         const sourceFile = program?.getSourceFile(tsDoc.filePath);
         if (!program || !sourceFile) {
@@ -532,18 +528,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
         }
 
         const typeChecker = program.getTypeChecker();
-        const result: ts.CodeFixAction[] = [];
+        const results: ts.CodeFixAction[] = [];
         const quote = getQuotePreference(sourceFile, userPreferences);
 
         for (const diagnostic of diagnostics) {
-            const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
-            if (
-                !htmlNode.attributes ||
-                !Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
-            ) {
-                continue;
-            }
-
             const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start));
             const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end));
 
@@ -553,66 +541,165 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
                 ts.isIdentifier
             );
 
-            const type = identifier && typeChecker.getContextualType(identifier);
-
-            // if it's not union typescript should be able to do it. no need to enhance
-            if (!type || !type.isUnion()) {
+            if (!identifier) {
                 continue;
             }
 
-            const nonNullable = type.getNonNullableType();
+            const isQuickFixTargetTargetStore =
+                identifier?.escapedText.toString().startsWith('$') && diagnostic.code === 2304;
+            const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(
+                document,
+                diagnostic
+            );
 
-            if (
-                !(
-                    nonNullable.flags & ts.TypeFlags.Object &&
-                    (nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
-                )
-            ) {
-                continue;
+            if (isQuickFixTargetTargetStore) {
+                results.push(
+                    ...(await this.getSvelteStoreQuickFixes(
+                        identifier,
+                        lang,
+                        document,
+                        tsDoc,
+                        userPreferences
+                    ))
+                );
             }
 
-            const signature = typeChecker.getSignaturesOfType(
-                nonNullable,
-                ts.SignatureKind.Call
-            )[0];
+            if (isQuickFixTargetEventHandler) {
+                results.push(
+                    ...this.getEventHandlerQuickFixes(
+                        identifier,
+                        tsDoc,
+                        typeChecker,
+                        quote,
+                        formatCodeBasis
+                    )
+                );
+            }
+        }
 
-            const parameters = signature.parameters.map((p) => {
-                const declaration = p.valueDeclaration ?? p.declarations?.[0];
-                const typeString = declaration
-                    ? typeChecker.typeToString(
-                          typeChecker.getTypeOfSymbolAtLocation(p, declaration)
-                      )
-                    : '';
+        return results;
+    }
 
-                return { name: p.name, typeString };
-            });
-            const returnType = typeChecker.typeToString(signature.getReturnType());
-            const useJsDoc =
-                tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
-            const parametersText = (
-                useJsDoc
-                    ? parameters.map((p) => p.name)
-                    : parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
-            ).join(', ');
-
-            const jsDoc = useJsDoc
-                ? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
-                : [];
-
-            const newText = [
-                ...jsDoc,
-                `function ${identifier.text}(${parametersText})${
-                    useJsDoc ? '' : ': ' + returnType
-                } {`,
-                formatCodeBasis.indent +
-                    `throw new Error(${quote}Function not implemented.${quote})` +
-                    formatCodeBasis.semi,
-                '}'
-            ]
-                .map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
-                .join('');
-
-            result.push({
+    private async getSvelteStoreQuickFixes(
+        identifier: ts.Identifier,
+        lang: ts.LanguageService,
+        document: Document,
+        tsDoc: DocumentSnapshot,
+        userPreferences: ts.UserPreferences
+    ): Promise<ts.CodeFixAction[]> {
+        const storeIdentifier = identifier.escapedText.toString().substring(1);
+        const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(
+            document,
+            tsDoc.scriptKind
+        );
+        const completion = lang.getCompletionsAtPosition(
+            tsDoc.filePath,
+            0,
+            userPreferences,
+            formatCodeSettings
+        );
+
+        if (!completion) {
+            return [];
+        }
+
+        const toFix = (c: ts.CompletionEntry) =>
+            lang
+                .getCompletionEntryDetails(
+                    tsDoc.filePath,
+                    0,
+                    c.name,
+                    formatCodeSettings,
+                    c.source,
+                    userPreferences,
+                    c.data
+                )
+                ?.codeActions?.map((a) => ({
+                    ...a,
+                    changes: a.changes.map((change) => {
+                        return {
+                            ...change,
+                            textChanges: change.textChanges.map((textChange) => {
+                                // For some reason, TS sometimes adds the `type` modifier. Remove it.
+                                return {
+                                    ...textChange,
+                                    newText: textChange.newText.replace(' type ', ' ')
+                                };
+                            })
+                        };
+                    }),
+                    fixName: 'import'
+                })) ?? [];
+
+        return flatten(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix));
+    }
+
+    /**
+     * Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
+     * We can remove this once TypeScript doesn't have this limitation.
+     */
+    private getEventHandlerQuickFixes(
+        identifier: ts.Identifier,
+        tsDoc: DocumentSnapshot,
+        typeChecker: ts.TypeChecker,
+        quote: string,
+        formatCodeBasis: FormatCodeBasis
+    ): ts.CodeFixAction[] {
+        const type = identifier && typeChecker.getContextualType(identifier);
+
+        // if it's not union typescript should be able to do it. no need to enhance
+        if (!type || !type.isUnion()) {
+            return [];
+        }
+
+        const nonNullable = type.getNonNullableType();
+
+        if (
+            !(
+                nonNullable.flags & ts.TypeFlags.Object &&
+                (nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
+            )
+        ) {
+            return [];
+        }
+
+        const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];
+
+        const parameters = signature.parameters.map((p) => {
+            const declaration = p.valueDeclaration ?? p.declarations?.[0];
+            const typeString = declaration
+                ? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
+                : '';
+
+            return { name: p.name, typeString };
+        });
+
+        const returnType = typeChecker.typeToString(signature.getReturnType());
+        const useJsDoc =
+            tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
+        const parametersText = (
+            useJsDoc
+                ? parameters.map((p) => p.name)
+                : parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
+        ).join(', ');
+
+        const jsDoc = useJsDoc
+            ? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
+            : [];
+
+        const newText = [
+            ...jsDoc,
+            `function ${identifier.text}(${parametersText})${useJsDoc ? '' : ': ' + returnType} {`,
+            formatCodeBasis.indent +
+                `throw new Error(${quote}Function not implemented.${quote})` +
+                formatCodeBasis.semi,
+            '}'
+        ]
+            .map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
+            .join('');
+
+        return [
+            {
                 description: `Add missing function declaration '${identifier.text}'`,
                 fixName: 'fixMissingFunctionDeclaration',
                 changes: [
@@ -626,10 +713,20 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
                         ]
                     }
                 ]
-            });
+            }
+        ];
+    }
+
+    private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) {
+        const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
+        if (
+            !htmlNode.attributes ||
+            !Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
+        ) {
+            return false;
         }
 
-        return result;
+        return true;
     }
 
     private async getApplicableRefactors(
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts
index d9594128..7410ae49 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts
@@ -5,6 +5,7 @@ import { throwError } from '../utils/error';
 
 export class Generics {
     private definitions: string[] = [];
+    private typeReferences: string[] = [];
     private references: string[] = [];
 
     constructor(private str: MagicString, private astOffset: number) {}
@@ -15,11 +16,11 @@ export class Generics {
                 throw new Error('Invalid $$Generic declaration: Only one type argument allowed');
             }
             if (node.type.typeArguments?.length === 1) {
-                this.definitions.push(
-                    `${node.name.text} extends ${node.type.typeArguments[0].getText()}`
-                );
+                const typeReference = node.type.typeArguments[0].getText();
+                this.typeReferences.push(typeReference);
+                this.definitions.push(`${node.name.text} extends ${typeReference}`);
             } else {
-                this.definitions.push(`${node.name.text}`);
+                this.definitions.push(node.name.text);
             }
             this.references.push(node.name.text);
             this.str.remove(this.astOffset + node.getStart(), this.astOffset + node.getEnd());
@@ -45,6 +46,10 @@ export class Generics {
         );
     }
 
+    getTypeReferences() {
+        return this.typeReferences;
+    }
+
     toDefinitionString(addIgnore = false) {
         const surround = addIgnore ? surroundWithIgnoreComments : (str: string) => str;
         return this.definitions.length ? surround(`<${this.definitions.join(',')}>`) : '';
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/InterfacesAndTypes.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/InterfacesAndTypes.ts
new file mode 100644
index 00000000..0c8ee79f
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/InterfacesAndTypes.ts
@@ -0,0 +1,58 @@
+import ts from 'typescript';
+import { flatten } from '../../utils/object';
+
+type TypeOrInterface = ts.InterfaceDeclaration | ts.TypeAliasDeclaration;
+
+export class InterfacesAndTypes {
+    node: TypeOrInterface | null = null;
+    private all: TypeOrInterface[] = [];
+    private references: Map<TypeOrInterface, ts.TypeReferenceNode[]> = new Map();
+
+    add(node: TypeOrInterface) {
+        this.all.push(node);
+    }
+
+    getNodesWithNames(names: string[]) {
+        return this.all.filter((node) => names.includes(node.name.text));
+    }
+
+    // The following could be used to create a informative error message in case
+    // someone has an interface that both references a generic and is used by one:
+
+    addReference(reference: ts.TypeReferenceNode) {
+        if (!this.node) {
+            return;
+        }
+
+        const references = this.references.get(this.node) || [];
+        references.push(reference);
+        this.references.set(this.node, references);
+    }
+
+    getNodesThatReferenceType(name: string) {
+        const nodes: TypeOrInterface[] = [];
+        for (const [node, references] of this.references) {
+            if (references.some((r) => r.typeName.getText() === name)) {
+                nodes.push(node);
+            }
+        }
+        return nodes;
+    }
+
+    getNodesThatRecursivelyReferenceType(name: string) {
+        let types: string[] = [name];
+        const nodes: Set<TypeOrInterface> = new Set();
+        while (types.length !== 0) {
+            const newTypes = flatten(
+                types.map((type) => this.getNodesThatReferenceType(type))
+            ).filter((t) => !nodes.has(t));
+            newTypes.forEach((t) => nodes.add(t));
+            types = newTypes.map((t) => t.name.text);
+        }
+        return [...nodes.values()];
+    }
+
+    getNodesThatRecursivelyReferenceTypes(names: string[]) {
+        return flatten(names.map((name) => this.getNodesThatRecursivelyReferenceType(name)));
+    }
+}
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts
index 91767a3c..9a0b4a5e 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts
@@ -1,5 +1,6 @@
 import MagicString from 'magic-string';
 import ts from 'typescript';
+import { moveNode } from '../utils/tsAst';
 
 /**
  * move imports to top of script so they appear outside our render function
@@ -11,62 +12,7 @@ export function handleImportDeclaration(
     scriptStart: number,
     sourceFile: ts.SourceFile
 ) {
-    const scanner = ts.createScanner(
-        sourceFile.languageVersion,
-        /*skipTrivia*/ false,
-        sourceFile.languageVariant
-    );
-
-    const comments = ts.getLeadingCommentRanges(node.getFullText(), 0) ?? [];
-    if (
-        !comments.some((comment) => comment.hasTrailingNewLine) &&
-        isNewGroup(sourceFile, node, scanner)
-    ) {
-        str.appendRight(node.getStart() + astOffset, '\n');
-    }
-
-    for (const comment of comments) {
-        const commentEnd = node.pos + comment.end + astOffset;
-        str.move(node.pos + comment.pos + astOffset, commentEnd, scriptStart + 1);
-
-        if (comment.hasTrailingNewLine) {
-            str.overwrite(commentEnd - 1, commentEnd, str.original[commentEnd - 1] + '\n');
-        }
-    }
-
-    str.move(node.getStart() + astOffset, node.end + astOffset, scriptStart + 1);
-    //add in a \n
-    const originalEndChar = str.original[node.end + astOffset - 1];
-
-    str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n');
-}
-
-/**
- * adopted from https://github.com/microsoft/TypeScript/blob/6e0447fdf165b1cec9fc80802abcc15bd23a268f/src/services/organizeImports.ts#L111
- */
-function isNewGroup(
-    sourceFile: ts.SourceFile,
-    topLevelImportDecl: ts.ImportDeclaration,
-    scanner: ts.Scanner
-) {
-    const startPos = topLevelImportDecl.getFullStart();
-    const endPos = topLevelImportDecl.getStart();
-    scanner.setText(sourceFile.text, startPos, endPos - startPos);
-
-    let numberOfNewLines = 0;
-    while (scanner.getTokenPos() < endPos) {
-        const tokenKind = scanner.scan();
-
-        if (tokenKind === ts.SyntaxKind.NewLineTrivia) {
-            numberOfNewLines++;
-
-            if (numberOfNewLines >= 2) {
-                return true;
-            }
-        }
-    }
-
-    return false;
+    return moveNode(node, str, astOffset, scriptStart, sourceFile);
 }
 
 /**
diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
index f3d67721..0fb1499c 100644
--- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
@@ -1,7 +1,7 @@
 import MagicString from 'magic-string';
 import { Node } from 'estree-walker';
 import * as ts from 'typescript';
-import { getBinaryAssignmentExpr, isNotPropertyNameOfImport } from './utils/tsAst';
+import { getBinaryAssignmentExpr, isNotPropertyNameOfImport, moveNode } from './utils/tsAst';
 import { ExportedNames, is$$PropsDeclaration } from './nodes/ExportedNames';
 import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames';
 import { ComponentEvents, is$$EventsDeclaration } from './nodes/ComponentEvents';
@@ -15,6 +15,7 @@ import {
     handleFirstInstanceImport,
     handleImportDeclaration
 } from './nodes/handleImportDeclaration';
+import { InterfacesAndTypes } from './nodes/InterfacesAndTypes';
 
 export interface InstanceScriptProcessResult {
     exportedNames: ExportedNames;
@@ -52,6 +53,7 @@ export function processInstanceScriptContent(
     const astOffset = script.content.start;
     const exportedNames = new ExportedNames(str, astOffset);
     const generics = new Generics(str, astOffset);
+    const interfacesAndTypes = new InterfacesAndTypes();
 
     const implicitTopLevelNames = new ImplicitTopLevelNames(str, astOffset);
     let uses$$props = false;
@@ -219,6 +221,12 @@ export function processInstanceScriptContent(
             implicitStoreValues.addImportStatement(node);
         }
 
+        if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
+            interfacesAndTypes.node = node;
+            interfacesAndTypes.add(node);
+            onLeaveCallbacks.push(() => (interfacesAndTypes.node = null));
+        }
+
         //handle stores etc
         if (ts.isIdentifier(node)) {
             handleIdentifier(node, parent);
@@ -264,12 +272,19 @@ export function processInstanceScriptContent(
 
     handleFirstInstanceImport(tsAst, astOffset, hasModuleScript, str);
 
+    // move interfaces and types out of the render function if they are referenced
+    // by a $$Generic, otherwise it will be used before being defined after the transformation
+    const nodesToMove = interfacesAndTypes.getNodesWithNames(generics.getTypeReferences());
+    for (const node of nodesToMove) {
+        moveNode(node, str, astOffset, script.start, tsAst);
+    }
+
     if (mode === 'dts') {
         // Transform interface declarations to type declarations because indirectly
         // using interfaces inside the return type of a function is forbidden.
         // This is not a problem for intellisense/type inference but it will
         // break dts generation (file will not be generated).
-        transformInterfacesToTypes(tsAst, str, astOffset);
+        transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove);
     }
 
     return {
@@ -283,32 +298,40 @@ export function processInstanceScriptContent(
     };
 }
 
-function transformInterfacesToTypes(tsAst: ts.SourceFile, str: MagicString, astOffset: any) {
-    tsAst.statements.filter(ts.isInterfaceDeclaration).forEach((node) => {
-        str.overwrite(
-            node.getStart() + astOffset,
-            node.getStart() + astOffset + 'interface'.length,
-            'type'
-        );
-
-        if (node.heritageClauses?.length) {
-            const extendsStart = node.heritageClauses[0].getStart() + astOffset;
-            str.overwrite(extendsStart, extendsStart + 'extends'.length, '=');
-
-            const extendsList = node.heritageClauses[0].types;
-            let prev = extendsList[0];
-            extendsList.slice(1).forEach((heritageClause) => {
-                str.overwrite(
-                    prev.getEnd() + astOffset,
-                    heritageClause.getStart() + astOffset,
-                    ' & '
-                );
-                prev = heritageClause;
-            });
-
-            str.appendLeft(node.heritageClauses[0].getEnd() + astOffset, ' & ');
-        } else {
-            str.prependLeft(str.original.indexOf('{', node.getStart() + astOffset), '=');
-        }
-    });
+function transformInterfacesToTypes(
+    tsAst: ts.SourceFile,
+    str: MagicString,
+    astOffset: any,
+    movedNodes: ts.Node[]
+) {
+    tsAst.statements
+        .filter(ts.isInterfaceDeclaration)
+        .filter((i) => !movedNodes.includes(i))
+        .forEach((node) => {
+            str.overwrite(
+                node.getStart() + astOffset,
+                node.getStart() + astOffset + 'interface'.length,
+                'type'
+            );
+
+            if (node.heritageClauses?.length) {
+                const extendsStart = node.heritageClauses[0].getStart() + astOffset;
+                str.overwrite(extendsStart, extendsStart + 'extends'.length, '=');
+
+                const extendsList = node.heritageClauses[0].types;
+                let prev = extendsList[0];
+                extendsList.slice(1).forEach((heritageClause) => {
+                    str.overwrite(
+                        prev.getEnd() + astOffset,
+                        heritageClause.getStart() + astOffset,
+                        ' & '
+                    );
+                    prev = heritageClause;
+                });
+
+                str.appendLeft(node.heritageClauses[0].getEnd() + astOffset, ' & ');
+            } else {
+                str.prependLeft(str.original.indexOf('{', node.getStart() + astOffset), '=');
+            }
+        });
 }
diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
index f58893dd..2d64cb9a 100644
--- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
@@ -1,3 +1,4 @@
+import MagicString from 'magic-string';
 import ts from 'typescript';
 
 export function isInterfaceOrTypeDeclaration(
@@ -206,3 +207,67 @@ export function isSafeToPrefixWithSemicolon(node: ts.Identifier): boolean {
         )
     );
 }
+
+/**
+ * move node to top of script so they appear outside our render function
+ */
+export function moveNode(
+    node: ts.Node,
+    str: MagicString,
+    astOffset: number,
+    scriptStart: number,
+    sourceFile: ts.SourceFile
+) {
+    const scanner = ts.createScanner(
+        sourceFile.languageVersion,
+        /*skipTrivia*/ false,
+        sourceFile.languageVariant
+    );
+
+    const comments = ts.getLeadingCommentRanges(node.getFullText(), 0) ?? [];
+    if (
+        !comments.some((comment) => comment.hasTrailingNewLine) &&
+        isNewGroup(sourceFile, node, scanner)
+    ) {
+        str.appendRight(node.getStart() + astOffset, '\n');
+    }
+
+    for (const comment of comments) {
+        const commentEnd = node.pos + comment.end + astOffset;
+        str.move(node.pos + comment.pos + astOffset, commentEnd, scriptStart + 1);
+
+        if (comment.hasTrailingNewLine) {
+            str.overwrite(commentEnd - 1, commentEnd, str.original[commentEnd - 1] + '\n');
+        }
+    }
+
+    str.move(node.getStart() + astOffset, node.end + astOffset, scriptStart + 1);
+    //add in a \n
+    const originalEndChar = str.original[node.end + astOffset - 1];
+
+    str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n');
+}
+
+/**
+ * adopted from https://github.com/microsoft/TypeScript/blob/6e0447fdf165b1cec9fc80802abcc15bd23a268f/src/services/organizeImports.ts#L111
+ */
+function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.Node, scanner: ts.Scanner) {
+    const startPos = topLevelImportDecl.getFullStart();
+    const endPos = topLevelImportDecl.getStart();
+    scanner.setText(sourceFile.text, startPos, endPos - startPos);
+
+    let numberOfNewLines = 0;
+    while (scanner.getTokenPos() < endPos) {
+        const tokenKind = scanner.scan();
+
+        if (tokenKind === ts.SyntaxKind.NewLineTrivia) {
+            numberOfNewLines++;
+
+            if (numberOfNewLines >= 2) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
diff --git a/packages/svelte2tsx/src/utils/object.ts b/packages/svelte2tsx/src/utils/object.ts
new file mode 100644
index 00000000..ade98610
--- /dev/null
+++ b/packages/svelte2tsx/src/utils/object.ts
@@ -0,0 +1,3 @@
+export function flatten<T>(arr: T[][]): T[] {
+    return arr.reduce((acc, val) => acc.concat(val), []);
+}
diff --git a/packages/typescript-plugin/src/svelte-snapshots.ts b/packages/typescript-plugin/src/svelte-snapshots.ts
index ba491ed0..5bbb199b 100644
--- a/packages/typescript-plugin/src/svelte-snapshots.ts
+++ b/packages/typescript-plugin/src/svelte-snapshots.ts
@@ -343,7 +343,7 @@ export class SvelteSnapshotManager {
 
                     // @ts-ignore
                     this.projectService.host[onReadSvelteFile].forEach((listener) =>
-                        listener(path, code, isTsFile, mapper)
+                        listener(path, svelteCode, isTsFile, mapper)
                     );
 
                     return code;


Test patch:
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expected.tsx
new file mode 100644
index 00000000..d6dd67b4
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expected.tsx
@@ -0,0 +1,38 @@
+///<reference types="svelte" />
+<></>;
+import type Foo from 'somewhere';
+interface ReferencedByGeneric {
+        b: true;
+        f: Foo;
+    }
+function render/*Ωignore_startΩ*/<A,B extends ReferencedByGeneric>/*Ωignore_endΩ*/() {
+
+    
+    interface ReferencesGeneric {
+        a: A;
+    }
+    
+    
+    
+    
+
+     let a: ReferencesGeneric/*Ωignore_startΩ*/;a = __sveltets_1_any(a);/*Ωignore_endΩ*/;
+     let b: B/*Ωignore_startΩ*/;b = __sveltets_1_any(b);/*Ωignore_endΩ*/;
+;
+() => (<></>);
+return { props: {a: a , b: b} as {a: ReferencesGeneric, b: B}, slots: {}, events: {} }}
+class __sveltets_Render<A,B extends ReferencedByGeneric> {
+    props() {
+        return render<A,B>().props;
+    }
+    events() {
+        return __sveltets_1_with_any_event(render<A,B>()).events;
+    }
+    slots() {
+        return render<A,B>().slots;
+    }
+}
+
+
+export default class Input__SvelteComponent_<A,B extends ReferencedByGeneric> extends Svelte2TsxComponent<ReturnType<__sveltets_Render<A,B>['props']>, ReturnType<__sveltets_Render<A,B>['events']>, ReturnType<__sveltets_Render<A,B>['slots']>> {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expectedv2.ts
new file mode 100644
index 00000000..6b96e92c
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/expectedv2.ts
@@ -0,0 +1,38 @@
+///<reference types="svelte" />
+;
+import type Foo from 'somewhere';
+interface ReferencedByGeneric {
+        b: true;
+        f: Foo;
+    }
+function render/*Ωignore_startΩ*/<A,B extends ReferencedByGeneric>/*Ωignore_endΩ*/() {
+
+    
+    interface ReferencesGeneric {
+        a: A;
+    }
+    
+    
+    
+    
+
+     let a: ReferencesGeneric/*Ωignore_startΩ*/;a = __sveltets_1_any(a);/*Ωignore_endΩ*/;
+     let b: B/*Ωignore_startΩ*/;b = __sveltets_1_any(b);/*Ωignore_endΩ*/;
+;
+async () => {};
+return { props: {a: a , b: b} as {a: ReferencesGeneric, b: B}, slots: {}, events: {} }}
+class __sveltets_Render<A,B extends ReferencedByGeneric> {
+    props() {
+        return render<A,B>().props;
+    }
+    events() {
+        return __sveltets_1_with_any_event(render<A,B>()).events;
+    }
+    slots() {
+        return render<A,B>().slots;
+    }
+}
+
+
+export default class Input__SvelteComponent_<A,B extends ReferencedByGeneric> extends Svelte2TsxComponent<ReturnType<__sveltets_Render<A,B>['props']>, ReturnType<__sveltets_Render<A,B>['events']>, ReturnType<__sveltets_Render<A,B>['slots']>> {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/input.svelte
new file mode 100644
index 00000000..7e1cfee1
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-$$generics-interface-references/input.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+    import type Foo from 'somewhere';
+    interface ReferencesGeneric {
+        a: A;
+    }
+    interface ReferencedByGeneric {
+        b: true;
+        f: Foo;
+    }
+    
+    type A = $$Generic;
+    type B = $$Generic<ReferencedByGeneric>;
+
+    export let a: ReferencesGeneric;
+    export let b: B;
+</script>