Repository: sveltejs/language-tools
Pull request #1918 (2023-03-07 10:23:55)

(feat) SvelteKit typings without types
This allows you to omit the `./$types` imports and still get proper typings, by inserting them under the hood. Example:

```ts
// +page.ts
export function load(event) {}
--->
export function load(event: import('./$types').PageLoadEvent) {}
```

This works for TS files, in Svelte files, and for `svelte-check`.

Works in special SvelteKit files (`+page/layout(.server).ts/js`) for all exports (`actions`/`csr`/`ssr`/`prerender`/`trailingSlash`/`load`)


Patch:
diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
index f7a0cf0a..3b904cb9 100644
--- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -1,4 +1,5 @@
 import { EncodedSourceMap, TraceMap, originalPositionFor } from '@jridgewell/trace-mapping';
+import path from 'path';
 import { walk } from 'svelte/compiler';
 import { TemplateNode } from 'svelte/types/compiler/interfaces';
 import { svelte2tsx, IExportedNames } from 'svelte2tsx';
@@ -23,11 +24,13 @@ import {
     getScriptKindFromAttributes,
     getScriptKindFromFileName,
     isSvelteFilePath,
-    getTsCheckComment
+    getTsCheckComment,
+    findExports
 } from './utils';
 import { Logger } from '../../logger';
-import { dirname, join, resolve } from 'path';
+import { dirname, resolve } from 'path';
 import { URI } from 'vscode-uri';
+import { surroundWithIgnoreComments } from './features/utils';
 
 /**
  * An error which occurred while trying to parse/preprocess the svelte file contents.
@@ -394,17 +397,42 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
     }
 }
 
+export const kitPageFiles = new Set([
+    '+page.ts',
+    '+page.js',
+    '+layout.ts',
+    '+layout.js',
+    '+page.server.ts',
+    '+page.server.js',
+    '+layout.server.ts',
+    '+layout.server.js'
+]);
+
 /**
  * A js/ts document snapshot suitable for the ts language service and the plugin.
  * Since no mapping has to be done here, it also implements the mapper interface.
+ * If it's a SvelteKit file (e.g. +page.ts), types will be auto-added if not explicitly typed.
  */
 export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSnapshot {
     scriptKind = getScriptKindFromFileName(this.filePath);
     scriptInfo = null;
     private lineOffsets?: number[];
+    private internalLineOffsets?: number[];
+    private addedCode: Array<{
+        generatedPos: number;
+        originalPos: number;
+        length: number;
+        inserted: string;
+        total: number;
+    }> = [];
+    private originalText = this.text;
+    private kitFile = '';
 
     constructor(public version: number, public readonly filePath: string, private text: string) {
         super(pathToUrl(filePath));
+        const basename = path.basename(this.filePath);
+        this.kitFile = kitPageFiles.has(basename) ? basename : '';
+        this.adjustText();
     }
 
     getText(start: number, end: number) {
@@ -431,22 +459,65 @@ export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSn
         return offsetAt(position, this.text, this.getLineOffsets());
     }
 
+    getGeneratedPosition(originalPosition: Position): Position {
+        if (!this.kitFile || this.addedCode.length === 0) {
+            return super.getGeneratedPosition(originalPosition);
+        }
+        const pos = this.originalOffsetAt(originalPosition);
+
+        let total = 0;
+        for (const added of this.addedCode) {
+            if (pos < added.generatedPos) break;
+            total += added.length;
+        }
+
+        return this.positionAt(pos + total);
+    }
+
+    getOriginalPosition(generatedPosition: Position): Position {
+        if (!this.kitFile || this.addedCode.length === 0) {
+            return super.getOriginalPosition(generatedPosition);
+        }
+        const pos = this.offsetAt(generatedPosition);
+
+        let total = 0;
+        let idx = 0;
+        for (; idx < this.addedCode.length; idx++) {
+            const added = this.addedCode[idx];
+            if (pos < added.generatedPos) break;
+            total += added.length;
+        }
+
+        if (idx > 0) {
+            const prev = this.addedCode[idx - 1];
+            // Special case: pos is in the middle of an added range
+            if (pos > prev.generatedPos && pos < prev.generatedPos + prev.length) {
+                total -= pos - prev.generatedPos;
+            }
+        }
+
+        return this.originalPositionAt(pos - total);
+    }
+
     update(changes: TextDocumentContentChangeEvent[]): void {
         for (const change of changes) {
             let start = 0;
             let end = 0;
             if ('range' in change) {
-                start = this.offsetAt(change.range.start);
-                end = this.offsetAt(change.range.end);
+                start = this.originalOffsetAt(change.range.start);
+                end = this.originalOffsetAt(change.range.end);
             } else {
-                end = this.getLength();
+                end = this.originalText.length;
             }
 
-            this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
+            this.originalText =
+                this.originalText.slice(0, start) + change.text + this.originalText.slice(end);
         }
 
+        this.adjustText();
         this.version++;
         this.lineOffsets = undefined;
+        this.internalLineOffsets = undefined;
     }
 
     protected getLineOffsets() {
@@ -455,6 +526,166 @@ export class JSOrTSDocumentSnapshot extends IdentityMapper implements DocumentSn
         }
         return this.lineOffsets;
     }
+
+    private originalOffsetAt(position: Position): number {
+        return offsetAt(position, this.originalText, this.getOriginalLineOffsets());
+    }
+
+    private originalPositionAt(offset: number): Position {
+        return positionAt(offset, this.originalText, this.getOriginalLineOffsets());
+    }
+
+    private getOriginalLineOffsets() {
+        if (!this.kitFile) {
+            return this.getLineOffsets();
+        }
+        if (!this.internalLineOffsets) {
+            this.internalLineOffsets = getLineOffsets(this.originalText);
+        }
+        return this.internalLineOffsets;
+    }
+
+    private adjustText() {
+        this.addedCode = [];
+
+        if (this.kitFile) {
+            const source = ts.createSourceFile(
+                this.filePath,
+                this.originalText,
+                ts.ScriptTarget.Latest,
+                true,
+                this.scriptKind
+            );
+
+            const insert = (pos: number, inserted: string) => {
+                const insertionIdx = this.addedCode.findIndex((c) => c.generatedPos > pos);
+                if (insertionIdx >= 0) {
+                    for (let i = insertionIdx; i < this.addedCode.length; i++) {
+                        this.addedCode[i].generatedPos += inserted.length;
+                        this.addedCode[i].total += inserted.length;
+                    }
+                    const prevTotal = this.addedCode[insertionIdx - 1]?.total ?? 0;
+                    this.addedCode.splice(insertionIdx, 0, {
+                        generatedPos: pos + prevTotal,
+                        originalPos: pos,
+                        length: inserted.length,
+                        inserted,
+                        total: prevTotal + inserted.length
+                    });
+                } else {
+                    const prevTotal = this.addedCode[this.addedCode.length - 1]?.total ?? 0;
+                    this.addedCode.push({
+                        generatedPos: pos + prevTotal,
+                        originalPos: pos,
+                        length: inserted.length,
+                        inserted,
+                        total: prevTotal + inserted.length
+                    });
+                }
+            };
+
+            const exports = findExports(source);
+            const isTsFile = this.filePath.endsWith('.ts');
+
+            // add type to load function if not explicitly typed
+            const load = exports.get('load');
+            if (
+                load?.type === 'function' &&
+                load.node.parameters.length === 1 &&
+                !load.node.parameters[0].type &&
+                (isTsFile || !ts.getJSDocType(load.node))
+            ) {
+                const pos = load.node.parameters[0].getEnd();
+                const inserted = surroundWithIgnoreComments(
+                    `: import('./$types').${this.kitFile.includes('layout') ? 'Layout' : 'Page'}${
+                        this.kitFile.includes('server') ? 'Server' : ''
+                    }LoadEvent`
+                );
+                insert(pos, inserted);
+            }
+
+            // add type to actions variable if not explicitly typed
+            const actions = exports.get('actions');
+            if (
+                actions?.type === 'var' &&
+                !actions.node.type &&
+                (isTsFile || !ts.getJSDocType(actions.node)) &&
+                actions.node.initializer &&
+                ts.isObjectLiteralExpression(actions.node.initializer)
+            ) {
+                const pos = actions.node.initializer.getEnd();
+                const inserted = surroundWithIgnoreComments(
+                    ` satisfies import('./$types').Actions`
+                );
+                insert(pos, inserted);
+            }
+
+            // add type to prerender variable if not explicitly typed
+            const prerender = exports.get('prerender');
+            if (
+                prerender?.type === 'var' &&
+                !prerender.node.type &&
+                (isTsFile || !ts.getJSDocType(prerender.node)) &&
+                prerender.node.initializer
+            ) {
+                const pos = prerender.node.name.getEnd();
+                const inserted = surroundWithIgnoreComments(
+                    ` : import('@sveltejs/kit').PrerenderOption`
+                );
+                insert(pos, inserted);
+            }
+
+            // add type to trailingSlash variable if not explicitly typed
+            const trailingSlash = exports.get('trailingSlash');
+            if (
+                trailingSlash?.type === 'var' &&
+                !trailingSlash.node.type &&
+                (isTsFile || !ts.getJSDocType(trailingSlash.node)) &&
+                trailingSlash.node.initializer
+            ) {
+                const pos = trailingSlash.node.name.getEnd();
+                const inserted = surroundWithIgnoreComments(` : 'never' | 'always' | 'ignore'`); // TODO this should be exported from kit
+                insert(pos, inserted);
+            }
+
+            // add type to ssr variable if not explicitly typed
+            const ssr = exports.get('ssr');
+            if (
+                ssr?.type === 'var' &&
+                !ssr.node.type &&
+                (isTsFile || !ts.getJSDocType(ssr.node)) &&
+                ssr.node.initializer
+            ) {
+                const pos = ssr.node.name.getEnd();
+                const inserted = surroundWithIgnoreComments(` : boolean`);
+                insert(pos, inserted);
+            }
+
+            // add type to csr variable if not explicitly typed
+            const csr = exports.get('csr');
+            if (
+                csr?.type === 'var' &&
+                !csr.node.type &&
+                (isTsFile || !ts.getJSDocType(csr.node)) &&
+                csr.node.initializer
+            ) {
+                const pos = csr.node.name.getEnd();
+                const inserted = surroundWithIgnoreComments(` : boolean`);
+                insert(pos, inserted);
+            }
+
+            // construct generated text from internal text and addedCode array
+            let pos = 0;
+            this.text = '';
+            for (const added of this.addedCode) {
+                this.text += this.originalText.slice(pos, added.originalPos) + added.inserted;
+                pos = added.originalPos;
+            }
+            this.text += this.originalText.slice(pos);
+        } else {
+            this.text = this.originalText;
+        }
+    }
 }
 
 const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\r?\n?$/;
diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts
index ce9649ac..495de510 100644
--- a/packages/language-server/src/plugins/typescript/features/utils.ts
+++ b/packages/language-server/src/plugins/typescript/features/utils.ts
@@ -75,14 +75,25 @@ export function isComponentAtPosition(
     return !!getNodeIfIsInComponentStartTag(doc.html, doc.offsetAt(originalPosition));
 }
 
+export const IGNORE_START_COMMENT = '/*Ωignore_startΩ*/';
+export const IGNORE_END_COMMENT = '/*Ωignore_endΩ*/';
+
+/**
+ * Surrounds given string with a start/end comment which marks it
+ * to be ignored by tooling.
+ */
+export function surroundWithIgnoreComments(str: string): string {
+    return IGNORE_START_COMMENT + str + IGNORE_END_COMMENT;
+}
+
 /**
  * Checks if this a section that should be completely ignored
  * because it's purely generated.
  */
 export function isInGeneratedCode(text: string, start: number, end: number = start) {
-    const lastStart = text.lastIndexOf('/*Ωignore_startΩ*/', start);
-    const lastEnd = text.lastIndexOf('/*Ωignore_endΩ*/', start);
-    const nextEnd = text.indexOf('/*Ωignore_endΩ*/', end);
+    const lastStart = text.lastIndexOf(IGNORE_START_COMMENT, start);
+    const lastEnd = text.lastIndexOf(IGNORE_END_COMMENT, start);
+    const nextEnd = text.indexOf(IGNORE_END_COMMENT, end);
     // if lastEnd === nextEnd, this means that the str was found at the index
     // up to which is searched for it
     return (lastStart > lastEnd || lastEnd === nextEnd) && lastStart < nextEnd;
diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts
index b9c00d1f..b518fa38 100644
--- a/packages/language-server/src/plugins/typescript/utils.ts
+++ b/packages/language-server/src/plugins/typescript/utils.ts
@@ -371,3 +371,50 @@ export function hasTsExtensions(fileName: string) {
         fileName.endsWith(ts.Extension.Ts)
     );
 }
+
+/**
+ * Finds the top level const/let/function exports of a source file.
+ */
+export function findExports(source: ts.SourceFile) {
+    const exports = new Map<
+        string,
+        | {
+              type: 'function';
+              node: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
+          }
+        | { type: 'var'; node: ts.VariableDeclaration }
+    >();
+    // TODO handle indirect exports?
+    for (const statement of source.statements) {
+        if (
+            ts.isFunctionDeclaration(statement) &&
+            statement.name &&
+            ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+        ) {
+            // export function x ...
+            exports.set(statement.name.text, { type: 'function', node: statement });
+        }
+        if (
+            ts.isVariableStatement(statement) &&
+            statement.declarationList.declarations.length === 1 &&
+            ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+        ) {
+            // export const x = ...
+            const declaration = statement.declarationList.declarations[0];
+            if (
+                declaration.initializer &&
+                (ts.isFunctionExpression(declaration.initializer) ||
+                    ts.isArrowFunction(declaration.initializer))
+            ) {
+                exports.set(declaration.name.getText(), {
+                    type: 'function',
+                    node: declaration.initializer
+                });
+            } else {
+                exports.set(declaration.name.getText(), { type: 'var', node: declaration });
+            }
+        }
+    }
+
+    return exports;
+}
diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts
index 7b7fa220..3cfd7e4f 100644
--- a/packages/language-server/src/svelte-check.ts
+++ b/packages/language-server/src/svelte-check.ts
@@ -1,4 +1,4 @@
-import { isAbsolute } from 'path';
+import { basename, isAbsolute } from 'path';
 import ts from 'typescript';
 import { Diagnostic, Position, Range } from 'vscode-languageserver';
 import { WorkspaceFolder } from 'vscode-languageserver-protocol';
@@ -16,6 +16,8 @@ import {
 import { createLanguageServices } from './plugins/css/service';
 import { convertRange, getDiagnosticTag, mapSeverity } from './plugins/typescript/utils';
 import { pathToUrl, urlToPath } from './utils';
+import { isInGeneratedCode } from './plugins/typescript/features/utils';
+import { kitPageFiles } from './plugins/typescript/DocumentSnapshot';
 
 export type SvelteCheckDiagnosticSource = 'js' | 'css' | 'svelte';
 
@@ -198,6 +200,7 @@ export class SvelteCheck {
                         (options.skipDefaultLibCheck && file.hasNoDefaultLib) ||
                         // ignore JS files in node_modules
                         /\/node_modules\/.+\.(c|m)?js$/.test(file.fileName);
+                    const isKitFile = kitPageFiles.has(basename(file.fileName));
 
                     const diagnostics = skipDiagnosticsForFile
                         ? []
@@ -205,20 +208,46 @@ export class SvelteCheck {
                               ...lang.getSyntacticDiagnostics(file.fileName),
                               ...lang.getSuggestionDiagnostics(file.fileName),
                               ...lang.getSemanticDiagnostics(file.fileName)
-                          ].map<Diagnostic>((diagnostic) => ({
-                              range: convertRange(
-                                  { positionAt: file.getLineAndCharacterOfPosition.bind(file) },
-                                  diagnostic
-                              ),
-                              severity: mapSeverity(diagnostic.category),
-                              source: diagnostic.source,
-                              message: ts.flattenDiagnosticMessageText(
-                                  diagnostic.messageText,
-                                  '\n'
-                              ),
-                              code: diagnostic.code,
-                              tags: getDiagnosticTag(diagnostic)
-                          }));
+                          ]
+                              .filter((diagnostic) => {
+                                  if (!isKitFile) {
+                                      return true;
+                                  }
+
+                                  if (
+                                      diagnostic.start === undefined ||
+                                      diagnostic.length === undefined
+                                  ) {
+                                      return true;
+                                  }
+
+                                  const text = lang
+                                      .getProgram()
+                                      ?.getSourceFile(file.fileName)
+                                      ?.getFullText();
+                                  return (
+                                      !text ||
+                                      !isInGeneratedCode(
+                                          text,
+                                          diagnostic.start,
+                                          diagnostic.start + diagnostic.length
+                                      )
+                                  );
+                              })
+                              .map<Diagnostic>((diagnostic) => ({
+                                  range: convertRange(
+                                      { positionAt: file.getLineAndCharacterOfPosition.bind(file) },
+                                      diagnostic
+                                  ),
+                                  severity: mapSeverity(diagnostic.category),
+                                  source: diagnostic.source,
+                                  message: ts.flattenDiagnosticMessageText(
+                                      diagnostic.messageText,
+                                      '\n'
+                                  ),
+                                  code: diagnostic.code,
+                                  tags: getDiagnosticTag(diagnostic)
+                              }));
 
                     return {
                         filePath: file.fileName,
diff --git a/packages/svelte-check/src/writers.ts b/packages/svelte-check/src/writers.ts
index 511468ed..aa18a1b3 100644
--- a/packages/svelte-check/src/writers.ts
+++ b/packages/svelte-check/src/writers.ts
@@ -83,25 +83,38 @@ export class HumanFriendlyWriter implements Writer {
     private getCodeLine(diagnostic: Diagnostic, text: string) {
         const startOffset = offsetAt(diagnostic.range.start, text);
         const endOffset = offsetAt(diagnostic.range.end, text);
-        const codePrev = text.substring(
-            offsetAt({ line: diagnostic.range.start.line, character: 0 }, text),
-            startOffset
+        const codePrev = this.removeGeneratedCode(
+            text.substring(
+                offsetAt({ line: diagnostic.range.start.line, character: 0 }, text),
+                startOffset
+            )
         );
         const codeHighlight = pc.magenta(text.substring(startOffset, endOffset));
-        const codePost = text.substring(
-            endOffset,
-            offsetAt({ line: diagnostic.range.end.line, character: Number.MAX_SAFE_INTEGER }, text)
+        const codePost = this.removeGeneratedCode(
+            text.substring(
+                endOffset,
+                offsetAt(
+                    { line: diagnostic.range.end.line, character: Number.MAX_SAFE_INTEGER },
+                    text
+                )
+            )
         );
         return codePrev + codeHighlight + codePost;
     }
 
     private getLine(line: number, text: string): string {
-        return text.substring(
-            offsetAt({ line, character: 0 }, text),
-            offsetAt({ line, character: Number.MAX_SAFE_INTEGER }, text)
+        return this.removeGeneratedCode(
+            text.substring(
+                offsetAt({ line, character: 0 }, text),
+                offsetAt({ line, character: Number.MAX_SAFE_INTEGER }, text)
+            )
         );
     }
 
+    private removeGeneratedCode(text: string): string {
+        return text.replace(/\/\*Ωignore_startΩ\*\/.+\/\*Ωignore_endΩ\*\//g, '');
+    }
+
     completion(
         _f: number,
         errorCount: number,
diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts
index b6e3acf2..15440537 100644
--- a/packages/svelte2tsx/src/svelte2tsx/index.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/index.ts
@@ -22,6 +22,7 @@ import { Generics } from './nodes/Generics';
 import { addComponentExport } from './addComponentExport';
 import { createRenderFunction } from './createRenderFunction';
 import { TemplateNode } from 'svelte/types/compiler/interfaces';
+import path from 'path';
 
 type TemplateProcessResult = {
     /**
@@ -315,6 +316,7 @@ export function svelte2tsx(
     options.mode = options.mode || 'ts';
 
     const str = new MagicString(svelte);
+    const basename = path.basename(options.filename || '');
     // process the htmlx as a svelte template
     let {
         htmlAst,
@@ -352,7 +354,7 @@ export function svelte2tsx(
         : instanceScriptTarget;
     const implicitStoreValues = new ImplicitStoreValues(resolvedStores, renderFunctionStart);
     //move the instance script and process the content
-    let exportedNames = new ExportedNames(str, 0);
+    let exportedNames = new ExportedNames(str, 0, basename);
     let generics = new Generics(str, 0);
     let uses$$SlotsInterface = false;
     if (scriptTag) {
@@ -366,7 +368,8 @@ export function svelte2tsx(
             events,
             implicitStoreValues,
             options.mode,
-            /**hasModuleScripts */ !!moduleScriptTag
+            /**hasModuleScripts */ !!moduleScriptTag,
+            basename
         );
         uses$$props = uses$$props || res.uses$$props;
         uses$$restProps = uses$$restProps || res.uses$$restProps;
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
index c8876ae8..d0cfc58d 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
@@ -18,6 +18,8 @@ interface ExportedName {
     doc?: string;
 }
 
+const kitPageFiles = new Set(['+page.svelte', '+layout.svelte']);
+
 export class ExportedNames {
     /**
      * Uses the $$Props type
@@ -33,7 +35,7 @@ export class ExportedNames {
     private doneDeclarationTransformation = new Set<ts.VariableDeclarationList>();
     private getters = new Set<string>();
 
-    constructor(private str: MagicString, private astOffset: number) {}
+    constructor(private str: MagicString, private astOffset: number, private basename: string) {}
 
     handleVariableStatement(node: ts.VariableStatement, parent: ts.Node): void {
         const exportModifier = findExportKeyword(node);
@@ -113,6 +115,24 @@ export class ExportedNames {
             const tsType = declaration.type;
             const jsDocType = ts.getJSDocType(declaration);
             const type = tsType || jsDocType;
+            const name = identifier.getText();
+            const isKitExport =
+                kitPageFiles.has(this.basename) &&
+                (name === 'data' || name === 'form' || name === 'snapshot');
+            // TS types are not allowed in JS files, but TS will still pick it up and the ignore comment will filter out the error
+            const kitType =
+                isKitExport && !type
+                    ? `: import('./$types').${
+                          name === 'data'
+                              ? this.basename.includes('layout')
+                                  ? 'LayoutData'
+                                  : 'PageData'
+                              : name === 'form'
+                              ? 'ActionData'
+                              : 'Snapshot'
+                      }`
+                    : '';
+            const end = declaration.end + this.astOffset;
 
             if (
                 ts.isIdentifier(identifier) &&
@@ -129,13 +149,14 @@ export class ExportedNames {
                         )))
             ) {
                 const name = identifier.getText();
-                const end = declaration.end + this.astOffset;
 
                 preprendStr(
                     this.str,
                     end,
-                    surroundWithIgnoreComments(`;${name} = __sveltets_2_any(${name});`)
+                    surroundWithIgnoreComments(`${kitType};${name} = __sveltets_2_any(${name});`)
                 );
+            } else if (kitType) {
+                preprendStr(this.str, end, surroundWithIgnoreComments(`${kitType}`));
             }
         };
 
diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
index 6e820c82..d7939d19 100644
--- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
@@ -39,7 +39,8 @@ export function processInstanceScriptContent(
     events: ComponentEvents,
     implicitStoreValues: ImplicitStoreValues,
     mode: 'ts' | 'dts',
-    hasModuleScript: boolean
+    hasModuleScript: boolean,
+    basename: string
 ): InstanceScriptProcessResult {
     const htmlx = str.original;
     const scriptContent = htmlx.substring(script.content.start, script.content.end);
@@ -51,7 +52,7 @@ export function processInstanceScriptContent(
         ts.ScriptKind.TS
     );
     const astOffset = script.content.start;
-    const exportedNames = new ExportedNames(str, astOffset);
+    const exportedNames = new ExportedNames(str, astOffset, basename);
     const generics = new Generics(str, astOffset);
     const interfacesAndTypes = new InterfacesAndTypes();
 
diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
index 2d64cb9a..a189c75b 100644
--- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
@@ -8,7 +8,9 @@ export function isInterfaceOrTypeDeclaration(
 }
 
 export function findExportKeyword(node: ts.Node) {
-    return node.modifiers?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword);
+    return ts.canHaveModifiers(node)
+        ? ts.getModifiers(node)?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword)
+        : undefined;
 }
 
 /**
diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts
index 8781c5be..387b1c5d 100644
--- a/packages/typescript-plugin/src/index.ts
+++ b/packages/typescript-plugin/src/index.ts
@@ -137,6 +137,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
                 snapshotManager,
                 logger,
                 configManager,
+                info,
                 modules.typescript
             ),
             projectSvelteFilesManager ?? {
diff --git a/packages/typescript-plugin/src/language-service/completions.ts b/packages/typescript-plugin/src/language-service/completions.ts
index b18c5a1b..eb340e5f 100644
--- a/packages/typescript-plugin/src/language-service/completions.ts
+++ b/packages/typescript-plugin/src/language-service/completions.ts
@@ -1,17 +1,75 @@
-import { basename, dirname, extname } from 'path';
-import type ts from 'typescript';
+import { basename, dirname } from 'path';
+import type ts from 'typescript/lib/tsserverlibrary';
 import { Logger } from '../logger';
-import { isSvelteFilePath, replaceDeep } from '../utils';
+import { findNodeAtPosition, isSvelteFilePath, isTopLevelExport, replaceDeep } from '../utils';
+import { getVirtualLS, kitExports } from './sveltekit';
 
 type _ts = typeof ts;
 
 const componentPostfix = '__SvelteComponent_';
 
-export function decorateCompletions(ls: ts.LanguageService, ts: _ts, logger: Logger): void {
+export function decorateCompletions(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
+    logger: Logger
+): void {
     const getCompletionsAtPosition = ls.getCompletionsAtPosition;
-    ls.getCompletionsAtPosition = (fileName, position, options) => {
-        const completions = getCompletionsAtPosition(fileName, position, options);
+    ls.getCompletionsAtPosition = (fileName, position, options, settings) => {
+        let completions;
+
+        const result = getVirtualLS(fileName, info, ts);
+        if (result) {
+            const { languageService, toVirtualPos, toOriginalPos } = result;
+            completions = languageService.getCompletionsAtPosition(
+                fileName,
+                toVirtualPos(position),
+                options,
+                settings
+            );
+            if (completions) {
+                completions.entries = completions.entries.map((c) => {
+                    if (c.replacementSpan) {
+                        return {
+                            ...c,
+                            replacementSpan: {
+                                ...c.replacementSpan,
+                                start: toOriginalPos(c.replacementSpan.start).pos
+                            }
+                        };
+                    }
+                    return c;
+                });
+            }
+        }
+
+        completions =
+            completions ?? getCompletionsAtPosition(fileName, position, options, settings);
         if (!completions) {
+            // No completions hints at a top level export in the making
+            const source = ls.getProgram()?.getSourceFile(fileName);
+            const node = source && findNodeAtPosition(source, position);
+            if (node && isTopLevelExport(ts, node, source)) {
+                return {
+                    entries: Object.entries(kitExports).map(([key, value]) => ({
+                        kind: ts.ScriptElementKind.constElement,
+                        name: key,
+                        labelDetails: {
+                            description: value.documentation.map((d) => d.text).join('')
+                        },
+                        sortText: '0',
+                        data: {
+                            __sveltekit: key,
+                            exportName: key // TS needs this
+                        } as any
+                    })),
+                    isGlobalCompletion: false,
+                    isMemberCompletion: false,
+                    isNewIdentifierLocation: false,
+                    isIncomplete: true
+                };
+            }
+
             return completions;
         }
 
@@ -60,20 +118,6 @@ export function decorateCompletions(ls: ts.LanguageService, ts: _ts, logger: Log
             }
         }
 
-        if (basename(fileName).startsWith('+page') || basename(fileName).startsWith('+layout')) {
-            if (extname(fileName) === '.js') {
-                completions.entries.push({
-                    // TODO doesn't show up for "export" and "load" only at the bottom for "function"
-                    kind: ts.ScriptElementKind.functionElement,
-                    name: 'export function load() {}',
-                    sortText: '-99',
-                    kindModifiers: ts.ScriptElementKindModifier.exportedModifier,
-                    insertText: `/** @type {import('./$types').Foo} */\nexport function load() {}`
-                });
-            } else {
-            }
-        }
-
         return {
             ...completions,
             entries: completions.entries.map((entry) => {
@@ -101,17 +145,62 @@ export function decorateCompletions(ls: ts.LanguageService, ts: _ts, logger: Log
         preferences,
         data
     ) => {
+        if ((data as any)?.__sveltekit) {
+            const key = (data as any)?.__sveltekit as keyof typeof kitExports;
+            return {
+                name: key,
+                kind: ts.ScriptElementKind.constElement,
+                kindModifiers: ts.ScriptElementKindModifier.none,
+                displayParts: kitExports[key].displayParts,
+                documentation: kitExports[key].documentation
+            };
+        }
+
         const is$typeImport = (data as any)?.__is_sveltekit$typeImport;
+        let details: ts.CompletionEntryDetails | undefined;
 
-        const details = getCompletionEntryDetails(
-            fileName,
-            position,
-            entryName,
-            formatOptions,
-            source,
-            preferences,
-            data
-        );
+        const result = getVirtualLS(fileName, info, ts);
+        if (result) {
+            const { languageService, toVirtualPos, toOriginalPos } = result;
+            details = languageService.getCompletionEntryDetails(
+                fileName,
+                toVirtualPos(position),
+                entryName,
+                formatOptions,
+                source,
+                preferences,
+                data
+            );
+            if (details) {
+                details.codeActions = details.codeActions?.map((codeAction) => {
+                    codeAction.changes = codeAction.changes.map((change) => {
+                        change.textChanges = change.textChanges.map((textChange) => {
+                            return {
+                                ...textChange,
+                                span: {
+                                    ...textChange.span,
+                                    start: toOriginalPos(textChange.span.start).pos
+                                }
+                            };
+                        });
+                        return change;
+                    });
+                    return codeAction;
+                });
+            }
+        }
+
+        details =
+            details ??
+            getCompletionEntryDetails(
+                fileName,
+                position,
+                entryName,
+                formatOptions,
+                source,
+                preferences,
+                data
+            );
 
         if (details) {
             if (is$typeImport) {
@@ -157,6 +246,16 @@ export function decorateCompletions(ls: ts.LanguageService, ts: _ts, logger: Log
 
         return replaceDeep(svelteDetails, componentPostfix, '');
     };
+
+    const getSignatureHelpItems = ls.getSignatureHelpItems;
+    ls.getSignatureHelpItems = (fileName, position, options) => {
+        const result = getVirtualLS(fileName, info, ts);
+        if (result) {
+            const { languageService, toVirtualPos } = result;
+            return languageService.getSignatureHelpItems(fileName, toVirtualPos(position), options);
+        }
+        return getSignatureHelpItems(fileName, position, options);
+    };
 }
 
 function adjustPath(path: string) {
diff --git a/packages/typescript-plugin/src/language-service/definition.ts b/packages/typescript-plugin/src/language-service/definition.ts
index de292fae..40f0d27b 100644
--- a/packages/typescript-plugin/src/language-service/definition.ts
+++ b/packages/typescript-plugin/src/language-service/definition.ts
@@ -2,9 +2,14 @@ import type ts from 'typescript/lib/tsserverlibrary';
 import { Logger } from '../logger';
 import { SvelteSnapshotManager } from '../svelte-snapshots';
 import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';
+import { getVirtualLS } from './sveltekit';
+
+type _ts = typeof ts;
 
 export function decorateGetDefinition(
     ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
     snapshotManager: SvelteSnapshotManager,
     logger: Logger
 ): void {
@@ -12,7 +17,7 @@ export function decorateGetDefinition(
     ls.getDefinitionAndBoundSpan = (fileName, position) => {
         const definition = getDefinitionAndBoundSpan(fileName, position);
         if (!definition?.definitions) {
-            return definition;
+            return getKitDefinitions(ts, info, fileName, position);
         }
 
         return {
@@ -44,3 +49,24 @@ export function decorateGetDefinition(
         };
     };
 }
+
+function getKitDefinitions(
+    ts: _ts,
+    info: ts.server.PluginCreateInfo,
+    fileName: string,
+    position: number
+) {
+    const result = getVirtualLS(fileName, info, ts);
+    if (!result) return;
+    const { languageService, toOriginalPos, toVirtualPos } = result;
+    const virtualPos = toVirtualPos(position);
+    const definitions = languageService.getDefinitionAndBoundSpan(fileName, virtualPos);
+    if (!definitions) return;
+    // Assumption: This is only called when the original definitions didn't turn up anything.
+    // Therefore we are called on things like export function load ({ fetch }) .
+    // This means the textSpan needs conversion but none of the definitions because they are all referencing other files.
+    return {
+        ...definitions,
+        textSpan: { ...definitions.textSpan, start: toOriginalPos(definitions.textSpan.start).pos }
+    };
+}
diff --git a/packages/typescript-plugin/src/language-service/diagnostics.ts b/packages/typescript-plugin/src/language-service/diagnostics.ts
index 78fcd215..031a6dfb 100644
--- a/packages/typescript-plugin/src/language-service/diagnostics.ts
+++ b/packages/typescript-plugin/src/language-service/diagnostics.ts
@@ -1,14 +1,27 @@
 import type ts from 'typescript/lib/tsserverlibrary';
 import { Logger } from '../logger';
 import { isSvelteFilePath } from '../utils';
+import { getVirtualLS } from './sveltekit';
 
-export function decorateDiagnostics(ls: ts.LanguageService, logger: Logger): void {
-    decorateSyntacticDiagnostics(ls);
-    decorateSemanticDiagnostics(ls);
-    decorateSuggestionDiagnostics(ls);
+type _ts = typeof ts;
+
+export function decorateDiagnostics(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    typescript: typeof ts,
+    logger: Logger
+): void {
+    decorateSyntacticDiagnostics(ls, info, typescript, logger);
+    decorateSemanticDiagnostics(ls, info, typescript, logger);
+    decorateSuggestionDiagnostics(ls, info, typescript, logger);
 }
 
-function decorateSyntacticDiagnostics(ls: ts.LanguageService): void {
+function decorateSyntacticDiagnostics(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    typescript: typeof ts,
+    logger: Logger
+): void {
     const getSyntacticDiagnostics = ls.getSyntacticDiagnostics;
     ls.getSyntacticDiagnostics = (fileName: string) => {
         // Diagnostics inside Svelte files are done
@@ -16,11 +29,24 @@ function decorateSyntacticDiagnostics(ls: ts.LanguageService): void {
         if (isSvelteFilePath(fileName)) {
             return [];
         }
-        return getSyntacticDiagnostics(fileName);
+
+        const kitDiagnostics = getKitDiagnostics(
+            'getSyntacticDiagnostics',
+            fileName,
+            info,
+            typescript,
+            logger
+        );
+        return kitDiagnostics ?? getSyntacticDiagnostics(fileName);
     };
 }
 
-function decorateSemanticDiagnostics(ls: ts.LanguageService): void {
+function decorateSemanticDiagnostics(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    typescript: typeof ts,
+    logger: Logger
+): void {
     const getSemanticDiagnostics = ls.getSemanticDiagnostics;
     ls.getSemanticDiagnostics = (fileName: string) => {
         // Diagnostics inside Svelte files are done
@@ -28,11 +54,24 @@ function decorateSemanticDiagnostics(ls: ts.LanguageService): void {
         if (isSvelteFilePath(fileName)) {
             return [];
         }
-        return getSemanticDiagnostics(fileName);
+
+        const kitDiagnostics = getKitDiagnostics(
+            'getSemanticDiagnostics',
+            fileName,
+            info,
+            typescript,
+            logger
+        );
+        return kitDiagnostics ?? getSemanticDiagnostics(fileName);
     };
 }
 
-function decorateSuggestionDiagnostics(ls: ts.LanguageService): void {
+function decorateSuggestionDiagnostics(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    typescript: typeof ts,
+    logger: Logger
+): void {
     const getSuggestionDiagnostics = ls.getSuggestionDiagnostics;
     ls.getSuggestionDiagnostics = (fileName: string) => {
         // Diagnostics inside Svelte files are done
@@ -40,6 +79,67 @@ function decorateSuggestionDiagnostics(ls: ts.LanguageService): void {
         if (isSvelteFilePath(fileName)) {
             return [];
         }
-        return getSuggestionDiagnostics(fileName);
+
+        const kitDiagnostics = getKitDiagnostics(
+            'getSuggestionDiagnostics',
+            fileName,
+            info,
+            typescript,
+            logger
+        );
+        return kitDiagnostics ?? getSuggestionDiagnostics(fileName);
     };
 }
+
+function getKitDiagnostics<
+    T extends 'getSemanticDiagnostics' | 'getSuggestionDiagnostics' | 'getSyntacticDiagnostics'
+>(
+    methodName: T,
+    fileName: string,
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
+    logger?: Logger
+): ReturnType<ts.LanguageService[T]> | undefined {
+    const result = getVirtualLS(fileName, info, ts, logger);
+    if (!result) return;
+
+    const { languageService, toOriginalPos } = result;
+
+    const diagnostics = [];
+    for (let diagnostic of languageService[methodName](fileName)) {
+        if (!diagnostic.start || !diagnostic.length) {
+            diagnostics.push(diagnostic);
+            continue;
+        }
+
+        const mapped = toOriginalPos(diagnostic.start);
+        if (mapped.inGenerated) {
+            // If not "Cannot find module './$types' .." then filter out
+            if (diagnostic.code !== 2307) {
+                continue;
+            } else {
+                diagnostic = {
+                    ...diagnostic,
+                    // adjust length so it doesn't spill over to the next line
+                    length: 1,
+                    messageText:
+                        typeof diagnostic.messageText === 'string' &&
+                        diagnostic.messageText.includes('./$types')
+                            ? diagnostic.messageText +
+                              ` (this likely means that SvelteKit's type generation didn't run yet)`
+                            : diagnostic.messageText
+                };
+            }
+        }
+
+        diagnostic = {
+            ...diagnostic,
+            start: mapped.pos
+        };
+
+        diagnostics.push(diagnostic);
+    }
+
+    // @ts-ignore TS doesn't get the return type right
+    return diagnostics;
+}
diff --git a/packages/typescript-plugin/src/language-service/hover.ts b/packages/typescript-plugin/src/language-service/hover.ts
new file mode 100644
index 00000000..df1e8ed6
--- /dev/null
+++ b/packages/typescript-plugin/src/language-service/hover.ts
@@ -0,0 +1,41 @@
+import type ts from 'typescript/lib/tsserverlibrary';
+import { Logger } from '../logger';
+import { findNodeAtPosition, isTopLevelExport } from '../utils';
+import { getVirtualLS, kitExports } from './sveltekit';
+
+type _ts = typeof ts;
+
+export function decorateHover(
+    ls: ts.LanguageService,
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
+    logger: Logger
+): void {
+    const getQuickInfoAtPosition = ls.getQuickInfoAtPosition;
+
+    ls.getQuickInfoAtPosition = (fileName: string, position: number) => {
+        const result = getVirtualLS(fileName, info, ts);
+        if (!result) return getQuickInfoAtPosition(fileName, position);
+
+        const { languageService, toOriginalPos, toVirtualPos } = result;
+        const virtualPos = toVirtualPos(position);
+        const quickInfo = languageService.getQuickInfoAtPosition(fileName, virtualPos);
+        if (!quickInfo) return quickInfo;
+
+        const source = languageService.getProgram()?.getSourceFile(fileName);
+        const node = source && findNodeAtPosition(source, virtualPos);
+        if (node && isTopLevelExport(ts, node, source) && ts.isIdentifier(node)) {
+            const name = node.text;
+            if (name in kitExports) {
+                quickInfo.documentation = !quickInfo.documentation?.length
+                    ? kitExports[name as any as keyof typeof kitExports].documentation
+                    : quickInfo.documentation;
+            }
+        }
+
+        return {
+            ...quickInfo,
+            textSpan: { ...quickInfo.textSpan, start: toOriginalPos(quickInfo.textSpan.start).pos }
+        };
+    };
+}
diff --git a/packages/typescript-plugin/src/language-service/index.ts b/packages/typescript-plugin/src/language-service/index.ts
index ec3171ff..f3ca487c 100644
--- a/packages/typescript-plugin/src/language-service/index.ts
+++ b/packages/typescript-plugin/src/language-service/index.ts
@@ -8,6 +8,7 @@ import { decorateCompletions } from './completions';
 import { decorateGetDefinition } from './definition';
 import { decorateDiagnostics } from './diagnostics';
 import { decorateFindReferences } from './find-references';
+import { decorateHover } from './hover';
 import { decorateGetImplementation } from './implementation';
 import { decorateRename } from './rename';
 import { decorateUpdateImports } from './update-imports';
@@ -23,12 +24,13 @@ export function decorateLanguageService(
     snapshotManager: SvelteSnapshotManager,
     logger: Logger,
     configManager: ConfigManager,
+    info: ts.server.PluginCreateInfo,
     typescript: typeof ts
 ) {
     // Decorate using a proxy so we can dynamically enable/disable method
     // patches depending on the enabled state of our config
     const proxy = new Proxy(ls, createProxyHandler(configManager));
-    decorateLanguageServiceInner(proxy, snapshotManager, logger, typescript);
+    decorateLanguageServiceInner(proxy, snapshotManager, logger, info, typescript);
     return proxy;
 }
 
@@ -36,17 +38,19 @@ function decorateLanguageServiceInner(
     ls: ts.LanguageService,
     snapshotManager: SvelteSnapshotManager,
     logger: Logger,
+    info: ts.server.PluginCreateInfo,
     typescript: typeof ts
 ): ts.LanguageService {
     patchLineColumnOffset(ls, snapshotManager);
     decorateRename(ls, snapshotManager, logger);
-    decorateDiagnostics(ls, logger);
+    decorateDiagnostics(ls, info, typescript, logger);
     decorateFindReferences(ls, snapshotManager, logger);
-    decorateCompletions(ls, typescript, logger);
-    decorateGetDefinition(ls, snapshotManager, logger);
+    decorateCompletions(ls, info, typescript, logger);
+    decorateGetDefinition(ls, info, typescript, snapshotManager, logger);
     decorateGetImplementation(ls, snapshotManager, logger);
     decorateUpdateImports(ls, snapshotManager, logger);
     decorateCallHierarchy(ls, snapshotManager, typescript);
+    decorateHover(ls, info, typescript, logger);
     return ls;
 }
 
diff --git a/packages/typescript-plugin/src/language-service/sveltekit.ts b/packages/typescript-plugin/src/language-service/sveltekit.ts
new file mode 100644
index 00000000..2b72159d
--- /dev/null
+++ b/packages/typescript-plugin/src/language-service/sveltekit.ts
@@ -0,0 +1,663 @@
+import path from 'path';
+import type ts from 'typescript/lib/tsserverlibrary';
+import { Logger } from '../logger';
+type _ts = typeof ts;
+
+interface KitSnapshot {
+    file: ts.IScriptSnapshot;
+    version: string;
+    addedCode: Array<{
+        generatedPos: number;
+        originalPos: number;
+        length: number;
+        total: number;
+        inserted: string;
+    }>;
+}
+
+const cache = new WeakMap<
+    ts.server.PluginCreateInfo,
+    {
+        languageService: ts.LanguageService;
+        languageServiceHost: ts.LanguageServiceHost & {
+            getKitScriptSnapshotIfUpToDate: (fileName: string) => KitSnapshot | undefined;
+            upsertKitFile: (fileName: string) => void;
+        };
+    }
+>();
+
+export const kitExports = {
+    prerender: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'prerender',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'boolean',
+                kind: 'keyword'
+            },
+            {
+                text: ' | ',
+                kind: 'punctuation'
+            },
+            {
+                text: "'auto'",
+                kind: 'stringLiteral'
+            }
+        ],
+        documentation: [
+            {
+                text: 'Control whether or not this page is prerendered. More info: https://kit.svelte.dev/docs/page-options#prerender',
+                kind: 'text'
+            }
+        ]
+    },
+    ssr: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'ssr',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'boolean',
+                kind: 'keyword'
+            }
+        ],
+        documentation: [
+            {
+                text: 'Control whether or not this page is server-side rendered. More info: https://kit.svelte.dev/docs/page-options#ssr',
+                kind: 'text'
+            }
+        ]
+    },
+    csr: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'csr',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'boolean',
+                kind: 'keyword'
+            }
+        ],
+        documentation: [
+            {
+                text: 'Control whether or not this page is hydrated (i.e. if JS is delivered to the client). More info: https://kit.svelte.dev/docs/page-options#csr',
+                kind: 'text'
+            }
+        ]
+    },
+    trailingSlash: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'trailingSlash',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: "'auto' | 'always' | 'never'",
+                kind: 'stringLiteral'
+            }
+        ],
+        documentation: [
+            {
+                text: 'Control how SvelteKit should handle (missing) trailing slashes in the URL. More info: https://kit.svelte.dev/docs/page-options#trailingslash',
+                kind: 'text'
+            }
+        ]
+    },
+    config: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'config',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'Config',
+                kind: 'interfaceName'
+            }
+        ],
+        documentation: [
+            {
+                text:
+                    `With the concept of adapters, SvelteKit is able to run on a variety of platforms. ` +
+                    `Each of these might have specific configuration to further tweak the deployment, which you can configure here. ` +
+                    `More info: https://kit.svelte.dev/docs/page-options#config`,
+                kind: 'text'
+            }
+        ]
+    },
+    actions: {
+        displayParts: [
+            {
+                text: 'const',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'actions',
+                kind: 'localName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'Actions',
+                kind: 'interfaceName'
+            }
+        ],
+        documentation: [
+            {
+                text:
+                    `An object of methods which handle form POST requests. ` +
+                    `More info: https://kit.svelte.dev/docs/form-actions`,
+                kind: 'text'
+            }
+        ]
+    },
+    load: {
+        displayParts: [
+            {
+                text: 'export',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'function',
+                kind: 'keyword'
+            },
+            {
+                text: ' ',
+                kind: 'space'
+            },
+            {
+                text: 'load',
+                kind: 'localName'
+            },
+            {
+                text: '(',
+                kind: 'punctuation'
+            },
+            {
+                text: 'event',
+                kind: 'parameterName'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'LoadEvent',
+                kind: 'interfaceName'
+            },
+            {
+                text: ')',
+                kind: 'punctuation'
+            },
+            {
+                text: ': ',
+                kind: 'punctuation'
+            },
+            {
+                text: 'Promise',
+                kind: 'keyword'
+            },
+            {
+                text: '<',
+                kind: 'punctuation'
+            },
+            {
+                text: 'LoadOutput',
+                kind: 'interfaceName'
+            },
+            {
+                text: '>',
+                kind: 'punctuation'
+            }
+        ],
+        documentation: [
+            {
+                text: 'Loads data for the given page or layout. More info: https://kit.svelte.dev/docs/load',
+                kind: 'text'
+            }
+        ]
+    }
+} satisfies Record<
+    string,
+    { displayParts: ts.SymbolDisplayPart[]; documentation: ts.SymbolDisplayPart[] }
+>;
+
+export function getProxiedLanguageService(
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
+    logger?: Logger
+) {
+    const cachedProxiedLanguageService = cache.get(info);
+    if (cachedProxiedLanguageService) {
+        return cachedProxiedLanguageService;
+    }
+
+    const originalLanguageServiceHost = info.languageServiceHost;
+
+    class ProxiedLanguageServiceHost implements ts.LanguageServiceHost {
+        files: Record<string, KitSnapshot> = {};
+
+        log() {}
+
+        trace() {}
+
+        error() {}
+
+        getCompilationSettings() {
+            return originalLanguageServiceHost.getCompilationSettings();
+        }
+
+        getScriptIsOpen() {
+            return true;
+        }
+
+        getCurrentDirectory() {
+            return originalLanguageServiceHost.getCurrentDirectory();
+        }
+
+        getDefaultLibFileName(o: any) {
+            return originalLanguageServiceHost.getDefaultLibFileName(o);
+        }
+
+        resolveModuleNames = originalLanguageServiceHost.resolveModuleNames
+            ? (...args: any[]) => {
+                  return originalLanguageServiceHost.resolveModuleNames!(
+                      // @ts-ignore
+                      ...args
+                  );
+              }
+            : undefined;
+
+        resolveModuleNameLiterals = originalLanguageServiceHost.resolveModuleNameLiterals
+            ? (...args: any[]) => {
+                  return originalLanguageServiceHost.resolveModuleNameLiterals!(
+                      // @ts-ignore
+                      ...args
+                  );
+              }
+            : undefined;
+
+        getScriptVersion(fileName: string) {
+            const file = this.files[fileName];
+            if (!file) return originalLanguageServiceHost.getScriptVersion(fileName);
+            return file.version.toString();
+        }
+
+        getScriptSnapshot(fileName: string) {
+            const file = this.files[fileName];
+            if (!file) return originalLanguageServiceHost.getScriptSnapshot(fileName);
+            return file.file;
+        }
+
+        getScriptFileNames(): string[] {
+            const names: Set<string> = new Set(Object.keys(this.files));
+            const files = originalLanguageServiceHost.getScriptFileNames();
+            for (const file of files) {
+                names.add(file);
+            }
+            return [...names];
+        }
+
+        getKitScriptSnapshotIfUpToDate(fileName: string) {
+            if (
+                !this.files[fileName] ||
+                this.getScriptVersion(fileName) !==
+                    originalLanguageServiceHost.getScriptVersion(fileName)
+            ) {
+                return undefined;
+            }
+            return this.files[fileName];
+        }
+
+        upsertKitFile(fileName: string) {
+            const basename = path.basename(fileName);
+            if (!kitPageFiles.has(basename)) return;
+
+            const source = info.languageService.getProgram()?.getSourceFile(fileName);
+            if (!source) return;
+
+            const addedCode: KitSnapshot['addedCode'] = [];
+            const insert = (pos: number, inserted: string) => {
+                const insertionIdx = addedCode.findIndex((c) => c.generatedPos > pos);
+                if (insertionIdx >= 0) {
+                    for (let i = insertionIdx; i < addedCode.length; i++) {
+                        addedCode[i].generatedPos += inserted.length;
+                        addedCode[i].total += inserted.length;
+                    }
+                    const prevTotal = addedCode[insertionIdx - 1]?.total ?? 0;
+                    addedCode.splice(insertionIdx, 0, {
+                        generatedPos: pos + prevTotal,
+                        originalPos: pos,
+                        length: inserted.length,
+                        inserted,
+                        total: prevTotal + inserted.length
+                    });
+                } else {
+                    const prevTotal = addedCode[addedCode.length - 1]?.total ?? 0;
+                    addedCode.push({
+                        generatedPos: pos + prevTotal,
+                        originalPos: pos,
+                        length: inserted.length,
+                        inserted,
+                        total: prevTotal + inserted.length
+                    });
+                }
+            };
+
+            const isTsFile = basename.endsWith('.ts');
+            const exports = findExports(ts, source, isTsFile);
+
+            // add type to load function if not explicitly typed
+            const load = exports.get('load');
+            if (
+                load?.type === 'function' &&
+                load.node.parameters.length === 1 &&
+                !load.hasTypeDefinition
+            ) {
+                const pos = load.node.parameters[0].getEnd();
+                const inserted = `: import('./$types').${
+                    basename.includes('layout') ? 'Layout' : 'Page'
+                }${basename.includes('server') ? 'Server' : ''}LoadEvent`;
+
+                insert(pos, inserted);
+            }
+
+            // add type to actions variable if not explicitly typed
+            const actions = exports.get('actions');
+            if (actions?.type === 'var' && !actions.hasTypeDefinition && actions.node.initializer) {
+                const pos = actions.node.initializer.getEnd();
+                const inserted = ` satisfies import('./$types').Actions`;
+                insert(pos, inserted);
+            }
+
+            // add type to prerender variable if not explicitly typed
+            const prerender = exports.get('prerender');
+            if (
+                prerender?.type === 'var' &&
+                !prerender.hasTypeDefinition &&
+                prerender.node.initializer
+            ) {
+                const pos = prerender.node.name.getEnd();
+                const inserted = ` : import('@sveltejs/kit').PrerenderOption`;
+                insert(pos, inserted);
+            }
+
+            // add type to trailingSlash variable if not explicitly typed
+            const trailingSlash = exports.get('trailingSlash');
+            if (
+                trailingSlash?.type === 'var' &&
+                !trailingSlash.hasTypeDefinition &&
+                trailingSlash.node.initializer
+            ) {
+                const pos = trailingSlash.node.name.getEnd();
+                const inserted = ` : 'never' | 'always' | 'ignore'`; // TODO this should be exported from kit
+                insert(pos, inserted);
+            }
+
+            // add type to ssr variable if not explicitly typed
+            const ssr = exports.get('ssr');
+            if (ssr?.type === 'var' && !ssr.hasTypeDefinition && ssr.node.initializer) {
+                const pos = ssr.node.name.getEnd();
+                const inserted = ` : boolean`;
+                insert(pos, inserted);
+            }
+
+            // add type to csr variable if not explicitly typed
+            const csr = exports.get('csr');
+            if (csr?.type === 'var' && !csr.hasTypeDefinition && csr.node.initializer) {
+                const pos = csr.node.name.getEnd();
+                const inserted = ` : boolean`;
+                insert(pos, inserted);
+            }
+
+            // construct generated text from internal text and addedCode array
+            const originalText = source.getFullText();
+            let pos = 0;
+            let text = '';
+            for (const added of addedCode) {
+                text += originalText.slice(pos, added.originalPos) + added.inserted;
+                pos = added.originalPos;
+            }
+            text += originalText.slice(pos);
+
+            const snap = ts.ScriptSnapshot.fromString(text);
+            snap.getChangeRange = (_) => undefined;
+            this.files[fileName] = {
+                version: originalLanguageServiceHost.getScriptVersion(fileName),
+                file: snap,
+                addedCode
+            };
+            return this.files[fileName];
+        }
+
+        readFile(fileName: string) {
+            const file = this.files[fileName];
+            return file
+                ? file.file.getText(0, file.file.getLength())
+                : originalLanguageServiceHost.readFile(fileName);
+        }
+
+        fileExists(fileName: string) {
+            return (
+                this.files[fileName] !== undefined ||
+                originalLanguageServiceHost.fileExists(fileName)
+            );
+        }
+    }
+
+    const languageServiceHost = new ProxiedLanguageServiceHost();
+    const languageService = ts.createLanguageService(
+        languageServiceHost,
+        ts.createDocumentRegistry()
+    );
+    cache.set(info, { languageService, languageServiceHost });
+    return {
+        languageService,
+        languageServiceHost
+    };
+}
+
+const kitPageFiles = new Set([
+    '+page.ts',
+    '+page.js',
+    '+layout.ts',
+    '+layout.js',
+    '+page.server.ts',
+    '+page.server.js',
+    '+layout.server.ts',
+    '+layout.server.js'
+]);
+
+export function getVirtualLS(
+    fileName: string,
+    info: ts.server.PluginCreateInfo,
+    ts: _ts,
+    logger?: Logger
+) {
+    const proxy = getProxiedLanguageService(info, ts, logger);
+    const result =
+        proxy.languageServiceHost.getKitScriptSnapshotIfUpToDate(fileName) ??
+        proxy.languageServiceHost.upsertKitFile(fileName);
+
+    if (result) {
+        return {
+            languageService: proxy.languageService,
+            addedCode: result.addedCode,
+            toVirtualPos: (pos: number) => toVirtualPos(pos, result.addedCode),
+            toOriginalPos: (pos: number) => toOriginalPos(pos, result.addedCode)
+        };
+    }
+}
+
+export function toVirtualPos(pos: number, addedCode: KitSnapshot['addedCode']) {
+    let total = 0;
+    for (const added of addedCode) {
+        if (pos < added.originalPos) break;
+        total += added.length;
+    }
+    return pos + total;
+}
+
+export function toOriginalPos(pos: number, addedCode: KitSnapshot['addedCode']) {
+    let total = 0;
+    let idx = 0;
+    for (; idx < addedCode.length; idx++) {
+        const added = addedCode[idx];
+        if (pos < added.generatedPos) break;
+        total += added.length;
+    }
+
+    if (idx > 0) {
+        const prev = addedCode[idx - 1];
+        // If pos is in the middle of an added range, return the start of the addition
+        if (pos > prev.generatedPos && pos < prev.generatedPos + prev.length) {
+            return { pos: prev.originalPos, inGenerated: true };
+        }
+    }
+
+    return { pos: pos - total, inGenerated: false };
+}
+
+/**
+ * Finds the top level const/let/function exports of a source file.
+ */
+function findExports(ts: _ts, source: ts.SourceFile, isTsFile: boolean) {
+    const exports = new Map<
+        string,
+        | {
+              type: 'function';
+              node: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression;
+              hasTypeDefinition: boolean;
+          }
+        | {
+              type: 'var';
+              node: ts.VariableDeclaration;
+              hasTypeDefinition: boolean;
+          }
+    >();
+    // TODO handle indirect exports?
+    for (const statement of source.statements) {
+        if (
+            ts.isFunctionDeclaration(statement) &&
+            statement.name &&
+            ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+        ) {
+            // export function x ...
+            exports.set(statement.name.text, {
+                type: 'function',
+                node: statement,
+                hasTypeDefinition:
+                    !!statement.parameters[0]?.type ||
+                    (!isTsFile &&
+                        (!!ts.getJSDocType(statement) ||
+                            (statement.parameters[0] &&
+                                !!ts.getJSDocParameterTags(statement.parameters[0]).length)))
+            });
+        }
+        if (
+            ts.isVariableStatement(statement) &&
+            statement.declarationList.declarations.length === 1 &&
+            ts.getModifiers(statement)?.[0]?.kind === ts.SyntaxKind.ExportKeyword
+        ) {
+            // export const x = ...
+            const declaration = statement.declarationList.declarations[0];
+            const hasTypeDefinition =
+                !!declaration.type ||
+                (!isTsFile && !!ts.getJSDocType(declaration)) ||
+                (!!declaration.initializer && ts.isSatisfiesExpression(declaration.initializer));
+
+            if (
+                declaration.initializer &&
+                (ts.isFunctionExpression(declaration.initializer) ||
+                    ts.isArrowFunction(declaration.initializer))
+            ) {
+                exports.set(declaration.name.getText(), {
+                    type: 'function',
+                    node: declaration.initializer,
+                    hasTypeDefinition
+                });
+            } else {
+                exports.set(declaration.name.getText(), {
+                    type: 'var',
+                    node: declaration,
+                    hasTypeDefinition
+                });
+            }
+        }
+    }
+
+    return exports;
+}
diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts
index 8a051fd9..6da7c5cc 100644
--- a/packages/typescript-plugin/src/module-loader.ts
+++ b/packages/typescript-plugin/src/module-loader.ts
@@ -132,7 +132,7 @@ export function patchModuleLoader(
         compilerOptions: ts.CompilerOptions,
         containingSourceFile?: ts.SourceFile
     ): Array<ts.ResolvedModule | undefined> {
-        logger.log('Resolving modules names for ' + containingFile);
+        logger.debug('Resolving modules names for ' + containingFile);
         // Try resolving all module names with the original method first.
         // The ones that are undefined will be re-checked if they are a
         // Svelte file and if so, are resolved, too. This way we can defer
@@ -204,7 +204,7 @@ export function patchModuleLoader(
         containingSourceFile: ts.SourceFile,
         reusedNames: readonly ts.StringLiteralLike[] | undefined
     ): readonly ts.ResolvedModuleWithFailedLookupLocations[] {
-        logger.log('Resolving modules names for ' + containingFile);
+        logger.debug('Resolving modules names for ' + containingFile);
         // Try resolving all module names with the original method first.
         // The ones that are undefined will be re-checked if they are a
         // Svelte file and if so, are resolved, too. This way we can defer
diff --git a/packages/typescript-plugin/src/source-mapper.ts b/packages/typescript-plugin/src/source-mapper.ts
index 36f586f4..9e8814f2 100644
--- a/packages/typescript-plugin/src/source-mapper.ts
+++ b/packages/typescript-plugin/src/source-mapper.ts
@@ -61,7 +61,7 @@ function binarySearch<T extends object | number>(
     let high = array.length - 1;
     while (low <= high) {
         const i = low + ((high - low) >> 1);
-        const item = undefined === key ? array[i] : array[i][key];
+        const item = (undefined === key ? array[i] : array[i][key]) as number;
         if (item === target) {
             return i;
         }
diff --git a/packages/typescript-plugin/src/utils.ts b/packages/typescript-plugin/src/utils.ts
index fcc5d1c8..f3b5bdd8 100644
--- a/packages/typescript-plugin/src/utils.ts
+++ b/packages/typescript-plugin/src/utils.ts
@@ -1,5 +1,6 @@
 import type ts from 'typescript/lib/tsserverlibrary';
 import { SvelteSnapshot } from './svelte-snapshots';
+type _ts = typeof ts;
 
 export function isSvelteFilePath(filePath: string) {
     return filePath.endsWith('.svelte');
@@ -128,6 +129,56 @@ export function findNodeAtSpan<T extends ts.Node>(
     }
 }
 
+/**
+ * Finds node somewhere at position.
+ */
+export function findNodeAtPosition<T extends ts.Node>(
+    node: ts.Node,
+    pos: number,
+    predicate?: NodeTypePredicate<T>
+): T | void {
+    for (const child of node.getChildren()) {
+        const childStart = child.getStart();
+        if (pos < childStart) {
+            return;
+        }
+
+        const childEnd = child.getEnd();
+        if (pos > childEnd) {
+            continue;
+        }
+
+        const foundInChildren = findNodeAtPosition(child, pos, predicate);
+        if (foundInChildren) {
+            return foundInChildren;
+        }
+
+        if (!predicate) {
+            return child as T;
+        }
+        if (predicate(child)) {
+            return child;
+        }
+    }
+}
+
+/**
+ * True if is `export const/let/function`
+ */
+export function isTopLevelExport(ts: _ts, node: ts.Node, source: ts.SourceFile) {
+    return (
+        (ts.isVariableStatement(node) && source.statements.includes(node as any)) ||
+        (ts.isIdentifier(node) &&
+            node.parent &&
+            ts.isVariableDeclaration(node.parent) &&
+            source.statements.includes(node.parent?.parent?.parent as any)) ||
+        (ts.isIdentifier(node) &&
+            node.parent &&
+            ts.isFunctionDeclaration(node.parent) &&
+            source.statements.includes(node.parent as any))
+    );
+}
+
 const COMPONENT_SUFFIX = '__SvelteComponent_';
 
 export function isGeneratedSvelteComponentName(className: string) {


Test patch:
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/$types.d.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/$types.d.ts
new file mode 100644
index 00000000..f1f8f1e0
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/$types.d.ts
@@ -0,0 +1,7 @@
+export interface PageLoadEvent<> {
+    test: {
+        exists: boolean;
+    };
+}
+
+export type PageData = ReturnType<typeof import('./+page').load>;
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.svelte
new file mode 100644
index 00000000..907f4b2f
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.svelte
@@ -0,0 +1,3 @@
+<script lang="ts">
+    export let data;
+</script>
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.ts
new file mode 100644
index 00000000..535b3eef
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/+page.ts
@@ -0,0 +1,5 @@
+// In a real SvelteKit application, $types are autogenerated, for this test we manually create them to our liking
+// @ts-expect-error - to silence tsc
+export function load(event) {
+    return event.test;
+}
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/expectedv2.json
new file mode 100644
index 00000000..d1df008d
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/expectedv2.json
@@ -0,0 +1,19 @@
+[
+    {
+        "range": {
+            "start": {
+                "line": 5,
+                "character": 1
+            },
+            "end": {
+                "line": 5,
+                "character": 5
+            }
+        },
+        "severity": 1,
+        "source": "ts",
+        "message": "Property 'data' is missing in type '{}' but required in type '{ data: { exists: boolean; }; }'.",
+        "code": 2741,
+        "tags": []
+    }
+]
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/input.svelte
new file mode 100644
index 00000000..b67fb733
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/sveltekit-autotypings/input.svelte
@@ -0,0 +1,6 @@
+<script lang="ts">
+    import Page from './+page.svelte';
+
+</script>
+
+<Page></Page>
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte
new file mode 100644
index 00000000..eecc3cc8
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte
@@ -0,0 +1,8 @@
+<script>
+    export let data;
+    export let form;
+    export const snapshot = {};
+
+    export let nope;
+    export let data: number;
+</script>
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/expectedv2.ts
new file mode 100644
index 00000000..3fd5b569
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/expectedv2.ts
@@ -0,0 +1,16 @@
+///<reference types="svelte" />
+;function render() {
+
+     let data/*Ωignore_startΩ*/: import('./$types').PageData;data = __sveltets_2_any(data);/*Ωignore_endΩ*/;
+     let form/*Ωignore_startΩ*/: import('./$types').ActionData;form = __sveltets_2_any(form);/*Ωignore_endΩ*/;
+     const snapshot = {};
+
+     let nope/*Ωignore_startΩ*/;nope = __sveltets_2_any(nope);/*Ωignore_endΩ*/;
+     let data: number/*Ωignore_startΩ*/;data = __sveltets_2_any(data);/*Ωignore_endΩ*/;
+;
+async () => {};
+return { props: {data: data , form: form , snapshot: snapshot , nope: nope}, slots: {}, events: {} }}
+
+export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['snapshot'], __sveltets_2_with_any_event(render()))) {
+    get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
+}
\ No newline at end of file