diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts
index 66a06315b..6c756a5bd 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/index.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts
@@ -1,7 +1,7 @@
import { Node } from 'estree-walker';
import MagicString from 'magic-string';
import svelte from 'svelte/compiler';
-import { parseHtmlx } from '../htmlxparser';
+import { parseHtmlx } from '../utils/htmlxparser';
import { handleActionDirective } from './nodes/action-directive';
import { handleAnimateDirective } from './nodes/animation-directive';
import { handleAttribute } from './nodes/attribute';
diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts
index 687ca8de7..02f176deb 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts
@@ -2,7 +2,7 @@ import MagicString from 'magic-string';
import { Node } from 'estree-walker';
import { getSlotName } from '../../utils/svelteAst';
import { beforeStart } from '../utils/node-utils';
-import { getSingleSlotDef } from '../../nodes/slot';
+import { getSingleSlotDef } from '../../svelte2tsx/nodes/slot';
/**
* Handle `` and slot-specific transformations.
diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts
index 27b517aff..cc04752fc 100644
--- a/packages/svelte2tsx/src/interfaces.ts
+++ b/packages/svelte2tsx/src/interfaces.ts
@@ -1,26 +1,5 @@
-import MagicString from 'magic-string';
import { Node } from 'estree-walker';
import { ArrayPattern, ObjectPattern, Identifier } from 'estree';
-import { ExportedNames } from './nodes/ExportedNames';
-import { ComponentEvents } from './nodes/ComponentEvents';
-
-export interface InstanceScriptProcessResult {
- exportedNames: ExportedNames;
- events: ComponentEvents;
- uses$$props: boolean;
- uses$$restProps: boolean;
- uses$$slots: boolean;
- getters: Set;
-}
-
-export interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
- str: MagicString;
- scriptTag: Node;
- scriptDestination: number;
- slots: Map>;
- events: ComponentEvents;
- isTsFile: boolean;
-}
export interface NodeRange {
start: number;
@@ -54,19 +33,3 @@ export interface BaseDirective extends Node {
name: string;
modifiers: string[];
}
-
-export interface AddComponentExportPara {
- str: MagicString;
- uses$$propsOr$$restProps: boolean;
- strictMode: boolean;
- /**
- * If true, not fallback to `CustomEvent`
- * -> all unknown events will throw a type error
- * */
- strictEvents: boolean;
- isTsFile: boolean;
- getters: Set;
- /** A named export allows for TSDoc-compatible docstrings */
- className?: string;
- componentDocumentation?: string | null;
-}
diff --git a/packages/svelte2tsx/src/svelte2tsx.ts b/packages/svelte2tsx/src/svelte2tsx.ts
deleted file mode 100644
index cd591534a..000000000
--- a/packages/svelte2tsx/src/svelte2tsx.ts
+++ /dev/null
@@ -1,1119 +0,0 @@
-import dedent from 'dedent-js';
-import { pascalCase } from 'pascal-case';
-import MagicString from 'magic-string';
-import path from 'path';
-import { parseHtmlx } from './htmlxparser';
-import { convertHtmlxToJsx } from './htmlxtojsx';
-import { Node } from 'estree-walker';
-import * as ts from 'typescript';
-import { findExportKeyword, getBinaryAssignmentExpr } from './utils/tsAst';
-import { EventHandler } from './nodes/event-handler';
-import {
- InstanceScriptProcessResult,
- CreateRenderFunctionPara,
- AddComponentExportPara,
-} from './interfaces';
-import { createRenderFunctionGetterStr, createClassGetters } from './nodes/exportgetters';
-import { ExportedNames } from './nodes/ExportedNames';
-import * as astUtil from './utils/svelteAst';
-import { SlotHandler } from './nodes/slot';
-import TemplateScope from './nodes/TemplateScope';
-import { ImplicitTopLevelNames } from './nodes/ImplicitTopLevelNames';
-import {
- ComponentEvents,
- ComponentEventsFromInterface,
- ComponentEventsFromEventsMap,
-} from './nodes/ComponentEvents';
-import {
- handleScopeAndResolveLetVarForSlot,
- handleScopeAndResolveForSlot
-} from './nodes/handleScopeAndResolveForSlot';
-
-type TemplateProcessResult = {
- uses$$props: boolean;
- uses$$restProps: boolean;
- uses$$slots: boolean;
- slots: Map>;
- scriptTag: Node;
- moduleScriptTag: Node;
- /** To be added later as a comment on the default class export */
- componentDocumentation: string | null;
- events: ComponentEvents;
-};
-
-class Scope {
- declared: Set = new Set();
- parent: Scope;
-
- constructor(parent?: Scope) {
- this.parent = parent;
- }
-}
-
-type pendingStoreResolution = {
- node: T;
- parent: T;
- scope: Scope;
-};
-
-/**
- * Add this tag to a HTML comment in a Svelte component and its contents will
- * be added as a docstring in the resulting JSX for the component class.
- */
-const COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG = '@component';
-
-/**
- * A component class name suffix is necessary to prevent class name clashes
- * like reported in https://github.com/sveltejs/language-tools/issues/294
- */
-const COMPONENT_SUFFIX = '__SvelteComponent_';
-
-function processSvelteTemplate(str: MagicString): TemplateProcessResult {
- const htmlxAst = parseHtmlx(str.original);
-
- let uses$$props = false;
- let uses$$restProps = false;
- let uses$$slots = false;
-
- let componentDocumentation = null;
-
- //track if we are in a declaration scope
- let isDeclaration = false;
-
- //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes
- //which prevents us just changing all instances of Identity that start with $
-
- const pendingStoreResolutions: pendingStoreResolution[] = [];
- let scope = new Scope();
- const pushScope = () => (scope = new Scope(scope));
- const popScope = () => (scope = scope.parent);
-
- const handleStore = (node: Node, parent: Node) => {
- //handle assign to
- if (
- parent.type == 'AssignmentExpression' &&
- parent.left == node &&
- parent.operator == '='
- ) {
- const dollar = str.original.indexOf('$', node.start);
- str.remove(dollar, dollar + 1);
- str.overwrite(node.end, str.original.indexOf('=', node.end) + 1, '.set(');
- str.appendLeft(parent.end, ')');
- return;
- }
- // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.)
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment
- const operators = [
- '+=',
- '-=',
- '*=',
- '/=',
- '%=',
- '**=',
- '<<=',
- '>>=',
- '>>>=',
- '&=',
- '^=',
- '|=',
- ];
- if (
- parent.type == 'AssignmentExpression' &&
- parent.left == node &&
- operators.includes(parent.operator)
- ) {
- const storename = node.name.slice(1); // drop the $
- const operator = parent.operator.substring(0, parent.operator.length - 1); // drop the = sign
- str.overwrite(
- parent.start,
- str.original.indexOf('=', node.end) + 1,
- `${storename}.set( __sveltets_store_get(${storename}) ${operator}`,
- );
- str.appendLeft(parent.end, ')');
- return;
- }
- // handle $store++, $store--, ++$store, --$store
- if (parent.type == 'UpdateExpression') {
- let simpleOperator;
- if (parent.operator === '++') simpleOperator = '+';
- if (parent.operator === '--') simpleOperator = '-';
- if (simpleOperator) {
- const storename = node.name.slice(1); // drop the $
- str.overwrite(
- parent.start,
- parent.end,
- `${storename}.set( __sveltets_store_get(${storename}) ${simpleOperator} 1)`,
- );
- } else {
- console.warn(
- `Warning - unrecognized UpdateExpression operator ${parent.operator}!
- This is an edge case unaccounted for in svelte2tsx, please file an issue:
- https://github.com/sveltejs/language-tools/issues/new/choose
- `,
- str.original.slice(parent.start, parent.end),
- );
- }
- return;
- }
-
- //rewrite get
- const dollar = str.original.indexOf('$', node.start);
- str.overwrite(dollar, dollar + 1, '__sveltets_store_get(');
- str.prependLeft(node.end, ')');
- };
-
- const resolveStore = (pending: pendingStoreResolution) => {
- let { node, parent, scope } = pending;
- const name = node.name;
- while (scope) {
- if (scope.declared.has(name)) {
- //we were manually declared, this isn't a store access.
- return;
- }
- scope = scope.parent;
- }
- //We haven't been resolved, we must be a store read/write, handle it.
- handleStore(node, parent);
- };
-
- const enterBlockStatement = () => pushScope();
- const leaveBlockStatement = () => popScope();
-
- const enterFunctionDeclaration = () => pushScope();
- const leaveFunctionDeclaration = () => popScope();
-
- const enterArrowFunctionExpression = () => pushScope();
- const leaveArrowFunctionExpression = () => popScope();
-
- const handleComment = (node: Node) => {
- if (
- 'data' in node &&
- typeof node.data === 'string' &&
- node.data.includes(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG)
- ) {
- componentDocumentation = node.data
- .replace(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG, '')
- .trim();
- }
- };
-
- const handleIdentifier = (node: Node, parent: Node, prop: string) => {
- if (node.name === '$$props') {
- uses$$props = true;
- return;
- }
- if (node.name === '$$restProps') {
- uses$$restProps = true;
- return;
- }
-
- if (node.name === '$$slots') {
- uses$$slots = true;
- return;
- }
-
- //handle potential store
- if (node.name[0] == '$') {
- if (isDeclaration) {
- if (astUtil.isObjectKey(parent, prop)) return;
- scope.declared.add(node.name);
- } else {
- if (astUtil.isMember(parent, prop) && !parent.computed)
- return;
- if (astUtil.isObjectKey(parent, prop)) return;
- pendingStoreResolutions.push({ node, parent, scope });
- }
- return;
- }
- };
-
- // All script tags, no matter at what level, are listed within the root children.
- // To get the top level scripts, filter out all those that are part of children's children.
- // Those have another type ('Element' with name 'script').
- const scriptTags = (htmlxAst.children).filter((child) => child.type === 'Script');
- let topLevelScripts = scriptTags;
- const handleScriptTag = (node: Node, parent: Node) => {
- if (parent !== htmlxAst && node.name === 'script') {
- topLevelScripts = topLevelScripts.filter(
- (tag) => tag.start !== node.start || tag.end !== node.end,
- );
- }
- };
- const getTopLevelScriptTags = () => {
- let scriptTag: Node = null;
- let moduleScriptTag: Node = null;
- // should be 2 at most, one each, so using forEach is safe
- topLevelScripts.forEach((tag) => {
- if (
- tag.attributes &&
- tag.attributes.find(
- (a) => a.name == 'context' && a.value.length == 1 && a.value[0].raw == 'module',
- )
- ) {
- moduleScriptTag = tag;
- } else {
- scriptTag = tag;
- }
- });
- return { scriptTag, moduleScriptTag };
- };
- const blankOtherScriptTags = () => {
- scriptTags
- .filter((tag) => !topLevelScripts.includes(tag))
- .forEach((tag) => {
- str.remove(tag.start, tag.end);
- });
- };
-
-
- const handleStyleTag = (node: Node) => {
- str.remove(node.start, node.end);
- };
-
- const slotHandler = new SlotHandler(str.original);
- let templateScope = new TemplateScope();
-
- const handleEach = (node: Node) => {
- templateScope = templateScope.child();
-
- if (node.context) {
- handleScopeAndResolveForSlotInner(node.context, node.expression, node);
- }
- };
-
- const handleAwait = (node: Node) => {
- templateScope = templateScope.child();
- if (node.value) {
- handleScopeAndResolveForSlotInner(node.value, node.expression, node.then);
- }
- if (node.error) {
- handleScopeAndResolveForSlotInner(node.error, node.expression, node.catch);
- }
- };
-
- const handleComponentLet = (component: Node) => {
- templateScope = templateScope.child();
- const lets = slotHandler.getSlotConsumerOfComponent(component);
-
- for (const { letNode, slotName } of lets) {
- handleScopeAndResolveLetVarForSlot({
- letNode,
- slotName,
- slotHandler,
- templateScope,
- component
- });
- }
- };
-
- const handleScopeAndResolveForSlotInner = (
- identifierDef: Node,
- initExpression: Node,
- owner: Node
- ) => {
- handleScopeAndResolveForSlot({
- identifierDef,
- initExpression,
- slotHandler,
- templateScope,
- owner,
- });
- };
-
- const eventHandler = new EventHandler();
-
- const onHtmlxWalk = (node: Node, parent: Node, prop: string) => {
- if (
- prop == 'params' &&
- (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
- ) {
- isDeclaration = true;
- }
- if (prop == 'id' && parent.type == 'VariableDeclarator') {
- isDeclaration = true;
- }
-
- switch (node.type) {
- case 'Comment':
- handleComment(node);
- break;
- case 'Identifier':
- handleIdentifier(node, parent, prop);
- break;
- case 'Slot':
- slotHandler.handleSlot(node, templateScope);
- break;
- case 'Style':
- handleStyleTag(node);
- break;
- case 'Element':
- handleScriptTag(node, parent);
- break;
- case 'BlockStatement':
- enterBlockStatement();
- break;
- case 'FunctionDeclaration':
- enterFunctionDeclaration();
- break;
- case 'ArrowFunctionExpression':
- enterArrowFunctionExpression();
- break;
- case 'EventHandler':
- eventHandler.handleEventHandler(node, parent);
- break;
- case 'VariableDeclarator':
- isDeclaration = true;
- break;
- case 'EachBlock':
- handleEach(node);
- break;
- case 'AwaitBlock':
- handleAwait(node);
- break;
- case 'InlineComponent':
- handleComponentLet(node);
- break;
- }
- };
-
- const onHtmlxLeave = (node: Node, parent: Node, prop: string, _index: number) => {
- if (
- prop == 'params' &&
- (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
- ) {
- isDeclaration = false;
- }
-
- if (prop == 'id' && parent.type == 'VariableDeclarator') {
- isDeclaration = false;
- }
- const onTemplateScopeLeave = () => {
- templateScope = templateScope.parent;
- };
-
- switch (node.type) {
- case 'BlockStatement':
- leaveBlockStatement();
- break;
- case 'FunctionDeclaration':
- leaveFunctionDeclaration();
- break;
- case 'ArrowFunctionExpression':
- leaveArrowFunctionExpression();
- break;
- case 'EachBlock':
- onTemplateScopeLeave();
- break;
- case 'AwaitBlock':
- onTemplateScopeLeave();
- break;
- case 'InlineComponent':
- onTemplateScopeLeave();
- break;
- }
- };
-
- convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave);
-
- // resolve scripts
- const { scriptTag, moduleScriptTag } = getTopLevelScriptTags();
- blankOtherScriptTags();
-
- //resolve stores
- pendingStoreResolutions.map(resolveStore);
-
- return {
- moduleScriptTag,
- scriptTag,
- slots: slotHandler.getSlotDef(),
- events: new ComponentEventsFromEventsMap(eventHandler),
- uses$$props,
- uses$$restProps,
- uses$$slots,
- componentDocumentation,
- };
-}
-
-function processInstanceScriptContent(
- str: MagicString,
- script: Node,
- events: ComponentEvents,
-): InstanceScriptProcessResult {
- const htmlx = str.original;
- const scriptContent = htmlx.substring(script.content.start, script.content.end);
- const tsAst = ts.createSourceFile(
- 'component.ts.svelte',
- scriptContent,
- ts.ScriptTarget.Latest,
- true,
- ts.ScriptKind.TS,
- );
- const astOffset = script.content.start;
- const exportedNames = new ExportedNames();
- const getters = new Set();
-
- const implicitTopLevelNames = new ImplicitTopLevelNames();
- let uses$$props = false;
- let uses$$restProps = false;
- let uses$$slots = false;
-
- //track if we are in a declaration scope
- let isDeclaration = false;
-
- //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes
- //which prevents us just changing all instances of Identity that start with $
- const pendingStoreResolutions: pendingStoreResolution[] = [];
-
- let scope = new Scope();
- const rootScope = scope;
-
- const pushScope = () => (scope = new Scope(scope));
- const popScope = () => (scope = scope.parent);
-
- const addExport = (
- name: ts.BindingName,
- target: ts.BindingName = null,
- type: ts.TypeNode = null,
- required = false,
- ) => {
- if (name.kind != ts.SyntaxKind.Identifier) {
- throw Error('export source kind not supported ' + name);
- }
- if (target && target.kind != ts.SyntaxKind.Identifier) {
- throw Error('export target kind not supported ' + target);
- }
- if (target) {
- exportedNames.set(name.text, {
- type: type?.getText(),
- identifierText: (target as ts.Identifier).text,
- required,
- });
- } else {
- exportedNames.set(name.text, {});
- }
- };
- const addGetter = (node: ts.Identifier) => {
- if (!node) {
- return;
- }
- getters.add(node.text);
- };
-
- const removeExport = (start: number, end: number) => {
- const exportStart = str.original.indexOf('export', start + astOffset);
- const exportEnd = exportStart + (end - start);
- str.remove(exportStart, exportEnd);
- };
-
- const propTypeAssertToUserDefined = (node: ts.VariableDeclarationList) => {
- const hasInitializers = node.declarations.filter((declaration) => declaration.initializer);
- const handleTypeAssertion = (declaration: ts.VariableDeclaration) => {
- const identifier = declaration.name;
- const tsType = declaration.type;
- const jsDocType = ts.getJSDocType(declaration);
- const type = tsType || jsDocType;
-
- if (!ts.isIdentifier(identifier) || !type) {
- return;
- }
- const name = identifier.getText();
- const end = declaration.end + astOffset;
-
- str.appendLeft(end, `;${name} = __sveltets_any(${name});`);
- };
-
- const findComma = (target: ts.Node) =>
- target.getChildren().filter((child) => child.kind === ts.SyntaxKind.CommaToken);
- const splitDeclaration = () => {
- const commas = node
- .getChildren()
- .filter((child) => child.kind === ts.SyntaxKind.SyntaxList)
- .map(findComma)
- .reduce((current, previous) => [...current, ...previous], []);
-
- commas.forEach((comma) => {
- const start = comma.getStart() + astOffset;
- const end = comma.getEnd() + astOffset;
- str.overwrite(start, end, ';let ', { contentOnly: true });
- });
- };
- splitDeclaration();
-
- for (const declaration of hasInitializers) {
- handleTypeAssertion(declaration);
- }
- };
-
- const handleStore = (ident: ts.Node, parent: ts.Node) => {
- // handle assign to
- // eslint-disable-next-line max-len
- if (
- parent &&
- ts.isBinaryExpression(parent) &&
- parent.operatorToken.kind == ts.SyntaxKind.EqualsToken &&
- parent.left == ident
- ) {
- //remove $
- const dollar = str.original.indexOf('$', ident.getStart() + astOffset);
- str.remove(dollar, dollar + 1);
- // replace = with .set(
- str.overwrite(ident.end + astOffset, parent.operatorToken.end + astOffset, '.set(');
- // append )
- str.appendLeft(parent.end + astOffset, ')');
- return;
- }
- // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.)
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment
- const operators = {
- [ts.SyntaxKind.PlusEqualsToken]: '+',
- [ts.SyntaxKind.MinusEqualsToken]: '-',
- [ts.SyntaxKind.AsteriskEqualsToken]: '*',
- [ts.SyntaxKind.SlashEqualsToken]: '/',
- [ts.SyntaxKind.PercentEqualsToken]: '%',
- [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: '**',
- [ts.SyntaxKind.LessThanLessThanEqualsToken]: '<<',
- [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>',
- [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>',
- [ts.SyntaxKind.AmpersandEqualsToken]: '&',
- [ts.SyntaxKind.CaretEqualsToken]: '^',
- [ts.SyntaxKind.BarEqualsToken]: '|',
- };
- if (
- ts.isBinaryExpression(parent) &&
- parent.left == ident &&
- Object.keys(operators).find((x) => x === String(parent.operatorToken.kind))
- ) {
- const storename = ident.getText().slice(1); // drop the $
- const operator = operators[parent.operatorToken.kind];
- str.overwrite(
- parent.getStart() + astOffset,
- str.original.indexOf('=', ident.end + astOffset) + 1,
- `${storename}.set( __sveltets_store_get(${storename}) ${operator}`,
- );
- str.appendLeft(parent.end + astOffset, ')');
- return;
- }
- // handle $store++, $store--, ++$store, --$store
- if (
- (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
- parent.operator !==
- ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */
- ) {
- let simpleOperator: string;
- if (parent.operator === ts.SyntaxKind.PlusPlusToken) {
- simpleOperator = '+';
- }
- if (parent.operator === ts.SyntaxKind.MinusMinusToken) {
- simpleOperator = '-';
- }
-
- if (simpleOperator) {
- const storename = ident.getText().slice(1); // drop the $
- str.overwrite(
- parent.getStart() + astOffset,
- parent.end + astOffset,
- `${storename}.set( __sveltets_store_get(${storename}) ${simpleOperator} 1)`,
- );
- return;
- } else {
- console.warn(
- `Warning - unrecognized UnaryExpression operator ${parent.operator}!
- This is an edge case unaccounted for in svelte2tsx, please file an issue:
- https://github.com/sveltejs/language-tools/issues/new/choose
- `,
- parent.getText(),
- );
- }
- }
-
- // we must be on the right or not part of assignment
- const dollar = str.original.indexOf('$', ident.getStart() + astOffset);
- str.overwrite(dollar, dollar + 1, '__sveltets_store_get(');
- str.appendLeft(ident.end + astOffset, ')');
- };
-
- const resolveStore = (pending: pendingStoreResolution) => {
- let { node, parent, scope } = pending;
- const name = (node as ts.Identifier).text;
- while (scope) {
- if (scope.declared.has(name)) {
- //we were manually declared, this isn't a store access.
- return;
- }
- scope = scope.parent;
- }
- //We haven't been resolved, we must be a store read/write, handle it.
- handleStore(node, parent);
- };
-
- const handleIdentifier = (ident: ts.Identifier, parent: ts.Node) => {
- if (ident.text === '$$props') {
- uses$$props = true;
- return;
- }
- if (ident.text === '$$restProps') {
- uses$$restProps = true;
- return;
- }
- if (ident.text === '$$slots') {
- uses$$slots = true;
- return;
- }
-
- if (ts.isLabeledStatement(parent) && parent.label == ident) {
- return;
- }
-
- if (isDeclaration || ts.isParameter(parent)) {
- if (!ts.isBindingElement(ident.parent) || ident.parent.name == ident) {
- // we are a key, not a name, so don't care
- if (ident.text.startsWith('$') || scope == rootScope) {
- // track all top level declared identifiers and all $ prefixed identifiers
- scope.declared.add(ident.text);
- }
- }
- } else {
- //track potential store usage to be resolved
- if (ident.text.startsWith('$')) {
- if (
- (!ts.isPropertyAccessExpression(parent) || parent.expression == ident) &&
- (!ts.isPropertyAssignment(parent) || parent.initializer == ident)
- ) {
- pendingStoreResolutions.push({ node: ident, parent, scope });
- }
- }
- }
- };
-
- const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => {
- ts.forEachChild(list, (node) => {
- if (ts.isVariableDeclaration(node)) {
- if (ts.isIdentifier(node.name)) {
- addExport(node.name, node.name, node.type, !node.initializer);
- } else if (
- ts.isObjectBindingPattern(node.name) ||
- ts.isArrayBindingPattern(node.name)
- ) {
- ts.forEachChild(node.name, (element) => {
- if (ts.isBindingElement(element)) {
- addExport(element.name);
- }
- });
- }
- }
- });
- };
-
- const wrapExpressionWithInvalidate = (expression: ts.Expression | undefined) => {
- if (!expression) {
- return;
- }
-
- const start = expression.getStart() + astOffset;
- const end = expression.getEnd() + astOffset;
-
- // () => ({})
- if (ts.isObjectLiteralExpression(expression)) {
- str.appendLeft(start, '(');
- str.appendRight(end, ')');
- }
-
- str.prependLeft(start, '__sveltets_invalidate(() => ');
- str.appendRight(end, ')');
- // Not adding ';' at the end because right now this function is only invoked
- // in situations where there is a line break of ; guaranteed to be present (else the code is invalid)
- };
-
- const walk = (node: ts.Node, parent: ts.Node) => {
- type onLeaveCallback = () => void;
- const onLeaveCallbacks: onLeaveCallback[] = [];
-
- if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentEvents') {
- events = new ComponentEventsFromInterface(node);
- }
-
- if (ts.isVariableStatement(node)) {
- const exportModifier = findExportKeyword(node);
- if (exportModifier) {
- const isLet = node.declarationList.flags === ts.NodeFlags.Let;
- const isConst = node.declarationList.flags === ts.NodeFlags.Const;
-
- if (isLet) {
- handleExportedVariableDeclarationList(node.declarationList);
- propTypeAssertToUserDefined(node.declarationList);
- } else if (isConst) {
- node.declarationList.forEachChild((n) => {
- if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
- addGetter(n.name);
- }
- });
- }
- removeExport(exportModifier.getStart(), exportModifier.end);
- }
- }
-
- if (ts.isFunctionDeclaration(node)) {
- if (node.modifiers) {
- const exportModifier = findExportKeyword(node);
- if (exportModifier) {
- removeExport(exportModifier.getStart(), exportModifier.end);
- addGetter(node.name);
- }
- }
-
- pushScope();
- onLeaveCallbacks.push(() => popScope());
- }
-
- if (ts.isClassDeclaration(node)) {
- const exportModifier = findExportKeyword(node);
- if (exportModifier) {
- removeExport(exportModifier.getStart(), exportModifier.end);
- addGetter(node.name);
- }
- }
-
- if (ts.isBlock(node)) {
- pushScope();
- onLeaveCallbacks.push(() => popScope());
- }
-
- if (ts.isArrowFunction(node)) {
- pushScope();
- onLeaveCallbacks.push(() => popScope());
- }
-
- if (ts.isExportDeclaration(node)) {
- const { exportClause } = node;
- if (ts.isNamedExports(exportClause)) {
- for (const ne of exportClause.elements) {
- if (ne.propertyName) {
- addExport(ne.propertyName, ne.name);
- } else {
- addExport(ne.name);
- }
- }
- //we can remove entire statement
- removeExport(node.getStart(), node.end);
- }
- }
-
- //move imports to top of script so they appear outside our render function
- if (ts.isImportDeclaration(node)) {
- 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');
- }
-
- if (ts.isVariableDeclaration(parent) && parent.name == node) {
- isDeclaration = true;
- onLeaveCallbacks.push(() => (isDeclaration = false));
- }
-
- if (ts.isBindingElement(parent) && parent.name == node) {
- isDeclaration = true;
- onLeaveCallbacks.push(() => (isDeclaration = false));
- }
-
- if (ts.isImportClause(node)) {
- isDeclaration = true;
- onLeaveCallbacks.push(() => (isDeclaration = false));
- }
-
- //handle stores etc
- if (ts.isIdentifier(node)) {
- handleIdentifier(node, parent);
- }
-
- //track implicit declarations in reactive blocks at the top level
- if (
- ts.isLabeledStatement(node) &&
- parent == tsAst && //top level
- node.label.text == '$' &&
- node.statement
- ) {
- const binaryExpression = getBinaryAssignmentExpr(node);
- if (binaryExpression) {
- implicitTopLevelNames.add(node);
- wrapExpressionWithInvalidate(binaryExpression.right);
- } else {
- const start = node.getStart() + astOffset;
- const end = node.getEnd() + astOffset;
-
- str.prependLeft(start, ';() => {');
- str.prependRight(end, '}');
- }
- }
-
- //to save a bunch of condition checks on each node, we recurse into processChild which skips all the checks for top level items
- ts.forEachChild(node, (n) => walk(n, node));
- //fire off the on leave callbacks
- onLeaveCallbacks.map((c) => c());
- };
-
- //walk the ast and convert to tsx as we go
- tsAst.forEachChild((n) => walk(n, tsAst));
-
- //resolve stores
- pendingStoreResolutions.map(resolveStore);
-
- // declare implicit reactive variables we found in the script
- implicitTopLevelNames.modifyCode(rootScope.declared, astOffset, str);
-
- const firstImport = tsAst.statements
- .filter(ts.isImportDeclaration)
- .sort((a, b) => a.end - b.end)[0];
- if (firstImport) {
- str.appendRight(firstImport.getStart() + astOffset, '\n');
- }
-
- return {
- exportedNames,
- events,
- uses$$props,
- uses$$restProps,
- uses$$slots,
- getters,
- };
-}
-
-function formatComponentDocumentation(contents?: string | null) {
- if (!contents) return '';
- if (!contents.includes('\n')) {
- return `/** ${contents} */\n`;
- }
-
- const lines = dedent(contents)
- .split('\n')
- .map((line) => ` *${line ? ` ${line}` : ''}`)
- .join('\n');
-
- return `/**\n${lines}\n */\n`;
-}
-
-function addComponentExport({
- str,
- uses$$propsOr$$restProps,
- strictMode,
- strictEvents,
- isTsFile,
- getters,
- className,
- componentDocumentation,
-}: AddComponentExportPara) {
- const eventsDef = strictEvents ? 'render' : '__sveltets_with_any_event(render)';
- const propDef =
- // Omit partial-wrapper only if both strict mode and ts file, because
- // in a js file the user has no way of telling the language that
- // the prop is optional
- strictMode && isTsFile
- ? uses$$propsOr$$restProps
- ? `__sveltets_with_any(${eventsDef})`
- : eventsDef
- : `__sveltets_partial${uses$$propsOr$$restProps ? '_with_any' : ''}(${eventsDef})`;
-
- const doc = formatComponentDocumentation(componentDocumentation);
-
- const statement =
- `\n\n${doc}export default class${
- className ? ` ${className}` : ''
- } extends createSvelte2TsxComponent(${propDef}) {` +
- createClassGetters(getters) +
- '\n}';
-
- str.append(statement);
-}
-
-/**
- * Returns a Svelte-compatible component name from a filename. Svelte
- * components must use capitalized tags, so we try to transform the filename.
- *
- * https://svelte.dev/docs#Tags
- */
-export function classNameFromFilename(filename: string): string | undefined {
- try {
- const withoutExtensions = path.parse(filename).name?.split('.')[0];
- const inPascalCase = pascalCase(withoutExtensions);
- return `${inPascalCase}${COMPONENT_SUFFIX}`;
- } catch (error) {
- console.warn(`Failed to create a name for the component class from filename ${filename}`);
- return undefined;
- }
-}
-
-function processModuleScriptTag(str: MagicString, script: Node) {
- const htmlx = str.original;
-
- const scriptStartTagEnd = htmlx.indexOf('>', script.start) + 1;
- const scriptEndTagStart = htmlx.lastIndexOf('<', script.end - 1);
-
- str.overwrite(script.start, scriptStartTagEnd, '>;');
- str.overwrite(scriptEndTagStart, script.end, ';<>');
-}
-
-function createRenderFunction({
- str,
- scriptTag,
- scriptDestination,
- slots,
- getters,
- events,
- exportedNames,
- isTsFile,
- uses$$props,
- uses$$restProps,
- uses$$slots,
-}: CreateRenderFunctionPara) {
- const htmlx = str.original;
- let propsDecl = '';
-
- if (uses$$props) {
- propsDecl += ' let $$props = __sveltets_allPropsType();';
- }
- if (uses$$restProps) {
- propsDecl += ' let $$restProps = __sveltets_restPropsType();';
- }
-
- if (uses$$slots) {
- propsDecl +=
- ' let $$slots = __sveltets_slotsType({' +
- Array.from(slots.keys())
- .map((name) => `${name}: ''`)
- .join(', ') +
- '});';
- }
-
- 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`);
-
- 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<>`);
- }
-
- const slotsAsDef =
- '{' +
- Array.from(slots.entries())
- .map(([name, attrs]) => {
- const attrsAsString = Array.from(attrs.entries())
- .map(([exportName, expr]) => `${exportName}:${expr}`)
- .join(', ');
- return `'${name}': {${attrsAsString}}`;
- })
- .join(', ') +
- '}';
-
- const returnString =
- `\nreturn { props: ${exportedNames.createPropsStr(
- isTsFile,
- )}, slots: ${slotsAsDef}, getters: ${createRenderFunctionGetterStr(getters)}` +
- `, events: ${events.toDefString()} }}`;
-
- // wrap template with callback
- if (scriptTag) {
- str.append(');');
- }
-
- str.append(returnString);
-}
-
-export function svelte2tsx(
- svelte: string,
- options?: { filename?: string; strictMode?: boolean; isTsFile?: boolean },
-) {
- const str = new MagicString(svelte);
- // process the htmlx as a svelte template
- let {
- moduleScriptTag,
- scriptTag,
- slots,
- uses$$props,
- uses$$slots,
- uses$$restProps,
- events,
- componentDocumentation,
- } = processSvelteTemplate(str);
-
- /* Rearrange the script tags so that module is first, and instance second followed finally by the template
- * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough
- * since if the module script was already at 0, it wouldn't move (which is fine) but would mean the order would be swapped when the script tag tried to move to 0
- * In this case we instead have to move it to moduleScriptTag.end. We track the location for the script move in the MoveInstanceScriptTarget var
- */
- let instanceScriptTarget = 0;
-
- if (moduleScriptTag) {
- if (moduleScriptTag.start != 0) {
- //move our module tag to the top
- str.move(moduleScriptTag.start, moduleScriptTag.end, 0);
- } else {
- //since our module script was already at position 0, we need to move our instance script tag to the end of it.
- instanceScriptTarget = moduleScriptTag.end;
- }
- }
-
- //move the instance script and process the content
- let exportedNames = new ExportedNames();
- let getters = new Set();
- 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) {
- str.move(scriptTag.start, scriptTag.end, instanceScriptTarget);
- }
- const res = processInstanceScriptContent(str, scriptTag, events);
- uses$$props = uses$$props || res.uses$$props;
- uses$$restProps = uses$$restProps || res.uses$$restProps;
- uses$$slots = uses$$slots || res.uses$$slots;
-
- ({ exportedNames, events, getters } = res);
- }
-
- //wrap the script tag and template content in a function returning the slot and exports
- createRenderFunction({
- str,
- scriptTag,
- scriptDestination: instanceScriptTarget,
- slots,
- events,
- getters,
- exportedNames,
- isTsFile: options?.isTsFile,
- uses$$props,
- uses$$restProps,
- uses$$slots,
- });
-
- // we need to process the module script after the instance script has moved otherwise we get warnings about moving edited items
- if (moduleScriptTag) {
- processModuleScriptTag(str, moduleScriptTag);
- }
-
- const className = options?.filename && classNameFromFilename(options?.filename);
-
- addComponentExport({
- str,
- uses$$propsOr$$restProps: uses$$props || uses$$restProps,
- strictMode: !!options?.strictMode,
- strictEvents: events instanceof ComponentEventsFromInterface,
- isTsFile: options?.isTsFile,
- getters,
- className,
- componentDocumentation,
- });
-
- str.prepend('///\n');
-
- return {
- code: str.toString(),
- map: str.generateMap({ hires: true, source: options?.filename }),
- exportedNames,
- events,
- };
-}
diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts
new file mode 100644
index 000000000..9cda421c7
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/index.ts
@@ -0,0 +1,490 @@
+import { Node } from 'estree-walker';
+import MagicString from 'magic-string';
+import { pascalCase } from 'pascal-case';
+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 { EventHandler } from './nodes/event-handler';
+import { ExportedNames } from './nodes/ExportedNames';
+import { createClassGetters, createRenderFunctionGetterStr } from './nodes/exportgetters';
+import {
+ handleScopeAndResolveForSlot,
+ handleScopeAndResolveLetVarForSlot,
+} from './nodes/handleScopeAndResolveForSlot';
+import { Scripts } from './nodes/Scripts';
+import { SlotHandler } from './nodes/slot';
+import { Stores } from './nodes/Stores';
+import TemplateScope from './nodes/TemplateScope';
+import {
+ InstanceScriptProcessResult,
+ processInstanceScriptContent,
+} from './processInstanceScriptContent';
+import { processModuleScriptTag } from './processModuleScriptTag';
+import { ScopeStack } from './utils/Scope';
+
+interface CreateRenderFunctionPara extends InstanceScriptProcessResult {
+ str: MagicString;
+ scriptTag: Node;
+ scriptDestination: number;
+ slots: Map>;
+ events: ComponentEvents;
+ isTsFile: boolean;
+}
+
+interface AddComponentExportPara {
+ str: MagicString;
+ uses$$propsOr$$restProps: boolean;
+ strictMode: boolean;
+ /**
+ * If true, not fallback to `CustomEvent`
+ * -> all unknown events will throw a type error
+ * */
+ strictEvents: boolean;
+ isTsFile: boolean;
+ getters: Set;
+ fileName?: string;
+ componentDocumentation: ComponentDocumentation;
+}
+
+type TemplateProcessResult = {
+ uses$$props: boolean;
+ uses$$restProps: boolean;
+ uses$$slots: boolean;
+ slots: Map>;
+ scriptTag: Node;
+ moduleScriptTag: Node;
+ /** To be added later as a comment on the default class export */
+ componentDocumentation: ComponentDocumentation;
+ events: ComponentEvents;
+};
+
+/**
+ * A component class name suffix is necessary to prevent class name clashes
+ * like reported in https://github.com/sveltejs/language-tools/issues/294
+ */
+const COMPONENT_SUFFIX = '__SvelteComponent_';
+
+function processSvelteTemplate(str: MagicString): TemplateProcessResult {
+ const htmlxAst = parseHtmlx(str.original);
+
+ let uses$$props = false;
+ let uses$$restProps = false;
+ let uses$$slots = false;
+
+ const componentDocumentation = new ComponentDocumentation();
+
+ //track if we are in a declaration scope
+ const isDeclaration = { value: false };
+
+ //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes
+ //which prevents us just changing all instances of Identity that start with $
+
+ const scopeStack = new ScopeStack();
+ const stores = new Stores(scopeStack, str, isDeclaration);
+ const scripts = new Scripts(htmlxAst);
+
+ const handleIdentifier = (node: Node) => {
+ if (node.name === '$$props') {
+ uses$$props = true;
+ return;
+ }
+ if (node.name === '$$restProps') {
+ uses$$restProps = true;
+ return;
+ }
+
+ if (node.name === '$$slots') {
+ uses$$slots = true;
+ return;
+ }
+ };
+
+ const handleStyleTag = (node: Node) => {
+ str.remove(node.start, node.end);
+ };
+
+ const slotHandler = new SlotHandler(str.original);
+ let templateScope = new TemplateScope();
+
+ const handleEach = (node: Node) => {
+ templateScope = templateScope.child();
+
+ if (node.context) {
+ handleScopeAndResolveForSlotInner(node.context, node.expression, node);
+ }
+ };
+
+ const handleAwait = (node: Node) => {
+ templateScope = templateScope.child();
+ if (node.value) {
+ handleScopeAndResolveForSlotInner(node.value, node.expression, node.then);
+ }
+ if (node.error) {
+ handleScopeAndResolveForSlotInner(node.error, node.expression, node.catch);
+ }
+ };
+
+ const handleComponentLet = (component: Node) => {
+ templateScope = templateScope.child();
+ const lets = slotHandler.getSlotConsumerOfComponent(component);
+
+ for (const { letNode, slotName } of lets) {
+ handleScopeAndResolveLetVarForSlot({
+ letNode,
+ slotName,
+ slotHandler,
+ templateScope,
+ component,
+ });
+ }
+ };
+
+ const handleScopeAndResolveForSlotInner = (
+ identifierDef: Node,
+ initExpression: Node,
+ owner: Node,
+ ) => {
+ handleScopeAndResolveForSlot({
+ identifierDef,
+ initExpression,
+ slotHandler,
+ templateScope,
+ owner,
+ });
+ };
+
+ const eventHandler = new EventHandler();
+
+ const onHtmlxWalk = (node: Node, parent: Node, prop: string) => {
+ if (
+ prop == 'params' &&
+ (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
+ ) {
+ isDeclaration.value = true;
+ }
+ if (prop == 'id' && parent.type == 'VariableDeclarator') {
+ isDeclaration.value = true;
+ }
+
+ switch (node.type) {
+ case 'Comment':
+ componentDocumentation.handleComment(node);
+ break;
+ case 'Identifier':
+ handleIdentifier(node);
+ stores.handleIdentifier(node, parent, prop);
+ break;
+ case 'Slot':
+ slotHandler.handleSlot(node, templateScope);
+ break;
+ case 'Style':
+ handleStyleTag(node);
+ break;
+ case 'Element':
+ scripts.handleScriptTag(node, parent);
+ break;
+ case 'BlockStatement':
+ scopeStack.push();
+ break;
+ case 'FunctionDeclaration':
+ scopeStack.push();
+ break;
+ case 'ArrowFunctionExpression':
+ scopeStack.push();
+ break;
+ case 'EventHandler':
+ eventHandler.handleEventHandler(node, parent);
+ break;
+ case 'VariableDeclarator':
+ isDeclaration.value = true;
+ break;
+ case 'EachBlock':
+ handleEach(node);
+ break;
+ case 'AwaitBlock':
+ handleAwait(node);
+ break;
+ case 'InlineComponent':
+ handleComponentLet(node);
+ break;
+ }
+ };
+
+ const onHtmlxLeave = (node: Node, parent: Node, prop: string, _index: number) => {
+ if (
+ prop == 'params' &&
+ (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
+ ) {
+ isDeclaration.value = false;
+ }
+
+ if (prop == 'id' && parent.type == 'VariableDeclarator') {
+ isDeclaration.value = false;
+ }
+ const onTemplateScopeLeave = () => {
+ templateScope = templateScope.parent;
+ };
+
+ switch (node.type) {
+ case 'BlockStatement':
+ scopeStack.pop();
+ break;
+ case 'FunctionDeclaration':
+ scopeStack.pop();
+ break;
+ case 'ArrowFunctionExpression':
+ scopeStack.pop();
+ break;
+ case 'EachBlock':
+ onTemplateScopeLeave();
+ break;
+ case 'AwaitBlock':
+ onTemplateScopeLeave();
+ break;
+ case 'InlineComponent':
+ onTemplateScopeLeave();
+ break;
+ }
+ };
+
+ convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave);
+
+ // resolve scripts
+ const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags();
+ scripts.blankOtherScriptTags(str);
+
+ //resolve stores
+ stores.resolveStores();
+
+ return {
+ moduleScriptTag,
+ scriptTag,
+ slots: slotHandler.getSlotDef(),
+ events: new ComponentEventsFromEventsMap(eventHandler),
+ uses$$props,
+ uses$$restProps,
+ uses$$slots,
+ componentDocumentation,
+ };
+}
+
+function addComponentExport({
+ str,
+ uses$$propsOr$$restProps,
+ strictMode,
+ strictEvents,
+ isTsFile,
+ getters,
+ fileName,
+ componentDocumentation,
+}: AddComponentExportPara) {
+ const eventsDef = strictEvents ? 'render' : '__sveltets_with_any_event(render)';
+ const propDef =
+ // Omit partial-wrapper only if both strict mode and ts file, because
+ // in a js file the user has no way of telling the language that
+ // the prop is optional
+ strictMode && isTsFile
+ ? uses$$propsOr$$restProps
+ ? `__sveltets_with_any(${eventsDef})`
+ : eventsDef
+ : `__sveltets_partial${uses$$propsOr$$restProps ? '_with_any' : ''}(${eventsDef})`;
+
+ const doc = componentDocumentation.getFormatted();
+ const className = fileName && classNameFromFilename(fileName);
+
+ const statement =
+ `\n\n${doc}export default class${
+ className ? ` ${className}` : ''
+ } extends createSvelte2TsxComponent(${propDef}) {` +
+ createClassGetters(getters) +
+ '\n}';
+
+ str.append(statement);
+}
+
+/**
+ * Returns a Svelte-compatible component name from a filename. Svelte
+ * components must use capitalized tags, so we try to transform the filename.
+ *
+ * https://svelte.dev/docs#Tags
+ */
+function classNameFromFilename(filename: string): string | undefined {
+ try {
+ const withoutExtensions = path.parse(filename).name?.split('.')[0];
+ const inPascalCase = pascalCase(withoutExtensions);
+ return `${inPascalCase}${COMPONENT_SUFFIX}`;
+ } catch (error) {
+ console.warn(`Failed to create a name for the component class from filename ${filename}`);
+ return undefined;
+ }
+}
+
+function createRenderFunction({
+ str,
+ scriptTag,
+ scriptDestination,
+ slots,
+ getters,
+ events,
+ exportedNames,
+ isTsFile,
+ uses$$props,
+ uses$$restProps,
+ uses$$slots,
+}: CreateRenderFunctionPara) {
+ const htmlx = str.original;
+ let propsDecl = '';
+
+ if (uses$$props) {
+ propsDecl += ' let $$props = __sveltets_allPropsType();';
+ }
+ if (uses$$restProps) {
+ propsDecl += ' let $$restProps = __sveltets_restPropsType();';
+ }
+
+ if (uses$$slots) {
+ propsDecl +=
+ ' let $$slots = __sveltets_slotsType({' +
+ Array.from(slots.keys())
+ .map((name) => `${name}: ''`)
+ .join(', ') +
+ '});';
+ }
+
+ 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`);
+
+ 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<>`);
+ }
+
+ const slotsAsDef =
+ '{' +
+ Array.from(slots.entries())
+ .map(([name, attrs]) => {
+ const attrsAsString = Array.from(attrs.entries())
+ .map(([exportName, expr]) => `${exportName}:${expr}`)
+ .join(', ');
+ return `'${name}': {${attrsAsString}}`;
+ })
+ .join(', ') +
+ '}';
+
+ const returnString =
+ `\nreturn { props: ${exportedNames.createPropsStr(
+ isTsFile,
+ )}, slots: ${slotsAsDef}, getters: ${createRenderFunctionGetterStr(getters)}` +
+ `, events: ${events.toDefString()} }}`;
+
+ // wrap template with callback
+ if (scriptTag) {
+ str.append(');');
+ }
+
+ str.append(returnString);
+}
+
+export function svelte2tsx(
+ svelte: string,
+ options?: { filename?: string; strictMode?: boolean; isTsFile?: boolean },
+) {
+ const str = new MagicString(svelte);
+ // process the htmlx as a svelte template
+ let {
+ moduleScriptTag,
+ scriptTag,
+ slots,
+ uses$$props,
+ uses$$slots,
+ uses$$restProps,
+ events,
+ componentDocumentation,
+ } = processSvelteTemplate(str);
+
+ /* Rearrange the script tags so that module is first, and instance second followed finally by the template
+ * This is a bit convoluted due to some trouble I had with magic string. A simple str.move(start,end,0) for each script wasn't enough
+ * since if the module script was already at 0, it wouldn't move (which is fine) but would mean the order would be swapped when the script tag tried to move to 0
+ * In this case we instead have to move it to moduleScriptTag.end. We track the location for the script move in the MoveInstanceScriptTarget var
+ */
+ let instanceScriptTarget = 0;
+
+ if (moduleScriptTag) {
+ if (moduleScriptTag.start != 0) {
+ //move our module tag to the top
+ str.move(moduleScriptTag.start, moduleScriptTag.end, 0);
+ } else {
+ //since our module script was already at position 0, we need to move our instance script tag to the end of it.
+ instanceScriptTarget = moduleScriptTag.end;
+ }
+ }
+
+ //move the instance script and process the content
+ let exportedNames = new ExportedNames();
+ let getters = new Set();
+ 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) {
+ str.move(scriptTag.start, scriptTag.end, instanceScriptTarget);
+ }
+ const res = processInstanceScriptContent(str, scriptTag, events);
+ uses$$props = uses$$props || res.uses$$props;
+ uses$$restProps = uses$$restProps || res.uses$$restProps;
+ uses$$slots = uses$$slots || res.uses$$slots;
+
+ ({ exportedNames, events, getters } = res);
+ }
+
+ //wrap the script tag and template content in a function returning the slot and exports
+ createRenderFunction({
+ str,
+ scriptTag,
+ scriptDestination: instanceScriptTarget,
+ slots,
+ events,
+ getters,
+ exportedNames,
+ isTsFile: options?.isTsFile,
+ uses$$props,
+ uses$$restProps,
+ uses$$slots,
+ });
+
+ // we need to process the module script after the instance script has moved otherwise we get warnings about moving edited items
+ if (moduleScriptTag) {
+ processModuleScriptTag(str, moduleScriptTag);
+ }
+
+ addComponentExport({
+ str,
+ uses$$propsOr$$restProps: uses$$props || uses$$restProps,
+ strictMode: !!options?.strictMode,
+ strictEvents: events instanceof ComponentEventsFromInterface,
+ isTsFile: options?.isTsFile,
+ getters,
+ fileName: options?.filename,
+ componentDocumentation,
+ });
+
+ str.prepend('///\n');
+
+ return {
+ code: str.toString(),
+ map: str.generateMap({ hires: true, source: options?.filename }),
+ exportedNames,
+ events,
+ };
+}
diff --git a/packages/svelte2tsx/src/knownevents.ts b/packages/svelte2tsx/src/svelte2tsx/knownevents.ts
similarity index 100%
rename from packages/svelte2tsx/src/knownevents.ts
rename to packages/svelte2tsx/src/svelte2tsx/knownevents.ts
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts
new file mode 100644
index 000000000..43eb98074
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentDocumentation.ts
@@ -0,0 +1,40 @@
+import { Node } from 'estree-walker';
+import dedent from 'dedent-js';
+
+/**
+ * Add this tag to a HTML comment in a Svelte component and its contents will
+ * be added as a docstring in the resulting JSX for the component class.
+ */
+const COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG = '@component';
+
+export class ComponentDocumentation {
+ private componentDocumentation = '';
+
+ handleComment = (node: Node) => {
+ if (
+ 'data' in node &&
+ typeof node.data === 'string' &&
+ node.data.includes(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG)
+ ) {
+ this.componentDocumentation = node.data
+ .replace(COMPONENT_DOCUMENTATION_HTML_COMMENT_TAG, '')
+ .trim();
+ }
+ };
+
+ getFormatted() {
+ if (!this.componentDocumentation) {
+ return '';
+ }
+ if (!this.componentDocumentation.includes('\n')) {
+ return `/** ${this.componentDocumentation} */\n`;
+ }
+
+ const lines = dedent(this.componentDocumentation)
+ .split('\n')
+ .map((line) => ` *${line ? ` ${line}` : ''}`)
+ .join('\n');
+
+ return `/**\n${lines}\n */\n`;
+ }
+}
diff --git a/packages/svelte2tsx/src/nodes/ComponentEvents.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts
similarity index 100%
rename from packages/svelte2tsx/src/nodes/ComponentEvents.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/ComponentEvents.ts
diff --git a/packages/svelte2tsx/src/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
similarity index 100%
rename from packages/svelte2tsx/src/nodes/ExportedNames.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts
diff --git a/packages/svelte2tsx/src/nodes/ImplicitTopLevelNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts
similarity index 100%
rename from packages/svelte2tsx/src/nodes/ImplicitTopLevelNames.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/ImplicitTopLevelNames.ts
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts
new file mode 100644
index 000000000..26bb7c752
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Scripts.ts
@@ -0,0 +1,49 @@
+import { Node } from 'estree-walker';
+import MagicString from 'magic-string';
+
+export class Scripts {
+ // All script tags, no matter at what level, are listed within the root children.
+ // To get the top level scripts, filter out all those that are part of children's children.
+ // Those have another type ('Element' with name 'script').
+ private scriptTags = (this.htmlxAst.children as Node[]).filter(
+ (child) => child.type === 'Script',
+ );
+ private topLevelScripts = this.scriptTags;
+
+ constructor(private htmlxAst: Node) {}
+
+ handleScriptTag = (node: Node, parent: Node) => {
+ if (parent !== this.htmlxAst && node.name === 'script') {
+ this.topLevelScripts = this.topLevelScripts.filter(
+ (tag) => tag.start !== node.start || tag.end !== node.end,
+ );
+ }
+ };
+
+ getTopLevelScriptTags(): { scriptTag: Node; moduleScriptTag: Node } {
+ let scriptTag: Node = null;
+ let moduleScriptTag: Node = null;
+ // should be 2 at most, one each, so using forEach is safe
+ this.topLevelScripts.forEach((tag) => {
+ if (
+ tag.attributes &&
+ tag.attributes.find(
+ (a) => a.name == 'context' && a.value.length == 1 && a.value[0].raw == 'module',
+ )
+ ) {
+ moduleScriptTag = tag;
+ } else {
+ scriptTag = tag;
+ }
+ });
+ return { scriptTag, moduleScriptTag };
+ }
+
+ blankOtherScriptTags(str: MagicString): void {
+ this.scriptTags
+ .filter((tag) => !this.topLevelScripts.includes(tag))
+ .forEach((tag) => {
+ str.remove(tag.start, tag.end);
+ });
+ }
+}
diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts
new file mode 100644
index 000000000..196efe879
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Stores.ts
@@ -0,0 +1,117 @@
+import MagicString from 'magic-string';
+import { Node } from 'estree-walker';
+import { ScopeStack, Scope } from '../utils/Scope';
+import { isObjectKey, isMember } from '../../utils/svelteAst';
+
+export function handleStore(node: Node, parent: Node, str: MagicString): void {
+ //handle assign to
+ if (parent.type == 'AssignmentExpression' && parent.left == node && parent.operator == '=') {
+ const dollar = str.original.indexOf('$', node.start);
+ str.remove(dollar, dollar + 1);
+ str.overwrite(node.end, str.original.indexOf('=', node.end) + 1, '.set(');
+ str.appendLeft(parent.end, ')');
+ return;
+ }
+ // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.)
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment
+ const operators = ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '^=', '|='];
+ if (
+ parent.type == 'AssignmentExpression' &&
+ parent.left == node &&
+ operators.includes(parent.operator)
+ ) {
+ const storename = node.name.slice(1); // drop the $
+ const operator = parent.operator.substring(0, parent.operator.length - 1); // drop the = sign
+ str.overwrite(
+ parent.start,
+ str.original.indexOf('=', node.end) + 1,
+ `${storename}.set( __sveltets_store_get(${storename}) ${operator}`,
+ );
+ str.appendLeft(parent.end, ')');
+ return;
+ }
+ // handle $store++, $store--, ++$store, --$store
+ if (parent.type == 'UpdateExpression') {
+ let simpleOperator;
+ if (parent.operator === '++') simpleOperator = '+';
+ if (parent.operator === '--') simpleOperator = '-';
+ if (simpleOperator) {
+ const storename = node.name.slice(1); // drop the $
+ str.overwrite(
+ parent.start,
+ parent.end,
+ `${storename}.set( __sveltets_store_get(${storename}) ${simpleOperator} 1)`,
+ );
+ } else {
+ console.warn(
+ `Warning - unrecognized UpdateExpression operator ${parent.operator}!
+ This is an edge case unaccounted for in svelte2tsx, please file an issue:
+ https://github.com/sveltejs/language-tools/issues/new/choose
+ `,
+ str.original.slice(parent.start, parent.end),
+ );
+ }
+ return;
+ }
+
+ //rewrite get
+ const dollar = str.original.indexOf('$', node.start);
+ str.overwrite(dollar, dollar + 1, '__sveltets_store_get(');
+ str.prependLeft(node.end, ')');
+}
+
+type PendingStoreResolution = {
+ node: T;
+ parent: T;
+ scope: Scope;
+};
+
+const reservedNames = new Set(['$$props', '$$restProps', '$$slots']);
+
+export class Stores {
+ pendingStoreResolutions: PendingStoreResolution[] = [];
+
+ constructor(
+ private scope: ScopeStack,
+ private str: MagicString,
+ private isDeclaration: { value: boolean },
+ ) {}
+
+ handleIdentifier(node: Node, parent: Node, prop: string): void {
+ if (node.name[0] !== '$' || reservedNames.has(node.name)) {
+ return;
+ }
+
+ //handle potential store
+ if (this.isDeclaration.value) {
+ if (isObjectKey(parent, prop)) {
+ return;
+ }
+ this.scope.current.declared.add(node.name);
+ } else {
+ if (isMember(parent, prop) && !parent.computed) {
+ return;
+ }
+ if (isObjectKey(parent, prop)) {
+ return;
+ }
+ this.pendingStoreResolutions.push({ node, parent, scope: this.scope.current });
+ }
+ }
+
+ resolveStores(): void {
+ this.pendingStoreResolutions.forEach((pending) => {
+ let { node, parent, scope } = pending;
+ const name = node.name;
+ while (scope) {
+ if (scope.declared.has(name)) {
+ //we were manually declared, this isn't a store access.
+ return;
+ }
+ scope = scope.parent;
+ }
+ //We haven't been resolved, we must be a store read/write, handle it.
+ handleStore(node, parent, this.str);
+ });
+ }
+}
diff --git a/packages/svelte2tsx/src/nodes/TemplateScope.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts
similarity index 96%
rename from packages/svelte2tsx/src/nodes/TemplateScope.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts
index a6d8c9ac4..2e926e428 100644
--- a/packages/svelte2tsx/src/nodes/TemplateScope.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/TemplateScope.ts
@@ -1,5 +1,5 @@
import { Node } from 'estree-walker';
-import { WithName } from '../interfaces';
+import { WithName } from '../../interfaces';
/**
* adopted from https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/TemplateScope.ts
diff --git a/packages/svelte2tsx/src/nodes/event-handler.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts
similarity index 100%
rename from packages/svelte2tsx/src/nodes/event-handler.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/event-handler.ts
diff --git a/packages/svelte2tsx/src/nodes/exportgetters.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts
similarity index 100%
rename from packages/svelte2tsx/src/nodes/exportgetters.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/exportgetters.ts
diff --git a/packages/svelte2tsx/src/nodes/handleScopeAndResolveForSlot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts
similarity index 94%
rename from packages/svelte2tsx/src/nodes/handleScopeAndResolveForSlot.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts
index a207b6a4e..feb275a50 100644
--- a/packages/svelte2tsx/src/nodes/handleScopeAndResolveForSlot.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleScopeAndResolveForSlot.ts
@@ -1,8 +1,8 @@
import { Node } from 'estree-walker';
-import { BaseDirective, SvelteIdentifier } from '../interfaces';
+import { BaseDirective, SvelteIdentifier } from '../../interfaces';
import TemplateScope from './TemplateScope';
import { SlotHandler } from './slot';
-import { isIdentifier, isDestructuringPatterns } from '../utils/svelteAst';
+import { isIdentifier, isDestructuringPatterns } from '../../utils/svelteAst';
import { extract_identifiers as extractIdentifiers } from 'periscopic';
export function handleScopeAndResolveForSlot({
diff --git a/packages/svelte2tsx/src/nodes/slot.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts
similarity index 97%
rename from packages/svelte2tsx/src/nodes/slot.ts
rename to packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts
index 9fd5535d6..30443dc12 100644
--- a/packages/svelte2tsx/src/nodes/slot.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/nodes/slot.ts
@@ -7,10 +7,10 @@ import {
isObjectValueShortHand,
isObjectValue,
getSlotName,
-} from '../utils/svelteAst';
+} from '../../utils/svelteAst';
import TemplateScope from './TemplateScope';
-import { SvelteIdentifier, WithName, BaseDirective } from '../interfaces';
-import { getTypeForComponent } from '../htmlxtojsx/utils/node-utils';
+import { SvelteIdentifier, WithName, BaseDirective } from '../../interfaces';
+import { getTypeForComponent } from '../../htmlxtojsx/utils/node-utils';
function attributeStrValueAsJsExpression(attr: Node): string {
if (attr.value.length == 0) return "''"; //wut?
diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
new file mode 100644
index 000000000..5e214508e
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts
@@ -0,0 +1,467 @@
+import MagicString from 'magic-string';
+import { Node } from 'estree-walker';
+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 { Scope } from './utils/Scope';
+
+export interface InstanceScriptProcessResult {
+ exportedNames: ExportedNames;
+ events: ComponentEvents;
+ uses$$props: boolean;
+ uses$$restProps: boolean;
+ uses$$slots: boolean;
+ getters: Set;
+}
+
+type PendingStoreResolution = {
+ node: T;
+ parent: T;
+ scope: Scope;
+};
+
+export function processInstanceScriptContent(
+ str: MagicString,
+ script: Node,
+ events: ComponentEvents,
+): InstanceScriptProcessResult {
+ const htmlx = str.original;
+ const scriptContent = htmlx.substring(script.content.start, script.content.end);
+ const tsAst = ts.createSourceFile(
+ 'component.ts.svelte',
+ scriptContent,
+ ts.ScriptTarget.Latest,
+ true,
+ ts.ScriptKind.TS,
+ );
+ const astOffset = script.content.start;
+ const exportedNames = new ExportedNames();
+ const getters = new Set();
+
+ const implicitTopLevelNames = new ImplicitTopLevelNames();
+ let uses$$props = false;
+ let uses$$restProps = false;
+ let uses$$slots = false;
+
+ //track if we are in a declaration scope
+ let isDeclaration = false;
+
+ //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes
+ //which prevents us just changing all instances of Identity that start with $
+ const pendingStoreResolutions: PendingStoreResolution[] = [];
+
+ let scope = new Scope();
+ const rootScope = scope;
+
+ const pushScope = () => (scope = new Scope(scope));
+ const popScope = () => (scope = scope.parent);
+
+ const addExport = (
+ name: ts.BindingName,
+ target: ts.BindingName = null,
+ type: ts.TypeNode = null,
+ required = false,
+ ) => {
+ if (name.kind != ts.SyntaxKind.Identifier) {
+ throw Error('export source kind not supported ' + name);
+ }
+ if (target && target.kind != ts.SyntaxKind.Identifier) {
+ throw Error('export target kind not supported ' + target);
+ }
+ if (target) {
+ exportedNames.set(name.text, {
+ type: type?.getText(),
+ identifierText: (target as ts.Identifier).text,
+ required,
+ });
+ } else {
+ exportedNames.set(name.text, {});
+ }
+ };
+ const addGetter = (node: ts.Identifier) => {
+ if (!node) {
+ return;
+ }
+ getters.add(node.text);
+ };
+
+ const removeExport = (start: number, end: number) => {
+ const exportStart = str.original.indexOf('export', start + astOffset);
+ const exportEnd = exportStart + (end - start);
+ str.remove(exportStart, exportEnd);
+ };
+
+ const propTypeAssertToUserDefined = (node: ts.VariableDeclarationList) => {
+ const hasInitializers = node.declarations.filter((declaration) => declaration.initializer);
+ const handleTypeAssertion = (declaration: ts.VariableDeclaration) => {
+ const identifier = declaration.name;
+ const tsType = declaration.type;
+ const jsDocType = ts.getJSDocType(declaration);
+ const type = tsType || jsDocType;
+
+ if (!ts.isIdentifier(identifier) || !type) {
+ return;
+ }
+ const name = identifier.getText();
+ const end = declaration.end + astOffset;
+
+ str.appendLeft(end, `;${name} = __sveltets_any(${name});`);
+ };
+
+ const findComma = (target: ts.Node) =>
+ target.getChildren().filter((child) => child.kind === ts.SyntaxKind.CommaToken);
+ const splitDeclaration = () => {
+ const commas = node
+ .getChildren()
+ .filter((child) => child.kind === ts.SyntaxKind.SyntaxList)
+ .map(findComma)
+ .reduce((current, previous) => [...current, ...previous], []);
+
+ commas.forEach((comma) => {
+ const start = comma.getStart() + astOffset;
+ const end = comma.getEnd() + astOffset;
+ str.overwrite(start, end, ';let ', { contentOnly: true });
+ });
+ };
+ splitDeclaration();
+
+ for (const declaration of hasInitializers) {
+ handleTypeAssertion(declaration);
+ }
+ };
+
+ const handleStore = (ident: ts.Node, parent: ts.Node) => {
+ // handle assign to
+ // eslint-disable-next-line max-len
+ if (
+ parent &&
+ ts.isBinaryExpression(parent) &&
+ parent.operatorToken.kind == ts.SyntaxKind.EqualsToken &&
+ parent.left == ident
+ ) {
+ //remove $
+ const dollar = str.original.indexOf('$', ident.getStart() + astOffset);
+ str.remove(dollar, dollar + 1);
+ // replace = with .set(
+ str.overwrite(ident.end + astOffset, parent.operatorToken.end + astOffset, '.set(');
+ // append )
+ str.appendLeft(parent.end + astOffset, ')');
+ return;
+ }
+ // handle Assignment operators ($store +=, -=, *=, /=, %=, **=, etc.)
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Assignment
+ const operators = {
+ [ts.SyntaxKind.PlusEqualsToken]: '+',
+ [ts.SyntaxKind.MinusEqualsToken]: '-',
+ [ts.SyntaxKind.AsteriskEqualsToken]: '*',
+ [ts.SyntaxKind.SlashEqualsToken]: '/',
+ [ts.SyntaxKind.PercentEqualsToken]: '%',
+ [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: '**',
+ [ts.SyntaxKind.LessThanLessThanEqualsToken]: '<<',
+ [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: '>>',
+ [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>',
+ [ts.SyntaxKind.AmpersandEqualsToken]: '&',
+ [ts.SyntaxKind.CaretEqualsToken]: '^',
+ [ts.SyntaxKind.BarEqualsToken]: '|',
+ };
+ if (
+ ts.isBinaryExpression(parent) &&
+ parent.left == ident &&
+ Object.keys(operators).find((x) => x === String(parent.operatorToken.kind))
+ ) {
+ const storename = ident.getText().slice(1); // drop the $
+ const operator = operators[parent.operatorToken.kind];
+ str.overwrite(
+ parent.getStart() + astOffset,
+ str.original.indexOf('=', ident.end + astOffset) + 1,
+ `${storename}.set( __sveltets_store_get(${storename}) ${operator}`,
+ );
+ str.appendLeft(parent.end + astOffset, ')');
+ return;
+ }
+ // handle $store++, $store--, ++$store, --$store
+ if (
+ (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
+ parent.operator !==
+ ts.SyntaxKind.ExclamationToken /* `!$store` does not need processing */
+ ) {
+ let simpleOperator: string;
+ if (parent.operator === ts.SyntaxKind.PlusPlusToken) {
+ simpleOperator = '+';
+ }
+ if (parent.operator === ts.SyntaxKind.MinusMinusToken) {
+ simpleOperator = '-';
+ }
+
+ if (simpleOperator) {
+ const storename = ident.getText().slice(1); // drop the $
+ str.overwrite(
+ parent.getStart() + astOffset,
+ parent.end + astOffset,
+ `${storename}.set( __sveltets_store_get(${storename}) ${simpleOperator} 1)`,
+ );
+ return;
+ } else {
+ console.warn(
+ `Warning - unrecognized UnaryExpression operator ${parent.operator}!
+ This is an edge case unaccounted for in svelte2tsx, please file an issue:
+ https://github.com/sveltejs/language-tools/issues/new/choose
+ `,
+ parent.getText(),
+ );
+ }
+ }
+
+ // we must be on the right or not part of assignment
+ const dollar = str.original.indexOf('$', ident.getStart() + astOffset);
+ str.overwrite(dollar, dollar + 1, '__sveltets_store_get(');
+ str.appendLeft(ident.end + astOffset, ')');
+ };
+
+ const resolveStore = (pending: PendingStoreResolution) => {
+ let { node, parent, scope } = pending;
+ const name = (node as ts.Identifier).text;
+ while (scope) {
+ if (scope.declared.has(name)) {
+ //we were manually declared, this isn't a store access.
+ return;
+ }
+ scope = scope.parent;
+ }
+ //We haven't been resolved, we must be a store read/write, handle it.
+ handleStore(node, parent);
+ };
+
+ const handleIdentifier = (ident: ts.Identifier, parent: ts.Node) => {
+ if (ident.text === '$$props') {
+ uses$$props = true;
+ return;
+ }
+ if (ident.text === '$$restProps') {
+ uses$$restProps = true;
+ return;
+ }
+ if (ident.text === '$$slots') {
+ uses$$slots = true;
+ return;
+ }
+
+ if (ts.isLabeledStatement(parent) && parent.label == ident) {
+ return;
+ }
+
+ if (isDeclaration || ts.isParameter(parent)) {
+ if (!ts.isBindingElement(ident.parent) || ident.parent.name == ident) {
+ // we are a key, not a name, so don't care
+ if (ident.text.startsWith('$') || scope == rootScope) {
+ // track all top level declared identifiers and all $ prefixed identifiers
+ scope.declared.add(ident.text);
+ }
+ }
+ } else {
+ //track potential store usage to be resolved
+ if (ident.text.startsWith('$')) {
+ if (
+ (!ts.isPropertyAccessExpression(parent) || parent.expression == ident) &&
+ (!ts.isPropertyAssignment(parent) || parent.initializer == ident)
+ ) {
+ pendingStoreResolutions.push({ node: ident, parent, scope });
+ }
+ }
+ }
+ };
+
+ const handleExportedVariableDeclarationList = (list: ts.VariableDeclarationList) => {
+ ts.forEachChild(list, (node) => {
+ if (ts.isVariableDeclaration(node)) {
+ if (ts.isIdentifier(node.name)) {
+ addExport(node.name, node.name, node.type, !node.initializer);
+ } else if (
+ ts.isObjectBindingPattern(node.name) ||
+ ts.isArrayBindingPattern(node.name)
+ ) {
+ ts.forEachChild(node.name, (element) => {
+ if (ts.isBindingElement(element)) {
+ addExport(element.name);
+ }
+ });
+ }
+ }
+ });
+ };
+
+ const wrapExpressionWithInvalidate = (expression: ts.Expression | undefined) => {
+ if (!expression) {
+ return;
+ }
+
+ const start = expression.getStart() + astOffset;
+ const end = expression.getEnd() + astOffset;
+
+ // () => ({})
+ if (ts.isObjectLiteralExpression(expression)) {
+ str.appendLeft(start, '(');
+ str.appendRight(end, ')');
+ }
+
+ str.prependLeft(start, '__sveltets_invalidate(() => ');
+ str.appendRight(end, ')');
+ // Not adding ';' at the end because right now this function is only invoked
+ // in situations where there is a line break of ; guaranteed to be present (else the code is invalid)
+ };
+
+ const walk = (node: ts.Node, parent: ts.Node) => {
+ type onLeaveCallback = () => void;
+ const onLeaveCallbacks: onLeaveCallback[] = [];
+
+ if (ts.isInterfaceDeclaration(node) && node.name.text === 'ComponentEvents') {
+ events = new ComponentEventsFromInterface(node);
+ }
+
+ if (ts.isVariableStatement(node)) {
+ const exportModifier = findExportKeyword(node);
+ if (exportModifier) {
+ const isLet = node.declarationList.flags === ts.NodeFlags.Let;
+ const isConst = node.declarationList.flags === ts.NodeFlags.Const;
+
+ if (isLet) {
+ handleExportedVariableDeclarationList(node.declarationList);
+ propTypeAssertToUserDefined(node.declarationList);
+ } else if (isConst) {
+ node.declarationList.forEachChild((n) => {
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name)) {
+ addGetter(n.name);
+ }
+ });
+ }
+ removeExport(exportModifier.getStart(), exportModifier.end);
+ }
+ }
+
+ if (ts.isFunctionDeclaration(node)) {
+ if (node.modifiers) {
+ const exportModifier = findExportKeyword(node);
+ if (exportModifier) {
+ removeExport(exportModifier.getStart(), exportModifier.end);
+ addGetter(node.name);
+ }
+ }
+
+ pushScope();
+ onLeaveCallbacks.push(() => popScope());
+ }
+
+ if (ts.isClassDeclaration(node)) {
+ const exportModifier = findExportKeyword(node);
+ if (exportModifier) {
+ removeExport(exportModifier.getStart(), exportModifier.end);
+ addGetter(node.name);
+ }
+ }
+
+ if (ts.isBlock(node)) {
+ pushScope();
+ onLeaveCallbacks.push(() => popScope());
+ }
+
+ if (ts.isArrowFunction(node)) {
+ pushScope();
+ onLeaveCallbacks.push(() => popScope());
+ }
+
+ if (ts.isExportDeclaration(node)) {
+ const { exportClause } = node;
+ if (ts.isNamedExports(exportClause)) {
+ for (const ne of exportClause.elements) {
+ if (ne.propertyName) {
+ addExport(ne.propertyName, ne.name);
+ } else {
+ addExport(ne.name);
+ }
+ }
+ //we can remove entire statement
+ removeExport(node.getStart(), node.end);
+ }
+ }
+
+ //move imports to top of script so they appear outside our render function
+ if (ts.isImportDeclaration(node)) {
+ 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');
+ }
+
+ if (ts.isVariableDeclaration(parent) && parent.name == node) {
+ isDeclaration = true;
+ onLeaveCallbacks.push(() => (isDeclaration = false));
+ }
+
+ if (ts.isBindingElement(parent) && parent.name == node) {
+ isDeclaration = true;
+ onLeaveCallbacks.push(() => (isDeclaration = false));
+ }
+
+ if (ts.isImportClause(node)) {
+ isDeclaration = true;
+ onLeaveCallbacks.push(() => (isDeclaration = false));
+ }
+
+ //handle stores etc
+ if (ts.isIdentifier(node)) {
+ handleIdentifier(node, parent);
+ }
+
+ //track implicit declarations in reactive blocks at the top level
+ if (
+ ts.isLabeledStatement(node) &&
+ parent == tsAst && //top level
+ node.label.text == '$' &&
+ node.statement
+ ) {
+ const binaryExpression = getBinaryAssignmentExpr(node);
+ if (binaryExpression) {
+ implicitTopLevelNames.add(node);
+ wrapExpressionWithInvalidate(binaryExpression.right);
+ } else {
+ const start = node.getStart() + astOffset;
+ const end = node.getEnd() + astOffset;
+
+ str.prependLeft(start, ';() => {');
+ str.prependRight(end, '}');
+ }
+ }
+
+ //to save a bunch of condition checks on each node, we recurse into processChild which skips all the checks for top level items
+ ts.forEachChild(node, (n) => walk(n, node));
+ //fire off the on leave callbacks
+ onLeaveCallbacks.map((c) => c());
+ };
+
+ //walk the ast and convert to tsx as we go
+ tsAst.forEachChild((n) => walk(n, tsAst));
+
+ //resolve stores
+ pendingStoreResolutions.map(resolveStore);
+
+ // declare implicit reactive variables we found in the script
+ implicitTopLevelNames.modifyCode(rootScope.declared, astOffset, str);
+
+ const firstImport = tsAst.statements
+ .filter(ts.isImportDeclaration)
+ .sort((a, b) => a.end - b.end)[0];
+ if (firstImport) {
+ str.appendRight(firstImport.getStart() + astOffset, '\n');
+ }
+
+ return {
+ exportedNames,
+ events,
+ uses$$props,
+ uses$$restProps,
+ uses$$slots,
+ getters,
+ };
+}
diff --git a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts
new file mode 100644
index 000000000..c544c5fda
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts
@@ -0,0 +1,11 @@
+import MagicString from 'magic-string';
+import { Node } from 'estree-walker';
+export function processModuleScriptTag(str: MagicString, script: Node) {
+ const htmlx = str.original;
+
+ const scriptStartTagEnd = htmlx.indexOf('>', script.start) + 1;
+ const scriptEndTagStart = htmlx.lastIndexOf('<', script.end - 1);
+
+ str.overwrite(script.start, scriptStartTagEnd, '>;');
+ str.overwrite(scriptEndTagStart, script.end, ';<>');
+}
diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts b/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts
new file mode 100644
index 000000000..675b3d599
--- /dev/null
+++ b/packages/svelte2tsx/src/svelte2tsx/utils/Scope.ts
@@ -0,0 +1,20 @@
+export class Scope {
+ declared: Set = new Set();
+ parent: Scope;
+
+ constructor(parent?: Scope) {
+ this.parent = parent;
+ }
+}
+
+export class ScopeStack {
+ current = new Scope();
+
+ push() {
+ this.current = new Scope(this.current);
+ }
+
+ pop() {
+ this.current = this.current.parent;
+ }
+}
diff --git a/packages/svelte2tsx/src/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
similarity index 100%
rename from packages/svelte2tsx/src/utils/tsAst.ts
rename to packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts
diff --git a/packages/svelte2tsx/src/htmlxparser.ts b/packages/svelte2tsx/src/utils/htmlxparser.ts
similarity index 100%
rename from packages/svelte2tsx/src/htmlxparser.ts
rename to packages/svelte2tsx/src/utils/htmlxparser.ts