- 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
238 lines
8.7 KiB
JavaScript
238 lines
8.7 KiB
JavaScript
import * as SV from './svelte.js';
|
|
import * as ES from './es.js';
|
|
import * as TS from './ts.js';
|
|
import { isNotWhitespace } from './ast.js';
|
|
import { isCommentToken } from '@eslint-community/eslint-utils';
|
|
import { OffsetContext } from './offset-context.js';
|
|
/**
|
|
* Normalize options.
|
|
* @param type The type of indentation.
|
|
* @param options Other options.
|
|
* @param defaultOptions The default value of options.
|
|
* @returns Normalized options.
|
|
*/
|
|
function parseOptions(options, defaultOptions) {
|
|
const ret = {
|
|
indentChar: ' ',
|
|
indentScript: true,
|
|
indentSize: 2,
|
|
switchCase: 1,
|
|
alignAttributesVertically: false,
|
|
ignoredNodes: [],
|
|
...defaultOptions
|
|
};
|
|
if (Number.isSafeInteger(options.indent)) {
|
|
ret.indentSize = Number(options.indent);
|
|
}
|
|
else if (options.indent === 'tab') {
|
|
ret.indentChar = '\t';
|
|
ret.indentSize = 1;
|
|
}
|
|
if (typeof options.indentScript === 'boolean') {
|
|
ret.indentScript = options.indentScript;
|
|
}
|
|
if (options.switchCase != null && Number.isSafeInteger(options.switchCase)) {
|
|
ret.switchCase = options.switchCase;
|
|
}
|
|
if (options.ignoredNodes != null) {
|
|
ret.ignoredNodes = options.ignoredNodes;
|
|
}
|
|
if (options.alignAttributesVertically && ret.indentChar === ' ') {
|
|
ret.alignAttributesVertically = true;
|
|
}
|
|
else if (ret.indentChar !== ' ') {
|
|
ret.alignAttributesVertically = false;
|
|
}
|
|
return ret;
|
|
}
|
|
/**
|
|
* Creates AST event handlers for html-indent.
|
|
*
|
|
* @param context The rule context.
|
|
* @param defaultOptions The default value of options.
|
|
* @returns AST event handlers.
|
|
*/
|
|
export function defineVisitor(context, defaultOptions) {
|
|
if (!context.filename.endsWith('.svelte'))
|
|
return {};
|
|
const options = parseOptions(context.options[0] || {}, defaultOptions);
|
|
const sourceCode = context.sourceCode;
|
|
const offsets = new OffsetContext({ sourceCode, options });
|
|
/**
|
|
* Get the text of the indentation part of the given location.
|
|
*/
|
|
function getIndentText({ line, column }) {
|
|
return sourceCode.lines[line - 1].slice(0, column);
|
|
}
|
|
/**
|
|
* Validate the given token with the pre-calculated expected indentation.
|
|
*/
|
|
function validateToken(token, expectedIndent) {
|
|
const line = token.loc.start.line;
|
|
const indentText = getIndentText(token.loc.start);
|
|
// `indentText` contains non-whitespace characters.
|
|
if (indentText.trim() !== '') {
|
|
return;
|
|
}
|
|
const actualIndent = token.loc.start.column;
|
|
const mismatchCharIndexes = [];
|
|
for (let i = 0; i < indentText.length; ++i) {
|
|
if (indentText[i] !== options.indentChar) {
|
|
mismatchCharIndexes.push(i);
|
|
}
|
|
}
|
|
if (actualIndent !== expectedIndent) {
|
|
const loc = {
|
|
start: { line, column: 0 },
|
|
end: { line, column: actualIndent }
|
|
};
|
|
context.report({
|
|
loc,
|
|
messageId: 'unexpectedIndentation',
|
|
data: {
|
|
expectedIndent: String(expectedIndent),
|
|
actualIndent: String(actualIndent),
|
|
expectedUnit: options.indentChar === '\t' ? 'tab' : 'space',
|
|
actualUnit: mismatchCharIndexes.length
|
|
? 'whitespace'
|
|
: options.indentChar === '\t'
|
|
? 'tab'
|
|
: 'space',
|
|
expectedIndentPlural: expectedIndent === 1 ? '' : 's',
|
|
actualIndentPlural: actualIndent === 1 ? '' : 's'
|
|
},
|
|
fix(fixer) {
|
|
return fixer.replaceTextRange([sourceCode.getIndexFromLoc(loc.start), sourceCode.getIndexFromLoc(loc.end)], options.indentChar.repeat(expectedIndent));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
for (const i of mismatchCharIndexes) {
|
|
const loc = {
|
|
start: { line, column: i },
|
|
end: { line, column: i + 1 }
|
|
};
|
|
context.report({
|
|
loc,
|
|
messageId: 'unexpectedChar',
|
|
data: {
|
|
expected: JSON.stringify(options.indentChar),
|
|
actual: JSON.stringify(indentText[i])
|
|
},
|
|
fix(fixer) {
|
|
return fixer.replaceTextRange([sourceCode.getIndexFromLoc(loc.start), sourceCode.getIndexFromLoc(loc.end)], options.indentChar);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/** Process line tokens */
|
|
function processLine(tokens, prevComments, prevToken, calculator) {
|
|
const firstToken = tokens[0];
|
|
const actualIndent = firstToken.loc.start.column;
|
|
const expectedIndent = calculator.getExpectedIndentFromTokens(tokens);
|
|
if (expectedIndent == null) {
|
|
calculator.saveExpectedIndent(tokens, actualIndent);
|
|
return;
|
|
}
|
|
calculator.saveExpectedIndent(tokens, Math.min(...tokens
|
|
.map((t) => calculator.getExpectedIndentFromToken(t))
|
|
.filter((i) => i != null)));
|
|
let prev = prevToken;
|
|
if (prevComments.length) {
|
|
if (prev && prev.loc.end.line < prevComments[0].loc.start.line) {
|
|
validateToken(prevComments[0], expectedIndent);
|
|
}
|
|
prev = prevComments[prevComments.length - 1];
|
|
}
|
|
if (prev && prev.loc.end.line < tokens[0].loc.start.line) {
|
|
validateToken(tokens[0], expectedIndent);
|
|
}
|
|
}
|
|
const indentContext = {
|
|
sourceCode,
|
|
options,
|
|
offsets
|
|
};
|
|
const nodesVisitor = {
|
|
...ES.defineVisitor(indentContext),
|
|
...SV.defineVisitor(indentContext),
|
|
...TS.defineVisitor(indentContext)
|
|
};
|
|
const knownNodes = new Set(Object.keys(nodesVisitor));
|
|
/**
|
|
* Build a visitor combined with a visitor to handle the given ignore selector.
|
|
*/
|
|
function compositingIgnoresVisitor(visitor) {
|
|
for (const ignoreSelector of options.ignoredNodes) {
|
|
const key = `${ignoreSelector}:exit`;
|
|
if (visitor[key]) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
|
|
const handler = visitor[key];
|
|
visitor[key] = function (node, ...args) {
|
|
const ret = handler.call(this, node, ...args);
|
|
offsets.ignore(node);
|
|
return ret;
|
|
};
|
|
}
|
|
else {
|
|
visitor[key] = (node) => offsets.ignore(node);
|
|
}
|
|
}
|
|
return visitor;
|
|
}
|
|
return compositingIgnoresVisitor({
|
|
...nodesVisitor,
|
|
'*:exit'(node) {
|
|
// Ignore tokens of unknown nodes.
|
|
if (!knownNodes.has(node.type)) {
|
|
// debugger
|
|
// console.log(node.type, node.loc!.start.line)
|
|
offsets.ignore(node);
|
|
}
|
|
},
|
|
'Program:exit'(node) {
|
|
const calculator = offsets.getOffsetCalculator();
|
|
let prevToken = null;
|
|
for (const { prevComments, tokens } of iterateLineTokens()) {
|
|
processLine(tokens, prevComments, prevToken, calculator);
|
|
prevToken = tokens[tokens.length - 1];
|
|
}
|
|
/** Iterate line tokens */
|
|
function* iterateLineTokens() {
|
|
let line = 0;
|
|
let prevComments = [];
|
|
let bufferTokens = [];
|
|
for (const token of sourceCode.getTokens(node, {
|
|
includeComments: true,
|
|
filter: isNotWhitespace
|
|
})) {
|
|
const thisLine = token.loc.start.line;
|
|
if (line === thisLine || bufferTokens.length === 0) {
|
|
bufferTokens.push(token);
|
|
}
|
|
else {
|
|
if (isCommentToken(bufferTokens[0]) && bufferTokens.every(isCommentToken)) {
|
|
prevComments.push(bufferTokens[0]);
|
|
}
|
|
else {
|
|
yield {
|
|
prevComments,
|
|
tokens: bufferTokens
|
|
};
|
|
prevComments = [];
|
|
}
|
|
bufferTokens = [token];
|
|
}
|
|
line = thisLine;
|
|
}
|
|
if (bufferTokens.length && !bufferTokens.every(isCommentToken)) {
|
|
yield {
|
|
prevComments,
|
|
tokens: bufferTokens
|
|
};
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|