Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) be able to type props/events/slots #437

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions 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<string>;
componentDef: ts.InterfaceDeclaration | undefined;
}

export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
Expand Down
53 changes: 43 additions & 10 deletions packages/svelte2tsx/src/svelte2tsx.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -821,6 +833,7 @@ function processInstanceScriptContent(str: MagicString, script: Node): InstanceS
uses$$props,
uses$$restProps,
getters,
componentDef,
};
}

Expand All @@ -844,6 +857,7 @@ function addComponentExport(
strictMode: boolean,
isTsFile: boolean,
getters: Set<string>,
componentDef?: ts.InterfaceDeclaration,
/** A named export allows for TSDoc-compatible docstrings */
className?: string,
componentDocumentation?: string | null,
Expand All @@ -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}';

Expand Down Expand Up @@ -905,6 +926,7 @@ function createRenderFunction({
getters,
events,
exportedNames,
componentDef,
isTsFile,
uses$$props,
uses$$restProps,
Expand All @@ -919,19 +941,27 @@ 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
str.overwrite(scriptEndTagStart, scriptTag.end, ';\n() => (<>', {
contentOnly: true,
});
} else {
str.prependRight(scriptDestination, `</>;function render() {${propsDecl}\n<>`);
str.prependRight(
scriptDestination,
`</>;${componentDefString}function render() {${propsDecl}\n<>`,
);
}

const slotsAsDef =
Expand Down Expand Up @@ -996,6 +1026,7 @@ export function svelte2tsx(
//move the instance script and process the content
let exportedNames = new ExportedNames();
let getters = new Set<string>();
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) {
Expand All @@ -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
Expand All @@ -1017,6 +1048,7 @@ export function svelte2tsx(
events,
getters,
exportedNames,
componentDef,
isTsFile: options?.isTsFile,
uses$$props,
uses$$restProps,
Expand All @@ -1035,6 +1067,7 @@ export function svelte2tsx(
!!options?.strictMode,
options?.isTsFile,
getters,
componentDef,
className,
componentDocumentation,
);
Expand Down
29 changes: 28 additions & 1 deletion 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']>`
);
}
@@ -0,0 +1,19 @@
<></>;
interface ComponentDef<T> {
props: {items: T[]}
events: {select: CustomEvent<T>}
slots: {item: T}
}
function render() {

interface ComponentDef<T> {
props: {items: T[]}
events: {select: CustomEvent<T>}
slots: {item: T}
}
;
() => (<></>);
return { props: {}, slots: {}, getters: {}, events: {} }}
render();
export default class Input__SvelteComponent_<T> extends Svelte2TsxComponent<ComponentDef<T>['props'], ComponentDef<T>['events'],ComponentDef<T>['slots']> {
}
@@ -0,0 +1,7 @@
<script lang="ts">
interface ComponentDef<T> {
props: {items: T[]}
events: {select: CustomEvent<T>}
slots: {item: T}
}
</script>
@@ -0,0 +1,19 @@
<></>;
interface ComponentDef {
props: {items: any[]}
events: {select: CustomEvent<number>}
slots: {item: any}
}
function render() {

interface ComponentDef {
props: {items: any[]}
events: {select: CustomEvent<number>}
slots: {item: any}
}
;
() => (<></>);
return { props: {}, slots: {}, getters: {}, events: {} }}
render();
export default class Input__SvelteComponent_ extends Svelte2TsxComponent<ComponentDef['props'], ComponentDef['events'],ComponentDef['slots']> {
}
@@ -0,0 +1,7 @@
<script lang="ts">
interface ComponentDef {
props: {items: any[]}
events: {select: CustomEvent<number>}
slots: {item: any}
}
</script>