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,7 @@
import type * as ESTree from "estree";
import type { Comment, Token } from "../ast/index.js";
import type { Context } from "./index.js";
/** Fix locations */
export declare function fixLocations(node: ESTree.Node, tokens: Token[], comments: Comment[], offset: number, visitorKeys: {
[type: string]: string[];
} | undefined, ctx: Context): void;

View File

@@ -0,0 +1,62 @@
import { traverseNodes } from "../traverse.js";
/** Fix locations */
export function fixLocations(node, tokens, comments, offset, visitorKeys, ctx) {
if (offset === 0) {
return;
}
const traversed = new Set();
traverseNodes(node, {
visitorKeys,
enterNode: (n) => {
if (traversed.has(n)) {
return;
}
traversed.add(n);
if (traversed.has(n.range)) {
if (!traversed.has(n.loc)) {
// However, `Node#loc` may not be shared.
const locs = ctx.getConvertLocation({
start: n.range[0],
end: n.range[1],
});
applyLocs(n, locs);
traversed.add(n.loc);
}
}
else {
const start = n.range[0] + offset;
const end = n.range[1] + offset;
const locs = ctx.getConvertLocation({ start, end });
applyLocs(n, locs);
traversed.add(n.range);
traversed.add(n.loc);
}
},
leaveNode: Function.prototype,
});
for (const t of tokens) {
const start = t.range[0] + offset;
const end = t.range[1] + offset;
const locs = ctx.getConvertLocation({ start, end });
applyLocs(t, locs);
}
for (const t of comments) {
const start = t.range[0] + offset;
const end = t.range[1] + offset;
const locs = ctx.getConvertLocation({ start, end });
applyLocs(t, locs);
}
}
/**
* applyLocs
*/
function applyLocs(target, locs) {
target.loc = locs.loc;
target.range = locs.range;
if (typeof target.start === "number") {
delete target.start;
}
if (typeof target.end === "number") {
delete target.end;
}
}

View File

@@ -0,0 +1,116 @@
import type { Comment, Locations, SvelteElement, SvelteHTMLElement, SvelteScriptElement, SvelteSnippetBlock, SvelteStyleElement, Token } from "../ast/index.js";
import type ESTree from "estree";
import type * as SvAST from "../parser/svelte-ast-types.js";
import type * as Compiler from "../parser/svelte-ast-types-for-v5.js";
import { ScriptLetContext } from "./script-let.js";
import { LetDirectiveCollections } from "./let-directive-collection.js";
import { type NormalizedParserOptions } from "../parser/parser-options.js";
export declare class ScriptsSourceCode {
private raw;
private trimmedRaw;
readonly attrs: Record<string, string | undefined>;
private _appendScriptLets;
separateIndexes: number[];
constructor(script: string, attrs: Record<string, string | undefined>);
getCurrentVirtualCode(): string;
getCurrentVirtualCodeInfo(): {
script: string;
render: string;
rootScope: string;
};
getCurrentVirtualCodeLength(): number;
addLet(letCode: string, kind: "generics" | "snippet" | "render"): {
start: number;
end: number;
};
stripCode(start: number, end: number): void;
}
export type ContextSourceCode = {
template: string;
scripts: ScriptsSourceCode;
};
export declare class Context {
readonly code: string;
readonly parserOptions: NormalizedParserOptions;
readonly sourceCode: ContextSourceCode;
readonly tokens: Token[];
readonly comments: Comment[];
private readonly locs;
private readonly locsMap;
readonly scriptLet: ScriptLetContext;
readonly letDirCollections: LetDirectiveCollections;
readonly slots: Set<SvelteHTMLElement>;
readonly elements: Map<SvelteElement, SvAST.Element | SvAST.InlineComponent | SvAST.Window | SvAST.Document | SvAST.Body | SvAST.Head | SvAST.Title | SvAST.Options | SvAST.SlotTemplate | SvAST.Slot | Compiler.ElementLike>;
readonly snippets: SvelteSnippetBlock[];
private readonly state;
private readonly blocks;
constructor(code: string, parserOptions: NormalizedParserOptions);
getLocFromIndex(index: number): {
line: number;
column: number;
};
getIndexFromLoc(loc: {
line: number;
column: number;
}): number;
/**
* Get the location information of the given node.
* @param node The node.
*/
getConvertLocation(node: {
start: number;
end: number;
} | ESTree.Node): Locations;
addComment(comment: Comment): void;
/**
* Add token to tokens
*/
addToken(type: Token["type"], range: {
start: number;
end: number;
}): Token;
/**
* get text
*/
getText(range: {
start: number;
end: number;
} | ESTree.Node): string;
isTypeScript(): boolean;
stripScriptCode(start: number, end: number): void;
findBlock(element: SvelteScriptElement | SvelteStyleElement | SvelteElement): Block | undefined;
findSelfClosingBlock(element: SvelteElement): SelfClosingBlock | undefined;
}
type Block = {
tag: "script" | "style" | "template";
originalTag: string;
attrs: Compiler.Attribute[];
selfClosing?: false;
contentRange: [number, number];
startTagRange: [number, number];
endTagRange: [number, number];
} | SelfClosingBlock;
type SelfClosingBlock = {
tag: "script" | "style" | "template";
originalTag: string;
attrs: Compiler.Attribute[];
selfClosing: true;
startTagRange: [number, number];
};
export declare class LinesAndColumns {
private readonly lineStartIndices;
constructor(code: string);
getLocFromIndex(index: number): {
line: number;
column: number;
};
getIndexFromLoc(loc: {
line: number;
column: number;
}): number;
/**
* Get the location information of the given indexes.
*/
getLocations(start: number, end: number): Locations;
}
export {};

View File

@@ -0,0 +1,340 @@
import { ScriptLetContext } from "./script-let.js";
import { LetDirectiveCollections } from "./let-directive-collection.js";
import { parseAttributes } from "../parser/html.js";
import { sortedLastIndex } from "../utils/index.js";
import { isTypeScript, } from "../parser/parser-options.js";
export class ScriptsSourceCode {
constructor(script, attrs) {
this._appendScriptLets = null;
this.separateIndexes = [];
this.raw = script;
this.trimmedRaw = script.trimEnd();
this.attrs = attrs;
this.separateIndexes = [script.length];
}
getCurrentVirtualCode() {
if (this._appendScriptLets == null) {
return this.raw;
}
return (this.trimmedRaw +
this._appendScriptLets.separate +
this._appendScriptLets.beforeSpaces +
this._appendScriptLets.render +
this._appendScriptLets.snippet +
this._appendScriptLets.generics);
}
getCurrentVirtualCodeInfo() {
if (this._appendScriptLets == null) {
return { script: this.raw, render: "", rootScope: "" };
}
return {
script: this.trimmedRaw + this._appendScriptLets.separate,
render: this._appendScriptLets.beforeSpaces + this._appendScriptLets.render,
rootScope: this._appendScriptLets.snippet + this._appendScriptLets.generics,
};
}
getCurrentVirtualCodeLength() {
if (this._appendScriptLets == null) {
return this.raw.length;
}
return (this.trimmedRaw.length +
this._appendScriptLets.separate.length +
this._appendScriptLets.beforeSpaces.length +
this._appendScriptLets.render.length +
this._appendScriptLets.snippet.length +
this._appendScriptLets.generics.length);
}
addLet(letCode, kind) {
if (this._appendScriptLets == null) {
const currentLength = this.trimmedRaw.length;
this.separateIndexes = [currentLength, currentLength + 1];
const after = this.raw.slice(currentLength + 2);
this._appendScriptLets = {
separate: "\n;",
beforeSpaces: after,
render: "",
snippet: "",
generics: "",
};
}
const start = this.getCurrentVirtualCodeLength();
this._appendScriptLets[kind] += letCode;
return {
start,
end: this.getCurrentVirtualCodeLength(),
};
}
stripCode(start, end) {
this.raw =
this.raw.slice(0, start) +
this.raw.slice(start, end).replace(/[^\n\r ]/g, " ") +
this.raw.slice(end);
this.trimmedRaw =
this.trimmedRaw.slice(0, start) +
this.trimmedRaw.slice(start, end).replace(/[^\n\r ]/g, " ") +
this.trimmedRaw.slice(end);
}
}
export class Context {
constructor(code, parserOptions) {
this.tokens = [];
this.comments = [];
this.locsMap = new Map();
this.letDirCollections = new LetDirectiveCollections();
this.slots = new Set();
this.elements = new Map();
this.snippets = [];
// ----- States ------
this.state = {};
this.blocks = [];
this.code = code;
this.parserOptions = parserOptions;
this.locs = new LinesAndColumns(code);
const spaces = code.replace(/[^\n\r ]/g, " ");
let templateCode = "";
let scriptCode = "";
const scriptAttrs = {};
let start = 0;
for (const block of extractBlocks(code)) {
if (block.tag === "template") {
if (block.selfClosing) {
continue;
}
const lang = block.attrs.find((attr) => attr.name === "lang");
if (!lang || !Array.isArray(lang.value)) {
continue;
}
const langValue = lang.value[0];
if (!langValue ||
langValue.type !== "Text" ||
langValue.data === "html") {
continue;
}
}
this.blocks.push(block);
if (block.selfClosing) {
// Self-closing blocks are temporarily replaced with `<s---->` or `<t---->` tag
// because the svelte compiler cannot parse self-closing block(script, style) tags.
// It will be restored later in `convertHTMLElement()` processing.
templateCode += `${code.slice(start, block.startTagRange[0] + 2 /* `<` and first letter */)}${"-".repeat(block.tag.length - 1 /* skip first letter */)}${code.slice(block.startTagRange[0] + 1 /* skip `<` */ + block.tag.length, block.startTagRange[1])}`;
scriptCode += spaces.slice(start, block.startTagRange[1]);
start = block.startTagRange[1];
}
else {
templateCode +=
code.slice(start, block.contentRange[0]) +
spaces.slice(block.contentRange[0], block.contentRange[1]);
if (block.tag === "script") {
scriptCode +=
spaces.slice(start, block.contentRange[0]) +
code.slice(...block.contentRange);
for (const attr of block.attrs) {
if (Array.isArray(attr.value)) {
const attrValue = attr.value[0];
scriptAttrs[attr.name] =
attrValue && attrValue.type === "Text"
? attrValue.data
: undefined;
}
}
}
else {
scriptCode += spaces.slice(start, block.contentRange[1]);
}
start = block.contentRange[1];
}
}
templateCode += code.slice(start);
scriptCode += spaces.slice(start);
this.sourceCode = {
template: templateCode,
scripts: new ScriptsSourceCode(scriptCode, scriptAttrs),
};
this.scriptLet = new ScriptLetContext(this);
}
getLocFromIndex(index) {
let loc = this.locsMap.get(index);
if (!loc) {
loc = this.locs.getLocFromIndex(index);
this.locsMap.set(index, loc);
}
return {
line: loc.line,
column: loc.column,
};
}
getIndexFromLoc(loc) {
return this.locs.getIndexFromLoc(loc);
}
/**
* Get the location information of the given node.
* @param node The node.
*/
getConvertLocation(node) {
const { start, end } = node;
return {
range: [start, end],
loc: {
start: this.getLocFromIndex(start),
end: this.getLocFromIndex(end),
},
};
}
addComment(comment) {
this.comments.push(comment);
}
/**
* Add token to tokens
*/
addToken(type, range) {
const token = {
type,
value: this.getText(range),
...this.getConvertLocation(range),
};
this.tokens.push(token);
return token;
}
/**
* get text
*/
getText(range) {
return this.code.slice(range.start, range.end);
}
isTypeScript() {
if (this.state.isTypeScript != null) {
return this.state.isTypeScript;
}
const lang = this.sourceCode.scripts.attrs.lang;
return (this.state.isTypeScript = isTypeScript(this.parserOptions, lang));
}
stripScriptCode(start, end) {
this.sourceCode.scripts.stripCode(start, end);
}
findBlock(element) {
const tag = element.type === "SvelteScriptElement"
? "script"
: element.type === "SvelteStyleElement"
? "style"
: element.name.name.toLowerCase();
return this.blocks.find((block) => block.tag === tag &&
!block.selfClosing &&
element.range[0] <= block.contentRange[0] &&
block.contentRange[1] <= element.range[1]);
}
findSelfClosingBlock(element) {
return this.blocks.find((block) => Boolean(block.selfClosing &&
element.startTag.range[0] <= block.startTagRange[0] &&
block.startTagRange[1] <= element.startTag.range[1]));
}
}
function isValidStartTagOpenIndex(code, startTagOpenIndex) {
const prev = code.slice(0, startTagOpenIndex);
return />\s*$|^\s*$/m.test(prev);
}
/** Extract <script> blocks */
function* extractBlocks(code) {
const startTagOpenRe = /<!--[\s\S]*?-->|<(script|style|template)([\s>])/giu;
const endScriptTagRe = /<\/script>/giu;
const endStyleTagRe = /<\/style>/giu;
const endTemplateTagRe = /<\/template>/giu;
let startTagOpenMatch;
while ((startTagOpenMatch = startTagOpenRe.exec(code))) {
const [, tag, nextChar] = startTagOpenMatch;
if (!tag) {
continue;
}
const startTagStart = startTagOpenMatch.index;
if (!isValidStartTagOpenIndex(code, startTagStart)) {
continue;
}
let startTagEnd = startTagOpenRe.lastIndex;
const lowerTag = tag.toLowerCase();
let attrs = [];
if (!nextChar.trim()) {
const attrsData = parseAttributes(code, startTagOpenRe.lastIndex);
attrs = attrsData.attributes;
startTagEnd = attrsData.index;
if (code[startTagEnd] === "/" && code[startTagEnd + 1] === ">") {
yield {
tag: lowerTag,
originalTag: tag,
attrs,
selfClosing: true,
startTagRange: [startTagStart, startTagEnd + 2],
};
continue;
}
if (code[startTagEnd] === ">") {
startTagEnd++;
}
else {
continue;
}
}
const endTagRe = lowerTag === "script"
? endScriptTagRe
: lowerTag === "style"
? endStyleTagRe
: endTemplateTagRe;
endTagRe.lastIndex = startTagEnd;
const endTagMatch = endTagRe.exec(code);
if (endTagMatch) {
const endTagStart = endTagMatch.index;
const endTagEnd = endTagRe.lastIndex;
yield {
tag: lowerTag,
originalTag: tag,
attrs,
startTagRange: [startTagStart, startTagEnd],
contentRange: [startTagEnd, endTagStart],
endTagRange: [endTagStart, endTagEnd],
};
startTagOpenRe.lastIndex = endTagEnd;
}
}
}
export class LinesAndColumns {
constructor(code) {
const len = code.length;
const lineStartIndices = [0];
for (let index = 0; index < len; index++) {
const c = code[index];
if (c === "\r") {
const next = code[index + 1] || "";
if (next === "\n") {
index++;
}
lineStartIndices.push(index + 1);
}
else if (c === "\n") {
lineStartIndices.push(index + 1);
}
}
this.lineStartIndices = lineStartIndices;
}
getLocFromIndex(index) {
const lineNumber = sortedLastIndex(this.lineStartIndices, (target) => target - index);
return {
line: lineNumber,
column: index - this.lineStartIndices[lineNumber - 1],
};
}
getIndexFromLoc(loc) {
const lineStartIndex = this.lineStartIndices[loc.line - 1];
const positionIndex = lineStartIndex + loc.column;
return positionIndex;
}
/**
* Get the location information of the given indexes.
*/
getLocations(start, end) {
return {
range: [start, end],
loc: {
start: this.getLocFromIndex(start),
end: this.getLocFromIndex(end),
},
};
}
}

View File

@@ -0,0 +1,15 @@
import type { SvelteLetDirective, SvelteName } from "../ast/index.js";
import type * as ESTree from "estree";
import type { ScriptLetBlockParam, ScriptLetCallback } from "./script-let.js";
/** A class that collects pattern nodes for Let directives. */
export declare class LetDirectiveCollection {
private readonly list;
getLetParams(): ScriptLetBlockParam[];
addPattern(pattern: ESTree.Pattern | SvelteName, directive: SvelteLetDirective, typing: string, ...callbacks: ScriptLetCallback<ESTree.Pattern>[]): ScriptLetCallback<ESTree.Pattern>[];
}
export declare class LetDirectiveCollections {
private readonly stack;
beginExtract(): void;
getCollection(): LetDirectiveCollection;
extract(): LetDirectiveCollection;
}

View File

@@ -0,0 +1,36 @@
/** A class that collects pattern nodes for Let directives. */
export class LetDirectiveCollection {
constructor() {
this.list = [];
}
getLetParams() {
return this.list;
}
addPattern(pattern, directive, typing, ...callbacks) {
this.list.push({
node: pattern,
parent: directive,
typing,
callback(node, options) {
for (const callback of callbacks) {
callback(node, options);
}
},
});
return callbacks;
}
}
export class LetDirectiveCollections {
constructor() {
this.stack = [];
}
beginExtract() {
this.stack.push(new LetDirectiveCollection());
}
getCollection() {
return this.stack[this.stack.length - 1];
}
extract() {
return this.stack.pop();
}
}

View File

@@ -0,0 +1,91 @@
import type { ScopeManager, Scope } from "eslint-scope";
import type * as ESTree from "estree";
import type { TSESTree } from "@typescript-eslint/types";
import type { Context } from "./index.js";
import type { Comment, SvelteEachBlock, SvelteIfBlock, SvelteName, SvelteNode, SvelteSnippetBlock, Token } from "../ast/index.js";
import type { ESLintExtendedProgram } from "../parser/index.js";
export type ScriptLetCallback<E extends ESTree.Node> = (es: E, options: ScriptLetCallbackOption) => void;
export type ScriptLetCallbackOption = {
getScope: (node: ESTree.Node) => Scope;
registerNodeToScope: (node: any, scope: Scope) => void;
scopeManager: ScopeManager;
visitorKeys?: {
[type: string]: string[];
};
};
export type ScriptLetRestoreCallback = (node: ESTree.Node, tokens: Token[], comments: Comment[], options: ScriptLetRestoreCallbackOption) => void;
type ScriptLetRestoreCallbackOption = {
getScope: (node: ESTree.Node) => Scope;
registerNodeToScope: (node: any, scope: Scope) => void;
scopeManager: ScopeManager;
visitorKeys?: {
[type: string]: string[];
};
addPostProcess: (callback: () => void) => void;
};
type StatementNodeType = `${TSESTree.Statement["type"]}`;
type TypeGenHelper = {
generateUniqueId: (base: string) => string;
};
type ObjectShorthandProperty = ESTree.Property & {
key: ESTree.Identifier;
value: ESTree.Identifier;
};
export type ScriptLetBlockParam = {
node: ESTree.Pattern | SvelteName;
parent: SvelteNode;
typing: string;
callback: (node: ESTree.Pattern, options: ScriptLetCallbackOption) => void;
};
/**
* A class that handles script fragments.
* The script fragment AST node remaps and connects to the original directive AST node.
*/
export declare class ScriptLetContext {
private readonly script;
private readonly ctx;
private readonly restoreCallbacks;
private readonly programRestoreCallbacks;
private readonly closeScopeCallbacks;
private readonly unique;
private currentScriptScopeKind;
constructor(ctx: Context);
addExpression<E extends ESTree.Expression>(expression: E | SvelteName, parent: SvelteNode, typing?: string | null, ...callbacks: ScriptLetCallback<E>[]): ScriptLetCallback<E>[];
addExpressionFromRange<E extends ESTree.Expression>(range: [number, number], parent: SvelteNode, typing?: string | null, ...callbacks: ScriptLetCallback<E>[]): ScriptLetCallback<E>[];
addObjectShorthandProperty(identifier: SvelteName, parent: SvelteNode, ...callbacks: ScriptLetCallback<ObjectShorthandProperty>[]): void;
addVariableDeclarator(declarator: ESTree.VariableDeclarator | ESTree.AssignmentExpression, parent: SvelteNode, ...callbacks: ScriptLetCallback<ESTree.VariableDeclarator>[]): ScriptLetCallback<ESTree.VariableDeclarator>[];
addGenericTypeAliasDeclaration(param: TSESTree.TSTypeParameter, callbackId: (id: TSESTree.Identifier, type: TSESTree.TypeNode) => void, callbackDefault: (type: TSESTree.TypeNode) => void): void;
nestIfBlock(expression: ESTree.Expression, ifBlock: SvelteIfBlock, callback: ScriptLetCallback<ESTree.Expression>): void;
nestEachBlock(expression: ESTree.Expression, context: ESTree.Pattern | null, indexRange: {
start: number;
end: number;
} | null, eachBlock: SvelteEachBlock, callback: (expr: ESTree.Expression, ctx: ESTree.Pattern | null, index: ESTree.Identifier | null) => void): void;
nestSnippetBlock(id: ESTree.Identifier, closeParentIndex: number, snippetBlock: SvelteSnippetBlock, kind: "snippet" | null, callback: (id: ESTree.Identifier, params: ESTree.Pattern[]) => void): void;
nestBlock(block: SvelteNode, params?: ScriptLetBlockParam[] | ((helper: TypeGenHelper | null) => {
param: ScriptLetBlockParam;
preparationScript?: {
script: string;
nodeType: StatementNodeType;
}[];
})): void;
closeScope(): void;
addProgramRestore(callback: ScriptLetRestoreCallback): void;
private appendScript;
private appendScriptWithoutOffset;
private pushScope;
/**
* Restore AST nodes
*/
restore(result: ESLintExtendedProgram): void;
/**
* Restore AST nodes
*/
private restoreNodes;
/**
* Restore program node
*/
private restoreProgram;
private remapNodes;
private generateUniqueId;
}
export {};

View File

@@ -0,0 +1,872 @@
import { getWithLoc } from "../parser/converts/common.js";
import { getScopeFromNode, removeAllScopeAndVariableAndReference, removeIdentifierVariable, removeReference, removeScope, } from "../scope/index.js";
import { getKeys, traverseNodes, getNodes } from "../traverse.js";
import { UniqueIdGenerator } from "./unique.js";
import { fixLocations } from "./fix-locations.js";
/**
* Get node range
*/
function getNodeRange(node, code) {
const loc = "range" in node
? { start: node.range[0], end: node.range[1] }
: getWithLoc(node);
let start = loc.start;
let end = loc.end;
let openingParenCount = 0;
let closingParenCount = 0;
if (node.leadingComments) {
const commentStart = getWithLoc(node.leadingComments[0]).start;
if (commentStart < start) {
start = commentStart;
// Extract the number of parentheses before the node.
let leadingEnd = loc.start;
for (let index = node.leadingComments.length - 1; index >= 0; index--) {
const comment = node.leadingComments[index];
const loc = getWithLoc(comment);
for (const c of code.slice(loc.end, leadingEnd).trim()) {
if (c === "(")
openingParenCount++;
}
leadingEnd = loc.start;
}
}
}
if (node.trailingComments) {
const commentEnd = getWithLoc(node.trailingComments[node.trailingComments.length - 1]).end;
if (end < commentEnd) {
end = commentEnd;
// Extract the number of parentheses after the node.
let trailingStart = loc.end;
for (const comment of node.trailingComments) {
const loc = getWithLoc(comment);
for (const c of code.slice(trailingStart, loc.start).trim()) {
if (c === ")")
closingParenCount++;
}
trailingStart = loc.end;
}
}
}
// Adjust the range so that the parentheses match up.
if (openingParenCount < closingParenCount) {
for (; openingParenCount < closingParenCount && start >= 0; start--) {
const c = code[start].trim();
if (c)
continue;
if (c !== "(")
break;
openingParenCount++;
}
}
else if (openingParenCount > closingParenCount) {
for (; openingParenCount > closingParenCount && end < code.length; end++) {
const c = code[end].trim();
if (c)
continue;
if (c !== ")")
break;
closingParenCount++;
}
}
return [start, end];
}
/**
* A class that handles script fragments.
* The script fragment AST node remaps and connects to the original directive AST node.
*/
export class ScriptLetContext {
constructor(ctx) {
this.restoreCallbacks = [];
this.programRestoreCallbacks = [];
this.closeScopeCallbacks = [];
this.unique = new UniqueIdGenerator();
this.currentScriptScopeKind = "render";
this.script = ctx.sourceCode.scripts;
this.ctx = ctx;
}
addExpression(expression, parent, typing, ...callbacks) {
const range = getNodeRange(expression, this.ctx.code);
return this.addExpressionFromRange(range, parent, typing, ...callbacks);
}
addExpressionFromRange(range, parent, typing, ...callbacks) {
const part = this.ctx.code.slice(...range);
const isTS = typing && this.ctx.isTypeScript();
this.appendScript(`(${part})${isTS ? `as (${typing})` : ""};`, range[0] - 1, this.currentScriptScopeKind, "ExpressionStatement", (st, tokens, comments, result) => {
const exprSt = st;
const tsAs = isTS
? exprSt.expression
: null;
const node = tsAs?.expression || exprSt.expression;
// Process for nodes
for (const callback of callbacks) {
callback(node, result);
}
tokens.shift(); // (
tokens.pop(); // )
tokens.pop(); // ;
if (isTS) {
for (const scope of extractTypeNodeScopes(tsAs.typeAnnotation, result)) {
removeScope(result.scopeManager, scope);
}
this.remapNodes([
{
offset: range[0] - 1,
range,
newNode: node,
},
], tokens, comments, result.visitorKeys);
}
node.parent = parent;
// Disconnect the tree structure.
exprSt.expression = null;
});
return callbacks;
}
addObjectShorthandProperty(identifier, parent, ...callbacks) {
const range = getNodeRange(identifier, this.ctx.code);
const part = this.ctx.code.slice(...range);
this.appendScript(`({${part}});`, range[0] - 2, this.currentScriptScopeKind, "ExpressionStatement", (st, tokens, _comments, result) => {
const exprSt = st;
const objectExpression = exprSt.expression;
const node = objectExpression.properties[0];
// Process for nodes
for (const callback of callbacks) {
callback(node, result);
}
node.key.parent = parent;
node.value.parent = parent;
tokens.shift(); // (
tokens.shift(); // {
tokens.pop(); // }
tokens.pop(); // )
tokens.pop(); // ;
// Disconnect the tree structure.
exprSt.expression = null;
});
}
addVariableDeclarator(declarator, parent, ...callbacks) {
const range = declarator.type === "VariableDeclarator"
? // As of Svelte v5-next.65, VariableDeclarator nodes do not have location information.
[
getNodeRange(declarator.id, this.ctx.code)[0],
getNodeRange(declarator.init, this.ctx.code)[1],
]
: getNodeRange(declarator, this.ctx.code);
const part = this.ctx.code.slice(...range);
this.appendScript(`const ${part};`, range[0] - 6, this.currentScriptScopeKind, "VariableDeclaration", (st, tokens, _comments, result) => {
const decl = st;
const node = decl.declarations[0];
// Process for nodes
for (const callback of callbacks) {
callback(node, result);
}
const scope = result.getScope(decl);
for (const variable of scope.variables) {
for (const def of variable.defs) {
if (def.parent === decl) {
def.parent = parent;
}
}
}
node.parent = parent;
tokens.shift(); // const
tokens.pop(); // ;
// Disconnect the tree structure.
decl.declarations = [];
});
return callbacks;
}
addGenericTypeAliasDeclaration(param, callbackId, callbackDefault) {
const ranges = getTypeParameterRanges(this.ctx.code, param);
let scriptLet = `type ${this.ctx.code.slice(...ranges.idRange)}`;
if (ranges.constraintRange) {
scriptLet += ` = ${this.ctx.code.slice(...ranges.constraintRange)};`;
// |extends|
}
else {
scriptLet += " = unknown;";
}
this.appendScript(scriptLet, ranges.idRange[0] - 5, "generics", "TSTypeAliasDeclaration", (st, tokens, _comments, result) => {
const decl = st;
const id = decl.id;
const typeAnnotation = decl.typeAnnotation;
// Process for nodes
callbackId(id, typeAnnotation);
const scope = result.getScope(decl);
for (const variable of scope.variables) {
for (const def of variable.defs) {
if (def.node === decl) {
def.node = param;
}
}
}
id.parent = param;
typeAnnotation.parent = param;
tokens.shift(); // type
if (ranges.constraintRange) {
const eqToken = tokens[1];
eqToken.type = "Keyword";
eqToken.value = "extends";
eqToken.range[0] = eqToken.range[0] - 1;
eqToken.range[1] = eqToken.range[0] + 7;
tokens.pop(); // ;
}
else {
tokens.pop(); // ;
tokens.pop(); // unknown
tokens.pop(); // =
}
// Disconnect the tree structure.
delete decl.id;
delete decl.typeAnnotation;
delete decl.typeParameters;
});
if (ranges.defaultRange) {
const eqDefaultType = this.ctx.code.slice(ranges.constraintRange?.[1] ?? ranges.idRange[1], ranges.defaultRange[1]);
const id = this.generateUniqueId(eqDefaultType);
const scriptLet = `type ${id}${eqDefaultType};`;
this.appendScript(scriptLet, ranges.defaultRange[0] - 5 - id.length - 1, "generics", "TSTypeAliasDeclaration", (st, tokens, _comments, result) => {
const decl = st;
const typeAnnotation = decl.typeAnnotation;
// Process for nodes
callbackDefault(typeAnnotation);
const scope = result.getScope(decl);
removeIdentifierVariable(decl.id, scope);
typeAnnotation.parent = param;
tokens.shift(); // type
tokens.shift(); // ${id}
tokens.pop(); // ;
// Disconnect the tree structure.
delete decl.id;
delete decl.typeAnnotation;
delete decl.typeParameters;
});
}
}
nestIfBlock(expression, ifBlock, callback) {
const range = getNodeRange(expression, this.ctx.code);
const part = this.ctx.code.slice(...range);
const restore = this.appendScript(`if(${part}){`, range[0] - 3, this.currentScriptScopeKind, "IfStatement", (st, tokens, _comments, result) => {
const ifSt = st;
const node = ifSt.test;
const scope = result.getScope(ifSt.consequent);
// Process for nodes
callback(node, result);
node.parent = ifBlock;
// Process for scope
result.registerNodeToScope(ifBlock, scope);
tokens.shift(); // if
tokens.shift(); // (
tokens.pop(); // )
tokens.pop(); // {
tokens.pop(); // }
// Disconnect the tree structure.
ifSt.test = null;
ifSt.consequent = null;
});
this.pushScope(restore, "}", this.currentScriptScopeKind);
}
nestEachBlock(expression, context, indexRange, eachBlock, callback) {
const exprRange = getNodeRange(expression, this.ctx.code);
const ctxRange = context && getNodeRange(context, this.ctx.code);
let source = "Array.from(";
const exprOffset = source.length;
source += `${this.ctx.code.slice(...exprRange)}).forEach((`;
const ctxOffset = source.length;
if (ctxRange) {
source += this.ctx.code.slice(...ctxRange);
}
else {
source += "__$ctx__";
}
let idxOffset = null;
if (indexRange) {
source += ",";
idxOffset = source.length;
source += this.ctx.code.slice(indexRange.start, indexRange.end);
}
source += ")=>{";
const restore = this.appendScript(source, exprRange[0] - exprOffset, this.currentScriptScopeKind, "ExpressionStatement", (st, tokens, comments, result) => {
const expSt = st;
const call = expSt.expression;
const fn = call.arguments[0];
const callArrayFrom = call.callee
.object;
const expr = callArrayFrom.arguments[0];
const ctx = fn.params[0];
const idx = (fn.params[1] ?? null);
const scope = result.getScope(fn.body);
// Process for nodes
callback(expr, context ? ctx : null, idx);
// Process for scope
result.registerNodeToScope(eachBlock, scope);
for (const v of scope.variables) {
for (const def of v.defs) {
if (def.node === fn) {
def.node = eachBlock;
}
}
}
if (!context) {
// remove `__$ctx__` variable
removeIdentifierVariable(ctx, scope);
}
// remove Array reference
const arrayId = callArrayFrom.callee
.object;
const ref = scope.upper.references.find((r) => r.identifier === arrayId);
if (ref) {
removeReference(ref, scope.upper);
}
expr.parent = eachBlock;
ctx.parent = eachBlock;
if (idx) {
idx.parent = eachBlock;
}
tokens.shift(); // Array
tokens.shift(); // .
tokens.shift(); // from
tokens.shift(); // (
tokens.pop(); // )
tokens.pop(); // =>
tokens.pop(); // {
tokens.pop(); // }
tokens.pop(); // )
tokens.pop(); // ;
const map = [
{
offset: exprOffset,
range: exprRange,
newNode: expr,
},
];
if (ctxRange) {
map.push({
offset: ctxOffset,
range: ctxRange,
newNode: ctx,
});
}
if (indexRange) {
map.push({
offset: idxOffset,
range: [indexRange.start, indexRange.end],
newNode: idx,
});
}
this.remapNodes(map, tokens, comments, result.visitorKeys);
// Disconnect the tree structure.
expSt.expression = null;
});
this.pushScope(restore, "});", this.currentScriptScopeKind);
}
nestSnippetBlock(id, closeParentIndex, snippetBlock,
// If set to null, `currentScriptScopeKind` will be used.
kind, callback) {
const scopeKind = kind || this.currentScriptScopeKind;
const idRange = getNodeRange(id, this.ctx.code);
const part = this.ctx.code.slice(idRange[0], closeParentIndex + 1);
const restore = this.appendScript(`function ${part}{`, idRange[0] - 9, scopeKind, "FunctionDeclaration", (st, tokens, _comments, result) => {
const fnDecl = st;
const idNode = fnDecl.id;
const params = [...fnDecl.params];
const scope = result.getScope(fnDecl);
// Process for nodes
callback(idNode, params);
idNode.parent = snippetBlock;
for (const param of params) {
param.parent = snippetBlock;
}
// Process for scope
result.registerNodeToScope(snippetBlock, scope);
tokens.shift(); // function
tokens.pop(); // {
tokens.pop(); // }
// Disconnect the tree structure.
fnDecl.id = null;
fnDecl.params = [];
});
this.pushScope(restore, "}", scopeKind);
}
nestBlock(block, params) {
let resolvedParams;
if (typeof params === "function") {
if (this.ctx.isTypeScript()) {
const generatedTypes = params({
generateUniqueId: (base) => this.generateUniqueId(base),
});
resolvedParams = [generatedTypes.param];
if (generatedTypes.preparationScript) {
for (const preparationScript of generatedTypes.preparationScript) {
this.appendScriptWithoutOffset(preparationScript.script, this.currentScriptScopeKind, preparationScript.nodeType, (node, tokens, comments, result) => {
tokens.length = 0;
comments.length = 0;
removeAllScopeAndVariableAndReference(node, result);
});
}
}
}
else {
const generatedTypes = params(null);
resolvedParams = [generatedTypes.param];
}
}
else {
resolvedParams = params;
}
if (!resolvedParams || resolvedParams.length === 0) {
const restore = this.appendScript(`{`, block.range[0], this.currentScriptScopeKind, "BlockStatement", (st, tokens, _comments, result) => {
const blockSt = st;
// Process for scope
const scope = result.getScope(blockSt);
result.registerNodeToScope(block, scope);
tokens.length = 0; // clear tokens
// Disconnect the tree structure.
blockSt.body = null;
});
this.pushScope(restore, "}", this.currentScriptScopeKind);
}
else {
const sortedParams = [...resolvedParams]
.map((d) => {
return {
...d,
range: getNodeRange(d.node, this.ctx.code),
};
})
.sort((a, b) => {
const [pA] = a.range;
const [pB] = b.range;
return pA - pB;
});
const maps = [];
let source = "";
for (let index = 0; index < sortedParams.length; index++) {
const param = sortedParams[index];
const range = param.range;
if (source) {
source += ",";
}
const offset = source.length + 1; /* ( */
source += this.ctx.code.slice(...range);
maps.push({
index: maps.length,
offset,
range,
});
if (this.ctx.isTypeScript()) {
source += `: (${param.typing})`;
}
}
const restore = this.appendScript(`(${source})=>{`, maps[0].range[0] - 1, this.currentScriptScopeKind, "ExpressionStatement", (st, tokens, comments, result) => {
const exprSt = st;
const fn = exprSt.expression;
const scope = result.getScope(fn.body);
// Process for nodes
for (let index = 0; index < fn.params.length; index++) {
const p = fn.params[index];
sortedParams[index].callback(p, result);
if (this.ctx.isTypeScript()) {
const typeAnnotation = p.typeAnnotation;
delete p.typeAnnotation;
p.range[1] = typeAnnotation.range[0];
p.loc.end = {
line: typeAnnotation.loc.start.line,
column: typeAnnotation.loc.start.column,
};
removeAllScopeAndVariableAndReference(typeAnnotation, result);
}
p.parent = sortedParams[index].parent;
}
// Process for scope
result.registerNodeToScope(block, scope);
for (const v of scope.variables) {
for (const def of v.defs) {
if (def.node === fn) {
def.node = block;
}
}
}
tokens.shift(); // (
tokens.pop(); // )
tokens.pop(); // =>
tokens.pop(); // {
tokens.pop(); // }
tokens.pop(); // ;
this.remapNodes(maps.map((m) => {
return {
offset: m.offset,
range: m.range,
newNode: fn.params[m.index],
};
}), tokens, comments, result.visitorKeys);
// Disconnect the tree structure.
exprSt.expression = null;
});
this.pushScope(restore, "};", this.currentScriptScopeKind);
}
}
closeScope() {
this.closeScopeCallbacks.pop()();
}
addProgramRestore(callback) {
this.programRestoreCallbacks.push(callback);
}
appendScript(text, offset, kind, nodeType, callback) {
const resultCallback = this.appendScriptWithoutOffset(text, kind, nodeType, (node, tokens, comments, result) => {
fixLocations(node, tokens, comments, offset - resultCallback.start, result.visitorKeys, this.ctx);
callback(node, tokens, comments, result);
});
return resultCallback;
}
appendScriptWithoutOffset(text, kind, nodeType, callback) {
const { start: startOffset, end: endOffset } = this.script.addLet(text, kind);
const restoreCallback = {
start: startOffset,
end: endOffset,
nodeType,
callback,
};
this.restoreCallbacks.push(restoreCallback);
return restoreCallback;
}
pushScope(restoreCallback, closeToken, kind) {
const upper = this.currentScriptScopeKind;
this.currentScriptScopeKind = kind;
this.closeScopeCallbacks.push(() => {
this.script.addLet(closeToken, kind);
this.currentScriptScopeKind = upper;
restoreCallback.end = this.script.getCurrentVirtualCodeLength();
});
}
/**
* Restore AST nodes
*/
restore(result) {
const nodeToScope = getNodeToScope(result.scopeManager);
const postprocessList = [];
const callbackOption = {
getScope,
registerNodeToScope,
scopeManager: result.scopeManager,
visitorKeys: result.visitorKeys,
addPostProcess: (cb) => postprocessList.push(cb),
};
this.restoreNodes(result, callbackOption);
this.restoreProgram(result, callbackOption);
postprocessList.forEach((p) => p());
// Helpers
/** Get scope */
function getScope(node) {
return getScopeFromNode(result.scopeManager, node);
}
/** Register node to scope */
function registerNodeToScope(node, scope) {
// If we replace the `scope.block` at this time,
// the scope restore calculation will not work, so we will replace the `scope.block` later.
postprocessList.push(() => {
const beforeBlock = scope.block;
scope.block = node;
for (const variable of [
...scope.variables,
...(scope.upper?.variables ?? []),
]) {
for (const def of variable.defs) {
if (def.node === beforeBlock) {
def.node = node;
}
}
}
});
const scopes = nodeToScope.get(node);
if (scopes) {
scopes.push(scope);
}
else {
nodeToScope.set(node, [scope]);
}
}
}
/**
* Restore AST nodes
*/
restoreNodes(result, callbackOption) {
let orderedRestoreCallback = this.restoreCallbacks.shift();
if (!orderedRestoreCallback) {
return;
}
const separateIndexes = this.script.separateIndexes;
const tokens = result.ast.tokens;
const processedTokens = [];
const comments = result.ast.comments;
const processedComments = [];
let tok;
while ((tok = tokens.shift())) {
if (separateIndexes.includes(tok.range[0]) && tok.value === ";") {
break;
}
if (orderedRestoreCallback.start <= tok.range[0]) {
tokens.unshift(tok);
break;
}
processedTokens.push(tok);
}
while ((tok = comments.shift())) {
if (orderedRestoreCallback.start <= tok.range[0]) {
comments.unshift(tok);
break;
}
processedComments.push(tok);
}
const targetNodes = new Map();
const removeStatements = [];
traverseNodes(result.ast, {
visitorKeys: result.visitorKeys,
enterNode: (node) => {
while (node.range && separateIndexes.includes(node.range[1] - 1)) {
node.range[1]--;
node.loc.end.column--;
}
if (node.loc.end.column < 0) {
node.loc.end = this.ctx.getLocFromIndex(node.range[1]);
}
if (node.parent === result.ast &&
separateIndexes[0] <= node.range[0]) {
removeStatements.push(node);
}
if (!orderedRestoreCallback) {
return;
}
if (orderedRestoreCallback.nodeType === node.type &&
orderedRestoreCallback.start <= node.range[0] &&
node.range[1] <= orderedRestoreCallback.end) {
targetNodes.set(node, orderedRestoreCallback);
orderedRestoreCallback = this.restoreCallbacks.shift();
}
//
},
leaveNode(node) {
const restoreCallback = targetNodes.get(node);
if (!restoreCallback) {
return;
}
const startIndex = {
token: tokens.findIndex((t) => restoreCallback.start <= t.range[0]),
comment: comments.findIndex((t) => restoreCallback.start <= t.range[0]),
};
if (startIndex.comment === -1) {
startIndex.comment = comments.length;
}
const endIndex = {
token: tokens.findIndex((t) => restoreCallback.end < t.range[1], startIndex.token),
comment: comments.findIndex((t) => restoreCallback.end < t.range[1], startIndex.comment),
};
if (endIndex.token === -1) {
endIndex.token = tokens.length;
}
if (endIndex.comment === -1) {
endIndex.comment = comments.length;
}
const targetTokens = tokens.splice(startIndex.token, endIndex.token - startIndex.token);
const targetComments = comments.splice(startIndex.comment, endIndex.comment - startIndex.comment);
restoreCallback.callback(node, targetTokens, targetComments, callbackOption);
processedTokens.push(...targetTokens);
processedComments.push(...targetComments);
},
});
for (const st of removeStatements) {
const index = result.ast.body.indexOf(st);
result.ast.body.splice(index, 1);
}
result.ast.tokens = processedTokens;
result.ast.comments = processedComments;
}
/**
* Restore program node
*/
restoreProgram(result, callbackOption) {
for (const callback of this.programRestoreCallbacks) {
callback(result.ast, result.ast.tokens, result.ast.comments, callbackOption);
}
}
remapNodes(maps, tokens, comments, visitorKeys) {
const targetMaps = [...maps];
const first = targetMaps.shift();
let tokenIndex = 0;
for (; tokenIndex < tokens.length; tokenIndex++) {
const token = tokens[tokenIndex];
if (first.range[1] <= token.range[0]) {
break;
}
}
for (const map of targetMaps) {
const startOffset = map.offset - first.offset + first.range[0];
const endOffset = startOffset + (map.range[1] - map.range[0]);
let removeCount = 0;
let target = tokens[tokenIndex];
while (target && target.range[1] <= startOffset) {
removeCount++;
target = tokens[tokenIndex + removeCount];
}
if (removeCount) {
tokens.splice(tokenIndex, removeCount);
}
const bufferTokens = [];
for (; tokenIndex < tokens.length; tokenIndex++) {
const token = tokens[tokenIndex];
if (endOffset <= token.range[0]) {
break;
}
bufferTokens.push(token);
}
fixLocations(map.newNode, bufferTokens, comments.filter((t) => startOffset <= t.range[0] && t.range[1] <= endOffset), map.range[0] - startOffset, visitorKeys, this.ctx);
}
tokens.splice(tokenIndex);
}
generateUniqueId(base) {
return this.unique.generate(base, this.ctx.code, this.script.getCurrentVirtualCode());
}
}
function getTypeParameterRanges(code, param) {
const idRange = [
param.range[0],
param.constraint || param.default ? param.name.range[1] : param.range[1],
];
let constraintRange = null;
let defaultRange = null;
if (param.constraint) {
constraintRange = [
param.constraint.range[0],
param.default ? param.constraint.range[1] : param.range[1],
];
const index = getTokenIndex(code, (code, index) => code.startsWith("extends", index), idRange[1], param.constraint.range[0]);
if (index != null) {
idRange[1] = index;
constraintRange[0] = index + 7;
}
}
if (param.default) {
defaultRange = [param.default.range[0], param.range[1]];
const index = getTokenIndex(code, (code, index) => code[index] === "=", constraintRange?.[1] ?? idRange[1], param.default.range[0]);
if (index != null) {
(constraintRange ?? idRange)[1] = index;
defaultRange[0] = index + 1;
}
}
return { idRange, constraintRange, defaultRange };
}
function getTokenIndex(code, targetToken, start, end) {
let index = start;
while (index < end) {
if (targetToken(code, index)) {
return index;
}
if (code.startsWith("//", index)) {
const lfIndex = code.indexOf("\n", index);
if (lfIndex >= 0) {
index = lfIndex + 1;
continue;
}
return null;
}
if (code.startsWith("/*", index)) {
const endIndex = code.indexOf("*/", index);
if (endIndex >= 0) {
index = endIndex + 2;
continue;
}
return null;
}
index++;
}
return null;
}
/** Get the node to scope map from given scope manager */
function getNodeToScope(scopeManager) {
if ("__nodeToScope" in scopeManager) {
return scopeManager.__nodeToScope;
}
// transform scopeManager
const nodeToScope = new WeakMap();
for (const scope of scopeManager.scopes) {
const scopes = nodeToScope.get(scope.block);
if (scopes) {
scopes.push(scope);
}
else {
nodeToScope.set(scope.block, [scope]);
}
}
scopeManager.acquire = function (node, inner) {
/**
* predicate
*/
function predicate(testScope) {
if (testScope.type === "function" && testScope.functionExpressionScope) {
return false;
}
return true;
}
const scopes = nodeToScope.get(node);
if (!scopes || scopes.length === 0) {
return null;
}
// Heuristic selection from all scopes.
// If you would like to get all scopes, please use ScopeManager#acquireAll.
if (scopes.length === 1) {
return scopes[0];
}
if (inner) {
for (let i = scopes.length - 1; i >= 0; --i) {
const scope = scopes[i];
if (predicate(scope)) {
return scope;
}
}
}
else {
for (let i = 0, iz = scopes.length; i < iz; ++i) {
const scope = scopes[i];
if (predicate(scope)) {
return scope;
}
}
}
return null;
};
return nodeToScope;
}
/** Extract the type scope of the given node. */
function extractTypeNodeScopes(node, result) {
const scopes = new Set();
for (const scope of iterateTypeNodeScopes(node)) {
scopes.add(scope);
}
return scopes;
/** Iterate the type scope of the given node. */
function* iterateTypeNodeScopes(node) {
if (node.type === "TSParenthesizedType") {
// Skip TSParenthesizedType.
yield* iterateTypeNodeScopes(node.typeAnnotation);
}
else if (node.type === "TSConditionalType") {
yield result.getScope(node);
// `falseType` of `TSConditionalType` is sibling scope.
const falseType = node.falseType;
yield* iterateTypeNodeScopes(falseType);
}
else if (node.type === "TSFunctionType" ||
node.type === "TSMappedType" ||
node.type === "TSConstructorType") {
yield result.getScope(node);
}
else {
const typeNode = node;
for (const key of getKeys(typeNode, result.visitorKeys)) {
for (const child of getNodes(typeNode, key)) {
yield* iterateTypeNodeScopes(child);
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
export declare class UniqueIdGenerator {
private uniqueIdSeq;
private readonly usedUniqueIds;
generate(base: string, ...texts: string[]): string;
}

View File

@@ -0,0 +1,15 @@
export class UniqueIdGenerator {
constructor() {
this.uniqueIdSeq = 1;
this.usedUniqueIds = new Set();
}
generate(base, ...texts) {
const hasId = (id) => this.usedUniqueIds.has(id) || texts.some((t) => t.includes(id));
let candidate = `$_${base.replace(/\W/g, "_")}${this.uniqueIdSeq++}`;
while (hasId(candidate)) {
candidate = `$_${base.replace(/\W/g, "_")}${this.uniqueIdSeq++}`;
}
this.usedUniqueIds.add(candidate);
return candidate;
}
}