feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,6 @@
import type { SvelteAttribute, SvelteShorthandAttribute, SvelteAttachTag, SvelteDirective, SvelteSpreadAttribute, SvelteStartTag, SvelteStyleDirective } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
/** Convert for Attributes */
export declare function convertAttributes(attributes: (SvAST.AttributeOrDirective | Compiler.Attribute | Compiler.SpreadAttribute | Compiler.AttachTag | Compiler.Directive)[], parent: SvelteStartTag, ctx: Context): IterableIterator<SvelteAttribute | SvelteShorthandAttribute | SvelteSpreadAttribute | SvelteAttachTag | SvelteDirective | SvelteStyleDirective>;

View File

@@ -0,0 +1,672 @@
import { getWithLoc, indexOf } from "./common.js";
import { convertMustacheTag } from "./mustache.js";
import { convertTextToLiteral } from "./text.js";
import { ParseError } from "../../errors.js";
import { svelteVersion } from "../svelte-version.js";
import { hasTypeInfo } from "../../utils/index.js";
import { getModifiers } from "../compat.js";
/** Convert for Attributes */
export function* convertAttributes(attributes, parent, ctx) {
for (const attr of attributes) {
if (attr.type === "Attribute") {
yield convertAttribute(attr, parent, ctx);
continue;
}
if (attr.type === "SpreadAttribute" || attr.type === "Spread") {
yield convertSpreadAttribute(attr, parent, ctx);
continue;
}
if (attr.type === "AttachTag") {
yield convertAttachTag(attr, parent, ctx);
continue;
}
if (attr.type === "BindDirective" || attr.type === "Binding") {
yield convertBindingDirective(attr, parent, ctx);
continue;
}
if (attr.type === "OnDirective" || attr.type === "EventHandler") {
yield convertEventHandlerDirective(attr, parent, ctx);
continue;
}
if (attr.type === "ClassDirective" || attr.type === "Class") {
yield convertClassDirective(attr, parent, ctx);
continue;
}
if (attr.type === "StyleDirective") {
yield convertStyleDirective(attr, parent, ctx);
continue;
}
if (attr.type === "TransitionDirective" || attr.type === "Transition") {
yield convertTransitionDirective(attr, parent, ctx);
continue;
}
if (attr.type === "AnimateDirective" || attr.type === "Animation") {
yield convertAnimationDirective(attr, parent, ctx);
continue;
}
if (attr.type === "UseDirective" || attr.type === "Action") {
yield convertActionDirective(attr, parent, ctx);
continue;
}
if (attr.type === "LetDirective") {
yield convertLetDirective(attr, parent, ctx);
continue;
}
if (attr.type === "Let") {
yield convertLetDirective(attr, parent, ctx);
continue;
}
if (attr.type === "Ref") {
throw new ParseError("Ref are not supported.", attr.start, ctx);
}
if (attr.type === "Style") {
throw new ParseError(`Svelte v3.46.0 is no longer supported. Please use Svelte>=v3.46.1.`, attr.start, ctx);
}
throw new ParseError(`Unknown directive or attribute (${attr.type}) are not supported.`, attr.start, ctx);
}
}
/** Convert for Attribute */
function convertAttribute(node, parent, ctx) {
const attribute = {
type: "SvelteAttribute",
boolean: false,
key: null,
value: [],
parent,
...ctx.getConvertLocation(node),
};
const keyStart = ctx.code.indexOf(node.name, node.start);
const keyRange = { start: keyStart, end: keyStart + node.name.length };
attribute.key = {
type: "SvelteName",
name: node.name,
parent: attribute,
...ctx.getConvertLocation(keyRange),
};
if (node.value === true) {
// Boolean attribute
attribute.boolean = true;
ctx.addToken("HTMLIdentifier", keyRange);
return attribute;
}
const value = node.value;
const shorthand = isAttributeShorthandForSvelteV4(value) ||
isAttributeShorthandForSvelteV5(value);
if (shorthand) {
const key = {
...attribute.key,
type: "Identifier",
};
const sAttr = {
type: "SvelteShorthandAttribute",
key,
value: key,
parent,
loc: attribute.loc,
range: attribute.range,
};
key.parent = sAttr;
ctx.scriptLet.addObjectShorthandProperty(attribute.key, sAttr, (es) => {
if (
// FIXME: Older parsers may use the same node. In that case, do not replace.
// We will drop support for ESLint v7 in the next major version and remove this branch.
es.key !== es.value) {
sAttr.key = es.key;
}
sAttr.value = es.value;
});
return sAttr;
}
// Not required for shorthands. Therefore, register the token here.
ctx.addToken("HTMLIdentifier", keyRange);
processAttributeValue(value, attribute, parent, ctx);
return attribute;
function isAttributeShorthandForSvelteV4(value) {
return Array.isArray(value) && value[0]?.type === "AttributeShorthand";
}
function isAttributeShorthandForSvelteV5(value) {
return (!Array.isArray(value) &&
value.type === "ExpressionTag" &&
ctx.code[node.start] === "{" &&
ctx.code[node.end - 1] === "}");
}
}
/** Common process attribute value */
function processAttributeValue(nodeValue, attribute, attributeParent, ctx) {
const nodes = (Array.isArray(nodeValue) ? nodeValue : [nodeValue])
.filter((v) => v.type !== "Text" ||
// ignore empty
// https://github.com/sveltejs/svelte/pull/6539
v.start < v.end)
.map((v, index, array) => {
if (v.type === "Text") {
const next = array[index + 1];
if (next && next.start < v.end) {
// Maybe bug in Svelte can cause the completion index to shift.
return {
...v,
end: next.start,
};
}
}
return v;
});
if (nodes.length === 1 &&
(nodes[0].type === "ExpressionTag" || nodes[0].type === "MustacheTag") &&
attribute.type === "SvelteAttribute") {
const typing = buildAttributeType(attributeParent.parent, attribute.key.name, ctx);
const mustache = convertMustacheTag(nodes[0], attribute, typing, ctx);
attribute.value.push(mustache);
return;
}
for (const v of nodes) {
if (v.type === "Text") {
attribute.value.push(convertTextToLiteral(v, attribute, ctx));
continue;
}
if (v.type === "ExpressionTag" || v.type === "MustacheTag") {
const mustache = convertMustacheTag(v, attribute, null, ctx);
attribute.value.push(mustache);
continue;
}
const u = v;
throw new ParseError(`Unknown attribute value (${u.type}) are not supported.`, u.start, ctx);
}
}
/** Build attribute type */
function buildAttributeType(element, attrName, ctx) {
if (svelteVersion.gte(5) &&
attrName.startsWith("on") &&
(element.type !== "SvelteElement" || element.kind === "html")) {
return buildEventHandlerType(element, attrName.slice(2), ctx);
}
if (element.type !== "SvelteElement" || element.kind !== "component") {
return null;
}
const elementName = ctx.elements.get(element).name;
const componentPropsType = `import('svelte').ComponentProps<typeof ${elementName}>`;
return conditional({
check: `'${attrName}'`,
extends: `infer PROP`,
true: conditional({
check: `PROP`,
extends: `keyof ${componentPropsType}`,
true: `${componentPropsType}[PROP]`,
false: `never`,
}),
false: `never`,
});
/** Generate `C extends E ? T : F` type. */
function conditional(types) {
return `${types.check} extends ${types.extends}?(${types.true}):(${types.false})`;
}
}
/** Convert for Spread */
function convertSpreadAttribute(node, parent, ctx) {
const attribute = {
type: "SvelteSpreadAttribute",
argument: null,
parent,
...ctx.getConvertLocation(node),
};
const spreadStart = ctx.code.indexOf("...", node.start);
ctx.addToken("Punctuator", {
start: spreadStart,
end: spreadStart + 3,
});
ctx.scriptLet.addExpression(node.expression, attribute, null, (es) => {
attribute.argument = es;
});
return attribute;
}
function convertAttachTag(node, parent, ctx) {
const attachTag = {
type: "SvelteAttachTag",
expression: node.expression,
parent,
...ctx.getConvertLocation(node),
};
ctx.scriptLet.addExpression(node.expression, attachTag, null, (es) => {
attachTag.expression = es;
});
const atAttachStart = ctx.code.indexOf("@attach", attachTag.range[0]);
ctx.addToken("MustacheKeyword", {
start: atAttachStart,
end: atAttachStart + 7,
});
return attachTag;
}
/** Convert for Binding Directive */
function convertBindingDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Binding",
key: null,
shorthand: false,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processExpression(expression, shorthand) {
directive.shorthand = shorthand;
return ctx.scriptLet.addExpression(expression, directive, null, (es, { getScope }) => {
directive.expression = es;
if (isFunctionBindings(ctx, es)) {
directive.expression.type = "SvelteFunctionBindingsExpression";
return;
}
const scope = getScope(es);
const reference = scope.references.find((ref) => ref.identifier === es);
if (reference) {
// The bind directive does read and write.
reference.isWrite = () => true;
reference.isWriteOnly = () => false;
reference.isReadWrite = () => true;
reference.isReadOnly = () => false;
reference.isRead = () => true;
}
});
},
});
return directive;
}
/**
* Checks whether the given expression is Function bindings (added in Svelte 5.9.0) or not.
* See https://svelte.dev/docs/svelte/bind#Function-bindings
*/
function isFunctionBindings(ctx, expression) {
// Svelte 3/4 does not support Function bindings.
if (!svelteVersion.gte(5)) {
return false;
}
if (expression.type !== "SequenceExpression" ||
expression.expressions.length !== 2) {
return false;
}
const bindValueOpenIndex = ctx.code.lastIndexOf("{", expression.range[0]);
if (bindValueOpenIndex < 0)
return false;
const betweenText = ctx.code
.slice(bindValueOpenIndex + 1, expression.range[0])
// Strip comments
.replace(/\/\/[^\n]*\n|\/\*[\s\S]*?\*\//g, "")
.trim();
return !betweenText;
}
/** Convert for EventHandler Directive */
function convertEventHandlerDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "EventHandler",
key: null,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
const typing = buildEventHandlerType(parent.parent, node.name, ctx);
processDirective(node, directive, ctx, {
processExpression: buildProcessExpressionForExpression(directive, ctx, typing),
});
return directive;
}
/** Build event handler type */
function buildEventHandlerType(element, eventName, ctx) {
const nativeEventHandlerType = `(e:${conditional({
check: `'${eventName}'`,
extends: `infer EVT`,
true: conditional({
check: `EVT`,
extends: `keyof HTMLElementEventMap`,
true: `HTMLElementEventMap[EVT]`,
false: `CustomEvent<any>`,
}),
false: `never`,
})})=>void`;
if (element.type !== "SvelteElement") {
return nativeEventHandlerType;
}
const elementName = ctx.elements.get(element).name;
if (element.kind === "component") {
const componentEventsType = `import('svelte').ComponentEvents<${elementName}>`;
return `(e:${conditional({
check: `0`,
extends: `(1 & ${componentEventsType})`,
// `componentEventsType` is `any`
// `@typescript-eslint/parser` currently cannot parse `*.svelte` import types correctly.
// So if we try to do a correct type parsing, it's argument type will be `any`.
// A workaround is to inject the type directly, as `CustomEvent<any>` is better than `any`.
true: `CustomEvent<any>`,
// `componentEventsType` has an exact type.
false: conditional({
check: `'${eventName}'`,
extends: `infer EVT`,
true: conditional({
check: `EVT`,
extends: `keyof ${componentEventsType}`,
true: `${componentEventsType}[EVT]`,
false: `CustomEvent<any>`,
}),
false: `never`,
}),
})})=>void`;
}
if (element.kind === "special") {
if (elementName === "svelte:component")
return `(e:CustomEvent<any>)=>void`;
return nativeEventHandlerType;
}
const attrName = `on:${eventName}`;
const svelteHTMLElementsType = "import('svelte/elements').SvelteHTMLElements";
return conditional({
check: `'${elementName}'`,
extends: "infer EL",
true: conditional({
check: `EL`,
extends: `keyof ${svelteHTMLElementsType}`,
true: conditional({
check: `'${attrName}'`,
extends: "infer ATTR",
true: conditional({
check: `ATTR`,
extends: `keyof ${svelteHTMLElementsType}[EL]`,
true: `${svelteHTMLElementsType}[EL][ATTR]`,
false: nativeEventHandlerType,
}),
false: `never`,
}),
false: nativeEventHandlerType,
}),
false: `never`,
});
/** Generate `C extends E ? T : F` type. */
function conditional(types) {
return `${types.check} extends ${types.extends}?(${types.true}):(${types.false})`;
}
}
/** Convert for Class Directive */
function convertClassDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Class",
key: null,
shorthand: false,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processExpression(expression, shorthand) {
directive.shorthand = shorthand;
return ctx.scriptLet.addExpression(expression, directive);
},
});
return directive;
}
/** Convert for Style Directive */
function convertStyleDirective(node, parent, ctx) {
const directive = {
type: "SvelteStyleDirective",
key: null,
shorthand: false,
value: [],
parent,
...ctx.getConvertLocation(node),
};
processDirectiveKey(node, directive, ctx);
const keyName = directive.key.name;
if (node.value === true) {
const shorthandDirective = directive;
shorthandDirective.shorthand = true;
ctx.scriptLet.addExpression(keyName, shorthandDirective.key, null, (expression) => {
if (expression.type !== "Identifier") {
throw new ParseError(`Expected JS identifier or attribute value.`, expression.range[0], ctx);
}
shorthandDirective.key.name = expression;
});
return shorthandDirective;
}
ctx.addToken("HTMLIdentifier", {
start: keyName.range[0],
end: keyName.range[1],
});
processAttributeValue(node.value, directive, parent, ctx);
return directive;
}
/** Convert for Transition Directive */
function convertTransitionDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Transition",
intro: node.intro,
outro: node.outro,
key: null,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processExpression: buildProcessExpressionForExpression(directive, ctx, null),
processName: (name) => ctx.scriptLet.addExpression(name, directive.key, null, buildExpressionTypeChecker(["Identifier"], ctx)),
});
return directive;
}
/** Convert for Animation Directive */
function convertAnimationDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Animation",
key: null,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processExpression: buildProcessExpressionForExpression(directive, ctx, null),
processName: (name) => ctx.scriptLet.addExpression(name, directive.key, null, buildExpressionTypeChecker(["Identifier"], ctx)),
});
return directive;
}
/** Convert for Action Directive */
function convertActionDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Action",
key: null,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processExpression: buildProcessExpressionForExpression(directive, ctx, `Parameters<typeof ${node.name}>[1]`),
processName: (name) => ctx.scriptLet.addExpression(name, directive.key, null, buildExpressionTypeChecker(["Identifier", "MemberExpression"], ctx)),
});
return directive;
}
/** Convert for Let Directive */
function convertLetDirective(node, parent, ctx) {
const directive = {
type: "SvelteDirective",
kind: "Let",
key: null,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
processDirective(node, directive, ctx, {
processPattern(pattern) {
return ctx.letDirCollections
.getCollection()
.addPattern(pattern, directive, buildLetDirectiveType(parent.parent, node.name, ctx));
},
processName: node.expression
? undefined
: (name) => {
// shorthand
ctx.letDirCollections
.getCollection()
.addPattern(name, directive, buildLetDirectiveType(parent.parent, node.name, ctx), (es) => {
directive.expression = es;
});
return [];
},
});
return directive;
}
/** Build let directive param type */
function buildLetDirectiveType(element, letName, ctx) {
if (element.type !== "SvelteElement") {
return "any";
}
let slotName = "default";
let componentName;
const svelteNode = ctx.elements.get(element);
const slotAttr = svelteNode.attributes.find((attr) => {
return attr.type === "Attribute" && attr.name === "slot";
});
if (slotAttr) {
if (Array.isArray(slotAttr.value) &&
slotAttr.value.length === 1 &&
slotAttr.value[0].type === "Text") {
slotName = slotAttr.value[0].data;
}
else {
return "any";
}
const parent = findParentComponent(element);
if (parent == null)
return "any";
componentName = ctx.elements.get(parent).name;
}
else {
if (element.kind === "component") {
componentName = svelteNode.name;
}
else {
const parent = findParentComponent(element);
if (parent == null)
return "any";
componentName = ctx.elements.get(parent).name;
}
}
return `${String(componentName)}['$$slot_def'][${JSON.stringify(slotName)}][${JSON.stringify(letName)}]`;
/** Find parent component element */
function findParentComponent(node) {
let parent = node.parent;
while (parent && parent.type !== "SvelteElement") {
parent = parent.parent;
}
if (!parent || parent.kind !== "component") {
return null;
}
return parent;
}
}
/** Common process for directive */
function processDirective(node, directive, ctx, processors) {
processDirectiveKey(node, directive, ctx);
processDirectiveExpression(node, directive, ctx, processors);
}
/** Common process for directive key */
function processDirectiveKey(node, directive, ctx) {
const colonIndex = ctx.code.indexOf(":", directive.range[0]);
ctx.addToken("HTMLIdentifier", {
start: directive.range[0],
end: colonIndex,
});
const nameIndex = ctx.code.indexOf(node.name, colonIndex + 1);
const nameRange = {
start: nameIndex,
end: nameIndex + node.name.length,
};
let keyEndIndex = nameRange.end;
// modifiers
if (ctx.code[nameRange.end] === "|") {
let nextStart = nameRange.end + 1;
let nextEnd = indexOf(ctx.code, (c) => c === "=" || c === ">" || c === "/" || c === "|" || !c.trim(), nextStart);
ctx.addToken("HTMLIdentifier", { start: nextStart, end: nextEnd });
while (ctx.code[nextEnd] === "|") {
nextStart = nextEnd + 1;
nextEnd = indexOf(ctx.code, (c) => c === "=" || c === ">" || c === "/" || c === "|" || !c.trim(), nextStart);
ctx.addToken("HTMLIdentifier", { start: nextStart, end: nextEnd });
}
keyEndIndex = nextEnd;
}
const key = (directive.key = {
type: "SvelteDirectiveKey",
name: null,
modifiers: getModifiers(node),
parent: directive,
...ctx.getConvertLocation({ start: node.start, end: keyEndIndex }),
});
// put name
key.name = {
type: "SvelteName",
name: node.name,
parent: key,
...ctx.getConvertLocation(nameRange),
};
}
/** Common process for directive expression */
function processDirectiveExpression(node, directive, ctx, processors) {
const key = directive.key;
const keyName = key.name;
let shorthand = false;
if (node.expression) {
shorthand =
node.expression.type === "Identifier" &&
node.expression.name === node.name &&
getWithLoc(node.expression).start === keyName.range[0];
if (shorthand && getWithLoc(node.expression).end !== keyName.range[1]) {
// The identifier location may be incorrect in some edge cases.
// e.g. bind:value=""
getWithLoc(node.expression).end = keyName.range[1];
}
if (processors.processExpression) {
processors.processExpression(node.expression, shorthand).push((es) => {
if (node.expression &&
(es.type === "SvelteFunctionBindingsExpression"
? "SequenceExpression"
: es.type) !== node.expression.type) {
throw new ParseError(`Expected ${node.expression.type}, but ${es.type} found.`, es.range[0], ctx);
}
directive.expression = es;
});
}
else {
processors.processPattern(node.expression, shorthand).push((es) => {
directive.expression = es;
});
}
}
if (!shorthand) {
if (processors.processName) {
processors.processName(keyName).push((es) => {
key.name = es;
});
}
else {
ctx.addToken("HTMLIdentifier", {
start: keyName.range[0],
end: keyName.range[1],
});
}
}
}
/** Build processExpression for Expression */
function buildProcessExpressionForExpression(directive, ctx, typing) {
return (expression) => {
if (hasTypeInfo(expression)) {
return ctx.scriptLet.addExpression(expression, directive, null);
}
return ctx.scriptLet.addExpression(expression, directive, typing);
};
}
/** Build expression type checker to script let callbacks */
function buildExpressionTypeChecker(expected, ctx) {
return (node) => {
if (!expected.includes(node.type)) {
throw new ParseError(`Expected JS ${expected.join(", or ")}, but ${node.type} found.`, node.range[0], ctx);
}
};
}

View File

@@ -0,0 +1,16 @@
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
import type { SvelteAwaitBlock, SvelteEachBlock, SvelteIfBlock, SvelteIfBlockAlone, SvelteIfBlockElseIf, SvelteKeyBlock, SvelteSnippetBlock } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
export declare function convertIfBlock(node: SvAST.IfBlock | Compiler.IfBlock, parent: SvelteIfBlock["parent"], ctx: Context): SvelteIfBlockAlone;
export declare function convertIfBlock(node: SvAST.IfBlock | Compiler.IfBlock, parent: SvelteIfBlock["parent"], ctx: Context, elseifContext?: {
start: number;
}): SvelteIfBlockElseIf;
/** Convert for EachBlock */
export declare function convertEachBlock(node: SvAST.EachBlock | Compiler.EachBlock, parent: SvelteEachBlock["parent"], ctx: Context): SvelteEachBlock;
/** Convert for AwaitBlock */
export declare function convertAwaitBlock(node: SvAST.AwaitBlock | Compiler.AwaitBlock, parent: SvelteAwaitBlock["parent"], ctx: Context): SvelteAwaitBlock;
/** Convert for KeyBlock */
export declare function convertKeyBlock(node: SvAST.KeyBlock | Compiler.KeyBlock, parent: SvelteKeyBlock["parent"], ctx: Context): SvelteKeyBlock;
/** Convert for SnippetBlock */
export declare function convertSnippetBlock(node: Compiler.SnippetBlock, parent: SvelteSnippetBlock["parent"], ctx: Context): SvelteSnippetBlock;

View File

@@ -0,0 +1,519 @@
import { convertChildren } from "./element.js";
import { getWithLoc, indexOf, lastIndexOf } from "./common.js";
import { getAlternateFromIfBlock, getBodyFromEachBlock, getCatchFromAwaitBlock, getChildren, getConsequentFromIfBlock, getFallbackFromEachBlock, getFragment, getPendingFromAwaitBlock, getTestFromIfBlock, getThenFromAwaitBlock, trimChildren, } from "../compat.js";
/** Get start index of block */
function startBlockIndex(code, endIndex, block) {
return lastIndexOf(code, (c, index) => {
if (c !== "{") {
return false;
}
for (let next = index + 1; next < code.length; next++) {
const nextC = code[next];
if (!nextC.trim()) {
continue;
}
return code.startsWith(block, next);
}
return false;
}, endIndex);
}
function startIndexFromFragment(fragment, getBeforeEndIndex) {
if (fragment.start != null) {
return fragment.start;
}
const children = getChildren(fragment);
return children.length ? children[0].start : getBeforeEndIndex();
}
function endIndexFromFragment(fragment, getBeforeEndIndex) {
if (fragment.end != null) {
return fragment.end;
}
const children = getChildren(fragment);
return children.length
? children[children.length - 1].end
: getBeforeEndIndex();
}
function endIndexFromBlock(fragment, lastExpression, ctx) {
return endIndexFromFragment(fragment, () => {
return ctx.code.indexOf("}", getWithLoc(lastExpression).end) + 1;
});
}
/** Convert for IfBlock */
export function convertIfBlock(node, parent, ctx, elseifContext) {
// {#if expr} {:else} {/if}
// {:else if expr} {/if}
const elseif = Boolean(elseifContext);
const nodeStart = startBlockIndex(ctx.code, elseifContext?.start ?? node.start, elseif ? ":else" : "#if");
const ifBlock = {
type: "SvelteIfBlock",
elseif: Boolean(elseif),
expression: null,
children: [],
else: null,
parent,
...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
};
const test = getTestFromIfBlock(node);
ctx.scriptLet.nestIfBlock(test, ifBlock, (es) => {
ifBlock.expression = es;
});
const consequent = getConsequentFromIfBlock(node);
for (const child of convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(getChildren(consequent)),
}, ifBlock, ctx)) {
ifBlock.children.push(child);
}
ctx.scriptLet.closeScope();
if (elseif) {
const index = ctx.code.indexOf("if", nodeStart);
ctx.addToken("MustacheKeyword", { start: index, end: index + 2 });
}
extractMustacheBlockTokens(ifBlock, ctx, { startOnly: elseif });
const elseFragment = getAlternateFromIfBlock(node);
if (!elseFragment) {
return ifBlock;
}
const elseStart = startBlockIndexForElse(elseFragment, consequent, test, ctx);
const elseChildren = getChildren(elseFragment);
if (elseChildren.length === 1) {
const c = elseChildren[0];
if (c.type === "IfBlock" && c.elseif) {
const elseBlock = {
type: "SvelteElseBlock",
elseif: true,
children: [],
parent: ifBlock,
...ctx.getConvertLocation({
start: elseStart,
end: c.end,
}),
};
ifBlock.else = elseBlock;
const elseIfBlock = convertIfBlock(c, elseBlock, ctx, {
start: elseStart,
});
// adjust loc
elseBlock.range[1] = elseIfBlock.range[1];
elseBlock.loc.end = {
line: elseIfBlock.loc.end.line,
column: elseIfBlock.loc.end.column,
};
elseBlock.children = [elseIfBlock];
return ifBlock;
}
}
const elseBlock = {
type: "SvelteElseBlock",
elseif: false,
children: [],
parent: ifBlock,
...ctx.getConvertLocation({
start: elseStart,
end: endIndexFromFragment(elseFragment, () => ctx.code.indexOf("}", elseStart + 5) + 1),
}),
};
ifBlock.else = elseBlock;
ctx.scriptLet.nestBlock(elseBlock);
for (const child of convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(elseChildren),
}, elseBlock, ctx)) {
elseBlock.children.push(child);
}
ctx.scriptLet.closeScope();
extractMustacheBlockTokens(elseBlock, ctx, { startOnly: true });
return ifBlock;
}
function startBlockIndexForElse(elseFragment, beforeFragment, lastExpression, ctx) {
const elseChildren = getChildren(elseFragment);
if (elseChildren.length > 0) {
const c = elseChildren[0];
if (c.type === "IfBlock" && c.elseif) {
const contentStart = getWithLoc(getTestFromIfBlock(c)).start;
if (contentStart <= c.start) {
return startBlockIndex(ctx.code, contentStart - 1, ":else");
}
}
return startBlockIndex(ctx.code, c.start, ":else");
}
const beforeEnd = endIndexFromFragment(beforeFragment, () => {
return ctx.code.indexOf("}", getWithLoc(lastExpression).end) + 1;
});
return startBlockIndex(ctx.code, beforeEnd, ":else");
}
/** Convert for EachBlock */
export function convertEachBlock(node, parent, ctx) {
// {#each expr as item, index (key)} {/each}
const nodeStart = startBlockIndex(ctx.code, node.start, "#each");
const eachBlock = {
type: "SvelteEachBlock",
expression: null,
context: null,
index: null,
key: null,
children: [],
else: null,
parent,
...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
};
let indexRange = null;
if (node.index) {
const start = ctx.code.indexOf(node.index, getWithLoc(node.context ?? node.expression).end);
indexRange = {
start,
end: start + node.index.length,
};
}
ctx.scriptLet.nestEachBlock(node.expression, node.context, indexRange, eachBlock, (expression, context, index) => {
eachBlock.expression = expression;
eachBlock.context = context;
eachBlock.index = index;
});
if (node.context) {
const asStart = ctx.code.indexOf("as", getWithLoc(node.expression).end);
ctx.addToken("Keyword", {
start: asStart,
end: asStart + 2,
});
}
if (node.key) {
ctx.scriptLet.addExpression(node.key, eachBlock, null, (key) => {
eachBlock.key = key;
});
}
const body = getBodyFromEachBlock(node);
eachBlock.children.push(...convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(getChildren(body)),
}, eachBlock, ctx));
ctx.scriptLet.closeScope();
extractMustacheBlockTokens(eachBlock, ctx);
const fallbackFragment = getFallbackFromEachBlock(node);
if (!fallbackFragment) {
return eachBlock;
}
const elseStart = startBlockIndexForElse(fallbackFragment, body, node.key || indexRange || node.context || node.expression, ctx);
const elseBlock = {
type: "SvelteElseBlock",
elseif: false,
children: [],
parent: eachBlock,
...ctx.getConvertLocation({
start: elseStart,
end: endIndexFromFragment(fallbackFragment, () => elseStart),
}),
};
eachBlock.else = elseBlock;
ctx.scriptLet.nestBlock(elseBlock);
elseBlock.children.push(...convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(getChildren(fallbackFragment)),
}, elseBlock, ctx));
ctx.scriptLet.closeScope();
extractMustacheBlockTokens(elseBlock, ctx, { startOnly: true });
return eachBlock;
}
/** Convert for AwaitBlock */
export function convertAwaitBlock(node, parent, ctx) {
const nodeStart = startBlockIndex(ctx.code, node.start, "#await");
const awaitBlock = {
type: "SvelteAwaitBlock",
expression: null,
kind: "await",
pending: null,
then: null,
catch: null,
parent,
...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
};
ctx.scriptLet.addExpression(node.expression, awaitBlock, null, (expression) => {
awaitBlock.expression = expression;
});
const pending = getPendingFromAwaitBlock(node);
if (pending) {
const pendingBlock = {
type: "SvelteAwaitPendingBlock",
children: [],
parent: awaitBlock,
...ctx.getConvertLocation({
start: awaitBlock.range[0],
end: endIndexFromBlock(pending, node.expression, ctx),
}),
};
ctx.scriptLet.nestBlock(pendingBlock);
pendingBlock.children.push(...convertChildren(pending, pendingBlock, ctx));
awaitBlock.pending = pendingBlock;
ctx.scriptLet.closeScope();
}
const then = getThenFromAwaitBlock(node);
if (then) {
const awaitThen = !pending;
if (awaitThen) {
awaitBlock.kind = "await-then";
}
const thenStart = awaitBlock.pending
? startBlockIndex(ctx.code, node.value
? getWithLoc(node.value).start
: startIndexFromFragment(then, () => {
return awaitBlock.pending.range[1];
}), ":then")
: nodeStart;
const thenBlock = {
type: "SvelteAwaitThenBlock",
awaitThen,
value: null,
children: [],
parent: awaitBlock,
...ctx.getConvertLocation({
start: thenStart,
end: endIndexFromFragment(then, () => {
return (ctx.code.indexOf("}", node.value ? getWithLoc(node.value).end : thenStart + 5) + 1);
}),
}),
};
if (node.value) {
const baseParam = {
node: node.value,
parent: thenBlock,
callback(value) {
thenBlock.value = value;
},
typing: "any",
};
ctx.scriptLet.nestBlock(thenBlock, (typeCtx) => {
if (!typeCtx) {
return {
param: baseParam,
};
}
const expression = ctx.getText(node.expression);
if (node.expression.type === "Literal") {
return {
param: {
...baseParam,
typing: expression,
},
};
}
const idAwaitThenValue = typeCtx.generateUniqueId("AwaitThenValue");
if (node.expression.type === "Identifier" &&
// We cannot use type annotations like `(x: Foo<x>)` if they have the same identifier name.
!hasIdentifierFor(node.expression.name, baseParam.node)) {
return {
preparationScript: [generateAwaitThenValueType(idAwaitThenValue)],
param: {
...baseParam,
typing: `${idAwaitThenValue}<(typeof ${expression})>`,
},
};
}
const id = typeCtx.generateUniqueId(expression);
return {
preparationScript: [
{
script: `const ${id} = ${expression};`,
nodeType: "VariableDeclaration",
},
generateAwaitThenValueType(idAwaitThenValue),
],
param: {
...baseParam,
typing: `${idAwaitThenValue}<(typeof ${id})>`,
},
};
});
}
else {
ctx.scriptLet.nestBlock(thenBlock);
}
thenBlock.children.push(...convertChildren(then, thenBlock, ctx));
if (awaitBlock.pending) {
extractMustacheBlockTokens(thenBlock, ctx, { startOnly: true });
}
else {
const thenIndex = ctx.code.indexOf("then", getWithLoc(node.expression).end);
ctx.addToken("MustacheKeyword", {
start: thenIndex,
end: thenIndex + 4,
});
}
awaitBlock.then = thenBlock;
ctx.scriptLet.closeScope();
}
const catchFragment = getCatchFromAwaitBlock(node);
if (catchFragment) {
const awaitCatch = !pending && !then;
if (awaitCatch) {
awaitBlock.kind = "await-catch";
}
const catchStart = awaitBlock.then || awaitBlock.pending
? startBlockIndex(ctx.code, node.error
? getWithLoc(node.error).start
: startIndexFromFragment(catchFragment, () => {
return (awaitBlock.then || awaitBlock.pending).range[1];
}), ":catch")
: nodeStart;
const catchBlock = {
type: "SvelteAwaitCatchBlock",
awaitCatch,
error: null,
children: [],
parent: awaitBlock,
...ctx.getConvertLocation({
start: catchStart,
end: endIndexFromFragment(catchFragment, () => {
return (ctx.code.indexOf("}", node.error ? getWithLoc(node.error).end : catchStart + 6) + 1);
}),
}),
};
if (node.error) {
ctx.scriptLet.nestBlock(catchBlock, [
{
node: node.error,
parent: catchBlock,
typing: "Error",
callback: (error) => {
catchBlock.error = error;
},
},
]);
}
else {
ctx.scriptLet.nestBlock(catchBlock);
}
catchBlock.children.push(...convertChildren(catchFragment, catchBlock, ctx));
if (awaitBlock.pending || awaitBlock.then) {
extractMustacheBlockTokens(catchBlock, ctx, { startOnly: true });
}
else {
const catchIndex = ctx.code.indexOf("catch", getWithLoc(node.expression).end);
ctx.addToken("MustacheKeyword", {
start: catchIndex,
end: catchIndex + 5,
});
}
awaitBlock.catch = catchBlock;
ctx.scriptLet.closeScope();
}
extractMustacheBlockTokens(awaitBlock, ctx);
return awaitBlock;
}
/** Convert for KeyBlock */
export function convertKeyBlock(node, parent, ctx) {
const nodeStart = startBlockIndex(ctx.code, node.start, "#key");
const keyBlock = {
type: "SvelteKeyBlock",
expression: null,
children: [],
parent,
...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
};
ctx.scriptLet.addExpression(node.expression, keyBlock, null, (expression) => {
keyBlock.expression = expression;
});
ctx.scriptLet.nestBlock(keyBlock);
keyBlock.children.push(...convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(getChildren(getFragment(node))),
}, keyBlock, ctx));
ctx.scriptLet.closeScope();
extractMustacheBlockTokens(keyBlock, ctx);
return keyBlock;
}
/** Convert for SnippetBlock */
export function convertSnippetBlock(node, parent, ctx) {
// {#snippet x(args)}...{/snippet}
const nodeStart = startBlockIndex(ctx.code, node.start, "#snippet");
const snippetBlock = {
type: "SvelteSnippetBlock",
id: null,
params: [],
children: [],
parent,
...ctx.getConvertLocation({ start: nodeStart, end: node.end }),
};
let beforeClosingParen;
if (node.parameters.length > 0) {
const lastParam = node.parameters[node.parameters.length - 1];
beforeClosingParen = lastParam.typeAnnotation ?? lastParam;
}
else {
beforeClosingParen = node.expression;
}
const closeParenIndex = ctx.code.indexOf(")", getWithLoc(beforeClosingParen).end);
const scopeKind = parent.type === "Program"
? "snippet"
: // use currentScriptScopeKind
null;
ctx.scriptLet.nestSnippetBlock(node.expression, closeParenIndex, snippetBlock, scopeKind, (id, params) => {
snippetBlock.id = id;
snippetBlock.params = params;
});
snippetBlock.children.push(...convertChildren({
nodes:
// Adjust for Svelte v5
trimChildren(node.body.nodes),
}, snippetBlock, ctx));
ctx.scriptLet.closeScope();
extractMustacheBlockTokens(snippetBlock, ctx);
ctx.snippets.push(snippetBlock);
return snippetBlock;
}
/** Extract mustache block tokens */
function extractMustacheBlockTokens(node, ctx, option) {
const startSectionNameStart = indexOf(ctx.code, (c) => Boolean(c.trim()), node.range[0] + 1);
const startSectionNameEnd = indexOf(ctx.code, (c) => c === "}" || !c.trim(), startSectionNameStart + 1);
ctx.addToken("MustacheKeyword", {
start: startSectionNameStart,
end: startSectionNameEnd,
});
if (option?.startOnly) {
return;
}
const endSectionNameEnd = lastIndexOf(ctx.code, (c) => Boolean(c.trim()), node.range[1] - 2) + 1;
const endSectionNameStart = lastIndexOf(ctx.code, (c) => c === "{" || c === "/" || !c.trim(), endSectionNameEnd - 1);
ctx.addToken("MustacheKeyword", {
start: endSectionNameStart,
end: endSectionNameEnd,
});
}
/** Generate Awaited like type code */
function generateAwaitThenValueType(id) {
return {
script: `type ${id}<T> = T extends null | undefined
? T
: T extends { then(value: infer F): any }
? F extends (value: infer V, ...args: any) => any
? ${id}<V>
: never
: T;`,
nodeType: "TSTypeAliasDeclaration",
};
}
/** Checks whether the given name identifier is exists or not. */
function hasIdentifierFor(name, node) {
if (node.type === "Identifier") {
return node.name === name;
}
if (node.type === "ObjectPattern") {
return node.properties.some((property) => property.type === "Property"
? hasIdentifierFor(name, property.value)
: hasIdentifierFor(name, property));
}
if (node.type === "ArrayPattern") {
return node.elements.some((element) => element && hasIdentifierFor(name, element));
}
if (node.type === "RestElement") {
return hasIdentifierFor(name, node.argument);
}
if (node.type === "AssignmentPattern") {
return hasIdentifierFor(name, node.left);
}
return false;
}

View File

@@ -0,0 +1,23 @@
import type ESTree from "estree";
/** indexOf */
export declare function indexOf(str: string, search: (c: string, index: number) => boolean, start: number, end?: number): number;
/** lastIndexOf */
export declare function lastIndexOf(str: string, search: (c: string, index: number) => boolean, end: number): number;
export declare function getWithLoc<N extends ESTree.Comment>(node: N): N & {
start: number;
end: number;
};
export declare function getWithLoc<N extends ESTree.Node | {
start: number;
end: number;
}>(node: N): N & {
start: number;
end: number;
};
export declare function getWithLoc<N extends ESTree.Node | {
start: number;
end: number;
}>(node: N | null | undefined): (N & {
start: number;
end: number;
}) | null | undefined;

View File

@@ -0,0 +1,25 @@
/** indexOf */
export function indexOf(str, search, start, end) {
const endIndex = end ?? str.length;
for (let index = start; index < endIndex; index++) {
const c = str[index];
if (search(c, index)) {
return index;
}
}
return -1;
}
/** lastIndexOf */
export function lastIndexOf(str, search, end) {
for (let index = end; index >= 0; index--) {
const c = str[index];
if (search(c, index)) {
return index;
}
}
return -1;
}
/** Get node with location */
export function getWithLoc(node) {
return node;
}

View File

@@ -0,0 +1,6 @@
import type { SvelteConstTag } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
/** Convert for ConstTag */
export declare function convertConstTag(node: SvAST.ConstTag | Compiler.ConstTag, parent: SvelteConstTag["parent"], ctx: Context): SvelteConstTag;

View File

@@ -0,0 +1,31 @@
import { getDeclaratorFromConstTag } from "../compat.js";
/** Convert for ConstTag */
export function convertConstTag(node, parent, ctx) {
const mustache = {
type: "SvelteConstTag",
declaration: null,
declarations: [],
parent,
...ctx.getConvertLocation(node),
};
// Link declaration and declarations for backward compatibility.
// TODO Remove in v2 and later.
Object.defineProperty(mustache, "declaration", {
get() {
return mustache.declarations[0];
},
set(value) {
mustache.declarations = [value];
},
enumerable: false,
});
ctx.scriptLet.addVariableDeclarator(getDeclaratorFromConstTag(node), mustache, (declaration) => {
mustache.declarations = [declaration];
});
const atConstStart = ctx.code.indexOf("@const", mustache.range[0]);
ctx.addToken("MustacheKeyword", {
start: atConstStart,
end: atConstStart + 6,
});
return mustache;
}

View File

@@ -0,0 +1,19 @@
import type { SvelteAwaitBlock, SvelteAwaitCatchBlock, SvelteAwaitPendingBlock, SvelteAwaitThenBlock, SvelteConstTag, SvelteDebugTag, SvelteEachBlock, SvelteElement, SvelteElseBlockAlone, SvelteHTMLComment, SvelteIfBlock, SvelteIfBlockAlone, SvelteKeyBlock, SvelteMustacheTag, SvelteProgram, SvelteRenderTag, SvelteScriptElement, SvelteSnippetBlock, SvelteStyleElement, SvelteText } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
import type { Child } from "../compat.js";
/** Convert for Fragment or Element or ... */
export declare function convertChildren(fragment: {
children?: SvAST.TemplateNode[];
} | Compiler.Fragment | {
nodes: (Child | SvAST.TemplateNode)[];
}, parent: SvelteProgram | SvelteElement | SvelteIfBlock | SvelteElseBlockAlone | SvelteEachBlock | SvelteAwaitPendingBlock | SvelteAwaitThenBlock | SvelteAwaitCatchBlock | SvelteKeyBlock | SvelteSnippetBlock, ctx: Context): IterableIterator<SvelteText | SvelteElement | SvelteMustacheTag | SvelteDebugTag | SvelteConstTag | SvelteRenderTag | SvelteIfBlockAlone | SvelteEachBlock | SvelteAwaitBlock | SvelteKeyBlock | SvelteSnippetBlock | SvelteHTMLComment>;
/** Extract element tag and tokens */
export declare function extractElementTags<E extends SvelteScriptElement | SvelteElement | SvelteStyleElement>(element: E, ctx: Context, options: {
buildNameNode: (openTokenRange: {
start: number;
end: number;
}) => E["name"];
extractAttribute?: boolean;
}): void;

View File

@@ -0,0 +1,663 @@
import { convertAwaitBlock, convertEachBlock, convertIfBlock, convertKeyBlock, convertSnippetBlock, } from "./block.js";
import { getWithLoc, indexOf } from "./common.js";
import { convertMustacheTag, convertDebugTag, convertRawMustacheTag, } from "./mustache.js";
import { convertText } from "./text.js";
import { convertAttributes } from "./attr.js";
import { convertConstTag } from "./const.js";
import { sortNodes } from "../sort.js";
import { ParseError } from "../../index.js";
import { convertRenderTag } from "./render.js";
import { getChildren, getFragment } from "../compat.js";
/** Convert for Fragment or Element or ... */
export function* convertChildren(fragment, parent, ctx) {
const children = getChildren(fragment);
if (!children)
return;
for (const child of children) {
if (child.type === "Comment") {
yield convertComment(child, parent, ctx);
continue;
}
if (child.type === "Text") {
if (!child.data && child.start === child.end) {
continue;
}
yield convertText(child, parent, ctx);
continue;
}
if (child.type === "RegularElement") {
yield convertHTMLElement(child, parent, ctx);
continue;
}
if (child.type === "Element") {
if (child.name.includes(":")) {
yield convertSpecialElement(child, parent, ctx);
}
else {
yield convertHTMLElement(child, parent, ctx);
}
continue;
}
if (child.type === "Component") {
yield convertComponentElement(child, parent, ctx);
continue;
}
if (child.type === "InlineComponent") {
if (child.name.includes(":")) {
yield convertSpecialElement(child, parent, ctx);
}
else {
yield convertComponentElement(child, parent, ctx);
}
continue;
}
if (child.type === "SvelteComponent" ||
child.type === "SvelteElement" ||
child.type === "SvelteSelf") {
yield convertSpecialElement(child, parent, ctx);
continue;
}
if (child.type === "SlotElement" || child.type === "Slot") {
yield convertSlotElement(child, parent, ctx);
continue;
}
if (child.type === "ExpressionTag" || child.type === "MustacheTag") {
yield convertMustacheTag(child, parent, null, ctx);
continue;
}
if (child.type === "HtmlTag" || child.type === "RawMustacheTag") {
yield convertRawMustacheTag(child, parent, ctx);
continue;
}
if (child.type === "IfBlock") {
// {#if expr} {/if}
yield convertIfBlock(child, parent, ctx);
continue;
}
if (child.type === "EachBlock") {
// {#each expr as item, index (key)} {/each}
yield convertEachBlock(child, parent, ctx);
continue;
}
if (child.type === "AwaitBlock") {
// {#await promise} {:then number} {:catch error} {/await}
yield convertAwaitBlock(child, parent, ctx);
continue;
}
if (child.type === "KeyBlock") {
// {#key expression}...{/key}
yield convertKeyBlock(child, parent, ctx);
continue;
}
if (child.type === "SnippetBlock") {
// {#snippet x(args)}...{/snippet}
yield convertSnippetBlock(child, parent, ctx);
continue;
}
if (child.type === "SvelteWindow" || child.type === "Window") {
yield convertWindowElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteBody" || child.type === "Body") {
yield convertBodyElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteHead" || child.type === "Head") {
yield convertHeadElement(child, parent, ctx);
continue;
}
if (child.type === "TitleElement" || child.type === "Title") {
yield convertTitleElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteOptions" || child.type === "Options") {
yield convertOptionsElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteFragment" || child.type === "SlotTemplate") {
yield convertSlotTemplateElement(child, parent, ctx);
continue;
}
if (child.type === "DebugTag") {
yield convertDebugTag(child, parent, ctx);
continue;
}
if (child.type === "ConstTag") {
yield convertConstTag(child, parent, ctx);
continue;
}
if (child.type === "RenderTag") {
yield convertRenderTag(child, parent, ctx);
continue;
}
if (child.type === "SvelteDocument" || child.type === "Document") {
yield convertDocumentElement(child, parent, ctx);
continue;
}
if (child.type === "SvelteBoundary") {
yield convertSvelteBoundaryElement(child, parent, ctx);
continue;
}
throw new Error(`Unknown type:${child.type}`);
}
}
/** Extract `let:` directives. */
function extractLetDirectives(fragment) {
const letDirectives = [];
const attributes = [];
for (const attr of fragment.attributes) {
if (attr.type === "LetDirective" || attr.type === "Let") {
letDirectives.push(attr);
}
else {
attributes.push(attr);
}
}
return { letDirectives, attributes };
}
/** Check if children needs a scope. */
function needScopeByChildren(fragment) {
const children = getChildren(fragment);
if (!children)
return false;
for (const child of children) {
if (child.type === "ConstTag") {
return true;
}
if (child.type === "SnippetBlock") {
return true;
}
}
return false;
}
/** Convert for HTML Comment */
function convertComment(node, parent, ctx) {
const comment = {
type: "SvelteHTMLComment",
value: node.data,
parent,
...ctx.getConvertLocation(node),
};
ctx.addToken("HTMLComment", node);
return comment;
}
/** Convert for HTMLElement */
function convertHTMLElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "html",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
if (element.name.name === "script" ||
element.name.name === "style" ||
(element.name.name === "template" && ctx.findBlock(element))) {
// Restore the block-like element.
for (const child of element.children) {
if (child.type === "SvelteText") {
child.value = ctx.code.slice(...child.range);
}
}
if (element.name.name === "script") {
ctx.stripScriptCode(element.startTag.range[1], element.endTag?.range[0] ?? element.range[1]);
}
}
if (element.startTag.selfClosing && element.name.name.endsWith("-")) {
// Restore the self-closing block.
const selfClosingBlock = /^[a-z]-+$/iu.test(element.name.name) &&
ctx.findSelfClosingBlock(element);
if (selfClosingBlock) {
element.name.name = selfClosingBlock.originalTag;
}
}
return element;
}
/** Convert for Special element. e.g. <svelte:self> */
function convertSpecialElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "special",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
const thisExpression = (node.type === "SvelteComponent" && node.expression) ||
(node.type === "SvelteElement" && node.tag) ||
(node.type === "InlineComponent" &&
elementName === "svelte:component" &&
node.expression) ||
(node.type === "Element" && elementName === "svelte:element" && node.tag);
if (thisExpression) {
processThisAttribute(node, thisExpression, element, ctx);
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
return element;
}
/** process `this=` */
function processThisAttribute(node, thisValue, element, ctx) {
const startIndex = findStartIndexOfThis(node, ctx);
const eqIndex = ctx.code.indexOf("=", startIndex + 4 /* t,h,i,s */);
let thisNode;
if (typeof thisValue === "string") {
// Svelte v4
// this="..."
thisNode = createSvelteAttribute(startIndex, eqIndex, thisValue);
}
else {
// this={...}
const valueStartIndex = indexOf(ctx.code, (c) => Boolean(c.trim()), eqIndex + 1);
if (thisValue.type === "Literal" &&
typeof thisValue.value === "string" &&
ctx.code[valueStartIndex] !== "{") {
thisNode = createSvelteAttribute(startIndex, eqIndex, thisValue.value);
}
else {
thisNode = createSvelteSpecialDirective(startIndex, eqIndex, thisValue);
}
}
const targetIndex = element.startTag.attributes.findIndex((attr) => thisNode.range[1] <= attr.range[0]);
if (targetIndex === -1) {
element.startTag.attributes.push(thisNode);
}
else {
element.startTag.attributes.splice(targetIndex, 0, thisNode);
}
/** Create SvelteAttribute */
function createSvelteAttribute(startIndex, eqIndex, thisValue) {
const valueStartIndex = indexOf(ctx.code, (c) => Boolean(c.trim()), eqIndex + 1);
const quote = ctx.code.startsWith(thisValue, valueStartIndex)
? null
: ctx.code[valueStartIndex];
const literalStartIndex = quote
? valueStartIndex + quote.length
: valueStartIndex;
const literalEndIndex = literalStartIndex + thisValue.length;
const endIndex = quote ? literalEndIndex + quote.length : literalEndIndex;
const thisAttr = {
type: "SvelteAttribute",
key: null,
boolean: false,
value: [],
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisAttr.key = {
type: "SvelteName",
name: "this",
parent: thisAttr,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
thisAttr.value.push({
type: "SvelteLiteral",
value: thisValue,
parent: thisAttr,
...ctx.getConvertLocation({
start: literalStartIndex,
end: literalEndIndex,
}),
});
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: startIndex + 4,
});
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: valueStartIndex,
end: literalStartIndex,
});
}
ctx.addToken("HTMLText", {
start: literalStartIndex,
end: literalEndIndex,
});
if (quote) {
// "
ctx.addToken("Punctuator", {
start: literalEndIndex,
end: endIndex,
});
}
return thisAttr;
}
/** Create SvelteSpecialDirective */
function createSvelteSpecialDirective(startIndex, eqIndex, expression) {
const closeIndex = ctx.code.indexOf("}", getWithLoc(expression).end);
const endIndex = indexOf(ctx.code, (c) => c === ">" || !c.trim(), closeIndex);
const thisDir = {
type: "SvelteSpecialDirective",
kind: "this",
key: null,
expression: null,
parent: element.startTag,
...ctx.getConvertLocation({ start: startIndex, end: endIndex }),
};
thisDir.key = {
type: "SvelteSpecialDirectiveKey",
parent: thisDir,
...ctx.getConvertLocation({ start: startIndex, end: eqIndex }),
};
// this
ctx.addToken("HTMLIdentifier", {
start: startIndex,
end: startIndex + 4,
});
// =
ctx.addToken("Punctuator", {
start: eqIndex,
end: eqIndex + 1,
});
ctx.scriptLet.addExpression(expression, thisDir, null, (es) => {
thisDir.expression = es;
});
return thisDir;
}
}
/** Find the start index of `this` */
function findStartIndexOfThis(node, ctx) {
// Get the end index of `svelte:element`
const startIndex = ctx.code.indexOf(node.name, node.start) + node.name.length;
const sortedAttrs = [...node.attributes].sort((a, b) => a.start - b.start);
// Find the start index of `this` from the end index of `svelte:element`.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
let thisIndex = indexOf(ctx.code, (_c, index) => ctx.code.startsWith("this", index), startIndex, sortedAttrs[0]?.start ?? node.end);
while (thisIndex < 0) {
if (sortedAttrs.length === 0)
throw new ParseError("Cannot resolved `this` attribute.", thisIndex, ctx);
// Step3: Find the start index of `this` from the end index of attribute.
// However, it only seeks to the start index of the first attribute (or the end index of element node).
const nextStartIndex = sortedAttrs.shift().end;
thisIndex = indexOf(ctx.code, (_c, index) => ctx.code.startsWith("this", index), nextStartIndex, sortedAttrs[0]?.start ?? node.end);
}
return thisIndex;
}
/** Convert for ComponentElement */
function convertComponentElement(node, parent, ctx) {
const locs = ctx.getConvertLocation(node);
const element = {
type: "SvelteElement",
kind: "component",
name: null,
startTag: {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: null,
range: [locs.range[0], null],
loc: {
start: {
line: locs.loc.start.line,
column: locs.loc.start.column,
},
end: null,
},
},
children: [],
endTag: null,
parent,
...locs,
};
ctx.elements.set(element, node);
element.startTag.parent = element;
const elementName = node.name;
const { letDirectives, attributes } = extractLetDirectives(node);
const letParams = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(...convertAttributes(letDirectives, element.startTag, ctx));
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
const fragment = getFragment(node);
if (!letParams.length && !needScopeByChildren(fragment)) {
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
element.children.push(...convertChildren(fragment, element, ctx));
}
else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(...convertAttributes(attributes, element.startTag, ctx));
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(fragment, element, ctx));
ctx.scriptLet.closeScope();
}
extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
const chains = elementName.split(".");
const id = chains.shift();
const idRange = {
start: openTokenRange.start,
end: openTokenRange.start + id.length,
};
// ctx.addToken("Identifier", idRange)
const identifier = {
type: "Identifier",
name: id,
// @ts-expect-error -- ignore
parent: element,
...ctx.getConvertLocation(idRange),
};
let object = identifier;
// eslint-disable-next-line func-style -- var
let esCallback = (es) => {
element.name = es;
};
let start = idRange.end + 1;
for (const name of chains) {
const range = { start, end: start + name.length };
ctx.addToken("HTMLIdentifier", range);
const mem = {
type: "SvelteMemberExpressionName",
object,
property: {
type: "SvelteName",
name,
parent: null,
...ctx.getConvertLocation(range),
},
parent: element,
...ctx.getConvertLocation({
start: openTokenRange.start,
end: range.end,
}),
};
mem.property.parent = mem;
object.parent = mem;
object = mem;
start = range.end + 1;
if (mem.object === identifier) {
esCallback = (es) => {
mem.object = es;
};
}
}
ctx.scriptLet.addExpression(identifier, identifier.parent, null, esCallback);
return object;
},
});
return element;
}
/** Convert for Slot */
function convertSlotElement(node, parent, ctx) {
// Slot translates to SvelteHTMLElement.
const element = convertHTMLElement(node, parent, ctx);
ctx.slots.add(element);
return element;
}
/** Convert for window element. e.g. <svelte:window> */
function convertWindowElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for document element. e.g. <svelte:document> */
function convertDocumentElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for body element. e.g. <svelte:body> */
function convertBodyElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for head element. e.g. <svelte:head> */
function convertHeadElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for title element. e.g. <title> */
function convertTitleElement(node, parent, ctx) {
return convertHTMLElement(node, parent, ctx);
}
/** Convert for options element. e.g. <svelte:options> */
function convertOptionsElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for <svelte:fragment> element. */
function convertSlotTemplateElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Convert for <svelte:boundary> element. */
function convertSvelteBoundaryElement(node, parent, ctx) {
return convertSpecialElement(node, parent, ctx);
}
/** Extract element tag and tokens */
export function extractElementTags(element, ctx, options) {
const startTagNameEnd = indexOf(ctx.code, (c) => c === "/" || c === ">" || !c.trim(), element.range[0] + 1);
const openTokenRange = {
start: element.range[0] + 1,
end: startTagNameEnd,
};
element.name = options.buildNameNode(openTokenRange);
const startTagEnd = ctx.code.indexOf(">", element.startTag.attributes[element.startTag.attributes.length - 1]
?.range[1] ?? openTokenRange.end) + 1;
element.startTag.range[1] = startTagEnd;
element.startTag.loc.end = ctx.getLocFromIndex(startTagEnd);
if (ctx.code[element.range[1] - 1] !== ">") {
// Have not end tag
return;
}
if (ctx.code[element.range[1] - 2] === "/") {
// self close
element.startTag.selfClosing = true;
return;
}
const endTagOpen = ctx.code.lastIndexOf("<", element.range[1] - 1);
if (endTagOpen <= startTagEnd - 1) {
// void element
return;
}
const endTagNameStart = endTagOpen + 2;
const endTagNameEnd = indexOf(ctx.code, (c) => c === ">" || !c.trim(), endTagNameStart);
const endTagClose = ctx.code.indexOf(">", endTagNameEnd);
element.endTag = {
type: "SvelteEndTag",
parent: element,
...ctx.getConvertLocation({ start: endTagOpen, end: endTagClose + 1 }),
};
ctx.addToken("HTMLIdentifier", {
start: endTagNameStart,
end: endTagNameEnd,
});
}

View File

@@ -0,0 +1 @@
export { convertSvelteRoot } from "./root.js";

View File

@@ -0,0 +1 @@
export { convertSvelteRoot } from "./root.js";

View File

@@ -0,0 +1,10 @@
import type { SvelteDebugTag, SvelteMustacheTag, SvelteMustacheTagRaw, SvelteMustacheTagText } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
/** Convert for MustacheTag */
export declare function convertMustacheTag(node: SvAST.MustacheTag | Compiler.ExpressionTag, parent: SvelteMustacheTag["parent"], typing: string | null, ctx: Context): SvelteMustacheTagText;
/** Convert for MustacheTag */
export declare function convertRawMustacheTag(node: SvAST.RawMustacheTag | Compiler.HtmlTag, parent: SvelteMustacheTag["parent"], ctx: Context): SvelteMustacheTagRaw;
/** Convert for DebugTag */
export declare function convertDebugTag(node: SvAST.DebugTag, parent: SvelteDebugTag["parent"], ctx: Context): SvelteDebugTag;

View File

@@ -0,0 +1,49 @@
import { hasTypeInfo } from "../../utils/index.js";
/** Convert for MustacheTag */
export function convertMustacheTag(node, parent, typing, ctx) {
return convertMustacheTag0(node, "text", parent, typing, ctx);
}
/** Convert for MustacheTag */
export function convertRawMustacheTag(node, parent, ctx) {
const mustache = convertMustacheTag0(node, "raw", parent, null, ctx);
const atHtmlStart = ctx.code.indexOf("@html", mustache.range[0]);
ctx.addToken("MustacheKeyword", {
start: atHtmlStart,
end: atHtmlStart + 5,
});
return mustache;
}
/** Convert for DebugTag */
export function convertDebugTag(node, parent, ctx) {
const mustache = {
type: "SvelteDebugTag",
identifiers: [],
parent,
...ctx.getConvertLocation(node),
};
for (const id of node.identifiers) {
ctx.scriptLet.addExpression(id, mustache, null, (es) => {
mustache.identifiers.push(es);
});
}
const atDebugStart = ctx.code.indexOf("@debug", mustache.range[0]);
ctx.addToken("MustacheKeyword", {
start: atDebugStart,
end: atDebugStart + 6,
});
return mustache;
}
/** Convert to MustacheTag */
function convertMustacheTag0(node, kind, parent, typing, ctx) {
const mustache = {
type: "SvelteMustacheTag",
kind,
expression: null,
parent,
...ctx.getConvertLocation(node),
};
ctx.scriptLet.addExpression(node.expression, mustache, hasTypeInfo(node.expression) ? null : typing, (es) => {
mustache.expression = es;
});
return mustache;
}

View File

@@ -0,0 +1,5 @@
import type { SvelteRenderTag } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
/** Convert for RenderTag */
export declare function convertRenderTag(node: Compiler.RenderTag, parent: SvelteRenderTag["parent"], ctx: Context): SvelteRenderTag;

View File

@@ -0,0 +1,21 @@
import { getWithLoc } from "./common.js";
/** Convert for RenderTag */
export function convertRenderTag(node, parent, ctx) {
const mustache = {
type: "SvelteRenderTag",
expression: null,
parent,
...ctx.getConvertLocation(node),
};
const callRange = getWithLoc(node.expression);
ctx.scriptLet.addExpressionFromRange([callRange.start, callRange.end], mustache, null, (expression) => {
mustache.expression = expression;
mustache.expression.parent = mustache;
});
const atRenderStart = ctx.code.indexOf("@render", mustache.range[0]);
ctx.addToken("MustacheKeyword", {
start: atRenderStart,
end: atRenderStart + 7,
});
return mustache;
}

View File

@@ -0,0 +1,8 @@
import type * as SvAST from "../svelte-ast-types.js";
import type * as Compiler from "../svelte-ast-types-for-v5.js";
import type { SvelteProgram } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
/**
* Convert root
*/
export declare function convertSvelteRoot(svelteAst: Compiler.Root | SvAST.AstLegacy, ctx: Context): SvelteProgram;

View File

@@ -0,0 +1,261 @@
import { convertChildren, extractElementTags } from "./element.js";
import { convertAttributes } from "./attr.js";
import { parseScriptWithoutAnalyzeScope } from "../script.js";
import { fixLocations } from "../../context/fix-locations.js";
import { getChildren, getFragmentFromRoot, getInstanceFromRoot, getModuleFromRoot, getOptionsFromRoot, } from "../compat.js";
import { sortNodes } from "../sort.js";
import { withoutProjectParserOptions } from "../parser-options.js";
/**
* Convert root
*/
export function convertSvelteRoot(svelteAst, ctx) {
const ast = {
type: "Program",
body: [],
comments: ctx.comments,
sourceType: "module",
tokens: ctx.tokens,
parent: null,
...ctx.getConvertLocation({ start: 0, end: ctx.code.length }),
};
const body = ast.body;
const snippetChildren = [];
const fragment = getFragmentFromRoot(svelteAst);
if (fragment) {
let children = getChildren(fragment);
const options = getOptionsFromRoot(svelteAst);
if (options) {
children = [...children];
if (!children.some((node, idx) => {
if (options.end <= node.start) {
children.splice(idx, 0, options);
return true;
}
return false;
})) {
children.push(options);
}
}
const nonSnippetChildren = [];
for (const child of children) {
if (child.type === "SnippetBlock") {
snippetChildren.push(child);
}
else {
nonSnippetChildren.push(child);
}
}
body.push(...convertChildren({ nodes: nonSnippetChildren }, ast, ctx));
}
let script = null;
const instance = getInstanceFromRoot(svelteAst);
if (instance) {
script = {
type: "SvelteScriptElement",
name: null,
startTag: null,
body: [],
endTag: null,
parent: ast,
...ctx.getConvertLocation(instance),
};
extractAttributes(script, ctx);
extractElementTags(script, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: "script",
parent: script,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
body.push(script);
}
const module = getModuleFromRoot(svelteAst);
if (module) {
const script = {
type: "SvelteScriptElement",
name: null,
startTag: null,
body: [],
endTag: null,
parent: ast,
...ctx.getConvertLocation(module),
};
extractAttributes(script, ctx);
extractElementTags(script, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: "script",
parent: script,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
body.push(script);
}
if (svelteAst.css) {
const style = {
type: "SvelteStyleElement",
name: null,
startTag: null,
children: [],
endTag: null,
parent: ast,
...ctx.getConvertLocation(svelteAst.css),
};
extractAttributes(style, ctx);
extractElementTags(style, ctx, {
buildNameNode: (openTokenRange) => {
ctx.addToken("HTMLIdentifier", openTokenRange);
const name = {
type: "SvelteName",
name: "style",
parent: style,
...ctx.getConvertLocation(openTokenRange),
};
return name;
},
});
if (style.endTag && style.startTag.range[1] < style.endTag.range[0]) {
const contentRange = {
start: style.startTag.range[1],
end: style.endTag.range[0],
};
ctx.addToken("HTMLText", contentRange);
style.children = [
{
type: "SvelteText",
value: ctx.code.slice(contentRange.start, contentRange.end),
parent: style,
...ctx.getConvertLocation(contentRange),
},
];
}
body.push(style);
}
body.push(...convertChildren({ nodes: snippetChildren }, ast, ctx));
if (script)
convertGenericsAttribute(script, ctx);
// Set the scope of the Program node.
ctx.scriptLet.addProgramRestore((node, _tokens, _comments, { scopeManager, registerNodeToScope, addPostProcess }) => {
const scopes = [];
for (const scope of scopeManager.scopes) {
if (scope.block === node) {
registerNodeToScope(ast, scope);
scopes.push(scope);
}
}
addPostProcess(() => {
// Reverts the node indicated by `block` to the original Program node.
// This state is incorrect, but `eslint-utils`'s `referenceTracker.iterateEsmReferences()` tracks import statements
// from Program nodes set to `block` in global scope. This can only be handled by the original Program node.
scopeManager.globalScope.block = node;
});
});
sortNodes(body);
return ast;
}
/** Extract attrs */
function extractAttributes(element, ctx) {
element.startTag = {
type: "SvelteStartTag",
attributes: [],
selfClosing: false,
parent: element,
range: [element.range[0], null],
loc: {
start: {
line: element.loc.start.line,
column: element.loc.start.column,
},
end: null,
},
};
const block = ctx.findBlock(element);
if (block) {
element.startTag.attributes.push(...convertAttributes(block.attrs, element.startTag, ctx));
}
}
/** Convert generics attribute */
function convertGenericsAttribute(script, ctx) {
const lang = ctx.sourceCode.scripts.attrs.lang;
if (lang !== "ts" && lang !== "typescript") {
return;
}
const genericsAttribute = script.startTag.attributes.find((attr) => {
return (attr.type === "SvelteAttribute" &&
attr.key.name === "generics" &&
attr.value.length === 1 &&
attr.value[0].type === "SvelteLiteral");
});
if (!genericsAttribute) {
return;
}
const value = genericsAttribute.value[0];
const genericValueCode = ctx.code.slice(value.range[0], value.range[1]);
const scriptLet = `void function<${genericValueCode}>(){}`;
let result;
try {
result = parseScriptWithoutAnalyzeScope(scriptLet, ctx.sourceCode.scripts.attrs, withoutProjectParserOptions(ctx.parserOptions));
}
catch {
// ignore
return;
}
delete genericsAttribute.boolean;
delete genericsAttribute.value;
// Remove value token indexes
sortNodes(ctx.tokens);
const firstTokenIndex = ctx.tokens.findIndex((token) => value.range[0] <= token.range[0] && token.range[1] <= value.range[1]);
const lastTokenCount = ctx.tokens
.slice(firstTokenIndex)
.findIndex((token) => value.range[1] <= token.range[0]);
ctx.tokens.splice(firstTokenIndex, lastTokenCount >= 0 ? lastTokenCount : Infinity);
const generics = genericsAttribute;
generics.type = "SvelteGenericsDirective";
generics.params = [];
result.ast.tokens.shift(); // void
result.ast.tokens.shift(); // function
result.ast.tokens.shift(); // <
result.ast.tokens.pop(); // }
result.ast.tokens.pop(); // {
result.ast.tokens.pop(); // )
result.ast.tokens.pop(); // (
result.ast.tokens.pop(); // >
fixLocations(result.ast, result.ast.tokens, result.ast.comments, value.range[0] - 14, result.visitorKeys, ctx);
const { ast } = result;
const statement = ast.body[0];
const rawExpression = statement.expression;
const fnDecl = rawExpression.argument;
const typeParameters = fnDecl
.typeParameters;
const params = typeParameters.params;
// Replace tokens
for (const tokensKey of ["tokens", "comments"]) {
for (const token of result.ast[tokensKey]) {
if (params.every((param) => token.range[1] <= param.range[0] ||
param.range[1] <= token.range[0])) {
ctx[tokensKey].push(token);
}
}
}
for (const param of params) {
param.parent = generics;
generics.params.push(param);
ctx.scriptLet.addGenericTypeAliasDeclaration(param, (id, typeNode) => {
param.name = id;
if (param.constraint) {
param.constraint = typeNode;
}
}, (typeNode) => {
param.default = typeNode;
});
}
}

View File

@@ -0,0 +1,7 @@
import type { SvelteLiteral, SvelteText } from "../../ast/index.js";
import type { Context } from "../../context/index.js";
import type * as SvAST from "../svelte-ast-types.js";
/** Convert for Text */
export declare function convertText(node: SvAST.Text, parent: SvelteText["parent"], ctx: Context): SvelteText;
/** Convert for Text to Literal */
export declare function convertTextToLiteral(node: SvAST.Text, parent: SvelteLiteral["parent"], ctx: Context): SvelteLiteral;

View File

@@ -0,0 +1,40 @@
/** Convert for Text */
export function convertText(node, parent, ctx) {
const text = {
type: "SvelteText",
value: node.data,
parent,
...ctx.getConvertLocation(node),
};
extractTextTokens(node, ctx);
return text;
}
/** Convert for Text to Literal */
export function convertTextToLiteral(node, parent, ctx) {
const text = {
type: "SvelteLiteral",
value: node.data,
parent,
...ctx.getConvertLocation(node),
};
extractTextTokens(node, ctx);
return text;
}
/** Extract tokens */
function extractTextTokens(node, ctx) {
const loc = node;
let start = loc.start;
let word = false;
for (let index = loc.start; index < loc.end; index++) {
if (word !== Boolean(ctx.code[index].trim())) {
if (start < index) {
ctx.addToken("HTMLText", { start, end: index });
}
word = !word;
start = index;
}
}
if (start < loc.end) {
ctx.addToken("HTMLText", { start, end: loc.end });
}
}