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(