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,21 @@
import type { AST } from 'svelte-eslint-parser';
import type { Linter } from 'eslint';
type Define = {
loc: AST.Position;
};
export declare class CommentDirectives {
private readonly reportUnusedDisableDirectives;
private readonly ruleId;
private readonly lineDisableDirectives;
private readonly blockDirectives;
constructor(options: {
reportUnusedDisableDirectives?: boolean;
ruleId: string;
});
filterMessages(messages: Linter.LintMessage[]): Linter.LintMessage[];
disableLine(line: number, rule: string | ((ruleId: string) => boolean), define: Define): void;
disableBlock(pos: AST.Position, rule: string | ((ruleId: string) => boolean), define: Define): void;
enableBlock(pos: AST.Position, rule: string | ((ruleId: string) => boolean), define: Define): void;
private block;
}
export {};

View File

@@ -0,0 +1,158 @@
const COMPUTED = Symbol();
const ALL = Symbol();
export class CommentDirectives {
constructor(options) {
this.lineDisableDirectives = new Map();
this.blockDirectives = new Map();
this.ruleId = options.ruleId;
this.reportUnusedDisableDirectives = Boolean(options?.reportUnusedDisableDirectives);
}
filterMessages(messages) {
const { lineDisableDirectives, blockDirectives, reportUnusedDisableDirectives } = this;
const usedDirectives = new Set();
if (reportUnusedDisableDirectives) {
const allBlocks = [];
for (const bs of blockDirectives.values()) {
for (const b of bs) {
allBlocks.push(b);
}
}
const blockEnableDirectives = new Set();
for (const block of allBlocks.sort((a, b) => comparePos(b.loc, a.loc))) {
if (block.kind === 'enable') {
if (block.targetRule === ALL) {
blockEnableDirectives.clear();
}
blockEnableDirectives.add(block);
}
else if (block.kind === 'disable') {
if (block.targetRule === ALL) {
for (const b of blockEnableDirectives) {
usedDirectives.add(b);
}
blockEnableDirectives.clear();
}
else {
for (const b of [...blockEnableDirectives]) {
if (block.targetRule === b.targetRule || b.targetRule === ALL) {
usedDirectives.add(b);
blockEnableDirectives.delete(b);
}
}
}
}
}
}
let filteredMessages = messages.filter(isEnable);
if (reportUnusedDisableDirectives) {
const usedDirectiveKeys = new Set([...usedDirectives].map((d) => locToKey(d.define.loc)));
filteredMessages = filteredMessages.filter((m) => {
if (m.ruleId !== this.ruleId) {
return true;
}
if (usedDirectiveKeys.has(messageToKey(m))) {
return false;
}
return true;
});
}
return filteredMessages;
/** Checks wether given rule is enable */
function isEnable(message) {
if (!message.ruleId) {
// Maybe fatal error
return true;
}
for (const disableLines of getFromRule(lineDisableDirectives, message.ruleId)) {
for (const disableLine of disableLines.get(message.line) ?? []) {
if (!disableLine.rule(message.ruleId)) {
continue;
}
usedDirectives.add(disableLine);
return false;
}
}
const blocks = getFromRule(blockDirectives, message.ruleId)
.reduce((p, c) => p.concat(c), [])
.sort((a, b) => comparePos(b.loc, a.loc));
for (const block of blocks) {
if (comparePos(message, block.loc) < 0) {
continue;
}
if (!block.rule(message.ruleId)) {
continue;
}
if (block.kind === 'enable') {
return true;
}
// block.kind === "disable"
usedDirectives.add(block);
return false;
}
return true;
}
/** Compare locations */
function comparePos(a, b) {
return a.line - b.line || a.column - b.column;
}
}
disableLine(line, rule, define) {
const key = typeof rule === 'string' ? rule : COMPUTED;
let disableLines = this.lineDisableDirectives.get(key);
if (!disableLines) {
disableLines = new Map();
this.lineDisableDirectives.set(key, disableLines);
}
let disableLine = disableLines.get(line);
if (!disableLine) {
disableLine = [];
disableLines.set(line, disableLine);
}
const disable = {
line,
rule: typeof rule === 'string' ? (id) => id === rule : rule,
define
};
disableLine.push(disable);
}
disableBlock(pos, rule, define) {
this.block(pos, rule, define, 'disable');
}
enableBlock(pos, rule, define) {
this.block(pos, rule, define, 'enable');
}
block(pos, rule, define, kind) {
const key = typeof rule === 'string' ? rule : COMPUTED;
let blocks = this.blockDirectives.get(key);
if (!blocks) {
blocks = [];
this.blockDirectives.set(key, blocks);
}
const disable = {
loc: pos,
rule: typeof rule === 'string' ? (id) => id === rule : rule,
kind,
targetRule: typeof rule === 'string' ? rule : ALL,
define
};
blocks.push(disable);
}
}
/** Get the list of directives from given rule */
function getFromRule(map, ruleId) {
return [map.get(ruleId), map.get(COMPUTED)].filter((a) => Boolean(a));
}
/**
* Gets the key of location
*/
function locToKey(location) {
return `line:${location.line},column${location.column}`;
}
/**
* Gets the key of message location
*/
function messageToKey(message) {
return `line:${message.line},column${
// -1 because +1 by ESLint's `report-translator`.
message.column - 1}`;
}

View File

@@ -0,0 +1,11 @@
import { CommentDirectives } from './comment-directives.js';
export declare class Shared {
readonly commentDirectives: CommentDirectives[];
newCommentDirectives(options: ConstructorParameters<typeof CommentDirectives>[0]): CommentDirectives;
}
/** Start sharing and make the data available. */
export declare function beginShared(filename: string): void;
/** Get the shared data and end the sharing. */
export declare function terminateShared(filename: string): Shared | null;
/** If sharing has started, get the shared data. */
export declare function getShared(filename: string): Shared | null;

View File

@@ -0,0 +1,26 @@
import { CommentDirectives } from './comment-directives.js';
export class Shared {
constructor() {
this.commentDirectives = [];
}
newCommentDirectives(options) {
const directives = new CommentDirectives(options);
this.commentDirectives.push(directives);
return directives;
}
}
const sharedMap = new Map();
/** Start sharing and make the data available. */
export function beginShared(filename) {
sharedMap.set(filename, new Shared());
}
/** Get the shared data and end the sharing. */
export function terminateShared(filename) {
const result = sharedMap.get(filename);
sharedMap.delete(filename);
return result ?? null;
}
/** If sharing has started, get the shared data. */
export function getShared(filename) {
return sharedMap.get(filename) ?? null;
}

View File

@@ -0,0 +1 @@
export declare const SVELTE_RUNES: Set<string>;

View File

@@ -0,0 +1,9 @@
export const SVELTE_RUNES = new Set([
'$state',
'$derived',
'$effect',
'$props',
'$bindable',
'$inspect',
'$host'
]);

View File

@@ -0,0 +1,5 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../types.js';
import type { ASTNodeWithParent } from '../../types-for-node.js';
/** Extract comments */
export declare function extractLeadingComments(context: RuleContext, node: ASTNodeWithParent): (AST.Token | AST.Comment)[];

View File

@@ -0,0 +1,28 @@
import { isOpeningParenToken } from '@eslint-community/eslint-utils';
/** Extract comments */
export function extractLeadingComments(context, node) {
const sourceCode = context.sourceCode;
const beforeToken = sourceCode.getTokenBefore(node, {
includeComments: false,
filter(token) {
if (isOpeningParenToken(token)) {
return false;
}
const astToken = token;
if (astToken.type === 'HTMLText') {
return false;
}
return astToken.type !== 'HTMLComment';
}
});
if (beforeToken) {
return sourceCode
.getTokensBetween(beforeToken, node, { includeComments: true })
.filter(isComment);
}
return sourceCode.getTokensBefore(node, { includeComments: true }).filter(isComment);
}
/** Checks whether given token is comment token */
function isComment(token) {
return token.type === 'HTMLComment' || token.type === 'Block' || token.type === 'Line';
}

View File

@@ -0,0 +1,15 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../types.js';
export type IgnoreItemWithoutCode = {
range: [number, number];
code: null;
token: AST.Token | AST.Comment;
};
export type IgnoreItem = {
range: [number, number];
code: string;
codeForV5: string;
token: AST.Token | AST.Comment;
};
/** Extract all svelte-ignore comment items */
export declare function getSvelteIgnoreItems(context: RuleContext): (IgnoreItem | IgnoreItemWithoutCode)[];

View File

@@ -0,0 +1,102 @@
const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore\s+/;
/**
* Map of legacy code -> new code
* See https://github.com/sveltejs/svelte/blob/c9202a889612df3c2fcb369096a5573668be99d6/packages/svelte/src/compiler/utils/extract_svelte_ignore.js#L6
*/
const V5_REPLACEMENTS = {
'non-top-level-reactive-declaration': 'reactive_declaration_invalid_placement',
'module-script-reactive-declaration': 'reactive_declaration_module_script',
'empty-block': 'block_empty',
'avoid-is': 'attribute_avoid_is',
'invalid-html-attribute': 'attribute_invalid_property_name',
'a11y-structure': 'a11y_figcaption_parent',
'illegal-attribute-character': 'attribute_illegal_colon',
'invalid-rest-eachblock-binding': 'bind_invalid_each_rest',
'unused-export-let': 'export_let_unused'
};
/** Extract all svelte-ignore comment items */
export function getSvelteIgnoreItems(context) {
const sourceCode = context.sourceCode;
const ignoreComments = [];
for (const comment of sourceCode.getAllComments()) {
const match = SVELTE_IGNORE_PATTERN.exec(comment.value);
if (!match) {
continue;
}
const codeListStart = match.index + match[0].length;
const codeList = comment.value.slice(codeListStart);
if (hasMissingCodeIgnore(codeList)) {
ignoreComments.push({
range: comment.range,
code: null,
token: comment
});
}
else {
const ignores = extractSvelteIgnore(comment.range[0] + 2, comment, codeList, codeListStart);
if (ignores) {
ignoreComments.push(...ignores);
}
}
}
for (const token of sourceCode.ast.tokens) {
if (token.type === 'HTMLComment') {
const text = token.value.slice(4, -3);
const match = SVELTE_IGNORE_PATTERN.exec(text);
if (!match) {
continue;
}
const codeListStart = match.index + match[0].length;
const codeList = text.slice(codeListStart);
if (hasMissingCodeIgnore(codeList)) {
ignoreComments.push({
range: token.range,
code: null,
token
});
}
else {
const ignores = extractSvelteIgnore(token.range[0] + 4, token, codeList, codeListStart);
if (ignores) {
ignoreComments.push(...ignores);
}
}
}
}
ignoreComments.sort((a, b) => b.range[0] - a.range[0]);
return ignoreComments;
}
/** Extract svelte-ignore rule names */
function extractSvelteIgnore(startIndex, token, codeList, ignoreStart) {
const start = startIndex + ignoreStart;
const results = [];
const separatorPattern = /\s*[\s,]\s*/g;
const separators = codeList.matchAll(separatorPattern);
let lastSeparatorEnd = 0;
for (const separator of separators) {
const code = codeList.slice(lastSeparatorEnd, separator.index);
if (code) {
results.push({
code,
codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'),
range: [start + lastSeparatorEnd, start + separator.index],
token
});
}
lastSeparatorEnd = separator.index + separator[0].length;
}
if (results.length === 0) {
const code = codeList;
results.push({
code,
codeForV5: V5_REPLACEMENTS[code] || code.replace(/-/gu, '_'),
range: [start, start + code.length],
token
});
}
return results;
}
/** Checks whether given comment has missing code svelte-ignore */
function hasMissingCodeIgnore(codeList) {
return !codeList.trim();
}

View File

@@ -0,0 +1,32 @@
import type { AST } from 'svelte-eslint-parser';
import type { RuleContext } from '../../types.js';
import type { IgnoreItem } from './ignore-comment.js';
export type SvelteCompileWarnings = {
warnings: Warning[];
unusedIgnores: IgnoreItem[];
kind: 'warn' | 'error';
stripStyleElements: AST.SvelteStyleElement[];
};
export type Loc = {
start?: {
line: number;
column: number;
character: number;
};
end?: {
line: number;
column: number;
character: number;
};
};
export type Warning = ({
code: string;
message: string;
} | {
code?: undefined;
message: string;
}) & Loc;
/**
* Get svelte compile warnings
*/
export declare function getSvelteCompileWarnings(context: RuleContext): SvelteCompileWarnings;

View File

@@ -0,0 +1,524 @@
import * as compiler from 'svelte/compiler';
import { decode } from '@jridgewell/sourcemap-codec';
import { LinesAndColumns } from '../../utils/lines-and-columns.js';
import { hasTypeScript, transform as transformWithTypescript } from './transform/typescript.js';
import { transform as transformWithBabel } from './transform/babel.js';
import { transform as transformWithPostCSS } from './transform/postcss.js';
import { transform as transformWithSass } from './transform/sass.js';
import { transform as transformWithLess } from './transform/less.js';
import { transform as transformWithStylus } from './transform/stylus.js';
import { getSvelteIgnoreItems } from './ignore-comment.js';
import { extractLeadingComments } from './extract-leading-comments.js';
import { findAttribute, getLangValue } from '../../utils/ast-utils.js';
import { getSvelteVersion } from '../../utils/svelte-context.js';
import path from 'path';
import fs from 'fs';
const STYLE_TRANSFORMS = {
postcss: transformWithPostCSS,
pcss: transformWithPostCSS,
scss: (node, text, context) => transformWithSass(node, text, context, 'scss'),
sass: (node, text, context) => transformWithSass(node, text, context, 'sass'),
less: transformWithLess,
stylus: transformWithStylus,
styl: transformWithStylus
};
const CSS_WARN_CODES = new Set([
'css-unused-selector',
'css_unused_selector',
'css-invalid-global',
'css-invalid-global-selector'
]);
const cacheAll = new WeakMap();
/**
* Get svelte compile warnings
*/
export function getSvelteCompileWarnings(context) {
const sourceCode = context.sourceCode;
const cache = cacheAll.get(sourceCode.ast);
if (cache) {
return cache;
}
const result = getSvelteCompileWarningsWithoutCache(context);
cacheAll.set(sourceCode.ast, result);
return result;
}
/**
* Get svelte compile warnings
*/
function getSvelteCompileWarningsWithoutCache(context) {
const sourceCode = context.sourceCode;
// Process for styles
const styleElementsWithNotCSS = [...extractStyleElementsWithLangOtherThanCSS(context)];
const stripStyleElements = [];
const transformResults = [];
for (const style of styleElementsWithNotCSS) {
const transform = STYLE_TRANSFORMS[style.lang];
if (transform) {
const result = transform(style.node, context.sourceCode.text, context);
if (result) {
transformResults.push(result);
continue;
}
}
stripStyleElements.push(style.node);
}
const stripStyleTokens = stripStyleElements.flatMap((e) => e.children);
const ignoreComments = getSvelteIgnoreItems(context).filter((item) => item.code != null);
const text = buildStrippedText(context, ignoreComments, stripStyleTokens);
transformResults.push(...transformScripts(context, text));
if (!transformResults.length) {
const warnings = getWarningsFromCode(text, context);
return {
...processIgnore(warnings.warnings, warnings.kind, stripStyleElements, ignoreComments, context),
kind: warnings.kind,
stripStyleElements
};
}
class RemapContext {
constructor() {
this.originalStart = 0;
this.code = '';
this.locs = null;
this.mapIndexes = [];
}
appendOriginal(endIndex) {
const codeStart = this.code.length;
const start = this.originalStart;
this.code += text.slice(start, endIndex);
this.originalStart = endIndex;
const offset = start - codeStart;
this.mapIndexes.push({
range: [codeStart, this.code.length],
remap(index) {
return index + offset;
}
});
}
postprocess() {
this.appendOriginal(text.length);
return this.code;
}
appendTranspile(output) {
const endIndex = output.inputRange[1];
const codeStart = this.code.length;
const start = this.originalStart;
const inputText = text.slice(start, endIndex);
const outputText = `${output.output}\n`;
this.code += outputText;
this.originalStart = endIndex;
let outputLocs = null;
let inputLocs = null;
let decoded = null;
this.mapIndexes.push({
range: [codeStart, this.code.length],
remap: (index) => {
outputLocs = outputLocs ?? new LinesAndColumns(outputText);
inputLocs = inputLocs ?? new LinesAndColumns(inputText);
const outputCodePos = outputLocs.getLocFromIndex(index - codeStart);
const inputCodePos = remapPosition(outputCodePos);
return inputLocs.getIndexFromLoc(inputCodePos) + start;
}
});
/** Remapping source position */
function remapPosition(pos) {
decoded = decoded ?? decode(output.mappings);
const lineMaps = decoded[pos.line - 1];
if (!lineMaps?.length) {
for (let line = pos.line - 1; line >= 0; line--) {
const prevLineMaps = decoded[line];
if (prevLineMaps?.length) {
const [, , sourceCodeLine, sourceCodeColumn] = prevLineMaps[prevLineMaps.length - 1];
return {
line: sourceCodeLine + 1,
column: sourceCodeColumn
};
}
}
return { line: -1, column: -1 };
}
for (let index = 0; index < lineMaps.length - 1; index++) {
const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[index];
if (generateCodeColumn <= pos.column && pos.column < lineMaps[index + 1][0]) {
return {
line: sourceCodeLine + 1,
column: sourceCodeColumn + (pos.column - generateCodeColumn)
};
}
}
const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[lineMaps.length - 1];
return {
line: sourceCodeLine + 1,
column: sourceCodeColumn + (pos.column - generateCodeColumn)
};
}
}
remapLocs(points) {
const mapIndexes = this.mapIndexes;
const locs = (this.locs = this.locs ?? new LinesAndColumns(this.code));
let start = undefined;
let end = undefined;
if (points.start) {
const index = locs.getIndexFromLoc(points.start);
const remapped = remapIndex(index);
if (remapped) {
start = { ...sourceCode.getLocFromIndex(remapped), character: remapped };
}
}
if (points.end) {
const index = locs.getIndexFromLoc(points.end);
const remapped = remapIndex(index - 1 /* include index */);
if (remapped) {
const character = remapped + 1; /* restore */
end = { ...sourceCode.getLocFromIndex(character), character };
}
}
return { start, end };
/** remap index */
function remapIndex(index) {
for (const mapIndex of mapIndexes) {
if (mapIndex.range[0] <= index && index < mapIndex.range[1]) {
return mapIndex.remap(index);
}
}
return null;
}
}
}
const remapContext = new RemapContext();
for (const result of transformResults.sort((a, b) => a.inputRange[0] - b.inputRange[0])) {
remapContext.appendOriginal(result.inputRange[0]);
remapContext.appendTranspile(result);
}
const code = remapContext.postprocess();
const baseWarnings = getWarningsFromCode(code, context);
const warnings = [];
for (const warn of baseWarnings.warnings) {
let loc = null;
/** Get re-mapped location */
// eslint-disable-next-line func-style -- ignore
const getLoc = function getLoc() {
if (loc) {
return loc;
}
return (loc = remapContext.remapLocs(warn));
};
warnings.push({
code: warn.code,
message: warn.message,
get start() {
return getLoc().start;
},
get end() {
return getLoc().end;
}
});
}
return {
...processIgnore(warnings, baseWarnings.kind, stripStyleElements, ignoreComments, context),
kind: baseWarnings.kind,
stripStyleElements
};
}
/**
* Extracts the style with the lang attribute other than CSS.
*/
function* extractStyleElementsWithLangOtherThanCSS(context) {
const sourceCode = context.sourceCode;
const root = sourceCode.ast;
for (const node of root.body) {
if (node.type === 'SvelteStyleElement') {
const lang = getLangValue(node);
if (lang != null && lang.toLowerCase() !== 'css') {
yield { node, lang: lang.toLowerCase() };
}
}
}
}
/**
* Build the text stripped of tokens that are not needed for compilation.
*/
function buildStrippedText(context, ignoreComments, stripStyleTokens) {
const sourceCode = context.sourceCode;
const baseText = sourceCode.text;
const stripTokens = new Set([...ignoreComments.map((item) => item.token), ...stripStyleTokens]);
if (!stripTokens.size) {
return baseText;
}
let code = '';
let start = 0;
for (const token of [...stripTokens].sort((a, b) => a.range[0] - b.range[0])) {
code +=
baseText.slice(start, token.range[0]) +
baseText.slice(...token.range).replace(/[^\t\n\r ]/g, ' ');
start = token.range[1];
}
code += baseText.slice(start);
return code;
}
/** Returns the result of transforming the required script for the transform. */
function* transformScripts(context, text) {
const transform = isUseTypeScript(context)
? hasTypeScript(context)
? transformWithTypescript
: transformWithBabel
: isUseBabel(context)
? transformWithBabel
: null;
const sourceCode = context.sourceCode;
if (transform) {
const root = sourceCode.ast;
for (const node of root.body) {
if (node.type === 'SvelteScriptElement') {
const result = transform(node, text, context);
if (result) {
yield result;
}
}
}
}
}
function isCustomElement(program) {
return program.body.some((body) => {
if (body.type !== 'SvelteElement' || body.kind !== 'special') {
return false;
}
if (body.name.name !== 'svelte:options') {
return false;
}
return Boolean(findAttribute(body, 'tag')) || Boolean(findAttribute(body, 'customElement'));
});
}
const svelteVersion = getSvelteVersion();
/**
* Get compile warnings
*/
function getWarningsFromCode(code, context) {
try {
const svelteConfig = context.sourceCode.parserServices.svelteParseContext?.svelteConfig;
const compilerOptions = svelteConfig?.compilerOptions ?? {};
const result = compiler.compile(code, {
...(svelteVersion === '5'
? {
experimental: {
async: compilerOptions.experimental?.async
}
}
: {}),
generate: false,
...(isCustomElement(context.sourceCode.ast) ? { customElement: true } : {})
});
return { warnings: result.warnings, kind: 'warn' };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
}
catch (e) {
return {
warnings: [
{
code: e.code,
message: e.message,
start: e.start,
end: e.end
}
],
kind: 'error'
};
}
}
/** Ignore process */
function processIgnore(warnings, kind, stripStyleElements, ignoreComments, context) {
if (kind === 'error') {
return {
warnings,
unusedIgnores: ignoreComments
};
}
const sourceCode = context.sourceCode;
const unusedIgnores = new Set(ignoreComments);
const remainingWarning = new Set(warnings);
for (const warning of warnings) {
if (!warning.code) {
continue;
}
let node = getWarningNode(warning);
while (node) {
for (const comment of extractLeadingComments(context, node).reverse()) {
const ignoreItem = ignoreComments.find((item) => item.token === comment &&
(item.code === warning.code || item.codeForV5 === warning.code));
if (ignoreItem) {
unusedIgnores.delete(ignoreItem);
remainingWarning.delete(warning);
break;
}
}
node = getIgnoreParent(node);
}
}
// Stripped styles are ignored from compilation and cannot determine css errors.
for (const node of stripStyleElements) {
for (const comment of extractLeadingComments(context, node).reverse()) {
const ignoreItem = ignoreComments.find((item) => item.token === comment &&
(CSS_WARN_CODES.has(item.code) || CSS_WARN_CODES.has(item.codeForV5)));
if (ignoreItem) {
unusedIgnores.delete(ignoreItem);
break;
}
}
}
return {
warnings: [...remainingWarning],
unusedIgnores: [...unusedIgnores]
};
/** Get ignore target parent node */
function getIgnoreParent(node) {
if (node.type !== 'SvelteElement' &&
node.type !== 'SvelteIfBlock' &&
node.type !== 'SvelteKeyBlock' &&
node.type !== 'SvelteEachBlock' &&
node.type !== 'SvelteAwaitBlock') {
return null;
}
const parent = node.parent;
if (parent.type === 'SvelteElseBlock') {
return parent.parent; // SvelteIfBlock or SvelteEachBlock
}
if (parent.type === 'SvelteAwaitPendingBlock' ||
parent.type === 'SvelteAwaitThenBlock' ||
parent.type === 'SvelteAwaitCatchBlock') {
return parent.parent; // SvelteAwaitBlock
}
if (parent.type !== 'SvelteElement' &&
parent.type !== 'SvelteIfBlock' &&
parent.type !== 'SvelteKeyBlock' &&
parent.type !== 'SvelteEachBlock'
// && parent.type !== "SvelteAwaitBlock"
) {
return null;
}
return parent;
}
/** Get warning node */
function getWarningNode(warning) {
const indexes = getWarningIndexes(warning);
if (indexes.start != null) {
const node = getWarningTargetNodeFromIndex(indexes.start);
if (node) {
return node;
}
if (indexes.end != null) {
const center = Math.floor(indexes.start + (indexes.end - indexes.start) / 2);
return getWarningTargetNodeFromIndex(center);
}
}
if (indexes.end != null) {
return getWarningTargetNodeFromIndex(indexes.end);
}
return null;
}
/**
* Get warning target node from the given index
*/
function getWarningTargetNodeFromIndex(index) {
let targetNode = sourceCode.getNodeByRangeIndex(index);
while (targetNode) {
if (targetNode.type === 'SvelteElement' || targetNode.type === 'SvelteStyleElement') {
return targetNode;
}
if (targetNode.parent) {
if (targetNode.parent.type === 'Program' ||
targetNode.parent.type === 'SvelteScriptElement') {
return targetNode;
}
}
else {
return null;
}
targetNode = targetNode.parent || null;
}
return null;
}
/** Get warning index */
function getWarningIndexes(warning) {
const start = warning.start && sourceCode.getIndexFromLoc(warning.start);
const end = warning.end && sourceCode.getIndexFromLoc(warning.end);
return { start, end };
}
}
/**
* Check if using TypeScript.
*/
function isUseTypeScript(context) {
const sourceCode = context.sourceCode;
if (sourceCode.parserServices.esTreeNodeToTSNodeMap)
return true;
const root = sourceCode.ast;
for (const node of root.body) {
if (node.type === 'SvelteScriptElement') {
const lang = getLangValue(node)?.toLowerCase();
if (lang === 'ts' || lang === 'typescript') {
return true;
}
}
}
return false;
}
/**
* Check if using Babel.
*/
function isUseBabel(context) {
const parser = context.parserOptions?.parser;
if (!parser) {
return false;
}
const sourceCode = context.sourceCode;
const root = sourceCode.ast;
let scriptLang = 'js';
for (const node of root.body) {
if (node.type === 'SvelteScriptElement') {
const lang = getLangValue(node)?.toLowerCase();
if (lang === 'ts' || lang === 'typescript') {
scriptLang = lang;
break;
}
}
}
const parserName = getParserName(scriptLang, parser);
if (!parserName) {
return false;
}
if (parserName === '@babel/eslint-parser') {
return true;
}
if (parserName.includes('@babel/eslint-parser')) {
let targetPath = parserName;
while (targetPath) {
const pkgPath = path.join(targetPath, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))?.name === '@babel/eslint-parser';
}
catch {
return false;
}
}
const parent = path.dirname(targetPath);
if (targetPath === parent) {
break;
}
targetPath = parent;
}
}
return false;
/** Get script parser name */
function getParserName(lang, parser) {
if (typeof parser === 'string') {
return parser;
}
else if (typeof parser === 'object') {
const name = parser[lang];
if (typeof name === 'string') {
return name;
}
}
return null;
}
}

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');
}