Repository: sveltejs/language-tools
Pull request #938 (2021-04-06 13:34:56)

(fix) handle multiple event dispatchers
Merge definitions of multiple `createEventDispatcher`-instantations as well as bubbled events.
#921


Patch:
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts
index 011e4302..09f1fff5 100644
--- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts
@@ -13,8 +13,8 @@ import { getVariableAtTopLevel, getLastLeadingDoc } from '../utils/tsAst';
  * The logic is as follows:
  * - If there exists a ComponentEvents interface definition, use that and skip the rest
  * - Else first try to find the `createEventDispatcher` import
- * - If it exists, try to find the variable where `createEventDispatcher()` is assigned to
- * - If that variable is found, try to find out if it's typed.
+ * - If it exists, try to find the variables where `createEventDispatcher()` is assigned to
+ * - For each variable found, try to find out if it's typed.
  *   - If yes, extract the event names and the event types from it
  *   - If no, track all invocations of it to get the event names
  */
@@ -108,8 +108,7 @@ class ComponentEventsFromEventsMap {
     private dispatchedEvents = new Set();
     private stringVars = new Map<string, string>();
     private eventDispatcherImport = '';
-    private eventDispatcherTyping?: string;
-    private dispatcherName = '';
+    private eventDispatchers: Array<{ name: string; typing?: string }> = [];
 
     constructor(private eventHandler: EventHandler) {
         this.events = this.extractEvents(eventHandler);
@@ -155,11 +154,14 @@ class ComponentEventsFromEventsMap {
             ts.isIdentifier(node.initializer.expression) &&
             node.initializer.expression.text === this.eventDispatcherImport
         ) {
-            this.dispatcherName = node.name.text;
+            const dispatcherName = node.name.text;
             const dispatcherTyping = node.initializer.typeArguments?.[0];
 
             if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) {
-                this.eventDispatcherTyping = dispatcherTyping.getText();
+                this.eventDispatchers.push({
+                    name: dispatcherName,
+                    typing: dispatcherTyping.getText()
+                });
                 dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => {
                     this.addToEvents(getName(member.name), {
                         type: `CustomEvent<${member.type?.getText() || 'any'}>`,
@@ -167,26 +169,35 @@ class ComponentEventsFromEventsMap {
                     });
                 });
             } else {
+                this.eventDispatchers.push({ name: dispatcherName });
                 this.eventHandler
-                    .getDispatchedEventsForIdentifier(this.dispatcherName)
-                    .forEach((evtName) => this.addToEvents(evtName));
+                    .getDispatchedEventsForIdentifier(dispatcherName)
+                    .forEach((evtName) => {
+                        this.addToEvents(evtName);
+                        this.dispatchedEvents.add(evtName);
+                    });
             }
         }
     }
 
     checkIfCallExpressionIsDispatch(node: ts.CallExpression) {
         if (
-            !this.eventDispatcherTyping &&
-            ts.isIdentifier(node.expression) &&
-            node.expression.text === this.dispatcherName
+            this.eventDispatchers.some(
+                (dispatcher) =>
+                    !dispatcher.typing &&
+                    ts.isIdentifier(node.expression) &&
+                    node.expression.text === dispatcher.name
+            )
         ) {
             const firstArg = node.arguments[0];
             if (ts.isStringLiteral(firstArg)) {
                 this.addToEvents(firstArg.text);
+                this.dispatchedEvents.add(firstArg.text);
             } else if (ts.isIdentifier(firstArg)) {
                 const str = this.stringVars.get(firstArg.text);
                 if (str) {
                     this.addToEvents(str);
+                    this.dispatchedEvents.add(str);
                 }
             }
         }
@@ -196,18 +207,26 @@ class ComponentEventsFromEventsMap {
         eventName: string,
         info: { type: string; doc?: string } = { type: 'CustomEvent<any>' }
     ) {
-        this.events.set(eventName, info);
-        this.dispatchedEvents.add(eventName);
+        if (this.events.has(eventName)) {
+            // If there are multiple definitions, merge them by falling back to any-typing
+            this.events.set(eventName, { type: 'CustomEvent<any>' });
+            this.dispatchedEvents.add(eventName);
+        } else {
+            this.events.set(eventName, info);
+        }
     }
 
     toDefString() {
-        if (this.eventDispatcherTyping) {
-            return `__sveltets_toEventTypings<${this.eventDispatcherTyping}>()`;
-        }
-
         return (
             '{' +
             [
+                ...this.eventDispatchers
+                    .map(
+                        (dispatcher) =>
+                            dispatcher.typing &&
+                            `...__sveltets_toEventTypings<${dispatcher.typing}>()`
+                    )
+                    .filter((str) => !!str),
                 ...this.eventHandler.bubbledEventsAsStrings(),
                 ...[...this.dispatchedEvents.keys()].map((e) => `'${e}': __sveltets_customEvent`)
             ].join(', ') +


Test patch:
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.js
new file mode 100644
index 00000000..ef47078e
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.js
@@ -0,0 +1,9 @@
+let assert = require('assert');
+
+module.exports = function ({ events }) {
+    assert.deepEqual(events.getAll(), [
+        { name: 'click', type: 'Event' },
+        { name: 'hi', type: 'CustomEvent<any>' },
+        { name: 'bye', type: 'CustomEvent<any>' }
+    ]);
+};
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.tsx
new file mode 100644
index 00000000..8313f134
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/expected.tsx
@@ -0,0 +1,21 @@
+///<reference types="svelte" />
+<></>;
+import { createEventDispatcher, abc } from "svelte";
+function render() {
+
+    
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher();
+    const dispatch2 = createEventDispatcher();
+
+    dispatch1('hi', true);
+    dispatch2('bye', true);
+;
+() => (<>
+
+<button onclick={undefined}></button></>);
+return { props: {}, slots: {}, getters: {}, events: {'click':__sveltets_mapElementEvent('click'), 'hi': __sveltets_customEvent, 'bye': __sveltets_customEvent} }}
+
+export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/input.svelte
new file mode 100644
index 00000000..c8be16c9
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatchers/input.svelte
@@ -0,0 +1,12 @@
+<script>
+    import { createEventDispatcher, abc } from "svelte";
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher();
+    const dispatch2 = createEventDispatcher();
+
+    dispatch1('hi', true);
+    dispatch2('bye', true);
+</script>
+
+<button on:click></button>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx
index a1a90731..be079bd0 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx
@@ -31,7 +31,7 @@ function render() {
 () => (<>
 
 <button onclick={() => dispatch('btn', '')}></button></>);
-return { props: {}, slots: {}, getters: {}, events: __sveltets_toEventTypings<{
+return { props: {}, slots: {}, getters: {}, events: {...__sveltets_toEventTypings<{
     /**
      * A DOC
      */
@@ -44,7 +44,7 @@ return { props: {}, slots: {}, getters: {}, events: __sveltets_toEventTypings<{
      */
     [bla]: boolean;
     // not this
-    btn: string;}>() }}
+    btn: string;}>()} }}
 
 export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial_ts(__sveltets_with_any_event(render))) {
 }
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.js
new file mode 100644
index 00000000..ef47078e
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.js
@@ -0,0 +1,9 @@
+let assert = require('assert');
+
+module.exports = function ({ events }) {
+    assert.deepEqual(events.getAll(), [
+        { name: 'click', type: 'Event' },
+        { name: 'hi', type: 'CustomEvent<any>' },
+        { name: 'bye', type: 'CustomEvent<any>' }
+    ]);
+};
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.tsx
new file mode 100644
index 00000000..8de72858
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/expected.tsx
@@ -0,0 +1,31 @@
+///<reference types="svelte" />
+<></>;
+import { createEventDispatcher, abc } from "svelte";
+function render() {
+
+    
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>();
+    const dispatch2 = createEventDispatcher<{hi: string;}>();
+    const dispatch3 = createEventDispatcher();
+
+    dispatch3('bye', true);
+;
+() => (<>
+
+<button onclick={undefined}></button></>);
+return { props: {}, slots: {}, getters: {}, events: {...__sveltets_toEventTypings<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>(), ...__sveltets_toEventTypings<{hi: string;}>(), 'click':__sveltets_mapElementEvent('click'), 'hi': __sveltets_customEvent, 'bye': __sveltets_customEvent} }}
+
+export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial_ts(__sveltets_with_any_event(render))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/input.svelte
new file mode 100644
index 00000000..e933ed4f
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers-same-event/input.svelte
@@ -0,0 +1,17 @@
+<script>
+    import { createEventDispatcher, abc } from "svelte";
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>();
+    const dispatch2 = createEventDispatcher<{hi: string;}>();
+    const dispatch3 = createEventDispatcher();
+
+    dispatch3('bye', true);
+</script>
+
+<button on:click></button>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.js
new file mode 100644
index 00000000..d8d4451b
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.js
@@ -0,0 +1,10 @@
+let assert = require('assert');
+
+module.exports = function ({ events }) {
+    assert.deepEqual(events.getAll(), [
+        { name: 'click', type: 'Event' },
+        { name: 'hi', type: 'CustomEvent<boolean>', doc: '\nA DOC\n' },
+        { name: 'btn', type: 'CustomEvent<string>', doc: undefined },
+        { name: 'bye', type: 'CustomEvent<any>' }
+    ]);
+};
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.tsx
new file mode 100644
index 00000000..e6c13a98
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/expected.tsx
@@ -0,0 +1,31 @@
+///<reference types="svelte" />
+<></>;
+import { createEventDispatcher, abc } from "svelte";
+function render() {
+
+    
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>();
+    const dispatch2 = createEventDispatcher<{btn: string;}>();
+    const dispatch3 = createEventDispatcher();
+
+    dispatch3('bye', true);
+;
+() => (<>
+
+<button onclick={undefined}></button></>);
+return { props: {}, slots: {}, getters: {}, events: {...__sveltets_toEventTypings<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>(), ...__sveltets_toEventTypings<{btn: string;}>(), 'click':__sveltets_mapElementEvent('click'), 'bye': __sveltets_customEvent} }}
+
+export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial_ts(__sveltets_with_any_event(render))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/input.svelte
new file mode 100644
index 00000000..3032507a
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatchers/input.svelte
@@ -0,0 +1,17 @@
+<script>
+    import { createEventDispatcher, abc } from "svelte";
+
+    const notDispatch = abc();
+    const dispatch1 = createEventDispatcher<{
+    /**
+     * A DOC
+     */
+    hi: boolean;
+    }>();
+    const dispatch2 = createEventDispatcher<{btn: string;}>();
+    const dispatch3 = createEventDispatcher();
+
+    dispatch3('bye', true);
+</script>
+
+<button on:click></button>
\ No newline at end of file