Repository: sveltejs/language-tools
Pull request #1083 (2021-07-03 02:35:15)

(fix) fix event completion with createEventDispatcher or with multiple declarations of the same event
The generic type parameter is not in the `valueDeclaration` property of the typescript symbol.


Patch:
diff --git a/.prettierignore b/.prettierignore
index 97dae252..3caa6aac 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,6 +2,7 @@ packages/svelte2tsx/*.d.ts
 packages/svelte2tsx/test/*/samples/*/*sx
 packages/svelte2tsx/test/*/samples/*/*.svelte
 packages/svelte2tsx/test/sourcemaps/samples/*
+packages/svelte2tsx/test/emitDts/samples/*/expected/**
 packages/language-server/test/**/*.svelte
 packages/language-server/test/**/testfiles/**/*.ts
 packages/svelte-vscode/syntaxes/*.yaml
diff --git a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
index d2d415e7..f911e427 100644
--- a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
+++ b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts
@@ -57,14 +57,16 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
         return type
             .getProperties()
             .map((prop) => {
-                if (!prop.valueDeclaration) {
+                // type would still be correct when there're multiple declarations
+                const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
+                if (!declaration) {
                     return;
                 }
 
                 return {
                     name: prop.name,
                     type: this.typeChecker.typeToString(
-                        this.typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration)
+                        this.typeChecker.getTypeOfSymbolAtLocation(prop, declaration)
                     ),
                     doc: ts.displayPartsToString(prop.getDocumentationComment(this.typeChecker))
                 };
diff --git a/packages/svelte2tsx/index.d.ts b/packages/svelte2tsx/index.d.ts
index 629ee4c7..74b6d4fa 100644
--- a/packages/svelte2tsx/index.d.ts
+++ b/packages/svelte2tsx/index.d.ts
@@ -47,3 +47,30 @@ export function svelte2tsx(
         mode?: 'tsx' | 'dts'
     }
 ): SvelteCompiledToTsx
+
+export interface EmitDtsConig {
+    /**
+     * Where to output the declaration files
+     */
+    declarationDir: string;
+    /**
+     * Path to `svelte-shims.d.ts` of `svelte2tsx`.
+     * Example: `require.resolve('svelte2tsx/svelte-shims.d.ts')`
+     */
+    svelteShimsPath: string;
+    /**
+     * If you want to emit types only for part of your project,
+     * then set this to the folder for which the types should be emitted.
+     * Most of the time you don't need this. For SvelteKit, this is for example
+     * set to `src/lib` by default.
+     */
+    libRoot?: string;
+}
+
+/**
+ * Searches for a jsconfig or tsconfig starting at `root` and emits d.ts files
+ * into `declarationDir` using the ambient file from `svelteShimsPath`.
+ * Note: Handwritten `d.ts` files are not copied over; TypeScript does not
+ * touch these files.
+ */
+export function emitDts(config: EmitDtsConig): Promise<void>;
diff --git a/packages/svelte2tsx/src/emitDts.ts b/packages/svelte2tsx/src/emitDts.ts
new file mode 100644
index 00000000..81817918
--- /dev/null
+++ b/packages/svelte2tsx/src/emitDts.ts
@@ -0,0 +1,225 @@
+import * as path from 'path';
+import ts from 'typescript';
+import { svelte2tsx } from './svelte2tsx';
+
+export interface EmitDtsConig {
+    declarationDir: string;
+    svelteShimsPath: string;
+    libRoot?: string;
+}
+
+export async function emitDts(config: EmitDtsConig) {
+    const svelteMap = await createSvelteMap(config);
+    const { options, filenames } = loadTsconfig(config, svelteMap);
+    const host = await createTsCompilerHost(options, svelteMap);
+    const program = ts.createProgram(filenames, options, host);
+    program.emit();
+}
+
+function loadTsconfig(config: EmitDtsConig, svelteMap: SvelteMap) {
+    const libRoot = config.libRoot || process.cwd();
+
+    const jsconfigFile = ts.findConfigFile(libRoot, ts.sys.fileExists, 'jsconfig.json');
+    let tsconfigFile = ts.findConfigFile(libRoot, ts.sys.fileExists);
+
+    if (!tsconfigFile && !jsconfigFile) {
+        throw new Error('Failed to locate tsconfig or jsconfig');
+    }
+
+    tsconfigFile = tsconfigFile || jsconfigFile;
+    if (jsconfigFile && isSubpath(path.dirname(tsconfigFile), path.dirname(jsconfigFile))) {
+        tsconfigFile = jsconfigFile;
+    }
+
+    tsconfigFile = path.isAbsolute(tsconfigFile) ? tsconfigFile : path.join(libRoot, tsconfigFile);
+    const basepath = path.dirname(tsconfigFile);
+    const { error, config: tsConfig } = ts.readConfigFile(tsconfigFile, ts.sys.readFile);
+
+    if (error) {
+        throw new Error('Malformed tsconfig\n' + JSON.stringify(error, null, 2));
+    }
+
+    // Rewire includes and files. This ensures that only the files inside the lib are traversed and
+    // that the outputted types have the correct directory depth.
+    // This is a little brittle because we then may include more than the user wants
+    const libPathRelative = path.relative(basepath, libRoot).split(path.sep).join('/');
+    if (libPathRelative) {
+        tsConfig.include = [`${libPathRelative}/**/*`];
+        tsConfig.files = [];
+    }
+
+    const { options, fileNames } = ts.parseJsonConfigFileContent(
+        tsConfig,
+        ts.sys,
+        basepath,
+        { sourceMap: false },
+        tsconfigFile,
+        undefined,
+        [{ extension: 'svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }]
+    );
+
+    const filenames = fileNames.map((name) => {
+        if (!isSvelteFilepath(name)) {
+            return name;
+        }
+        // We need to trick TypeScript into thinking that Svelte files
+        // are either TS or JS files in order to generate correct d.ts
+        // definition files.
+        const isTsFile = svelteMap.add(name);
+        return name + (isTsFile ? '.ts' : '.js');
+    });
+
+    // Add ambient functions so TS knows how to resolve its invocations in the
+    // code output of svelte2tsx.
+    filenames.push(config.svelteShimsPath);
+
+    return {
+        options: {
+            ...options,
+            noEmit: false, // Set to true in case of jsconfig, force false, else nothing is emitted
+            moduleResolution: ts.ModuleResolutionKind.NodeJs, // Classic if not set, which gives wrong results
+            declaration: true, // Needed for d.ts file generation
+            emitDeclarationOnly: true, // We only want d.ts file generation
+            declarationDir: config.declarationDir, // Where to put the declarations
+            allowNonTsExtensions: true
+        },
+        filenames
+    };
+}
+
+async function createTsCompilerHost(options: any, svelteMap: SvelteMap) {
+    const host = ts.createCompilerHost(options);
+    // TypeScript writes the files relative to the found tsconfig/jsconfig
+    // which - at least in the case of the tests - is wrong. Therefore prefix
+    // the output paths. See Typescript issue #25430 for more.
+    const pathPrefix = path.relative(process.cwd(), path.dirname(options.configFilePath));
+
+    const svelteSys: ts.System = {
+        ...ts.sys,
+        fileExists(originalPath) {
+            const path = ensureRealSvelteFilepath(originalPath);
+            const exists = ts.sys.fileExists(path);
+            if (exists && isSvelteFilepath(path)) {
+                const isTsFile = svelteMap.add(path);
+                if (
+                    (isTsFile && !isTsFilepath(originalPath)) ||
+                    (!isTsFile && isTsFilepath(originalPath))
+                ) {
+                    return false;
+                }
+            }
+            return exists;
+        },
+        readFile(path, encoding = 'utf-8') {
+            if (isVirtualSvelteFilepath(path) || isSvelteFilepath(path)) {
+                path = ensureRealSvelteFilepath(path);
+                return svelteMap.get(path);
+            } else {
+                return ts.sys.readFile(path, encoding);
+            }
+        },
+        readDirectory(path, extensions, exclude, include, depth) {
+            const extensionsWithSvelte = (extensions || []).concat('.svelte');
+            return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth);
+        },
+        writeFile(fileName, data, writeByteOrderMark) {
+            return ts.sys.writeFile(
+                pathPrefix ? path.join(pathPrefix, fileName) : fileName,
+                data,
+                writeByteOrderMark
+            );
+        }
+    };
+
+    host.fileExists = svelteSys.fileExists;
+    host.readFile = svelteSys.readFile;
+    host.readDirectory = svelteSys.readDirectory;
+    host.writeFile = svelteSys.writeFile;
+
+    host.resolveModuleNames = (
+        moduleNames,
+        containingFile,
+        _reusedNames,
+        _redirectedReference,
+        compilerOptions
+    ) => {
+        return moduleNames.map((moduleName) => {
+            return resolveModuleName(moduleName, containingFile, compilerOptions);
+        });
+    };
+
+    function resolveModuleName(name: string, containingFile: string, compilerOptions: any) {
+        // Delegate to the TS resolver first.
+        // If that does not bring up anything, try the Svelte Module loader
+        // which is able to deal with .svelte files.
+        const tsResolvedModule = ts.resolveModuleName(
+            name,
+            containingFile,
+            compilerOptions,
+            ts.sys
+        ).resolvedModule;
+        if (tsResolvedModule && !isVirtualSvelteFilepath(tsResolvedModule.resolvedFileName)) {
+            return tsResolvedModule;
+        }
+
+        return ts.resolveModuleName(name, containingFile, compilerOptions, svelteSys)
+            .resolvedModule;
+    }
+
+    return host;
+}
+
+interface SvelteMap {
+    add: (path: string) => boolean;
+    get: (key: string) => string | undefined;
+}
+
+/**
+ * Generates a map to which we add the transformed code of Svelte files
+ * early on when we first need to look at the file contents and can read
+ * those transformed source later on.
+ */
+async function createSvelteMap(config): Promise<SvelteMap> {
+    const svelteFiles = new Map();
+
+    function add(path: string): boolean {
+        const code = ts.sys.readFile(path, 'utf-8');
+        const isTsFile = // svelte-preprocess allows default languages
+            ['ts', 'typescript'].includes(config.preprocess?.defaultLanguages?.script) ||
+            /<script\s+[^>]*?lang=('|")(ts|typescript)('|")/.test(code);
+        const transformed = svelte2tsx(code, {
+            filename: path,
+            isTsFile,
+            mode: 'dts'
+        }).code;
+        svelteFiles.set(path, transformed);
+        return isTsFile;
+    }
+
+    return { add, get: (key: string) => svelteFiles.get(key) };
+}
+
+function isSvelteFilepath(filePath: string) {
+    return filePath.endsWith('.svelte');
+}
+
+function isTsFilepath(filePath: string) {
+    return filePath.endsWith('.ts');
+}
+
+function isVirtualSvelteFilepath(filePath: string) {
+    return filePath.endsWith('.svelte.ts') || filePath.endsWith('svelte.js');
+}
+
+function toRealSvelteFilepath(filePath: string) {
+    return filePath.slice(0, -3); // -'.js'.length || -'.ts'.length
+}
+
+function ensureRealSvelteFilepath(filePath: string) {
+    return isVirtualSvelteFilepath(filePath) ? toRealSvelteFilepath(filePath) : filePath;
+}
+
+function isSubpath(maybeParent: string, maybeChild: string) {
+    const relative = path.relative(maybeParent, maybeChild);
+    return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
+}
diff --git a/packages/svelte2tsx/src/index.ts b/packages/svelte2tsx/src/index.ts
index 1c32f786..ef3d5bcf 100644
--- a/packages/svelte2tsx/src/index.ts
+++ b/packages/svelte2tsx/src/index.ts
@@ -1,1 +1,2 @@
 export { svelte2tsx } from './svelte2tsx';
+export { emitDts } from './emitDts';
diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts
index 30459f7d..9a852fae 100644
--- a/packages/svelte2tsx/svelte-shims.d.ts
+++ b/packages/svelte2tsx/svelte-shims.d.ts
@@ -99,6 +99,7 @@ type SvelteAnimationReturnType = {
 type SvelteWithOptionalProps<Props, Keys extends keyof Props> = Omit<Props, Keys> & Partial<Pick<Props, Keys>>;
 type SvelteAllProps = { [index: string]: any }
 type SveltePropsAnyFallback<Props> = {[K in keyof Props]: Props[K] extends undefined ? any : Props[K]}
+type SvelteSlotsAnyFallback<Slots> = {[K in keyof Slots]: {[S in keyof Slots[K]]: Slots[K][S] extends undefined ? any : Slots[K][S]}}
 type SvelteRestProps = { [index: string]: any }
 type SvelteSlots = { [index: string]: any }
 type SvelteStore<T> = { subscribe: (run: (value: T) => any, invalidate?: any) => any }
@@ -134,19 +135,19 @@ declare function __sveltets_1_slotsType<Slots, Key extends keyof Slots>(slots: S
 
 declare function __sveltets_1_partial<Props = {}, Events = {}, Slots = {}>(
     render: {props: Props, events: Events, slots: Slots }
-): {props: Expand<SveltePropsAnyFallback<Props>>, events: Events, slots: Slots }
+): {props: Expand<SveltePropsAnyFallback<Props>>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> }
 declare function __sveltets_1_partial<Props = {}, Events = {}, Slots = {}, OptionalProps extends keyof Props = any>(
     optionalProps: OptionalProps[],
     render: {props: Props, events: Events, slots: Slots }
-): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalProps>>, events: Events, slots: Slots }
+): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalProps>>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> }
 
 declare function __sveltets_1_partial_with_any<Props = {}, Events = {}, Slots = {}>(
     render: {props: Props, events: Events, slots: Slots }
-): {props: Expand<SveltePropsAnyFallback<Props> & SvelteAllProps>, events: Events, slots: Slots }
+): {props: Expand<SveltePropsAnyFallback<Props> & SvelteAllProps>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> }
 declare function __sveltets_1_partial_with_any<Props = {}, Events = {}, Slots = {}, OptionalProps extends keyof Props = any>(
     optionalProps: OptionalProps[],
     render: {props: Props, events: Events, slots: Slots }
-): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalProps> & SvelteAllProps>, events: Events, slots: Slots }
+): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalProps> & SvelteAllProps>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> }
 
 
 declare function __sveltets_1_with_any<Props = {}, Events = {}, Slots = {}>(


Test patch:
diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
index 630094c6..a1c24f29 100644
--- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts
@@ -111,7 +111,7 @@ describe('CompletionProviderImpl', () => {
 
         const completions = await completionProvider.getCompletions(
             document,
-            Position.create(4, 5),
+            Position.create(5, 5),
             {
                 triggerKind: CompletionTriggerKind.Invoked
             }
@@ -158,7 +158,7 @@ describe('CompletionProviderImpl', () => {
 
         const completions = await completionProvider.getCompletions(
             document,
-            Position.create(4, 10),
+            Position.create(5, 10),
             {
                 triggerKind: CompletionTriggerKind.Invoked
             }
@@ -183,11 +183,11 @@ describe('CompletionProviderImpl', () => {
                     newText: 'on:a',
                     range: {
                         start: {
-                            line: 4,
+                            line: 5,
                             character: 7
                         },
                         end: {
-                            line: 4,
+                            line: 5,
                             character: 10
                         }
                     }
@@ -205,11 +205,11 @@ describe('CompletionProviderImpl', () => {
                     newText: 'on:b',
                     range: {
                         start: {
-                            line: 4,
+                            line: 5,
                             character: 7
                         },
                         end: {
-                            line: 4,
+                            line: 5,
                             character: 10
                         }
                     }
@@ -224,11 +224,11 @@ describe('CompletionProviderImpl', () => {
                     newText: 'on:c',
                     range: {
                         start: {
-                            line: 4,
+                            line: 5,
                             character: 7
                         },
                         end: {
-                            line: 4,
+                            line: 5,
                             character: 10
                         }
                     }
@@ -237,6 +237,33 @@ describe('CompletionProviderImpl', () => {
         ]);
     });
 
+    it('provides event completions from createEventDispatcher', async () => {
+        const { completionProvider, document } = setup('component-events-completion.svelte');
+
+        const completions = await completionProvider.getCompletions(
+            document,
+            Position.create(6, 5),
+            {
+                triggerKind: CompletionTriggerKind.Invoked
+            }
+        );
+
+        const eventCompletions = completions!.items.filter((item) => item.label.startsWith('on:'));
+
+        assert.deepStrictEqual(eventCompletions, <CompletionItem[]>[
+            {
+                detail: 'c: CustomEvent<boolean>',
+                documentation: {
+                    kind: 'markdown',
+                    value: 'abc'
+                },
+                label: 'on:c',
+                sortText: '-1',
+                textEdit: undefined
+            }
+        ]);
+    });
+
     it('provides event completion for components with type definition', async () => {
         const { completionProvider, document } = setup('component-events-completion-ts-def.svelte');
 
@@ -295,6 +322,42 @@ describe('CompletionProviderImpl', () => {
         ]);
     });
 
+    it('provides event completion for components with type definition having multiple declarations of the same event', async () => {
+        const { completionProvider, document } = setup('component-events-completion-ts-def.svelte');
+
+        const completions = await completionProvider.getCompletions(
+            document,
+            Position.create(6, 17),
+            {
+                triggerKind: CompletionTriggerKind.Invoked
+            }
+        );
+
+        const eventCompletions = completions!.items.filter((item) => item.label.startsWith('on:'));
+
+        assert.deepStrictEqual(eventCompletions, <CompletionItem[]>[
+            {
+                detail: 'event1: CustomEvent<string> | CustomEvent<number>',
+                label: 'on:event1',
+                sortText: '-1',
+                documentation: '',
+                textEdit: {
+                    newText: 'on:event1',
+                    range: {
+                        end: {
+                            character: 18,
+                            line: 6
+                        },
+                        start: {
+                            character: 15,
+                            line: 6
+                        }
+                    }
+                }
+            }
+        ]);
+    });
+
     it('does not provide completions inside style tag', async () => {
         const { completionProvider, document } = setup('completionsstyle.svelte');
 
diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
index 8dbe87a2..e4677856 100644
--- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
+++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts
@@ -1753,4 +1753,10 @@ describe('DiagnosticsProvider', () => {
             }
         ]);
     });
+
+    it('falls back to any for untyped props and slots in js', async () => {
+        const { plugin, document } = setup('using-untyped-js.svelte');
+        const diagnostics = await plugin.getDiagnostics(document);
+        assert.deepStrictEqual(diagnostics, []);
+    });
 });
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts b/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
index c94429d6..b6db147a 100644
--- a/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/ComponentDef.ts
@@ -17,6 +17,17 @@ export class ComponentDef extends SvelteComponentTyped<
              * documentation for let2
              */
             let2: string;
-        }
+        };
     }
 > {}
+
+export class ComponentDef2 extends SvelteComponentTyped<
+    {},
+    | {
+          event1: CustomEvent<number>;
+      }
+    | {
+          event1: CustomEvent<string>;
+      },
+    {}
+> {}
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion-ts-def.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion-ts-def.svelte
index 85203640..e777a3ee 100644
--- a/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion-ts-def.svelte
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion-ts-def.svelte
@@ -1,6 +1,7 @@
 <script>
-    import { ComponentDef } from './ComponentDef';
+    import { ComponentDef, ComponentDef2 } from './ComponentDef';
 </script>
 
 <ComponentDef on:></ComponentDef>
-<ComponentDef let:></ComponentDef>
\ No newline at end of file
+<ComponentDef let:></ComponentDef>
+<ComponentDef2 on:></ComponentDef2>
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion.svelte
index 03b98a91..168212c4 100644
--- a/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion.svelte
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-completion.svelte
@@ -1,5 +1,7 @@
 <script lang="ts">
     import CEI from './component-events-interface.svelte';
+    import CED from './component-events-event-dispatcher.svelte';
 </script>
 
-<CEI   on: />
\ No newline at end of file
+<CEI   on: />
+<CED   on: />
diff --git a/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-event-dispatcher.svelte b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-event-dispatcher.svelte
new file mode 100644
index 00000000..75998f3f
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/completions/component-events-event-dispatcher.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+    import { createEventDispatcher } from 'svelte';
+
+    const dispatch = createEventDispatcher<{
+        /**abc*/
+        c: boolean
+    }>()
+</script>
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/untyped-js.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/untyped-js.svelte
new file mode 100644
index 00000000..6153d1f8
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/untyped-js.svelte
@@ -0,0 +1,5 @@
+<script>
+    export let untyped;
+</script>
+
+<slot {untyped} />
\ No newline at end of file
diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-untyped-js.svelte b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-untyped-js.svelte
new file mode 100644
index 00000000..12cfdd93
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/using-untyped-js.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+    import Untyped from './untyped-js.svelte';
+</script>
+
+<Untyped untyped={true} let:untyped>{untyped.worksBecauseAny}</Untyped>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/emitDts/index.ts b/packages/svelte2tsx/test/emitDts/index.ts
new file mode 100644
index 00000000..5469c88c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/index.ts
@@ -0,0 +1,60 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import { join } from 'path';
+import { emitDts } from '../../src';
+
+function rimraf(path: string) {
+    ((fs as any).rmSync || fs.rmdirSync)(path, { recursive: true, force: true });
+}
+
+async function testEmitDts(sample: string) {
+    const cwd = join(__dirname, 'samples', sample);
+
+    try {
+        const config = fs.existsSync(join(cwd, 'config.json'))
+            ? JSON.parse(fs.readFileSync(join(cwd, 'config.json'), 'utf-8'))
+            : {};
+        await emitDts({
+            declarationDir: 'package',
+            svelteShimsPath: require.resolve(join(process.cwd(), 'svelte-shims.d.ts')),
+            ...config,
+            libRoot: config.libRoot ? join(cwd, config.libRoot) : cwd
+        });
+        const expectedFiles = fs.readdirSync(join(cwd, 'expected'));
+        const actual_files = fs.readdirSync(join(cwd, 'package'));
+        assert.strictEqual(
+            actual_files.length,
+            expectedFiles.length,
+            'Contains a different number of files. Expected ' +
+                expectedFiles.join(',') +
+                ' , got ' +
+                actual_files.join(',')
+        );
+
+        for (const file of actual_files) {
+            assert.strictEqual(
+                expectedFiles.includes(file),
+                true,
+                `Did not expect file or folder ${file}`
+            );
+            const expectedContent = fs.readFileSync(join(cwd, 'expected', file), 'utf-8');
+            const actualContent = fs.readFileSync(join(cwd, 'package', file), 'utf-8');
+            assert.strictEqual(
+                actualContent,
+                expectedContent,
+                `Expected equal file contents for ${file}`
+            );
+        }
+    } finally {
+        rimraf(join(cwd, 'package'));
+    }
+}
+
+describe('emitDts', async () => {
+    const samples = fs.readdirSync(join(__dirname, 'samples'));
+    let samplesToTest = samples.filter((s) => s.endsWith('.solo'));
+    samplesToTest = samplesToTest.length ? samplesToTest : samples;
+    for (const sample of samplesToTest) {
+        it(sample, async () => await testEmitDts(sample)).timeout(5000);
+    }
+});
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/config.json b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/config.json
new file mode 100644
index 00000000..dd4044b7
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/config.json
@@ -0,0 +1,3 @@
+{
+    "libRoot": "src/lib"
+}
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/Test.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/Test.svelte.d.ts
new file mode 100644
index 00000000..3e176b43
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/Test.svelte.d.ts
@@ -0,0 +1,36 @@
+/** @typedef {typeof __propDef.props}  TestProps */
+/** @typedef {typeof __propDef.events}  TestEvents */
+/** @typedef {typeof __propDef.slots}  TestSlots */
+export default class Test extends SvelteComponentTyped<{
+    astring: string;
+}, {
+    event: CustomEvent<any>;
+} & {
+    [evt: string]: CustomEvent<any>;
+}, {
+    default: {
+        astring: string;
+    };
+}> {
+    get astring(): string;
+}
+export type TestProps = typeof __propDef.props;
+export type TestEvents = typeof __propDef.events;
+export type TestSlots = typeof __propDef.slots;
+import { SvelteComponentTyped } from "svelte";
+declare const __propDef: {
+    props: {
+        astring: string;
+    };
+    events: {
+        event: CustomEvent<any>;
+    } & {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {
+        default: {
+            astring: string;
+        };
+    };
+};
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/index.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/index.d.ts
new file mode 100644
index 00000000..637e4820
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/expected/index.d.ts
@@ -0,0 +1,1 @@
+export { default as Test } from "./Test.svelte";
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/jsconfig.json b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/jsconfig.json
new file mode 100644
index 00000000..edbf1266
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/jsconfig.json
@@ -0,0 +1,6 @@
+{
+    "compilerOptions": {
+        "checkJs": true
+    },
+    "include": ["./src/**/*.d.ts", "./src/**/*.js", "./src/**/*.ts", "./src/**/*.svelte"]
+}
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/Test2.svelte b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/Test2.svelte
new file mode 100644
index 00000000..06a5a340
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/Test2.svelte
@@ -0,0 +1,6 @@
+<script>
+    /**
+     * @type {import('./foo').Foo}
+     */
+    export let foo;
+</script>
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/Test.svelte b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/Test.svelte
new file mode 100644
index 00000000..31d11839
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/Test.svelte
@@ -0,0 +1,12 @@
+<script>
+    import { createEventDispatcher } from 'svelte';
+    /**
+     * @type {string}
+     */
+    export const astring;
+
+    const dispatch = createEventDispatcher();
+    dispatch('event', true);
+</script>
+
+<slot {astring} />
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/index.js b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/index.js
new file mode 100644
index 00000000..4c44188c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript-libRoot/src/lib/index.js
@@ -0,0 +1,1 @@
+export { default as Test } from './Test.svelte';
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test.svelte.d.ts
new file mode 100644
index 00000000..3e176b43
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test.svelte.d.ts
@@ -0,0 +1,36 @@
+/** @typedef {typeof __propDef.props}  TestProps */
+/** @typedef {typeof __propDef.events}  TestEvents */
+/** @typedef {typeof __propDef.slots}  TestSlots */
+export default class Test extends SvelteComponentTyped<{
+    astring: string;
+}, {
+    event: CustomEvent<any>;
+} & {
+    [evt: string]: CustomEvent<any>;
+}, {
+    default: {
+        astring: string;
+    };
+}> {
+    get astring(): string;
+}
+export type TestProps = typeof __propDef.props;
+export type TestEvents = typeof __propDef.events;
+export type TestSlots = typeof __propDef.slots;
+import { SvelteComponentTyped } from "svelte";
+declare const __propDef: {
+    props: {
+        astring: string;
+    };
+    events: {
+        event: CustomEvent<any>;
+    } & {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {
+        default: {
+            astring: string;
+        };
+    };
+};
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test2.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test2.svelte.d.ts
new file mode 100644
index 00000000..d025bccb
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/Test2.svelte.d.ts
@@ -0,0 +1,23 @@
+/** @typedef {typeof __propDef.props}  Test2Props */
+/** @typedef {typeof __propDef.events}  Test2Events */
+/** @typedef {typeof __propDef.slots}  Test2Slots */
+export default class Test2 extends SvelteComponentTyped<{
+    foo: boolean;
+}, {
+    [evt: string]: CustomEvent<any>;
+}, {}> {
+}
+export type Test2Props = typeof __propDef.props;
+export type Test2Events = typeof __propDef.events;
+export type Test2Slots = typeof __propDef.slots;
+import { SvelteComponentTyped } from "svelte";
+declare const __propDef: {
+    props: {
+        foo: import('./foo').Foo;
+    };
+    events: {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {};
+};
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/expected/TestNoTypes.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/TestNoTypes.svelte.d.ts
new file mode 100644
index 00000000..4b00e8db
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/TestNoTypes.svelte.d.ts
@@ -0,0 +1,37 @@
+/** @typedef {typeof __propDef.props}  TestNoTypesProps */
+/** @typedef {typeof __propDef.events}  TestNoTypesEvents */
+/** @typedef {typeof __propDef.slots}  TestNoTypesSlots */
+export default class TestNoTypes extends SvelteComponentTyped<{
+    noType: any;
+    initializer?: string;
+}, {
+    event: CustomEvent<any>;
+} & {
+    [evt: string]: CustomEvent<any>;
+}, {
+    default: {
+        noType: any;
+    };
+}> {
+}
+export type TestNoTypesProps = typeof __propDef.props;
+export type TestNoTypesEvents = typeof __propDef.events;
+export type TestNoTypesSlots = typeof __propDef.slots;
+import { SvelteComponentTyped } from "svelte";
+declare const __propDef: {
+    props: {
+        noType: any;
+        initializer?: string;
+    };
+    events: {
+        event: CustomEvent<any>;
+    } & {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {
+        default: {
+            noType: any;
+        };
+    };
+};
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/expected/index.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/index.d.ts
new file mode 100644
index 00000000..637e4820
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/expected/index.d.ts
@@ -0,0 +1,1 @@
+export { default as Test } from "./Test.svelte";
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/jsconfig.json b/packages/svelte2tsx/test/emitDts/samples/javascript/jsconfig.json
new file mode 100644
index 00000000..edbf1266
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/jsconfig.json
@@ -0,0 +1,6 @@
+{
+    "compilerOptions": {
+        "checkJs": true
+    },
+    "include": ["./src/**/*.d.ts", "./src/**/*.js", "./src/**/*.ts", "./src/**/*.svelte"]
+}
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test.svelte b/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test.svelte
new file mode 100644
index 00000000..31d11839
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test.svelte
@@ -0,0 +1,12 @@
+<script>
+    import { createEventDispatcher } from 'svelte';
+    /**
+     * @type {string}
+     */
+    export const astring;
+
+    const dispatch = createEventDispatcher();
+    dispatch('event', true);
+</script>
+
+<slot {astring} />
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test2.svelte b/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test2.svelte
new file mode 100644
index 00000000..06a5a340
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/src/Test2.svelte
@@ -0,0 +1,6 @@
+<script>
+    /**
+     * @type {import('./foo').Foo}
+     */
+    export let foo;
+</script>
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/src/TestNoTypes.svelte b/packages/svelte2tsx/test/emitDts/samples/javascript/src/TestNoTypes.svelte
new file mode 100644
index 00000000..479de1d9
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/src/TestNoTypes.svelte
@@ -0,0 +1,9 @@
+<script>
+    import { createEventDispatcher } from 'svelte';
+    export let noType;
+    export let initializer = 'foo';
+    const dispatch = createEventDispatcher();
+    dispatch('event', true);
+</script>
+
+<slot {noType} />
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/src/foo.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript/src/foo.d.ts
new file mode 100644
index 00000000..c941ad7b
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/src/foo.d.ts
@@ -0,0 +1,1 @@
+export type Foo = boolean;
diff --git a/packages/svelte2tsx/test/emitDts/samples/javascript/src/index.js b/packages/svelte2tsx/test/emitDts/samples/javascript/src/index.js
new file mode 100644
index 00000000..4c44188c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/javascript/src/index.js
@@ -0,0 +1,1 @@
+export { default as Test } from './Test.svelte';
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test.svelte.d.ts
new file mode 100644
index 00000000..897acf8d
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test.svelte.d.ts
@@ -0,0 +1,23 @@
+import { SvelteComponentTyped } from "svelte";
+declare const __propDef: {
+    props: {
+        astring: string;
+    };
+    events: {
+        event: CustomEvent<any>;
+    } & {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {
+        default: {
+            astring: string;
+        };
+    };
+};
+export declare type TestProps = typeof __propDef.props;
+export declare type TestEvents = typeof __propDef.events;
+export declare type TestSlots = typeof __propDef.slots;
+export default class Test extends SvelteComponentTyped<TestProps, TestEvents, TestSlots> {
+    get astring(): string;
+}
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test2.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test2.svelte.d.ts
new file mode 100644
index 00000000..45e91c48
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/Test2.svelte.d.ts
@@ -0,0 +1,17 @@
+import { SvelteComponentTyped } from "svelte";
+import type { Foo } from './foo';
+declare const __propDef: {
+    props: {
+        foo: Foo;
+    };
+    events: {
+        [evt: string]: CustomEvent<any>;
+    };
+    slots: {};
+};
+export declare type Test2Props = typeof __propDef.props;
+export declare type Test2Events = typeof __propDef.events;
+export declare type Test2Slots = typeof __propDef.slots;
+export default class Test2 extends SvelteComponentTyped<Test2Props, Test2Events, Test2Slots> {
+}
+export {};
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/expected/index.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/index.d.ts
new file mode 100644
index 00000000..4c44188c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/expected/index.d.ts
@@ -0,0 +1,1 @@
+export { default as Test } from './Test.svelte';
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test.svelte b/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test.svelte
new file mode 100644
index 00000000..b73d6e03
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+    import { createEventDispatcher } from 'svelte';
+    export const astring: string;
+
+    const dispatch = createEventDispatcher();
+    dispatch('event', true);
+</script>
+
+<slot {astring} />
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test2.svelte b/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test2.svelte
new file mode 100644
index 00000000..aef3fb6f
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/src/Test2.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+    import type { Foo } from './foo';
+    export let foo: Foo;
+</script>
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/src/foo.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript/src/foo.d.ts
new file mode 100644
index 00000000..c941ad7b
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/src/foo.d.ts
@@ -0,0 +1,1 @@
+export type Foo = boolean;
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/src/index.ts b/packages/svelte2tsx/test/emitDts/samples/typescript/src/index.ts
new file mode 100644
index 00000000..4c44188c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/src/index.ts
@@ -0,0 +1,1 @@
+export { default as Test } from './Test.svelte';
diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript/tsconfig.json b/packages/svelte2tsx/test/emitDts/samples/typescript/tsconfig.json
new file mode 100644
index 00000000..5b465a5c
--- /dev/null
+++ b/packages/svelte2tsx/test/emitDts/samples/typescript/tsconfig.json
@@ -0,0 +1,14 @@
+{
+    "compilerOptions": {
+        "moduleResolution": "node",
+        "module": "es2020",
+        "lib": ["es2020", "DOM"],
+        "target": "es2019",
+        "isolatedModules": true,
+        "resolveJsonModule": true,
+        "esModuleInterop": true,
+        "allowJs": true,
+        "checkJs": true
+    },
+    "include": ["./src/**/*.d.ts", "./src/**/*.js", "./src/**/*.ts", "./src/**/*.svelte"]
+}
diff --git a/packages/svelte2tsx/test/test.ts b/packages/svelte2tsx/test/test.ts
index df10cacb..7e951279 100644
--- a/packages/svelte2tsx/test/test.ts
+++ b/packages/svelte2tsx/test/test.ts
@@ -1,3 +1,6 @@
+const { dirname } = require('path');
+const { join } = require('path');
+
 require('ts-node').register({
     project: 'test/tsconfig.json',
     transpileOnly: true
@@ -13,7 +16,9 @@ if (process.env.CI) {
 }
 
 const test_folders = glob('*/index.ts', { cwd: 'test' });
-const solo_folders = test_folders.filter((folder) => /\.solo$/.test(folder));
+const solo_folders = test_folders.filter(
+    (folder) => glob('**/*.solo', { cwd: join('test', dirname(folder)) }).length
+);
 
 if (solo_folders.length) {
     solo_folders.forEach((name) => require('./' + name));