- 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
185 lines
7.7 KiB
JavaScript
185 lines
7.7 KiB
JavaScript
import { createRule } from '../utils/index.js';
|
|
import { findAttribute, getLangValue } from '../utils/ast-utils.js';
|
|
export default createRule('block-lang', {
|
|
meta: {
|
|
docs: {
|
|
description: 'disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks.',
|
|
category: 'Best Practices',
|
|
recommended: false
|
|
},
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
enforceScriptPresent: {
|
|
type: 'boolean'
|
|
},
|
|
enforceStylePresent: {
|
|
type: 'boolean'
|
|
},
|
|
script: {
|
|
oneOf: [
|
|
{
|
|
type: ['string', 'null']
|
|
},
|
|
{
|
|
type: 'array',
|
|
items: {
|
|
type: ['string', 'null']
|
|
},
|
|
minItems: 1
|
|
}
|
|
]
|
|
},
|
|
style: {
|
|
oneOf: [
|
|
{
|
|
type: ['string', 'null']
|
|
},
|
|
{
|
|
type: 'array',
|
|
items: {
|
|
type: ['string', 'null']
|
|
},
|
|
minItems: 1
|
|
}
|
|
]
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
],
|
|
messages: {},
|
|
type: 'suggestion',
|
|
hasSuggestions: true
|
|
},
|
|
create(context) {
|
|
if (!context.sourceCode.parserServices.isSvelte) {
|
|
return {};
|
|
}
|
|
const enforceScriptPresent = context.options[0]?.enforceScriptPresent ?? false;
|
|
const enforceStylePresent = context.options[0]?.enforceStylePresent ?? false;
|
|
const scriptOption = context.options[0]?.script ?? null;
|
|
const allowedScriptLangs = Array.isArray(scriptOption)
|
|
? scriptOption
|
|
: [scriptOption];
|
|
const scriptNodes = [];
|
|
const styleOption = context.options[0]?.style ?? null;
|
|
const allowedStyleLangs = Array.isArray(styleOption)
|
|
? styleOption
|
|
: [styleOption];
|
|
const styleNodes = [];
|
|
return {
|
|
SvelteScriptElement(node) {
|
|
scriptNodes.push(node);
|
|
},
|
|
SvelteStyleElement(node) {
|
|
styleNodes.push(node);
|
|
},
|
|
'Program:exit'() {
|
|
if (scriptNodes.length === 0 && enforceScriptPresent) {
|
|
context.report({
|
|
loc: { line: 1, column: 1 },
|
|
message: `The <script> block should be present and its lang attribute should be ${prettyPrintLangs(allowedScriptLangs)}.`,
|
|
suggest: buildAddLangSuggestions(allowedScriptLangs, 'script', context.sourceCode)
|
|
});
|
|
}
|
|
for (const scriptNode of scriptNodes) {
|
|
if (!allowedScriptLangs.includes(getLangValue(scriptNode)?.toLowerCase() ?? null)) {
|
|
context.report({
|
|
node: scriptNode,
|
|
message: `The lang attribute of the <script> block should be ${prettyPrintLangs(allowedScriptLangs)}.`,
|
|
suggest: buildReplaceLangSuggestions(allowedScriptLangs, scriptNode)
|
|
});
|
|
}
|
|
}
|
|
if (styleNodes.length === 0 && enforceStylePresent) {
|
|
const sourceCode = context.sourceCode;
|
|
context.report({
|
|
loc: { line: 1, column: 1 },
|
|
message: `The <style> block should be present and its lang attribute should be ${prettyPrintLangs(allowedStyleLangs)}.`,
|
|
suggest: buildAddLangSuggestions(allowedStyleLangs, 'style', sourceCode)
|
|
});
|
|
}
|
|
for (const styleNode of styleNodes) {
|
|
if (!allowedStyleLangs.includes(getLangValue(styleNode)?.toLowerCase() ?? null)) {
|
|
context.report({
|
|
node: styleNode,
|
|
message: `The lang attribute of the <style> block should be ${prettyPrintLangs(allowedStyleLangs)}.`,
|
|
suggest: buildReplaceLangSuggestions(allowedStyleLangs, styleNode)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
function buildAddLangSuggestions(langs, tagName, sourceCode) {
|
|
return langs
|
|
.filter((lang) => lang != null && lang !== '')
|
|
.map((lang) => {
|
|
return {
|
|
desc: `Add a lang attribute to a <${tagName}> block with the value "${lang}".`,
|
|
fix: (fixer) => {
|
|
const langAttributeText = getLangAttributeText(lang ?? '', true);
|
|
return fixer.insertTextAfterRange(tagName === 'script' ? [0, 0] : [sourceCode.text.length, sourceCode.text.length], `<${tagName}${langAttributeText}>\n</${tagName}>\n\n`);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
function buildReplaceLangSuggestions(langs, node) {
|
|
const tagName = node.name.name;
|
|
const langAttribute = findAttribute(node, 'lang');
|
|
const filteredLangs = langs.filter((lang) => lang != null && lang !== '');
|
|
if (filteredLangs.length === 0 && langs.includes(null) && langAttribute !== null) {
|
|
return [
|
|
{
|
|
desc: `Replace a <${tagName}> block with the lang attribute omitted.`,
|
|
fix: (fixer) => {
|
|
return fixer.remove({
|
|
type: langAttribute.type,
|
|
range: [langAttribute.range[0] - 1, langAttribute.range[1]]
|
|
});
|
|
}
|
|
}
|
|
];
|
|
}
|
|
return filteredLangs.map((lang) => {
|
|
const langAttributeText = getLangAttributeText(lang ?? '', true);
|
|
if (langAttribute) {
|
|
return {
|
|
desc: `Replace a <${tagName}> block with the lang attribute set to "${lang}".`,
|
|
fix: (fixer) => {
|
|
return fixer.replaceText(langAttribute, langAttributeText.trim());
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
desc: `Add lang attribute to a <${tagName}> block with the value "${lang}".`,
|
|
fix: (fixer) => {
|
|
return fixer.insertTextBeforeRange([node.startTag.range[0] + tagName.length + 1, 0], langAttributeText);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
/**
|
|
* Prints the list of allowed languages, with special handling of the `null` option.
|
|
*/
|
|
function prettyPrintLangs(langs) {
|
|
const hasNull = langs.includes(null);
|
|
const nonNullLangs = langs.filter((lang) => lang !== null).map((lang) => `"${lang}"`);
|
|
if (nonNullLangs.length === 0) {
|
|
// No special behavior for `hasNull`, because that can never happen.
|
|
return 'omitted';
|
|
}
|
|
const hasNullText = hasNull ? 'either omitted or ' : '';
|
|
const nonNullText = nonNullLangs.length === 1 ? nonNullLangs[0] : `one of ${nonNullLangs.join(', ')}`;
|
|
return hasNullText + nonNullText;
|
|
}
|
|
/**
|
|
* Returns the lang attribute text, with special handling of the `null` lang option with respect to the `prependWhitespace` argument.
|
|
*/
|
|
function getLangAttributeText(lang, prependWhitespace) {
|
|
return `${prependWhitespace ? ' ' : ''}lang="${lang}"`;
|
|
}
|