- 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
108 lines
3.9 KiB
JavaScript
108 lines
3.9 KiB
JavaScript
import { createRule } from '../utils/index.js';
|
|
import { getScope } from '../utils/ast-utils.js';
|
|
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
|
|
import semver from 'semver';
|
|
// Writable derived were introduced in Svelte 5.25.0
|
|
const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0');
|
|
function isEffectOrEffectPre(node) {
|
|
if (node.callee.type === 'Identifier') {
|
|
return node.callee.name === '$effect';
|
|
}
|
|
if (node.callee.type === 'MemberExpression') {
|
|
return (node.callee.object.type === 'Identifier' &&
|
|
node.callee.object.name === '$effect' &&
|
|
node.callee.property.type === 'Identifier' &&
|
|
node.callee.property.name === 'pre');
|
|
}
|
|
return false;
|
|
}
|
|
function isValidFunctionArgument(argument) {
|
|
if ((argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') ||
|
|
argument.params.length !== 0) {
|
|
return false;
|
|
}
|
|
if (argument.body.type !== 'BlockStatement') {
|
|
return false;
|
|
}
|
|
return argument.body.body.length === 1;
|
|
}
|
|
function isValidAssignment(statement) {
|
|
if (statement.type !== 'ExpressionStatement')
|
|
return false;
|
|
const { expression } = statement;
|
|
return (expression.type === 'AssignmentExpression' &&
|
|
expression.operator === '=' &&
|
|
expression.left.type === 'Identifier');
|
|
}
|
|
function isStateVariable(init) {
|
|
return (init?.type === 'CallExpression' &&
|
|
init.callee.type === 'Identifier' &&
|
|
init.callee.name === '$state');
|
|
}
|
|
export default createRule('prefer-writable-derived', {
|
|
meta: {
|
|
docs: {
|
|
description: 'Prefer using writable $derived instead of $state and $effect',
|
|
category: 'Best Practices',
|
|
recommended: true
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
unexpected: 'Prefer using writable $derived instead of $state and $effect',
|
|
suggestRewrite: 'Rewrite $state and $effect to $derived'
|
|
},
|
|
type: 'suggestion',
|
|
conditions: [
|
|
{
|
|
svelteVersions: ['5'],
|
|
runes: [true, 'undetermined']
|
|
}
|
|
],
|
|
hasSuggestions: true
|
|
},
|
|
create(context) {
|
|
if (!shouldRun) {
|
|
return {};
|
|
}
|
|
return {
|
|
CallExpression: (node) => {
|
|
if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) {
|
|
return;
|
|
}
|
|
const argument = node.arguments[0];
|
|
if (!isValidFunctionArgument(argument)) {
|
|
return;
|
|
}
|
|
const statement = argument.body.body[0];
|
|
if (!isValidAssignment(statement)) {
|
|
return;
|
|
}
|
|
const { left, right } = statement.expression;
|
|
const scope = getScope(context, statement);
|
|
const reference = scope.references.find((ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name);
|
|
const def = reference?.resolved?.defs?.[0];
|
|
if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') {
|
|
return;
|
|
}
|
|
const { init } = def.node;
|
|
if (!isStateVariable(init)) {
|
|
return;
|
|
}
|
|
context.report({
|
|
node: def.node,
|
|
messageId: 'unexpected',
|
|
suggest: [
|
|
{
|
|
messageId: 'suggestRewrite',
|
|
fix: (fixer) => {
|
|
const rightCode = context.sourceCode.getText(right);
|
|
return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)];
|
|
}
|
|
}
|
|
]
|
|
});
|
|
}
|
|
};
|
|
}
|
|
});
|