diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
index 913cc54bc..acc2f9c82 100644
--- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
@@ -28,8 +28,39 @@ import { handleText } from './nodes/Text';
import { handleTransitionDirective } from './nodes/Transition';
import { handleImplicitChildren, handleSnippet, hoistSnippetBlock } from './nodes/SnippetBlock';
import { handleRenderTag } from './nodes/RenderTag';
+import { ComponentDocumentation } from '../svelte2tsx/nodes/ComponentDocumentation';
+import { ScopeStack } from '../svelte2tsx/utils/Scope';
+import { Stores } from '../svelte2tsx/nodes/Stores';
+import { Scripts } from '../svelte2tsx/nodes/Scripts';
+import { SlotHandler } from '../svelte2tsx/nodes/slot';
+import TemplateScope from '../svelte2tsx/nodes/TemplateScope';
+import {
+ handleScopeAndResolveForSlot,
+ handleScopeAndResolveLetVarForSlot
+} from '../svelte2tsx/nodes/handleScopeAndResolveForSlot';
+import { EventHandler } from '../svelte2tsx/nodes/event-handler';
+import { ComponentEvents } from '../svelte2tsx/nodes/ComponentEvents';
-type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void;
+export interface TemplateProcessResult {
+ /**
+ * The HTML part of the Svelte AST.
+ */
+ htmlAst: TemplateNode;
+ uses$$props: boolean;
+ uses$$restProps: boolean;
+ uses$$slots: boolean;
+ slots: Map>;
+ scriptTag: BaseNode;
+ moduleScriptTag: BaseNode;
+ /** Start/end positions of snippets that should be moved to the instance script or possibly even module script */
+ rootSnippets: Array<[number, number]>;
+ /** To be added later as a comment on the default class export */
+ componentDocumentation: ComponentDocumentation;
+ events: ComponentEvents;
+ resolvedStores: string[];
+ usesAccessors: boolean;
+ isRunes: boolean;
+}
function stripDoctype(str: MagicString): void {
const regex = /(\n)?/i;
@@ -46,18 +77,19 @@ function stripDoctype(str: MagicString): void {
export function convertHtmlxToJsx(
str: MagicString,
ast: TemplateNode,
- onWalk: Walker = null,
- onLeave: Walker = null,
+ tags: BaseNode[],
options: {
- svelte5Plus: boolean;
- preserveAttributeCase?: boolean;
+ emitOnTemplateError?: boolean;
+ namespace?: string;
+ accessors?: boolean;
+ mode?: 'ts' | 'dts';
typingsNamespace?: string;
+ svelte5Plus: boolean;
} = { svelte5Plus: false }
-) {
- const htmlx = str.original;
- options = { preserveAttributeCase: false, ...options };
+): TemplateProcessResult {
options.typingsNamespace = options.typingsNamespace || 'svelteHTML';
- htmlx;
+ const preserveAttributeCase = options.namespace === 'foreign';
+
stripDoctype(str);
const rootSnippets: Array<[number, number]> = [];
@@ -65,17 +97,131 @@ export function convertHtmlxToJsx(
const pendingSnippetHoistCheck = new Set();
+ let uses$$props = false;
+ let uses$$restProps = false;
+ let uses$$slots = false;
+ let usesAccessors = !!options.accessors;
+ let isRunes = 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, isDeclaration);
+ const scripts = new Scripts(ast);
+
+ const handleSvelteOptions = (node: BaseNode) => {
+ for (let i = 0; i < node.attributes.length; i++) {
+ const optionName = node.attributes[i].name;
+ const optionValue = node.attributes[i].value;
+
+ switch (optionName) {
+ case 'accessors':
+ if (Array.isArray(optionValue)) {
+ if (optionValue[0].type === 'MustacheTag') {
+ usesAccessors = optionValue[0].expression.value;
+ }
+ } else {
+ usesAccessors = true;
+ }
+ break;
+ case 'runes':
+ isRunes = true;
+ break;
+ }
+ }
+ };
+
+ const handleIdentifier = (node: BaseNode) => {
+ 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: BaseNode) => {
+ str.remove(node.start, node.end);
+ };
+
+ const slotHandler = new SlotHandler(str.original);
+ let templateScope = new TemplateScope();
+
+ const handleComponentLet = (component: BaseNode) => {
+ templateScope = templateScope.child();
+ const lets = slotHandler.getSlotConsumerOfComponent(component);
+
+ for (const { letNode, slotName } of lets) {
+ handleScopeAndResolveLetVarForSlot({
+ letNode,
+ slotName,
+ slotHandler,
+ templateScope,
+ component
+ });
+ }
+ };
+
+ const handleScopeAndResolveForSlotInner = (
+ identifierDef: BaseNode,
+ initExpression: BaseNode,
+ owner: BaseNode
+ ) => {
+ handleScopeAndResolveForSlot({
+ identifierDef,
+ initExpression,
+ slotHandler,
+ templateScope,
+ owner
+ });
+ };
+
+ const eventHandler = new EventHandler();
+
walk(ast as any, {
- enter: (estreeTypedNode, estreeTypedParent, prop: string, index: number) => {
+ enter: (estreeTypedNode, estreeTypedParent, prop: string) => {
const node = estreeTypedNode as TemplateNode;
const parent = estreeTypedParent as BaseNode;
+ if (
+ prop == 'params' &&
+ (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression')
+ ) {
+ isDeclaration.value = true;
+ }
+ if (prop == 'id' && parent.type == 'VariableDeclarator') {
+ isDeclaration.value = true;
+ }
+
try {
switch (node.type) {
+ case 'Identifier':
+ handleIdentifier(node);
+ stores.handleIdentifier(node, parent, prop);
+ eventHandler.handleIdentifier(node, parent, prop);
+ break;
case 'IfBlock':
handleIf(str, node);
break;
case 'EachBlock':
+ templateScope = templateScope.child();
+
+ if (node.context) {
+ handleScopeAndResolveForSlotInner(node.context, node.expression, node);
+ }
handleEach(str, node);
break;
case 'ElseBlock':
@@ -84,7 +230,13 @@ export function convertHtmlxToJsx(
case 'KeyBlock':
handleKey(str, node);
break;
+ case 'BlockStatement':
+ case 'FunctionDeclaration':
+ case 'ArrowFunctionExpression':
+ scopeStack.push();
+ break;
case 'SnippetBlock':
+ scopeStack.push();
handleSnippet(
str,
node,
@@ -96,7 +248,7 @@ export function convertHtmlxToJsx(
: undefined
);
if (parent === ast) {
- // root snippet -> move to instance script
+ // root snippet -> move to instance script or possibly even module script
rootSnippets.push([node.start, node.end]);
} else {
pendingSnippetHoistCheck.add(parent);
@@ -106,6 +258,7 @@ export function convertHtmlxToJsx(
handleMustacheTag(str, node, parent);
break;
case 'RawMustacheTag':
+ scripts.checkIfContainsScriptTag(node);
handleRawHtml(str, node);
break;
case 'DebugTag':
@@ -127,6 +280,7 @@ export function convertHtmlxToJsx(
if (options.svelte5Plus) {
handleImplicitChildren(node, element as InlineComponent);
}
+ handleComponentLet(node);
break;
case 'Element':
case 'Options':
@@ -139,6 +293,14 @@ export function convertHtmlxToJsx(
case 'SvelteBoundary':
case 'Slot':
case 'SlotTemplate':
+ if (node.type === 'Element') {
+ scripts.checkIfElementIsScriptTag(node, parent);
+ } else if (node.type === 'Options') {
+ handleSvelteOptions(node);
+ } else if (node.type === 'Slot') {
+ slotHandler.handleSlot(node, templateScope);
+ }
+
if (node.name !== '!DOCTYPE') {
if (element) {
element.child = new Element(
@@ -154,6 +316,7 @@ export function convertHtmlxToJsx(
}
break;
case 'Comment':
+ componentDocumentation.handleComment(node);
handleComment(str, node);
break;
case 'Binding':
@@ -173,12 +336,15 @@ export function convertHtmlxToJsx(
handleStyleDirective(str, node as StyleDirective, element as Element);
break;
case 'Action':
+ stores.handleDirective(node, str);
handleActionDirective(node as BaseDirective, element as Element);
break;
case 'Transition':
+ stores.handleDirective(node, str);
handleTransitionDirective(str, node as BaseDirective, element as Element);
break;
case 'Animation':
+ stores.handleDirective(node, str);
handleAnimateDirective(str, node as BaseDirective, element as Element);
break;
case 'Attribute':
@@ -186,7 +352,7 @@ export function convertHtmlxToJsx(
str,
node as Attribute,
parent,
- options.preserveAttributeCase,
+ preserveAttributeCase,
options.svelte5Plus,
element
);
@@ -195,6 +361,7 @@ export function convertHtmlxToJsx(
handleSpread(node, element);
break;
case 'EventHandler':
+ eventHandler.handleEventHandler(node, parent);
handleEventHandler(str, node as BaseDirective, element);
break;
case 'Let':
@@ -202,7 +369,7 @@ export function convertHtmlxToJsx(
str,
node,
parent,
- options.preserveAttributeCase,
+ preserveAttributeCase,
options.svelte5Plus,
element
);
@@ -210,9 +377,29 @@ export function convertHtmlxToJsx(
case 'Text':
handleText(str, node as Text, parent);
break;
- }
- if (onWalk) {
- onWalk(node, parent, prop, index);
+ case 'Style':
+ handleStyleTag(node);
+ break;
+ case 'VariableDeclarator':
+ isDeclaration.value = true;
+ break;
+ case 'AwaitBlock':
+ templateScope = templateScope.child();
+ if (node.value) {
+ handleScopeAndResolveForSlotInner(
+ node.value,
+ node.expression,
+ node.then
+ );
+ }
+ if (node.error) {
+ handleScopeAndResolveForSlotInner(
+ node.error,
+ node.expression,
+ node.catch
+ );
+ }
+ break;
}
} catch (e) {
console.error('Error walking node ', node, e);
@@ -220,17 +407,37 @@ export function convertHtmlxToJsx(
}
},
- leave: (estreeTypedNode, estreeTypedParent, prop: string, index: number) => {
+ leave: (estreeTypedNode, estreeTypedParent, prop: string) => {
const node = estreeTypedNode as TemplateNode;
const parent = estreeTypedParent as BaseNode;
+ 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;
+ };
+
try {
switch (node.type) {
- case 'IfBlock':
+ case 'BlockStatement':
+ case 'FunctionDeclaration':
+ case 'ArrowFunctionExpression':
+ case 'SnippetBlock':
+ scopeStack.pop();
break;
case 'EachBlock':
+ onTemplateScopeLeave();
break;
case 'AwaitBlock':
+ onTemplateScopeLeave();
handleAwait(str, node);
break;
case 'InlineComponent':
@@ -245,15 +452,15 @@ export function convertHtmlxToJsx(
case 'Document':
case 'Slot':
case 'SlotTemplate':
+ if (node.type === 'InlineComponent') {
+ onTemplateScopeLeave();
+ }
if (node.name !== '!DOCTYPE') {
element.performTransformation();
element = element.parent;
}
break;
}
- if (onLeave) {
- onLeave(node, parent, prop, index);
- }
} catch (e) {
console.error('Error leaving node ', node);
throw e;
@@ -261,11 +468,39 @@ export function convertHtmlxToJsx(
}
});
+ // hoist inner snippets to top of containing element
for (const node of pendingSnippetHoistCheck) {
hoistSnippetBlock(str, node);
}
- return rootSnippets;
+ // resolve scripts
+ const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags();
+ if (options.mode !== 'ts') {
+ scripts.blankOtherScriptTags(str);
+ }
+
+ //resolve stores
+ const resolvedStores = stores.getStoreNames();
+
+ return {
+ htmlAst: ast,
+ moduleScriptTag,
+ scriptTag,
+ rootSnippets,
+ slots: slotHandler.getSlotDef(),
+ events: new ComponentEvents(
+ eventHandler,
+ tags.some((tag) => tag.attributes?.some((a) => a.name === 'strictEvents')),
+ str
+ ),
+ uses$$props,
+ uses$$restProps,
+ uses$$slots,
+ componentDocumentation,
+ resolvedStores,
+ usesAccessors,
+ isRunes
+ };
}
/**
@@ -281,10 +516,13 @@ export function htmlx2jsx(
svelte5Plus: boolean;
}
) {
- const ast = parseHtmlx(htmlx, parse, { ...options }).htmlxAst;
+ const { htmlxAst, tags } = parseHtmlx(htmlx, parse, { ...options });
const str = new MagicString(htmlx);
- convertHtmlxToJsx(str, ast, null, null, options);
+ convertHtmlxToJsx(str, htmlxAst, tags, {
+ ...options,
+ namespace: options?.preserveAttributeCase ? 'foreign' : undefined
+ });
return {
map: str.generateMap({ hires: true }),
diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts
index 2daf71419..06f343ac4 100644
--- a/packages/svelte2tsx/src/svelte2tsx/index.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/index.ts
@@ -1,51 +1,15 @@
-import { Node } from 'estree-walker';
import MagicString from 'magic-string';
-import { convertHtmlxToJsx } from '../htmlxtojsx_v2';
+import { convertHtmlxToJsx, TemplateProcessResult } from '../htmlxtojsx_v2';
import { parseHtmlx } from '../utils/htmlxparser';
-import { ComponentDocumentation } from './nodes/ComponentDocumentation';
-import { ComponentEvents } from './nodes/ComponentEvents';
-import { EventHandler } from './nodes/event-handler';
+import { addComponentExport } from './addComponentExport';
+import { createRenderFunction } from './createRenderFunction';
import { ExportedNames } from './nodes/ExportedNames';
-import {
- handleScopeAndResolveForSlot,
- handleScopeAndResolveLetVarForSlot
-} from './nodes/handleScopeAndResolveForSlot';
+import { Generics } from './nodes/Generics';
import { ImplicitStoreValues } from './nodes/ImplicitStoreValues';
-import { Scripts } from './nodes/Scripts';
-import { SlotHandler } from './nodes/slot';
-import { Stores } from './nodes/Stores';
-import TemplateScope from './nodes/TemplateScope';
import { processInstanceScriptContent } from './processInstanceScriptContent';
import { createModuleAst, ModuleAst, processModuleScriptTag } from './processModuleScriptTag';
-import { ScopeStack } from './utils/Scope';
-import { Generics } from './nodes/Generics';
-import { addComponentExport } from './addComponentExport';
-import { createRenderFunction } from './createRenderFunction';
-// @ts-ignore
-import { TemplateNode } from 'svelte/types/compiler/interfaces';
import path from 'path';
-import { VERSION, parse } from 'svelte/compiler';
-
-type TemplateProcessResult = {
- /**
- * The HTML part of the Svelte AST.
- */
- htmlAst: TemplateNode;
- uses$$props: boolean;
- uses$$restProps: boolean;
- uses$$slots: boolean;
- slots: Map>;
- scriptTag: Node;
- moduleScriptTag: Node;
- /** Start/end positions of snippets that should be moved to the instance script */
- rootSnippets: Array<[number, number]>;
- /** To be added later as a comment on the default class export */
- componentDocumentation: ComponentDocumentation;
- events: ComponentEvents;
- resolvedStores: string[];
- usesAccessors: boolean;
- isRunes: boolean;
-};
+import { parse, VERSION } from 'svelte/compiler';
function processSvelteTemplate(
str: MagicString,
@@ -60,257 +24,7 @@ function processSvelteTemplate(
}
): TemplateProcessResult {
const { htmlxAst, tags } = parseHtmlx(str.original, parse, options);
-
- let uses$$props = false;
- let uses$$restProps = false;
- let uses$$slots = false;
- let usesAccessors = !!options.accessors;
- let isRunes = 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, isDeclaration);
- const scripts = new Scripts(htmlxAst);
-
- const handleSvelteOptions = (node: Node) => {
- for (let i = 0; i < node.attributes.length; i++) {
- const optionName = node.attributes[i].name;
- const optionValue = node.attributes[i].value;
-
- switch (optionName) {
- case 'accessors':
- if (Array.isArray(optionValue)) {
- if (optionValue[0].type === 'MustacheTag') {
- usesAccessors = optionValue[0].expression.value;
- }
- } else {
- usesAccessors = true;
- }
- break;
- case 'runes':
- isRunes = true;
- break;
- }
- }
- };
-
- 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 'Options':
- handleSvelteOptions(node);
- break;
- case 'Identifier':
- handleIdentifier(node);
- stores.handleIdentifier(node, parent, prop);
- eventHandler.handleIdentifier(node, parent, prop);
- break;
- case 'Transition':
- case 'Action':
- case 'Animation':
- stores.handleDirective(node, str);
- break;
- case 'Slot':
- slotHandler.handleSlot(node, templateScope);
- break;
- case 'Style':
- handleStyleTag(node);
- break;
- case 'Element':
- scripts.checkIfElementIsScriptTag(node, parent);
- break;
- case 'RawMustacheTag':
- scripts.checkIfContainsScriptTag(node);
- 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;
- }
- };
-
- const rootSnippets = convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, {
- preserveAttributeCase: options?.namespace == 'foreign',
- typingsNamespace: options.typingsNamespace,
- svelte5Plus: options.svelte5Plus
- });
-
- // resolve scripts
- const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags();
- if (options.mode !== 'ts') {
- scripts.blankOtherScriptTags(str);
- }
-
- //resolve stores
- const resolvedStores = stores.getStoreNames();
-
- return {
- htmlAst: htmlxAst,
- moduleScriptTag,
- scriptTag,
- rootSnippets,
- slots: slotHandler.getSlotDef(),
- events: new ComponentEvents(
- eventHandler,
- tags.some((tag) => tag.attributes?.some((a) => a.name === 'strictEvents')),
- str
- ),
- uses$$props,
- uses$$restProps,
- uses$$slots,
- componentDocumentation,
- resolvedStores,
- usesAccessors,
- isRunes
- };
+ return convertHtmlxToJsx(str, htmlxAst, tags, options);
}
export function svelte2tsx(