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,80 @@
import type * as Compiler from "svelte/compiler";
export type SvelteConfig = {
compilerOptions?: Compiler.CompileOptions;
extensions?: string[];
kit?: KitConfig;
preprocess?: unknown;
vitePlugin?: unknown;
onwarn?: (warning: Compiler.Warning, defaultHandler: (warning: Compiler.Warning) => void) => void;
warningFilter?: (warning: Compiler.Warning) => boolean;
[key: string]: unknown;
};
interface KitConfig {
adapter?: unknown;
alias?: Record<string, string>;
appDir?: string;
csp?: {
mode?: "hash" | "nonce" | "auto";
directives?: unknown;
reportOnly?: unknown;
};
csrf?: {
checkOrigin?: boolean;
};
embedded?: boolean;
env?: {
dir?: string;
publicPrefix?: string;
privatePrefix?: string;
};
files?: {
assets?: string;
hooks?: {
client?: string;
server?: string;
universal?: string;
};
lib?: string;
params?: string;
routes?: string;
serviceWorker?: string;
appTemplate?: string;
errorTemplate?: string;
};
inlineStyleThreshold?: number;
moduleExtensions?: string[];
outDir?: string;
output?: {
preloadStrategy?: "modulepreload" | "preload-js" | "preload-mjs";
};
paths?: {
assets?: "" | `http://${string}` | `https://${string}`;
base?: "" | `/${string}`;
relative?: boolean;
};
prerender?: {
concurrency?: number;
crawl?: boolean;
entries?: ("*" | `/${string}`)[];
handleHttpError?: unknown;
handleMissingId?: unknown;
handleEntryGeneratorMismatch?: unknown;
origin?: string;
};
serviceWorker?: {
register?: boolean;
files?(filepath: string): boolean;
};
typescript?: {
config?: (config: Record<string, any>) => Record<string, any> | void;
};
version?: {
name?: string;
pollInterval?: number;
};
}
/**
* Resolves svelte.config.
*/
export declare function resolveSvelteConfigFromOption(options: any): SvelteConfig | null;
export {};

View File

@@ -0,0 +1,62 @@
import path from "path";
import fs from "fs";
import { parseConfig } from "./parser.js";
const caches = new Map();
/**
* Resolves svelte.config.
*/
export function resolveSvelteConfigFromOption(options) {
if (options?.svelteConfig) {
return options.svelteConfig;
}
return resolveSvelteConfig(options?.filePath);
}
/**
* Resolves `svelte.config.js`.
* It searches the parent directories of the given file to find `svelte.config.js`,
* and returns the static analysis result for it.
*/
function resolveSvelteConfig(filePath) {
let cwd = filePath && fs.existsSync(filePath) ? path.dirname(filePath) : null;
if (cwd == null) {
if (typeof process === "undefined")
return null;
cwd = process.cwd();
}
const configFilePath = findConfigFilePath(cwd);
if (!configFilePath)
return null;
if (caches.has(configFilePath)) {
return caches.get(configFilePath) || null;
}
const code = fs.readFileSync(configFilePath, "utf8");
const config = parseConfig(code);
caches.set(configFilePath, config);
return config;
}
/**
* Searches from the current working directory up until finding the config filename.
* @param {string} cwd The current working directory to search from.
* @returns {string|undefined} The file if found or `undefined` if not.
*/
function findConfigFilePath(cwd) {
let directory = path.resolve(cwd);
const { root } = path.parse(directory);
const stopAt = path.resolve(directory, root);
while (directory !== stopAt) {
const target = path.resolve(directory, "svelte.config.js");
const stat = fs.existsSync(target)
? fs.statSync(target, {
throwIfNoEntry: false,
})
: null;
if (stat?.isFile()) {
return target;
}
const next = path.dirname(directory);
if (next === directory)
break;
directory = next;
}
return null;
}

View File

@@ -0,0 +1,2 @@
import type { SvelteConfig } from "./index.js";
export declare function parseConfig(code: string): SvelteConfig | null;

View File

@@ -0,0 +1,273 @@
import { getFallbackKeys, traverseNodes } from "../traverse.js";
import { getEspree } from "../parser/espree.js";
import { analyze } from "eslint-scope";
import { findVariable } from "../scope/index.js";
export function parseConfig(code) {
const espree = getEspree();
const ast = espree.parse(code, {
range: true,
loc: true,
ecmaVersion: espree.latestEcmaVersion,
sourceType: "module",
});
// Set parent nodes.
traverseNodes(ast, {
enterNode(node, parent) {
node.parent = parent;
},
leaveNode() {
/* do nothing */
},
});
// Analyze scopes.
const scopeManager = analyze(ast, {
ignoreEval: true,
nodejsScope: false,
ecmaVersion: espree.latestEcmaVersion,
sourceType: "module",
fallback: getFallbackKeys,
});
return parseAst(ast, scopeManager);
}
function parseAst(ast, scopeManager) {
const edd = ast.body.find((node) => node.type === "ExportDefaultDeclaration");
if (!edd)
return {};
const decl = edd.declaration;
if (decl.type === "ClassDeclaration" || decl.type === "FunctionDeclaration")
return {};
return parseSvelteConfigExpression(decl, scopeManager);
}
function parseSvelteConfigExpression(node, scopeManager) {
const evaluated = evaluateExpression(node, scopeManager);
if (evaluated?.type !== 1 /* EvaluatedType.object */)
return {};
const result = {};
// Returns only known properties.
const compilerOptions = evaluated.getProperty("compilerOptions");
if (compilerOptions?.type === 1 /* EvaluatedType.object */) {
result.compilerOptions = {};
const runes = compilerOptions.getProperty("runes")?.getStatic();
if (runes?.value != null) {
result.compilerOptions.runes = Boolean(runes.value);
}
}
const kit = evaluated.getProperty("kit");
if (kit?.type === 1 /* EvaluatedType.object */) {
result.kit = {};
const files = kit.getProperty("files")?.getStatic();
if (files?.value != null)
result.kit.files = files.value;
}
return result;
}
class EvaluatedLiteral {
constructor(value) {
this.type = 0 /* EvaluatedType.literal */;
this.value = value;
}
getStatic() {
return this;
}
}
/** Evaluating an object expression. */
class EvaluatedObject {
constructor(node, parseExpression) {
this.type = 1 /* EvaluatedType.object */;
this.cached = new Map();
this.node = node;
this.parseExpression = parseExpression;
}
/** Gets the evaluated value of the property with the given name. */
getProperty(key) {
return this.withCache(key, () => {
let unknown = false;
for (const prop of [...this.node.properties].reverse()) {
if (prop.type === "Property") {
const name = this.getKey(prop);
if (name === key)
return this.parseExpression(prop.value);
if (name == null)
unknown = true;
}
else if (prop.type === "SpreadElement") {
const evaluated = this.parseExpression(prop.argument);
if (evaluated?.type === 1 /* EvaluatedType.object */) {
const value = evaluated.getProperty(key);
if (value)
return value;
}
unknown = true;
}
}
return unknown ? null : new EvaluatedLiteral(undefined);
});
}
getStatic() {
const object = {};
for (const prop of this.node.properties) {
if (prop.type === "Property") {
const name = this.getKey(prop);
if (name == null)
return null;
const evaluated = this.withCache(name, () => this.parseExpression(prop.value))?.getStatic();
if (!evaluated)
return null;
object[name] = evaluated.value;
}
else if (prop.type === "SpreadElement") {
const evaluated = this.parseExpression(prop.argument)?.getStatic();
if (!evaluated)
return null;
Object.assign(object, evaluated.value);
}
}
return { value: object };
}
withCache(key, parse) {
if (this.cached.has(key))
return this.cached.get(key) || null;
const evaluated = parse();
this.cached.set(key, evaluated);
return evaluated;
}
getKey(node) {
if (!node.computed && node.key.type === "Identifier")
return node.key.name;
const evaluatedKey = this.parseExpression(node.key)?.getStatic();
if (evaluatedKey)
return String(evaluatedKey.value);
return null;
}
}
function evaluateExpression(node, scopeManager) {
const tracked = new Map();
return parseExpression(node);
function parseExpression(node) {
if (node.type === "Literal") {
return new EvaluatedLiteral(node.value);
}
if (node.type === "Identifier") {
return parseIdentifier(node);
}
if (node.type === "ObjectExpression") {
return new EvaluatedObject(node, parseExpression);
}
return null;
}
function parseIdentifier(node) {
const defs = getIdentifierDefinitions(node);
if (defs.length !== 1) {
if (defs.length === 0 && node.name === "undefined")
return new EvaluatedLiteral(undefined);
return null;
}
const def = defs[0];
if (def.type !== "Variable")
return null;
if (def.parent.kind !== "const" || !def.node.init)
return null;
const evaluated = parseExpression(def.node.init);
if (!evaluated)
return null;
const assigns = parsePatternAssign(def.name, def.node.id);
let result = evaluated;
while (assigns.length) {
const assign = assigns.shift();
if (assign.type === "member") {
if (result.type !== 1 /* EvaluatedType.object */)
return null;
const next = result.getProperty(assign.name);
if (!next)
return null;
result = next;
}
else if (assign.type === "assignment") {
if (result.type === 0 /* EvaluatedType.literal */ &&
result.value === undefined) {
const next = parseExpression(assign.node.right);
if (!next)
return null;
result = next;
}
}
}
return result;
}
function getIdentifierDefinitions(node) {
if (tracked.has(node))
return tracked.get(node);
tracked.set(node, []);
const defs = findVariable(scopeManager, node)?.defs;
if (!defs)
return [];
tracked.set(node, defs);
if (defs.length !== 1) {
const def = defs[0];
if (def.type === "Variable" &&
def.parent.kind === "const" &&
def.node.id.type === "Identifier" &&
def.node.init?.type === "Identifier") {
const newDef = getIdentifierDefinitions(def.node.init);
tracked.set(node, newDef);
return newDef;
}
}
return defs;
}
}
/**
* Returns the assignment path.
* For example,
* `let {a: {target}} = {}`
* -> `[{type: "member", name: 'a'}, {type: "member", name: 'target'}]`.
* `let {a: {target} = foo} = {}`
* -> `[{type: "member", name: 'a'}, {type: "assignment"}, {type: "member", name: 'target'}]`.
*/
function parsePatternAssign(node, root) {
return parse(root) || [];
function parse(target) {
if (node === target) {
return [];
}
if (target.type === "Identifier") {
return null;
}
if (target.type === "AssignmentPattern") {
const left = parse(target.left);
if (!left)
return null;
return [{ type: "assignment", node: target }, ...left];
}
if (target.type === "ObjectPattern") {
for (const prop of target.properties) {
if (prop.type === "Property") {
const name = !prop.computed && prop.key.type === "Identifier"
? prop.key.name
: prop.key.type === "Literal"
? String(prop.key.value)
: null;
if (!name)
continue;
const value = parse(prop.value);
if (!value)
return null;
return [{ type: "member", name }, ...value];
}
}
return null;
}
if (target.type === "ArrayPattern") {
for (const [index, element] of target.elements.entries()) {
if (!element)
continue;
const value = parse(element);
if (!value)
return null;
return [{ type: "member", name: String(index) }, ...value];
}
return null;
}
return null;
}
}