From 191b2b4a4b980911f9f71e1bbbc926c2ae140907 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 10 Aug 2020 09:59:29 +0200 Subject: [PATCH] (feat) be able to type props/events/slots Through a new reserved interface "ComponentDef" --- packages/svelte2tsx/src/interfaces.ts | 2 + packages/svelte2tsx/src/svelte2tsx.ts | 53 +++++++++++++++---- packages/svelte2tsx/src/utils/tsAst.ts | 29 +++++++++- .../expected.tsx | 19 +++++++ .../input.svelte | 7 +++ .../ts-componentdef-interface/expected.tsx | 19 +++++++ .../ts-componentdef-interface/input.svelte | 7 +++ 7 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/expected.tsx create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/input.svelte diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts index 2613bb5c6..db1f2136a 100644 --- a/packages/svelte2tsx/src/interfaces.ts +++ b/packages/svelte2tsx/src/interfaces.ts @@ -1,12 +1,14 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import { ExportedNames } from './nodes/ExportedNames'; +import ts from 'typescript'; export interface InstanceScriptProcessResult { exportedNames: ExportedNames; uses$$props: boolean; uses$$restProps: boolean; getters: Set; + componentDef: ts.InterfaceDeclaration | undefined; } export interface CreateRenderFunctionPara extends InstanceScriptProcessResult { diff --git a/packages/svelte2tsx/src/svelte2tsx.ts b/packages/svelte2tsx/src/svelte2tsx.ts index a9f3218c9..1a2f38132 100644 --- a/packages/svelte2tsx/src/svelte2tsx.ts +++ b/packages/svelte2tsx/src/svelte2tsx.ts @@ -7,7 +7,12 @@ import { convertHtmlxToJsx } from './htmlxtojsx'; import { Node } from 'estree-walker'; import * as ts from 'typescript'; import { createEventHandlerTransformer, eventMapToString } from './nodes/event-handler'; -import { findExortKeyword } from './utils/tsAst'; +import { + findExportKeyword, + getGenericsDefinitionString, + getGenericsUsageString, + getComponentClassUsingInterfaceString, +} from './utils/tsAst'; import { InstanceScriptProcessResult, CreateRenderFunctionPara } from './interfaces'; import { createRenderFunctionGetterStr, createClassGetters } from './nodes/exportgetters'; import { ExportedNames } from './nodes/ExportedNames'; @@ -220,7 +225,8 @@ function processSvelteTemplate(str: MagicString): TemplateProcessResult { if (parent.type == 'Property' && prop == 'key') return; scope.declared.add(node.name); } else { - if (parent.type == 'MemberExpression' && prop == 'property' && !parent.computed) return; + if (parent.type == 'MemberExpression' && prop == 'property' && !parent.computed) + return; if (parent.type == 'Property' && prop == 'key') return; pendingStoreResolutions.push({ node, parent, scope }); } @@ -660,12 +666,18 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS } }; + let componentDef: ts.InterfaceDeclaration | undefined; + const walk = (node: ts.Node, parent: ts.Node) => { type onLeaveCallback = () => void; const onLeaveCallbacks: onLeaveCallback[] = []; + if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentDef') { + componentDef = node; + } + if (ts.isVariableStatement(node)) { - const exportModifier = findExortKeyword(node); + const exportModifier = findExportKeyword(node); if (exportModifier) { const isLet = node.declarationList.flags === ts.NodeFlags.Let; const isConst = node.declarationList.flags === ts.NodeFlags.Const; @@ -686,7 +698,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS if (ts.isFunctionDeclaration(node)) { if (node.modifiers) { - const exportModifier = findExortKeyword(node); + const exportModifier = findExportKeyword(node); if (exportModifier) { removeExport(exportModifier.getStart(), exportModifier.end); addGetter(node.name); @@ -698,7 +710,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS } if (ts.isClassDeclaration(node)) { - const exportModifier = findExortKeyword(node); + const exportModifier = findExportKeyword(node); if (exportModifier) { removeExport(exportModifier.getStart(), exportModifier.end); addGetter(node.name); @@ -821,6 +833,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS uses$$props, uses$$restProps, getters, + componentDef, }; } @@ -844,6 +857,7 @@ function addComponentExport( strictMode: boolean, isTsFile: boolean, getters: Set, + componentDef?: ts.InterfaceDeclaration, /** A named export allows for TSDoc-compatible docstrings */ className?: string, componentDocumentation?: string | null, @@ -861,9 +875,16 @@ function addComponentExport( const doc = formatComponentDocumentation(componentDocumentation); const statement = - `\n\n${doc}export default class${ + `\n` + + // Call function to prevent "unused function" info + `${componentDef ? 'render();' : ''}` + + `\n${doc}export default class${ className ? ` ${className}` : '' - } extends createSvelte2TsxComponent(${propDef}) {` + + }${getGenericsDefinitionString(componentDef)} extends ${ + componentDef + ? getComponentClassUsingInterfaceString(componentDef) + : `createSvelte2TsxComponent(${propDef})` + } {` + createClassGetters(getters) + '\n}'; @@ -905,6 +926,7 @@ function createRenderFunction({ getters, events, exportedNames, + componentDef, isTsFile, uses$$props, uses$$restProps, @@ -919,11 +941,16 @@ function createRenderFunction({ propsDecl += ' let $$restProps = __sveltets_restPropsType();'; } + const componentDefString = componentDef ? `\n${componentDef.getText()}\n` : ''; if (scriptTag) { //I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead const scriptTagEnd = htmlx.lastIndexOf('>', scriptTag.content.start) + 1; str.overwrite(scriptTag.start, scriptTag.start + 1, ';'); - str.overwrite(scriptTag.start + 1, scriptTagEnd, `function render() {${propsDecl}\n`); + str.overwrite( + scriptTag.start + 1, + scriptTagEnd, + `${componentDefString}function render() {${propsDecl}\n`, + ); const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1); // wrap template with callback @@ -931,7 +958,10 @@ function createRenderFunction({ contentOnly: true, }); } else { - str.prependRight(scriptDestination, `;function render() {${propsDecl}\n<>`); + str.prependRight( + scriptDestination, + `;${componentDefString}function render() {${propsDecl}\n<>`, + ); } const slotsAsDef = @@ -996,6 +1026,7 @@ export function svelte2tsx( //move the instance script and process the content let exportedNames = new ExportedNames(); let getters = new Set(); + let componentDef: ts.InterfaceDeclaration | undefined; if (scriptTag) { //ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template) if (scriptTag.start != instanceScriptTarget) { @@ -1005,7 +1036,7 @@ export function svelte2tsx( uses$$props = uses$$props || res.uses$$props; uses$$restProps = uses$$restProps || res.uses$$restProps; - ({ exportedNames, getters } = res); + ({ exportedNames, getters, componentDef } = res); } //wrap the script tag and template content in a function returning the slot and exports @@ -1017,6 +1048,7 @@ export function svelte2tsx( events, getters, exportedNames, + componentDef, isTsFile: options?.isTsFile, uses$$props, uses$$restProps, @@ -1035,6 +1067,7 @@ export function svelte2tsx( !!options?.strictMode, options?.isTsFile, getters, + componentDef, className, componentDocumentation, ); diff --git a/packages/svelte2tsx/src/utils/tsAst.ts b/packages/svelte2tsx/src/utils/tsAst.ts index 852250c79..137fb1ac9 100644 --- a/packages/svelte2tsx/src/utils/tsAst.ts +++ b/packages/svelte2tsx/src/utils/tsAst.ts @@ -1,5 +1,32 @@ import ts from 'typescript'; -export function findExortKeyword(node: ts.Node) { +export function findExportKeyword(node: ts.Node) { return node.modifiers?.find((x) => x.kind == ts.SyntaxKind.ExportKeyword); } + +export function getGenericsDefinitionString(node: ts.InterfaceDeclaration | undefined): string { + if (!(node?.typeParameters?.length > 0)) { + return ''; + } + + return `<${node.typeParameters.map((param) => param.getText()).join(',')}>`; +} + +export function getGenericsUsageString(node: ts.InterfaceDeclaration | undefined): string { + if (!(node?.typeParameters?.length > 0)) { + return ''; + } + + return `<${node.typeParameters.map((param) => param.name.text).join(',')}>`; +} + +export function getComponentClassUsingInterfaceString( + componentDef: ts.InterfaceDeclaration, +): string { + return ( + `Svelte2TsxComponent<` + + `ComponentDef${getGenericsUsageString(componentDef)}['props'], ` + + `ComponentDef${getGenericsUsageString(componentDef)}['events'],` + + `ComponentDef${getGenericsUsageString(componentDef)}['slots']>` + ); +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/expected.tsx new file mode 100644 index 000000000..184074364 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/expected.tsx @@ -0,0 +1,19 @@ +<>; +interface ComponentDef { + props: {items: T[]} + events: {select: CustomEvent} + slots: {item: T} + } +function render() { + + interface ComponentDef { + props: {items: T[]} + events: {select: CustomEvent} + slots: {item: T} + } +; +() => (<>); +return { props: {}, slots: {}, getters: {}, events: {} }} +render(); +export default class Input__SvelteComponent_ extends Svelte2TsxComponent['props'], ComponentDef['events'],ComponentDef['slots']> { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/input.svelte new file mode 100644 index 000000000..16a065c8c --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface-generics/input.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/expected.tsx new file mode 100644 index 000000000..f028b145a --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/expected.tsx @@ -0,0 +1,19 @@ +<>; +interface ComponentDef { + props: {items: any[]} + events: {select: CustomEvent} + slots: {item: any} + } +function render() { + + interface ComponentDef { + props: {items: any[]} + events: {select: CustomEvent} + slots: {item: any} + } +; +() => (<>); +return { props: {}, slots: {}, getters: {}, events: {} }} +render(); +export default class Input__SvelteComponent_ extends Svelte2TsxComponent { +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/input.svelte new file mode 100644 index 000000000..7559abaad --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-componentdef-interface/input.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file