- 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
520 lines
19 KiB
JavaScript
520 lines
19 KiB
JavaScript
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;
|
|
}
|