From 62fc3430ccf3fdeab1397d792445860a7d90eabe Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 12 Sep 2020 12:39:10 +0200 Subject: [PATCH 01/10] (feat) event typings WIP - try to find event strings for better autocompletion - support typed createEventDispatcher --- packages/svelte2tsx/src/svelte2tsx/index.ts | 13 +- .../src/svelte2tsx/nodes/ComponentEvents.ts | 179 +++++++++++++++++- .../src/svelte2tsx/nodes/event-handler.ts | 2 +- .../processInstanceScriptContent.ts | 16 +- packages/svelte2tsx/svelte-shims.d.ts | 3 + .../event-dispatcher-events.solo/expected.js | 12 ++ .../event-dispatcher-events.solo/expected.tsx | 0 .../event-dispatcher-events.solo/input.svelte | 15 ++ .../expected.js | 12 ++ .../expected.tsx | 24 +++ .../input.svelte | 15 ++ 11 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 9cda421c7..8af14f921 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -5,11 +5,7 @@ import path from 'path'; import { convertHtmlxToJsx } from '../htmlxtojsx'; import { parseHtmlx } from '../utils/htmlxparser'; import { ComponentDocumentation } from './nodes/ComponentDocumentation'; -import { - ComponentEvents, - ComponentEventsFromEventsMap, - ComponentEventsFromInterface, -} from './nodes/ComponentEvents'; +import { ComponentEvents } from './nodes/ComponentEvents'; import { EventHandler } from './nodes/event-handler'; import { ExportedNames } from './nodes/ExportedNames'; import { createClassGetters, createRenderFunctionGetterStr } from './nodes/exportgetters'; @@ -262,11 +258,14 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult { //resolve stores stores.resolveStores(); + const events = new ComponentEvents(); + events.setEventHandler(eventHandler); + return { moduleScriptTag, scriptTag, slots: slotHandler.getSlotDef(), - events: new ComponentEventsFromEventsMap(eventHandler), + events, uses$$props, uses$$restProps, uses$$slots, @@ -472,7 +471,7 @@ export function svelte2tsx( str, uses$$propsOr$$restProps: uses$$props || uses$$restProps, strictMode: !!options?.strictMode, - strictEvents: events instanceof ComponentEventsFromInterface, + strictEvents: events.hasInterface(), isTsFile: options?.isTsFile, getters, fileName: options?.filename, diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index c5b8ba134..1002371d4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -2,13 +2,18 @@ import ts from 'typescript'; import { EventHandler } from './event-handler'; import { getVariableAtTopLevel } from '../utils/tsAst'; -export abstract class ComponentEvents { - protected events = new Map(); +export class ComponentEvents { + private componentEventsInterface?: ComponentEventsFromInterface; + private componentEventsFromEventsMap?: ComponentEventsFromEventsMap; + + private get eventsClass() { + return this.componentEventsInterface || this.componentEventsFromEventsMap; + } getAll(): { name: string; type?: string; doc?: string }[] { const entries: { name: string; type: string; doc?: string }[] = []; - const iterableEntries = this.events.entries(); + const iterableEntries = this.eventsClass.events.entries(); for (const entry of iterableEntries) { entries.push({ name: entry[0], ...entry[1] }); } @@ -16,12 +21,39 @@ export abstract class ComponentEvents { return entries; } - abstract toDefString(): string; + setEventHandler(eventHandler: EventHandler): void { + this.componentEventsFromEventsMap = new ComponentEventsFromEventsMap(eventHandler); + } + + setComponentEventsInterface(node: ts.InterfaceDeclaration): void { + this.componentEventsInterface = new ComponentEventsFromInterface(node); + } + + hasInterface(): boolean { + return !!this.componentEventsInterface; + } + + checkIfImportIsEventDispatcher(node: ts.ImportDeclaration): void { + this.componentEventsFromEventsMap.checkIfImportIsEventDispatcher(node); + } + + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration): void { + this.componentEventsFromEventsMap.checkIfDeclarationInstantiatedEventDispatcher(node); + } + + checkIfCallExpressionIsDispatch(node: ts.CallExpression): void { + this.componentEventsFromEventsMap.checkIfCallExpressionIsDispatch(node); + } + + toDefString(): string { + return this.eventsClass.toDefString(); + } } -export class ComponentEventsFromInterface extends ComponentEvents { +class ComponentEventsFromInterface { + events = new Map(); + constructor(node: ts.InterfaceDeclaration) { - super(); this.events = this.extractEvents(node); } @@ -115,14 +147,97 @@ export class ComponentEventsFromInterface extends ComponentEvents { } } -export class ComponentEventsFromEventsMap extends ComponentEvents { +class ComponentEventsFromEventsMap { + events = new Map(); + private dispatchedEvents = new Set(); + private stringVars = new Map(); + private hasEventDispatcherImport = false; + private eventDispatcherTyping?: string; + private dispatcherName = ''; + constructor(private eventHandler: EventHandler) { - super(); this.events = this.extractEvents(eventHandler); } + checkIfImportIsEventDispatcher(node: ts.ImportDeclaration) { + if (this.hasEventDispatcherImport) { + return; + } + if (ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text !== 'svelte') { + return; + } + + const namedImports = node.importClause?.namedBindings; + if (ts.isNamedImports(namedImports)) { + this.hasEventDispatcherImport = namedImports.elements.some( + (el) => el.name.text === 'createEventDispatcher', + ); + } + } + + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration) { + if (!ts.isIdentifier(node.name)) { + return; + } + + if (ts.isStringLiteral(node.initializer)) { + this.stringVars.set(node.name.text, node.initializer.text); + } + + if ( + this.hasEventDispatcherImport && + ts.isCallExpression(node.initializer) && + ts.isIdentifier(node.initializer.expression) && + node.initializer.expression.text === 'createEventDispatcher' + ) { + this.dispatcherName = node.name.text; + const dispatcherTyping = node.initializer.typeArguments?.[0]; + if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) { + this.eventDispatcherTyping = dispatcherTyping.getText(); + dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { + this.addToEvents(this.getName(member.name), { + type: member.type?.getText() || 'Event', + doc: undefined, // TODO + }); + }); + } + } + } + + checkIfCallExpressionIsDispatch(node: ts.CallExpression) { + if (ts.isIdentifier(node.expression) && node.expression.text === this.dispatcherName) { + const firstArg = node.arguments[0]; + if (ts.isStringLiteral(firstArg)) { + this.addToEvents(firstArg.text); + } else if (ts.isIdentifier(firstArg)) { + const str = this.stringVars.get(firstArg.text); + if (str) { + this.addToEvents(str); + } + } + } + } + + private addToEvents( + eventName: string, + info: { type: string; doc?: string } = { type: 'CustomEvent' }, + ) { + this.events.set(eventName, info); + this.dispatchedEvents.add(eventName); + } + toDefString() { - return this.eventHandler.eventMapToString(); + if (this.eventDispatcherTyping) { + return `{} as unknown as __sveltets_toEventTypings<${this.eventDispatcherTyping}>()`; + } + return ( + '{' + + this.eventHandler.eventMapToString() + + [...this.dispatchedEvents.keys()] + .map((e) => `'${e}': __sveltets_customEvent`) + .join(', ') + + '}' + ); } private extractEvents(eventHandler: EventHandler) { @@ -132,4 +247,50 @@ export class ComponentEventsFromEventsMap extends ComponentEvents { } return map; } + + private getName(prop: ts.PropertyName) { + if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { + return prop.text; + } + + if (ts.isComputedPropertyName(prop)) { + if (ts.isIdentifier(prop.expression)) { + const identifierName = prop.expression.text; + const identifierValue = this.getIdentifierValue(prop, identifierName); + if (!identifierValue) { + this.throwError(prop); + } + return identifierValue; + } + } + + this.throwError(prop); + } + + private getIdentifierValue(prop: ts.ComputedPropertyName, identifierName: string) { + const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); + if (variable && ts.isStringLiteral(variable.initializer)) { + return variable.initializer.text; + } + } + + private throwError(prop: ts.PropertyName) { + const error: any = new Error( + 'The ComponentEvents interface can only have properties of type ' + + 'Identifier, StringLiteral or ComputedPropertyName. ' + + 'In case of ComputedPropertyName, ' + + 'it must be a const declared within the component and initialized with a string.', + ); + error.start = toLineColumn(prop.getStart()); + error.end = toLineColumn(prop.getEnd()); + throw error; + + function toLineColumn(pos: number) { + const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); + return { + line: lineChar.line + 1, + column: lineChar.character, + }; + } + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts index 2ca21104a..33ee86554 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts @@ -30,7 +30,7 @@ export class EventHandler { } eventMapToString() { - return '{' + Array.from(this.events.entries()).map(eventMapEntryToString).join(', ') + '}'; + return Array.from(this.events.entries()).map(eventMapEntryToString).join(', '); } } diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index 1d9058528..18964b43a 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -4,7 +4,7 @@ import * as ts from 'typescript'; import { findExportKeyword, getBinaryAssignmentExpr } from './utils/tsAst'; import { ExportedNames } from './nodes/ExportedNames'; import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames'; -import { ComponentEvents, ComponentEventsFromInterface } from './nodes/ComponentEvents'; +import { ComponentEvents } from './nodes/ComponentEvents'; import { Scope } from './utils/Scope'; export interface InstanceScriptProcessResult { @@ -295,7 +295,7 @@ export function processInstanceScriptContent( const onLeaveCallbacks: onLeaveCallback[] = []; if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentEvents') { - events = new ComponentEventsFromInterface(node); + events.setComponentEventsInterface(node); } if (ts.isVariableStatement(node)) { @@ -364,12 +364,22 @@ export function processInstanceScriptContent( } } - //move imports to top of script so they appear outside our render function if (ts.isImportDeclaration(node)) { + //move imports to top of script so they appear outside our render function str.move(node.getStart() + astOffset, node.end + astOffset, script.start + 1); //add in a \n const originalEndChar = str.original[node.end + astOffset - 1]; str.overwrite(node.end + astOffset - 1, node.end + astOffset, originalEndChar + '\n'); + // Check if import is the event dispatcher + events.checkIfImportIsEventDispatcher(node); + } + + if (ts.isVariableDeclaration(node)) { + events.checkIfDeclarationInstantiatedEventDispatcher(node); + } + + if (ts.isCallExpression(node)) { + events.checkIfCallExpressionIsDispatch(node); } if (ts.isVariableDeclaration(parent) && parent.name == node) { diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts index b37329cee..070031bb9 100644 --- a/packages/svelte2tsx/svelte-shims.d.ts +++ b/packages/svelte2tsx/svelte-shims.d.ts @@ -153,6 +153,9 @@ declare function __sveltets_bubbleEventDef( events: any, eventKey: string ): any; +declare const __sveltets_customEvent: CustomEvent; +declare function __sveltets_toEventTypings(): {[Key in keyof Typings]: CustomEvent} & Record>; + declare function __sveltets_unionType(t1: T1, t2: T2): T1 | T2; declare function __sveltets_unionType(t1: T1, t2: T2, t3: T3): T1 | T2 | T3; declare function __sveltets_unionType(t1: T1, t2: T2, t3: T3, t4: T4): T1 | T2 | T3 | T4; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js new file mode 100644 index 000000000..d00461a9c --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js @@ -0,0 +1,12 @@ +let assert = require('assert') + +module.exports = function ({events}) { + assert.deepEqual( + events.getAll(), + [ + {name: 'hi', type: 'CustomEvent'}, + {name: 'bye', type: 'CustomEvent'}, + {name: 'btn', type: 'CustomEvent'} + ] + ); +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte new file mode 100644 index 000000000..93facad1c --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js new file mode 100644 index 000000000..d00461a9c --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js @@ -0,0 +1,12 @@ +let assert = require('assert') + +module.exports = function ({events}) { + assert.deepEqual( + events.getAll(), + [ + {name: 'hi', type: 'CustomEvent'}, + {name: 'bye', type: 'CustomEvent'}, + {name: 'btn', type: 'CustomEvent'} + ] + ); +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx new file mode 100644 index 000000000..36ba5ff3e --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx @@ -0,0 +1,24 @@ +/// +<>; +import { createEventDispatcher, abc } from "svelte"; +function render() { + + + + const notDispatch = abc(); + const bla = 'bye'; + const dispatch = createEventDispatcher<{hi: boolean; [bla]: boolean; btn: string;}>(); + + dispatch('hi', true); + + function bye() { + dispatch(bla, false); + } +; +() => (<> + +); +return { props: {}, slots: {}, getters: {}, events: {} as unknown as __sveltets_toEventTypings<{hi: boolean; [bla]: boolean; btn: string;}>() }} + +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/ts-event-dispatcher-typed.solo/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte new file mode 100644 index 000000000..7e1e4f674 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte @@ -0,0 +1,15 @@ + + + \ No newline at end of file From 2bcf41f16e0fad521a6c9d66889cb9fea89d1538 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 10:26:59 +0200 Subject: [PATCH 02/10] component event types from createEventDispatcher typing --- .../src/svelte2tsx/nodes/ComponentEvents.ts | 39 +++++++++++++++++-- packages/svelte2tsx/svelte-shims.d.ts | 2 +- .../expected.js | 6 +-- .../expected.tsx | 2 +- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 1002371d4..9a57f3a96 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -147,6 +147,35 @@ class ComponentEventsFromInterface { } } +/** +Alles in einer Klasse tracken + +Für Template: + +case "Identifier": + if (node.name === "createEventDispatcher") { + hasDispatchedEvents = true; + } + + if (prop === "callee") { + callee.push({ name: node.name, parent }); + } + break; + + +Für Script: + +- alle const/let initialisierungen tracken (const a = ''; let a = '') +- alle callExpressions tracken und deren erstes argument +- createEventDispatcher tracken, zu welcher const/let es initialisiert wird + + +Am Ende: +1. name von createEventDispatcher rausfinden +2. alle calle aus Template iterieren, und die behalten, die dispatch sind. +3. alle callExpressions aus script iterieren, und die behalten, die dispatch sind +-- für 2 und 3 : Für jede gucken, ob man Name bekommt. Entweder eine variable, dann aus const/let initialisierungen finden, oder ein StringLiteral. + */ class ComponentEventsFromEventsMap { events = new Map(); private dispatchedEvents = new Set(); @@ -196,7 +225,7 @@ class ComponentEventsFromEventsMap { this.eventDispatcherTyping = dispatcherTyping.getText(); dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { this.addToEvents(this.getName(member.name), { - type: member.type?.getText() || 'Event', + type: `CustomEvent<${member.type?.getText() || 'any'}>`, doc: undefined, // TODO }); }); @@ -205,7 +234,11 @@ class ComponentEventsFromEventsMap { } checkIfCallExpressionIsDispatch(node: ts.CallExpression) { - if (ts.isIdentifier(node.expression) && node.expression.text === this.dispatcherName) { + if ( + !this.eventDispatcherTyping && + ts.isIdentifier(node.expression) && + node.expression.text === this.dispatcherName + ) { const firstArg = node.arguments[0]; if (ts.isStringLiteral(firstArg)) { this.addToEvents(firstArg.text); @@ -228,7 +261,7 @@ class ComponentEventsFromEventsMap { toDefString() { if (this.eventDispatcherTyping) { - return `{} as unknown as __sveltets_toEventTypings<${this.eventDispatcherTyping}>()`; + return `__sveltets_toEventTypings<${this.eventDispatcherTyping}>()`; } return ( '{' + diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts index 070031bb9..ee382e4ec 100644 --- a/packages/svelte2tsx/svelte-shims.d.ts +++ b/packages/svelte2tsx/svelte-shims.d.ts @@ -154,7 +154,7 @@ declare function __sveltets_bubbleEventDef( ): any; declare const __sveltets_customEvent: CustomEvent; -declare function __sveltets_toEventTypings(): {[Key in keyof Typings]: CustomEvent} & Record>; +declare function __sveltets_toEventTypings(): {[Key in keyof Typings]: CustomEvent}; declare function __sveltets_unionType(t1: T1, t2: T2): T1 | T2; declare function __sveltets_unionType(t1: T1, t2: T2, t3: T3): T1 | T2 | T3; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js index d00461a9c..f080823e1 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js @@ -4,9 +4,9 @@ module.exports = function ({events}) { assert.deepEqual( events.getAll(), [ - {name: 'hi', type: 'CustomEvent'}, - {name: 'bye', type: 'CustomEvent'}, - {name: 'btn', type: 'CustomEvent'} + {name: 'hi', type: 'CustomEvent', doc: undefined}, + {name: 'bye', type: 'CustomEvent', doc: undefined}, + {name: 'btn', type: 'CustomEvent', doc: undefined} ] ); } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx index 36ba5ff3e..30f2574df 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx @@ -18,7 +18,7 @@ function render() { () => (<> ); -return { props: {}, slots: {}, getters: {}, events: {} as unknown as __sveltets_toEventTypings<{hi: boolean; [bla]: boolean; btn: string;}>() }} +return { props: {}, slots: {}, getters: {}, events: __sveltets_toEventTypings<{hi: boolean; [bla]: boolean; btn: string;}>() }} export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) { } \ No newline at end of file From c236ea347522300f73d362a901dd807a04d695d3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 10:48:33 +0200 Subject: [PATCH 03/10] event docs --- .../src/svelte2tsx/nodes/ComponentEvents.ts | 25 +++++++++++++++++-- .../svelte2tsx/src/svelte2tsx/utils/tsAst.ts | 14 +++++++++++ .../expected.js | 4 +-- .../expected.tsx | 24 ++++++++++++++++-- .../input.svelte | 12 ++++++++- 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 9a57f3a96..f21959925 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; import { EventHandler } from './event-handler'; -import { getVariableAtTopLevel } from '../utils/tsAst'; +import { getVariableAtTopLevel, getLeadingDoc } from '../utils/tsAst'; export class ComponentEvents { private componentEventsInterface?: ComponentEventsFromInterface; @@ -226,7 +226,7 @@ class ComponentEventsFromEventsMap { dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { this.addToEvents(this.getName(member.name), { type: `CustomEvent<${member.type?.getText() || 'any'}>`, - doc: undefined, // TODO + doc: this.getDoc(member), }); }); } @@ -326,4 +326,25 @@ class ComponentEventsFromEventsMap { }; } } + + private getDoc(member: ts.PropertySignature) { + let doc = undefined; + const comment = getLeadingDoc(member); + + if (comment) { + doc = comment + .split('\n') + .map((line) => + // Remove /** */ + line + .replace(/\s*\/\*\*/, '') + .replace(/\s*\*\//, '') + .replace(/\s*\*/, '') + .trim(), + ) + .join('\n'); + } + + return doc; + } } diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts index 8c7d66583..bbca23fd9 100644 --- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts +++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts @@ -120,3 +120,17 @@ export function getVariableAtTopLevel( } } } + +/** + * Get the leading multiline trivia doc of the node. + */ +export function getLeadingDoc(node: ts.Node): string | undefined { + const nodeText = node.getFullText(); + const comment = ts + .getLeadingCommentRanges(nodeText, 0) + ?.find((c) => c.kind === ts.SyntaxKind.MultiLineCommentTrivia); + + if (comment) { + return nodeText.substring(comment.pos, comment.end); + } +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js index f080823e1..a76bb956a 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js @@ -4,8 +4,8 @@ module.exports = function ({events}) { assert.deepEqual( events.getAll(), [ - {name: 'hi', type: 'CustomEvent', doc: undefined}, - {name: 'bye', type: 'CustomEvent', doc: undefined}, + {name: 'hi', type: 'CustomEvent', doc: '\nA DOC\n'}, + {name: 'bye', type: 'CustomEvent', doc: '\nANOTHER DOC\n'}, {name: 'btn', type: 'CustomEvent', doc: undefined} ] ); diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx index 30f2574df..53c489e3a 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx @@ -7,7 +7,17 @@ function render() { const notDispatch = abc(); const bla = 'bye'; - const dispatch = createEventDispatcher<{hi: boolean; [bla]: boolean; btn: string;}>(); + const dispatch = createEventDispatcher<{ + /** + * A DOC + */ + hi: boolean; + /** + * ANOTHER DOC + */ + [bla]: boolean; + // not this + btn: string;}>(); dispatch('hi', true); @@ -18,7 +28,17 @@ function render() { () => (<> ); -return { props: {}, slots: {}, getters: {}, events: __sveltets_toEventTypings<{hi: boolean; [bla]: boolean; btn: string;}>() }} +return { props: {}, slots: {}, getters: {}, events: __sveltets_toEventTypings<{ + /** + * A DOC + */ + hi: boolean; + /** + * ANOTHER DOC + */ + [bla]: boolean; + // not this + btn: string;}>() }} 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/ts-event-dispatcher-typed.solo/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte index 7e1e4f674..484d8754e 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte @@ -3,7 +3,17 @@ const notDispatch = abc(); const bla = 'bye'; - const dispatch = createEventDispatcher<{hi: boolean; [bla]: boolean; btn: string;}>(); + const dispatch = createEventDispatcher<{ + /** + * A DOC + */ + hi: boolean; + /** + * ANOTHER DOC + */ + [bla]: boolean; + // not this + btn: string;}>(); dispatch('hi', true); From 0ba98675c2fed36690ba7cc506ccfffa56967e5b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 11:02:57 +0200 Subject: [PATCH 04/10] cleanup, move doc extraction into common util --- .../src/svelte2tsx/nodes/ComponentEvents.ts | 194 ++++++------------ .../src/svelte2tsx/nodes/ExportedNames.ts | 11 +- .../expected.js | 0 .../expected.tsx | 0 .../input.svelte | 0 .../expected.js | 0 .../expected.tsx | 0 .../input.svelte | 0 8 files changed, 63 insertions(+), 142 deletions(-) rename packages/svelte2tsx/test/svelte2tsx/samples/{event-dispatcher-events.solo => event-dispatcher-events}/expected.js (100%) rename packages/svelte2tsx/test/svelte2tsx/samples/{event-dispatcher-events.solo => event-dispatcher-events}/expected.tsx (100%) rename packages/svelte2tsx/test/svelte2tsx/samples/{event-dispatcher-events.solo => event-dispatcher-events}/input.svelte (100%) rename packages/svelte2tsx/test/svelte2tsx/samples/{ts-event-dispatcher-typed.solo => ts-event-dispatcher-typed}/expected.js (100%) rename packages/svelte2tsx/test/svelte2tsx/samples/{ts-event-dispatcher-typed.solo => ts-event-dispatcher-typed}/expected.tsx (100%) rename packages/svelte2tsx/test/svelte2tsx/samples/{ts-event-dispatcher-typed.solo => ts-event-dispatcher-typed}/input.svelte (100%) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index f21959925..cdd58ce8b 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -65,86 +65,14 @@ class ComponentEventsFromInterface { const map = new Map(); node.members.filter(ts.isPropertySignature).forEach((member) => { - map.set(this.getName(member.name), { + map.set(getName(member.name), { type: member.type?.getText() || 'Event', - doc: this.getDoc(node, member), + doc: getDoc(member), }); }); return map; } - - private getName(prop: ts.PropertyName) { - if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { - return prop.text; - } - - if (ts.isComputedPropertyName(prop)) { - if (ts.isIdentifier(prop.expression)) { - const identifierName = prop.expression.text; - const identifierValue = this.getIdentifierValue(prop, identifierName); - if (!identifierValue) { - this.throwError(prop); - } - return identifierValue; - } - } - - this.throwError(prop); - } - - private getIdentifierValue(prop: ts.ComputedPropertyName, identifierName: string) { - const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); - if (variable && ts.isStringLiteral(variable.initializer)) { - return variable.initializer.text; - } - } - - private throwError(prop: ts.PropertyName) { - const error: any = new Error( - 'The ComponentEvents interface can only have properties of type ' + - 'Identifier, StringLiteral or ComputedPropertyName. ' + - 'In case of ComputedPropertyName, ' + - 'it must be a const declared within the component and initialized with a string.', - ); - error.start = toLineColumn(prop.getStart()); - error.end = toLineColumn(prop.getEnd()); - throw error; - - function toLineColumn(pos: number) { - const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); - return { - line: lineChar.line + 1, - column: lineChar.character, - }; - } - } - - private getDoc(node: ts.InterfaceDeclaration, member: ts.PropertySignature) { - let doc = undefined; - const comment = ts.getLeadingCommentRanges( - node.getText(), - member.getFullStart() - node.getStart(), - ); - - if (comment) { - doc = node - .getText() - .substring(comment[0].pos, comment[0].end) - .split('\n') - .map((line) => - // Remove /** */ - line - .replace(/\s*\/\*\*/, '') - .replace(/\s*\*\//, '') - .replace(/\s*\*/, '') - .trim(), - ) - .join('\n'); - } - - return doc; - } } /** @@ -205,7 +133,7 @@ class ComponentEventsFromEventsMap { } checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration) { - if (!ts.isIdentifier(node.name)) { + if (!ts.isIdentifier(node.name) || !node.initializer) { return; } @@ -224,9 +152,9 @@ class ComponentEventsFromEventsMap { if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) { this.eventDispatcherTyping = dispatcherTyping.getText(); dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { - this.addToEvents(this.getName(member.name), { + this.addToEvents(getName(member.name), { type: `CustomEvent<${member.type?.getText() || 'any'}>`, - doc: this.getDoc(member), + doc: getDoc(member), }); }); } @@ -280,71 +208,71 @@ class ComponentEventsFromEventsMap { } return map; } +} - private getName(prop: ts.PropertyName) { - if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { - return prop.text; - } +function getName(prop: ts.PropertyName) { + if (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) { + return prop.text; + } - if (ts.isComputedPropertyName(prop)) { - if (ts.isIdentifier(prop.expression)) { - const identifierName = prop.expression.text; - const identifierValue = this.getIdentifierValue(prop, identifierName); - if (!identifierValue) { - this.throwError(prop); - } - return identifierValue; + if (ts.isComputedPropertyName(prop)) { + if (ts.isIdentifier(prop.expression)) { + const identifierName = prop.expression.text; + const identifierValue = getIdentifierValue(prop, identifierName); + if (!identifierValue) { + throwError(prop); } + return identifierValue; } - - this.throwError(prop); } - private getIdentifierValue(prop: ts.ComputedPropertyName, identifierName: string) { - const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); - if (variable && ts.isStringLiteral(variable.initializer)) { - return variable.initializer.text; - } - } + throwError(prop); +} - private throwError(prop: ts.PropertyName) { - const error: any = new Error( - 'The ComponentEvents interface can only have properties of type ' + - 'Identifier, StringLiteral or ComputedPropertyName. ' + - 'In case of ComputedPropertyName, ' + - 'it must be a const declared within the component and initialized with a string.', - ); - error.start = toLineColumn(prop.getStart()); - error.end = toLineColumn(prop.getEnd()); - throw error; - - function toLineColumn(pos: number) { - const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); - return { - line: lineChar.line + 1, - column: lineChar.character, - }; - } +function getIdentifierValue(prop: ts.ComputedPropertyName, identifierName: string) { + const variable = getVariableAtTopLevel(prop.getSourceFile(), identifierName); + if (variable && ts.isStringLiteral(variable.initializer)) { + return variable.initializer.text; } +} - private getDoc(member: ts.PropertySignature) { - let doc = undefined; - const comment = getLeadingDoc(member); - - if (comment) { - doc = comment - .split('\n') - .map((line) => - // Remove /** */ - line - .replace(/\s*\/\*\*/, '') - .replace(/\s*\*\//, '') - .replace(/\s*\*/, '') - .trim(), - ) - .join('\n'); - } +function throwError(prop: ts.PropertyName) { + const error: any = new Error( + 'The ComponentEvents interface can only have properties of type ' + + 'Identifier, StringLiteral or ComputedPropertyName. ' + + 'In case of ComputedPropertyName, ' + + 'it must be a const declared within the component and initialized with a string.', + ); + error.start = toLineColumn(prop.getStart()); + error.end = toLineColumn(prop.getEnd()); + throw error; + + function toLineColumn(pos: number) { + const lineChar = prop.getSourceFile().getLineAndCharacterOfPosition(pos); + return { + line: lineChar.line + 1, + column: lineChar.character, + }; + } +} - return doc; +function getDoc(member: ts.PropertySignature) { + let doc = undefined; + const comment = getLeadingDoc(member); + + if (comment) { + doc = comment + .split('\n') + .map((line) => + // Remove /** */ + line + .replace(/\s*\/\*\*/, '') + .replace(/\s*\*\//, '') + .replace(/\s*\*/, '') + .trim(), + ) + .join('\n'); } + + return doc; } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index b34292ff3..334614e03 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { getLeadingDoc } from '../utils/tsAst'; export interface IExportedNames { has(name: string): boolean; @@ -49,15 +50,7 @@ export class ExportedNames const exportExpr = target?.parent?.parent?.parent; if (exportExpr) { - const fileText = exportExpr.getSourceFile().getFullText(); - const comment = ts.getLeadingCommentRanges(fileText, exportExpr.getFullStart()); - - if (comment) { - const [first] = comment; - if (first?.kind === ts.SyntaxKind.MultiLineCommentTrivia) { - doc = fileText.substring(first.pos, first.end); - } - } + doc = getLeadingDoc(exportExpr); } return doc; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.js rename to packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/expected.tsx rename to packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/input.svelte similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events.solo/input.svelte rename to packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/input.svelte diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.js similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.js rename to packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.js diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/expected.tsx rename to packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/expected.tsx diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/input.svelte similarity index 100% rename from packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed.solo/input.svelte rename to packages/svelte2tsx/test/svelte2tsx/samples/ts-event-dispatcher-typed/input.svelte From a09ef0baeee3ecafafaa6c077eb634347b102d4a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 11:43:30 +0200 Subject: [PATCH 05/10] track dispatched events in markup --- packages/svelte2tsx/src/svelte2tsx/index.ts | 1 + .../src/svelte2tsx/nodes/ComponentEvents.ts | 54 +++++++---------- .../src/svelte2tsx/nodes/event-handler.ts | 60 +++++++++++++------ .../event-dispatcher-events/expected.js | 2 +- .../event-dispatcher-events/expected.tsx | 24 ++++++++ 5 files changed, 91 insertions(+), 50 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 8af14f921..d5ab242ba 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -175,6 +175,7 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult { case 'Identifier': handleIdentifier(node); stores.handleIdentifier(node, parent, prop); + eventHandler.handleIdentifier(node, parent, prop); break; case 'Slot': slotHandler.handleSlot(node, templateScope); diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index cdd58ce8b..00048a40f 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -2,6 +2,22 @@ import ts from 'typescript'; import { EventHandler } from './event-handler'; import { getVariableAtTopLevel, getLeadingDoc } from '../utils/tsAst'; +/** + * This class accumulates all events that are dispatched from the component. + * It also tracks bubbled/forwarded events. + * + * It can not track events which are not fired through a variable + * which was not instantiated within the component with `createEventDispatcher`. + * This means that event dispatchers which are defined outside of the component and then imported do not get picked up. + * + * 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 yes, extract the event names and the event types from it + * - If no, track all invocations of it to get the event names + */ export class ComponentEvents { private componentEventsInterface?: ComponentEventsFromInterface; private componentEventsFromEventsMap?: ComponentEventsFromEventsMap; @@ -75,35 +91,6 @@ class ComponentEventsFromInterface { } } -/** -Alles in einer Klasse tracken - -Für Template: - -case "Identifier": - if (node.name === "createEventDispatcher") { - hasDispatchedEvents = true; - } - - if (prop === "callee") { - callee.push({ name: node.name, parent }); - } - break; - - -Für Script: - -- alle const/let initialisierungen tracken (const a = ''; let a = '') -- alle callExpressions tracken und deren erstes argument -- createEventDispatcher tracken, zu welcher const/let es initialisiert wird - - -Am Ende: -1. name von createEventDispatcher rausfinden -2. alle calle aus Template iterieren, und die behalten, die dispatch sind. -3. alle callExpressions aus script iterieren, und die behalten, die dispatch sind --- für 2 und 3 : Für jede gucken, ob man Name bekommt. Entweder eine variable, dann aus const/let initialisierungen finden, oder ein StringLiteral. - */ class ComponentEventsFromEventsMap { events = new Map(); private dispatchedEvents = new Set(); @@ -149,6 +136,7 @@ class ComponentEventsFromEventsMap { ) { this.dispatcherName = node.name.text; const dispatcherTyping = node.initializer.typeArguments?.[0]; + if (dispatcherTyping && ts.isTypeLiteralNode(dispatcherTyping)) { this.eventDispatcherTyping = dispatcherTyping.getText(); dispatcherTyping.members.filter(ts.isPropertySignature).forEach((member) => { @@ -157,6 +145,10 @@ class ComponentEventsFromEventsMap { doc: getDoc(member), }); }); + } else { + this.eventHandler + .getDispatchedEventsForIdentifier(this.dispatcherName) + .forEach((evtName) => this.addToEvents(evtName)); } } } @@ -193,7 +185,7 @@ class ComponentEventsFromEventsMap { } return ( '{' + - this.eventHandler.eventMapToString() + + this.eventHandler.bubbledEventsMapToString() + [...this.dispatchedEvents.keys()] .map((e) => `'${e}': __sveltets_customEvent`) .join(', ') + @@ -203,7 +195,7 @@ class ComponentEventsFromEventsMap { private extractEvents(eventHandler: EventHandler) { const map = new Map(); - for (const name of eventHandler.getEvents().keys()) { + for (const name of eventHandler.getBubbledEvents().keys()) { map.set(name, { type: 'Event' }); } return map; diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts index 33ee86554..5de08af79 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts @@ -1,36 +1,60 @@ import { Node } from 'estree-walker'; export class EventHandler { - events = new Map(); + private bubbledEvents = new Map(); + private callees: { name: string; parent: Node }[] = []; - handleEventHandler = (node: Node, parent: Node) => { + handleEventHandler(node: Node, parent: Node): void { const eventName = node.name; - const handleEventHandlerBubble = () => { - const componentEventDef = `__sveltets_instanceOf(${parent.name})`; - // eslint-disable-next-line max-len - const exp = `__sveltets_bubbleEventDef(${componentEventDef}.$$events_def, '${eventName}')`; - - const exist = this.events.get(eventName); - this.events.set(eventName, exist ? [].concat(exist, exp) : exp); - }; - // pass-through/ bubble if (!node.expression) { if (parent.type === 'InlineComponent') { - handleEventHandlerBubble(); + this.handleEventHandlerBubble(parent, eventName); } else { - this.events.set(eventName, getEventDefExpressionForNonCompoent(eventName, parent)); + this.bubbledEvents.set( + eventName, + getEventDefExpressionForNonCompoent(eventName, parent), + ); } } - }; + } + + handleIdentifier(node: Node, parent: Node, prop: string): void { + if (prop === 'callee') { + this.callees.push({ name: node.name, parent }); + } + } + + getBubbledEvents() { + return this.bubbledEvents; + } + + getDispatchedEventsForIdentifier(name: string) { + const eventNames = new Set(); + + this.callees.forEach((callee) => { + if (callee.name === name) { + const [name] = callee.parent.arguments; + + if (name.value !== undefined) { + eventNames.add(name.value); + } + } + }); + + return eventNames; + } - getEvents() { - return this.events; + bubbledEventsMapToString() { + return Array.from(this.bubbledEvents.entries()).map(eventMapEntryToString).join(', '); } - eventMapToString() { - return Array.from(this.events.entries()).map(eventMapEntryToString).join(', '); + private handleEventHandlerBubble(parent: Node, eventName: string): void { + const componentEventDef = `__sveltets_instanceOf(${parent.name})`; + const exp = `__sveltets_bubbleEventDef(${componentEventDef}.$$events_def, '${eventName}')`; + const exist = this.bubbledEvents.get(eventName); + this.bubbledEvents.set(eventName, exist ? [].concat(exist, exp) : exp); } } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js index d00461a9c..645047a7e 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.js @@ -4,9 +4,9 @@ module.exports = function ({events}) { assert.deepEqual( events.getAll(), [ + {name: 'btn', type: 'CustomEvent'}, {name: 'hi', type: 'CustomEvent'}, {name: 'bye', type: 'CustomEvent'}, - {name: 'btn', type: 'CustomEvent'} ] ); } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx index e69de29bb..9ab7257f0 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events/expected.tsx @@ -0,0 +1,24 @@ +/// +<>; +import { createEventDispatcher, abc } from "svelte"; +function render() { + + + + const notDispatch = abc(); + const dispatch = createEventDispatcher(); + + dispatch('hi', true); + + function bye() { + const bla = 'bye'; + dispatch(bla, false); + } +; +() => (<> + +); +return { props: {}, slots: {}, getters: {}, events: {'btn': __sveltets_customEvent, '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 From 9b70d296e55e5a41e25642ed10488bb9f253f4a2 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 11:51:29 +0200 Subject: [PATCH 06/10] create api from ComponentEvents so unnecessary stuff is not prevented from getting garbage collected --- packages/svelte2tsx/src/svelte2tsx/index.ts | 2 +- .../src/svelte2tsx/nodes/ComponentEvents.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index d5ab242ba..83aee9b41 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -485,6 +485,6 @@ export function svelte2tsx( code: str.toString(), map: str.generateMap({ hires: true, source: options?.filename }), exportedNames, - events, + events: events.createAPI(), }; } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 00048a40f..1327cd399 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -26,7 +26,11 @@ export class ComponentEvents { return this.componentEventsInterface || this.componentEventsFromEventsMap; } - getAll(): { name: string; type?: string; doc?: string }[] { + /** + * Collect state and create the API which will be part + * of the return object of the `svelte2tsx` function. + */ + createAPI() { const entries: { name: string; type: string; doc?: string }[] = []; const iterableEntries = this.eventsClass.events.entries(); @@ -34,7 +38,11 @@ export class ComponentEvents { entries.push({ name: entry[0], ...entry[1] }); } - return entries; + return { + getAll(): { name: string; type?: string; doc?: string }[] { + return entries; + }, + }; } setEventHandler(eventHandler: EventHandler): void { From bc9173ef99c464547f8f0e94cdb605e0dbd3370a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 11:53:51 +0200 Subject: [PATCH 07/10] cleanup --- packages/svelte2tsx/src/svelte2tsx/index.ts | 5 +---- .../svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 83aee9b41..c56836532 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -259,14 +259,11 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult { //resolve stores stores.resolveStores(); - const events = new ComponentEvents(); - events.setEventHandler(eventHandler); - return { moduleScriptTag, scriptTag, slots: slotHandler.getSlotDef(), - events, + events: new ComponentEvents(eventHandler), uses$$props, uses$$restProps, uses$$slots, diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 1327cd399..38ef5f49a 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -20,12 +20,16 @@ import { getVariableAtTopLevel, getLeadingDoc } from '../utils/tsAst'; */ export class ComponentEvents { private componentEventsInterface?: ComponentEventsFromInterface; - private componentEventsFromEventsMap?: ComponentEventsFromEventsMap; + private componentEventsFromEventsMap: ComponentEventsFromEventsMap; private get eventsClass() { return this.componentEventsInterface || this.componentEventsFromEventsMap; } + constructor(eventHandler: EventHandler) { + this.componentEventsFromEventsMap = new ComponentEventsFromEventsMap(eventHandler); + } + /** * Collect state and create the API which will be part * of the return object of the `svelte2tsx` function. @@ -45,10 +49,6 @@ export class ComponentEvents { }; } - setEventHandler(eventHandler: EventHandler): void { - this.componentEventsFromEventsMap = new ComponentEventsFromEventsMap(eventHandler); - } - setComponentEventsInterface(node: ts.InterfaceDeclaration): void { this.componentEventsInterface = new ComponentEventsFromInterface(node); } From 77097d4b7d41defc759568c89a3c148c98477807 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 13 Sep 2020 12:02:41 +0200 Subject: [PATCH 08/10] docs --- docs/preprocessors/typescript.md | 42 ++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/docs/preprocessors/typescript.md b/docs/preprocessors/typescript.md index 7345702ea..e55bf4f7d 100644 --- a/docs/preprocessors/typescript.md +++ b/docs/preprocessors/typescript.md @@ -44,7 +44,31 @@ Hit `ctrl-shift-p` or `cmd-shift-p` on mac, type `svelte restart`, and select `S ## Typing component events -When you are using TypeScript, you can type which events your component has by defining a reserved `interface` (_NOT_ `type`) called `ComponentEvents`: +When you are using TypeScript, you can type which events your component has in two ways: + +The first and possibly most often used way is to type the `createEventDispatcher` invocation like this: + +```html + +``` + +This will make sure that if you use `dispatch` that you can only invoke it with the specified names and its types. + +Note though that this will _NOT_ make the events strict so that you get type errors when trying to listen to other events when using the component. Due to Svelte's dynamic events creation, component events could be fired not only from a dispatcher created directly in the component, but from a dispatcher which is created as part of another import. This is almost impossible to infer. + +If you want strict events, you can do so by defining a reserved `interface` (_NOT_ `type`) called `ComponentEvents`: ```html ``` -> In case you ask why the events cannot be infered: Due to Svelte's dynamic nature, component events could be fired not only from a dispatcher created directly in the component, but from a dispatcher which is created as part of a mixin. This is almost impossible to infer, so we need you to tell us which events are possible. - ## Troubleshooting / FAQ ### I cannot use TS inside my script even when `lang="ts"` is present @@ -130,13 +152,13 @@ Create a `additional-svelte-jsx.d.ts` file: ```ts declare namespace svelte.JSX { - interface HTMLAttributes { - // If you want to use on:beforeinstallprompt - onbeforeinstallprompt?: (event: any) => any; - // If you want to use myCustomAttribute={..} (note: all lowercase) - mycustomattribute?: any; - // You can replace any with something more specific if you like - } + interface HTMLAttributes { + // If you want to use on:beforeinstallprompt + onbeforeinstallprompt?: (event: any) => any; + // If you want to use myCustomAttribute={..} (note: all lowercase) + mycustomattribute?: any; + // You can replace any with something more specific if you like + } } ``` From 4b89292e9351f94c249cd9906728c228e55a82b0 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 17 Sep 2020 08:10:22 +0200 Subject: [PATCH 09/10] support aliased import --- .../src/svelte2tsx/nodes/ComponentEvents.ts | 15 +++++++----- .../event-dispatcher-events-alias/expected.js | 12 ++++++++++ .../expected.tsx | 24 +++++++++++++++++++ .../input.svelte | 15 ++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.js create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 38ef5f49a..90d4e84ad 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -103,7 +103,7 @@ class ComponentEventsFromEventsMap { events = new Map(); private dispatchedEvents = new Set(); private stringVars = new Map(); - private hasEventDispatcherImport = false; + private eventDispatcherImport = ''; private eventDispatcherTyping?: string; private dispatcherName = ''; @@ -112,7 +112,7 @@ class ComponentEventsFromEventsMap { } checkIfImportIsEventDispatcher(node: ts.ImportDeclaration) { - if (this.hasEventDispatcherImport) { + if (this.eventDispatcherImport) { return; } if (ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text !== 'svelte') { @@ -121,9 +121,13 @@ class ComponentEventsFromEventsMap { const namedImports = node.importClause?.namedBindings; if (ts.isNamedImports(namedImports)) { - this.hasEventDispatcherImport = namedImports.elements.some( - (el) => el.name.text === 'createEventDispatcher', + const eventDispatcherImport = namedImports.elements.find( + // If it's an aliased import, propertyName is set + (el) => (el.propertyName || el.name).text === 'createEventDispatcher', ); + if (eventDispatcherImport) { + this.eventDispatcherImport = eventDispatcherImport.name.text; + } } } @@ -137,10 +141,9 @@ class ComponentEventsFromEventsMap { } if ( - this.hasEventDispatcherImport && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && - node.initializer.expression.text === 'createEventDispatcher' + node.initializer.expression.text === this.eventDispatcherImport ) { this.dispatcherName = node.name.text; const dispatcherTyping = node.initializer.typeArguments?.[0]; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.js b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.js new file mode 100644 index 000000000..645047a7e --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.js @@ -0,0 +1,12 @@ +let assert = require('assert') + +module.exports = function ({events}) { + assert.deepEqual( + events.getAll(), + [ + {name: 'btn', type: 'CustomEvent'}, + {name: 'hi', type: 'CustomEvent'}, + {name: 'bye', type: 'CustomEvent'}, + ] + ); +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.tsx new file mode 100644 index 000000000..007d13e14 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/expected.tsx @@ -0,0 +1,24 @@ +/// +<>; +import { createEventDispatcher as foo, abc } from "svelte"; +function render() { + + + + const notDispatch = abc(); + const dispatch = foo(); + + dispatch('hi', true); + + function bye() { + const bla = 'bye'; + dispatch(bla, false); + } +; +() => (<> + +); +return { props: {}, slots: {}, getters: {}, events: {'btn': __sveltets_customEvent, '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-dispatcher-events-alias/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/input.svelte new file mode 100644 index 000000000..9fc0d7a2d --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/event-dispatcher-events-alias/input.svelte @@ -0,0 +1,15 @@ + + + \ No newline at end of file From d7eebd8e3e1e2b398dcfad7a9a5b62c160406d2c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 17 Sep 2020 08:25:38 +0200 Subject: [PATCH 10/10] code style --- .../src/svelte2tsx/nodes/ComponentEvents.ts | 18 ++++++++++++++---- .../svelte2tsx/processInstanceScriptContent.ts | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts index 90d4e84ad..a7e690aae 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts @@ -61,6 +61,10 @@ export class ComponentEvents { this.componentEventsFromEventsMap.checkIfImportIsEventDispatcher(node); } + checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration): void { + this.componentEventsFromEventsMap.checkIfIsStringLiteralDeclaration(node); + } + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration): void { this.componentEventsFromEventsMap.checkIfDeclarationInstantiatedEventDispatcher(node); } @@ -131,15 +135,21 @@ class ComponentEventsFromEventsMap { } } + checkIfIsStringLiteralDeclaration(node: ts.VariableDeclaration) { + if ( + ts.isIdentifier(node.name) && + node.initializer && + ts.isStringLiteral(node.initializer) + ) { + this.stringVars.set(node.name.text, node.initializer.text); + } + } + checkIfDeclarationInstantiatedEventDispatcher(node: ts.VariableDeclaration) { if (!ts.isIdentifier(node.name) || !node.initializer) { return; } - if (ts.isStringLiteral(node.initializer)) { - this.stringVars.set(node.name.text, node.initializer.text); - } - if ( ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index 18964b43a..94e15e55b 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -375,6 +375,7 @@ export function processInstanceScriptContent( } if (ts.isVariableDeclaration(node)) { + events.checkIfIsStringLiteralDeclaration(node); events.checkIfDeclarationInstantiatedEventDispatcher(node); }