Repository: sveltejs/language-tools
Pull request #1936 (2023-03-13 16:48:36)

(fix) kit auto type fixes
- recognize export const snapshot
- add types at correct position when initializer given
- recognize +page/layout@ files


Patch:
diff --git a/packages/svelte2tsx/index.d.ts b/packages/svelte2tsx/index.d.ts
index 0b577aab..879fc610 100644
--- a/packages/svelte2tsx/index.d.ts
+++ b/packages/svelte2tsx/index.d.ts
@@ -115,7 +115,7 @@ export const internalHelpers: {
         fileName: string,
         options: InternalHelpers.KitFilesSettings
     ) => boolean;
-    isKitRouteFile: (fileName: string, basename: string) =>boolean,
+    isKitRouteFile: (basename: string) => boolean,
     isClientHooksFile: (
         fileName: string,
         basename: string,
diff --git a/packages/svelte2tsx/src/helpers/sveltekit.ts b/packages/svelte2tsx/src/helpers/sveltekit.ts
index 7e498e92..ad49f35f 100644
--- a/packages/svelte2tsx/src/helpers/sveltekit.ts
+++ b/packages/svelte2tsx/src/helpers/sveltekit.ts
@@ -26,7 +26,7 @@ const kitPageFiles = new Set(['+page', '+layout', '+page.server', '+layout.serve
 export function isKitFile(fileName: string, options: KitFilesSettings): boolean {
     const basename = path.basename(fileName);
     return (
-        isKitRouteFile(fileName, basename) ||
+        isKitRouteFile(basename) ||
         isServerHooksFile(fileName, basename, options.serverHooksPath) ||
         isClientHooksFile(fileName, basename, options.clientHooksPath) ||
         isParamsFile(fileName, basename, options.paramsPath)
@@ -36,12 +36,12 @@ export function isKitFile(fileName: string, options: KitFilesSettings): boolean
 /**
  * Determines whether or not a given file is a SvelteKit-specific route file
  */
-export function isKitRouteFile(fileName: string, basename: string): boolean {
+export function isKitRouteFile(basename: string): boolean {
     if (basename.includes('@')) {
         // +page@foo -> +page
         basename = basename.split('@')[0];
     } else {
-        basename = basename.slice(0, -path.extname(fileName).length);
+        basename = basename.slice(0, -path.extname(basename).length);
     }
 
     return kitPageFiles.has(basename);
@@ -97,7 +97,7 @@ export function upsertKitFile(
 ): { text: string; addedCode: AddedCode[] } {
     let basename = path.basename(fileName);
     const result =
-        upserKitRouteFile(ts, fileName, basename, getSource, surround) ??
+        upserKitRouteFile(ts, basename, getSource, surround) ??
         upserKitServerHooksFile(
             ts,
             fileName,
@@ -141,12 +141,11 @@ export function upsertKitFile(
 
 function upserKitRouteFile(
     ts: _ts,
-    fileName: string,
     basename: string,
     getSource: () => ts.SourceFile | undefined,
     surround: (text: string) => string
 ) {
-    if (!isKitRouteFile(fileName, basename)) return;
+    if (!isKitRouteFile(basename)) return;
 
     const source = getSource();
     if (!source) return;
diff --git a/packages/svelte2tsx/src/helpers/typescript.ts b/packages/svelte2tsx/src/helpers/typescript.ts
index 8f104fbe..7ebd88e8 100644
--- a/packages/svelte2tsx/src/helpers/typescript.ts
+++ b/packages/svelte2tsx/src/helpers/typescript.ts
@@ -52,11 +52,18 @@ export function findExports(ts: _ts, source: ts.SourceFile, isTsFile: boolean) {
                     (ts.isSatisfiesExpression(declaration.initializer) &&
                         ts.isParenthesizedExpression(declaration.initializer.expression) &&
                         (ts.isFunctionExpression(declaration.initializer.expression.expression) ||
-                            ts.isArrowFunction(declaration.initializer.expression.expression))))
+                            ts.isArrowFunction(declaration.initializer.expression.expression))) ||
+                    (ts.isParenthesizedExpression(declaration.initializer) &&
+                        (ts.isFunctionExpression(declaration.initializer.expression) ||
+                            ts.isArrowFunction(declaration.initializer.expression))))
             ) {
                 const node = ts.isSatisfiesExpression(declaration.initializer)
                     ? ((declaration.initializer.expression as ts.ParenthesizedExpression)
                           .expression as ts.FunctionExpression | ts.ArrowFunction)
+                    : ts.isParenthesizedExpression(declaration.initializer)
+                    ? (declaration.initializer.expression as
+                          | ts.FunctionExpression
+                          | ts.ArrowFunction)
                     : declaration.initializer;
                 exports.set(declaration.name.getText(), {
                     type: 'function',
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
index c9f3729c..fc9c811e 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
@@ -1,5 +1,6 @@
 import MagicString from 'magic-string';
 import ts from 'typescript';
+import { internalHelpers } from '../../helpers';
 import { surroundWithIgnoreComments } from '../../utils/ignore';
 import { preprendStr, overwriteStr } from '../../utils/magic-string';
 import { findExportKeyword, getLastLeadingDoc, isInterfaceOrTypeDeclaration } from '../utils/tsAst';
@@ -18,8 +19,6 @@ interface ExportedName {
     doc?: string;
 }
 
-const kitPageFiles = new Set(['+page.svelte', '+layout.svelte']);
-
 export class ExportedNames {
     /**
      * Uses the $$Props type
@@ -52,6 +51,17 @@ export class ExportedNames {
                 node.declarationList.forEachChild((n) => {
                     if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
                         this.addGetter(n.name);
+
+                        const type = n.type || ts.getJSDocType(n);
+                        const isKitExport =
+                            internalHelpers.isKitRouteFile(this.basename) &&
+                            n.name.getText() === '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').Snapshot` : '';
+                        const nameEnd = n.name.end + this.astOffset;
+                        if (kitType) {
+                            preprendStr(this.str, nameEnd, surroundWithIgnoreComments(kitType));
+                        }
                     }
                 });
             }
@@ -117,7 +127,7 @@ export class ExportedNames {
             const type = tsType || jsDocType;
             const name = identifier.getText();
             const isKitExport =
-                kitPageFiles.has(this.basename) &&
+                internalHelpers.isKitRouteFile(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 =
@@ -132,6 +142,7 @@ export class ExportedNames {
                               : 'Snapshot'
                       }`
                     : '';
+            const nameEnd = identifier.end + this.astOffset;
             const end = declaration.end + this.astOffset;
 
             if (
@@ -150,13 +161,26 @@ export class ExportedNames {
             ) {
                 const name = identifier.getText();
 
-                preprendStr(
-                    this.str,
-                    end,
-                    surroundWithIgnoreComments(`${kitType};${name} = __sveltets_2_any(${name});`)
-                );
+                if (nameEnd === end) {
+                    preprendStr(
+                        this.str,
+                        end,
+                        surroundWithIgnoreComments(
+                            `${kitType};${name} = __sveltets_2_any(${name});`
+                        )
+                    );
+                } else {
+                    if (kitType) {
+                        preprendStr(this.str, nameEnd, surroundWithIgnoreComments(kitType));
+                    }
+                    preprendStr(
+                        this.str,
+                        end,
+                        surroundWithIgnoreComments(`;${name} = __sveltets_2_any(${name});`)
+                    );
+                }
             } else if (kitType) {
-                preprendStr(this.str, end, surroundWithIgnoreComments(`${kitType}`));
+                preprendStr(this.str, nameEnd, surroundWithIgnoreComments(kitType));
             }
         };
 


Test patch:
diff --git a/packages/svelte2tsx/test/helpers/index.ts b/packages/svelte2tsx/test/helpers/index.ts
index 85561723..694ce7fc 100644
--- a/packages/svelte2tsx/test/helpers/index.ts
+++ b/packages/svelte2tsx/test/helpers/index.ts
@@ -41,4 +41,20 @@ describe('Internal Helpers - upsertKitFile', () => {
             `export async function GET(e: import('./$types').RequestEvent) : Response | Promise<Response> {}`
         );
     });
+
+    it('upserts load const with paranthesis', () => {
+        upsert(
+            '+page.ts',
+            `export const load = (async (e) => {});`,
+            `export const load = (async (e: import('./$types').PageLoadEvent) => {});`
+        );
+    });
+
+    it('recognizes page@', () => {
+        upsert('+page@.ts', `export const ssr = true;`, `export const ssr : boolean = true;`);
+    });
+
+    it('recognizes layout@foo', () => {
+        upsert('+layout@foo.ts', `export const ssr = true;`, `export const ssr : boolean = true;`);
+    });
 });
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte
index eecc3cc8..373454fd 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/+page.svelte
@@ -4,5 +4,6 @@
     export const snapshot = {};
 
     export let nope;
+    export let form = {}
     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
index 3fd5b569..9a08d45f 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/expectedv2.ts
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/sveltekit-autotypes/expectedv2.ts
@@ -3,14 +3,15 @@
 
      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 = {};
+     const snapshot/*Ωignore_startΩ*/: import('./$types').Snapshot/*Ωignore_endΩ*/ = {};
 
      let nope/*Ωignore_startΩ*/;nope = __sveltets_2_any(nope);/*Ωignore_endΩ*/;
+     let form/*Ωignore_startΩ*/: import('./$types').ActionData/*Ω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()))) {
+export default class Page__SvelteComponent_ extends __sveltets_2_createSvelte2TsxComponent(__sveltets_2_partial(['form','snapshot'], __sveltets_2_with_any_event(render()))) {
     get snapshot() { return __sveltets_2_nonNullable(this.$$prop_def.snapshot) }
 }
\ No newline at end of file