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,9 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transpile with babel
*/
export declare function transform(node: AST.SvelteScriptElement, text: string, context: RuleContext): TransformResult | null;
/** Check if project has Babel. */
export declare function hasBabel(context: RuleContext): boolean;

View File

@@ -0,0 +1,49 @@
import { loadModule } from '../../../utils/load-module.js';
/**
* Transpile with babel
*/
export function transform(node, text, context) {
const babel = loadBabel(context);
if (!babel) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
try {
const output = babel.transformSync(code, {
sourceType: 'module',
sourceMaps: true,
minified: false,
ast: false,
code: true,
cwd: context.cwd ?? process.cwd()
});
if (!output) {
return null;
}
return {
inputRange,
output: output.code,
mappings: output.map.mappings
};
}
catch {
return null;
}
}
/** Check if project has Babel. */
export function hasBabel(context) {
return Boolean(loadBabel(context));
}
/**
* Load babel
*/
function loadBabel(context) {
return loadModule(context, '@babel/core');
}

View File

@@ -0,0 +1,7 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transpile with less
*/
export declare function transform(node: AST.SvelteStyleElement, text: string, context: RuleContext): TransformResult | null;

View File

@@ -0,0 +1,47 @@
import { loadModule } from '../../../utils/load-module.js';
/**
* Transpile with less
*/
export function transform(node, text, context) {
const less = loadLess(context);
if (!less) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
const filename = `${context.filename}.less`;
try {
let output;
less.render(code, {
sourceMap: {},
syncImport: true,
filename,
lint: false
}, (_error, result) => {
output = result;
});
if (!output) {
return null;
}
return {
inputRange,
output: output.css,
mappings: JSON.parse(output.map).mappings
};
}
catch {
return null;
}
}
/**
* Load less
*/
function loadLess(context) {
return loadModule(context, 'less');
}

View File

@@ -0,0 +1,7 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transform with postcss
*/
export declare function transform(node: AST.SvelteStyleElement, text: string, context: RuleContext): TransformResult | null;

View File

@@ -0,0 +1,42 @@
import postcss from 'postcss';
import postcssLoadConfig from 'postcss-load-config';
/**
* Transform with postcss
*/
export function transform(node, text, context) {
const postcssConfig = context.settings?.svelte?.compileOptions?.postcss;
if (postcssConfig === false) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
const filename = `${context.filename}.css`;
try {
const configFilePath = postcssConfig?.configFilePath;
const config = postcssLoadConfig.sync({
cwd: context.cwd ?? process.cwd(),
from: filename
}, typeof configFilePath === 'string' ? configFilePath : undefined);
const result = postcss(config.plugins).process(code, {
...config.options,
map: {
inline: false
}
});
return {
inputRange,
output: result.content,
mappings: result.map.toJSON().mappings
};
}
catch {
// console.log(e)
return null;
}
}

View File

@@ -0,0 +1,7 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transpile with sass
*/
export declare function transform(node: AST.SvelteStyleElement, text: string, context: RuleContext, type: 'scss' | 'sass'): TransformResult | null;

View File

@@ -0,0 +1,41 @@
import { loadModule } from '../../../utils/load-module.js';
/**
* Transpile with sass
*/
export function transform(node, text, context, type) {
const sass = loadSass(context);
if (!sass) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
try {
const output = sass.compileString(code, {
sourceMap: true,
syntax: type === 'sass' ? 'indented' : undefined
});
if (!output) {
return null;
}
return {
inputRange,
output: output.css,
mappings: output.sourceMap.mappings
};
}
catch {
return null;
}
}
/**
* Load sass
*/
function loadSass(context) {
return loadModule(context, 'sass');
}

View File

@@ -0,0 +1,7 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transpile with stylus
*/
export declare function transform(node: AST.SvelteStyleElement, text: string, context: RuleContext): TransformResult | null;

View File

@@ -0,0 +1,45 @@
import { loadModule } from '../../../utils/load-module.js';
/**
* Transpile with stylus
*/
export function transform(node, text, context) {
const stylus = loadStylus(context);
if (!stylus) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
const filename = `${context.filename}.stylus`;
try {
let output;
const style = stylus(code, {
filename
}).set('sourcemap', {});
style.render((_error, code) => {
output = code;
});
if (output == null) {
return null;
}
return {
inputRange,
output,
mappings: style.sourcemap.mappings
};
}
catch {
return null;
}
}
/**
* Load stylus
*/
function loadStylus(context) {
return loadModule(context, 'stylus');
}

View File

@@ -0,0 +1,6 @@
import type { AST } from 'svelte-eslint-parser';
export type TransformResult = {
inputRange: AST.Range;
output: string;
mappings: string;
};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,9 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../../types.js';
import type { TransformResult } from './types.js';
/**
* Transpile with typescript
*/
export declare function transform(node: AST.SvelteScriptElement, text: string, context: RuleContext): TransformResult | null;
/** Check if project has TypeScript. */
export declare function hasTypeScript(context: RuleContext): boolean;

View File

@@ -0,0 +1,50 @@
import { loadModule } from '../../../utils/load-module.js';
/**
* Transpile with typescript
*/
export function transform(node, text, context) {
const ts = loadTs(context);
if (!ts) {
return null;
}
let inputRange;
if (node.endTag) {
inputRange = [node.startTag.range[1], node.endTag.range[0]];
}
else {
inputRange = [node.startTag.range[1], node.range[1]];
}
const code = text.slice(...inputRange);
try {
const output = ts.transpileModule(code, {
reportDiagnostics: false,
compilerOptions: {
target: context.sourceCode.parserServices.program?.getCompilerOptions()?.target ||
ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
preserveValueImports: true,
verbatimModuleSyntax: true,
sourceMap: true
}
});
return {
inputRange,
output: output.outputText,
mappings: JSON.parse(output.sourceMapText).mappings
};
}
catch {
return null;
}
}
/** Check if project has TypeScript. */
export function hasTypeScript(context) {
return Boolean(loadTs(context));
}
/**
* Load typescript
*/
function loadTs(context) {
return loadModule(context, 'typescript');
}