diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index f66d0b3a..31f01dea 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -81,6 +81,7 @@ export default defineConfig({ { text: 'If Without Curly Braces', link: '/rules/rrd/if-without-curly-braces' }, { text: 'Magic Numbers', link: '/rules/rrd/magic-numbers' }, { text: 'Nested Ternary', link: '/rules/rrd/nested-ternary' }, + { text: 'No Inline Styles', link: '/rules/rrd/no-inline-styles' }, { text: 'No Props Destructure', link: '/rules/rrd/no-prop-destructure' }, { text: 'No Var Declaration', link: '/rules/rrd/no-var-declaration' }, { text: 'Parameter Count', link: '/rules/rrd/parameter-count' }, diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md index 6bfa2ea3..05af096d 100644 --- a/docs/rules/rrd/index.md +++ b/docs/rules/rrd/index.md @@ -15,6 +15,7 @@ These ruleset is the most opinionated with rules that are not part of the _offic - [If Without Curly Braces](./if-without-curly-braces.md) - [Magic Numbers](./magic-numbers.md) - [Nested Ternary](./nested-ternary.md) +- [No Inline Styles](./no-inline-styles.md) - [No Prop Destructing](./no-prop-destructure.md) - [No Var Declaration](./no-var-declaration.md) - [Parameter Count](./parameter-count.md) diff --git a/docs/rules/rrd/no-inline-styles.md b/docs/rules/rrd/no-inline-styles.md new file mode 100644 index 00000000..b6e6a47c --- /dev/null +++ b/docs/rules/rrd/no-inline-styles.md @@ -0,0 +1,43 @@ +# No Inline Styles + +Checks if in an element within the template of an SFC uses a inline style + +## ❓ Why it's good to follow this rule? + +**Maintainability:** External CSS makes updates easier and keeps the codebase clean. +**Performance:** Reduces HTML file size and leverages browser caching. +**Consistency:** Ensures a uniform style across the entire website or application. + +## 😱 Examples of code for which this rule will throw a warning + +::: warning +The following code contains inline `styles` which are not recommended because they increase the html code and do not take advantage of browser caching +::: + +```vue + +``` + +## 🤩 How to fix it? + +::: tip +Using `style` with the `scoped` attribute and creating the necessary classes for the style you want to apply can be a good option. +::: + +```vue + + + +``` diff --git a/src/rules/rrd/index.ts b/src/rules/rrd/index.ts index 744c5263..9e1ef29d 100644 --- a/src/rules/rrd/index.ts +++ b/src/rules/rrd/index.ts @@ -21,3 +21,4 @@ export * from './shortVariableName' export * from './tooManyProps' export * from './vForWithIndexKey' export * from './zeroLengthComparison' +export * from './noInlineStyles' diff --git a/src/rules/rrd/noInlineStyles.test.ts b/src/rules/rrd/noInlineStyles.test.ts new file mode 100644 index 00000000..140e27bf --- /dev/null +++ b/src/rules/rrd/noInlineStyles.test.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import type { SFCTemplateBlock } from '@vue/compiler-sfc' +import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes' +import { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles } from './noInlineStyles' + +describe('checkNoInlineStyles', () => { + beforeEach(() => { + resetNoInlineStyles() + }) + + it('should not report files with inline styles', () => { + const template = { + content: ` + + `, + } as SFCTemplateBlock + const fileName = 'noInlineStyles.vue' + checkNoInlineStyles(template, fileName) + expect(reportNoInlineStyles().length).toBe(0) + expect(reportNoInlineStyles()).toStrictEqual([]) + }) + + it('should report files with inline styles', () => { + const template = { + content: ` + + `, + } as SFCTemplateBlock + const fileName = 'noInlineStyles-problem.vue' + checkNoInlineStyles(template, fileName) + expect(reportNoInlineStyles().length).toBe(2) + expect(reportNoInlineStyles()).toStrictEqual([ + { + file: fileName, + rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`, + description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`, + message: `line #2 ${BG_WARN}Found inline style: style="background: #fff;"${BG_RESET} 🚨`, + }, + { + file: fileName, + rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`, + description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`, + message: `line #4 ${BG_WARN}Found inline style: style="color: red;"${BG_RESET} 🚨`, + }, + ]) + }) +}) diff --git a/src/rules/rrd/noInlineStyles.ts b/src/rules/rrd/noInlineStyles.ts new file mode 100644 index 00000000..38f33a95 --- /dev/null +++ b/src/rules/rrd/noInlineStyles.ts @@ -0,0 +1,45 @@ +import type { SFCTemplateBlock } from '@vue/compiler-sfc' +import getLineNumber from '../getLineNumber' +import type { FileCheckResult, Offense } from '../../types' +import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes' + +const results: FileCheckResult[] = [] + +const checkNoInlineStyles = (template: SFCTemplateBlock | null, filePath: string) => { + if (!template) { + return + } + + const regex = /style\s*=\s*['"][^'"]*['"]/g + + const matches = [...template.content.matchAll(regex)] + let from = 0 + matches?.forEach((match) => { + const lineNumber = getLineNumber(template.content.trim(), match[0], from) + results.push({ + filePath, + message: `line #${lineNumber} ${BG_WARN}Found inline style: ${match[0]}${BG_RESET}`, + }) + from = lineNumber + }) +} + +const reportNoInlineStyles = () => { + const offenses: Offense[] = [] + + if (results.length > 0) { + results.forEach((result) => { + offenses.push({ + file: result.filePath, + rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`, + description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`, + message: `${result.message} 🚨`, + }) + }) + } + return offenses +} + +const resetNoInlineStyles = () => (results.length = 0) + +export { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles } diff --git a/src/rules/rules.ts b/src/rules/rules.ts index 4003cf46..6b605335 100644 --- a/src/rules/rules.ts +++ b/src/rules/rules.ts @@ -40,6 +40,7 @@ export const RULES = { 'ifWithoutCurlyBraces', 'magicNumbers', 'nestedTernary', + 'noInlineStyles', 'noPropDestructure', 'noVarDeclaration', 'parameterCount', diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts index fce4660f..6e3aa6d0 100644 --- a/src/rulesCheck.ts +++ b/src/rulesCheck.ts @@ -5,7 +5,7 @@ import { checkElementSelectorsWithScoped, checkImplicitParentChildCommunication import { checkGlobalStyle, checkSimpleProp, checkSingleNameComponent, checkVforNoKey, checkVifWithVfor } from './rules/vue-essential' import { checkElementAttributeOrder, checkTopLevelElementOrder } from './rules/vue-recommended' import { checkComponentFilenameCasing, checkComponentFiles, checkDirectiveShorthands, checkFullWordComponentName, checkMultiAttributeElements, checkPropNameCasing, checkQuotedAttributeValues, checkSelfClosingComponents, checkSimpleComputed, checkTemplateSimpleExpression } from './rules/vue-strong' -import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd' +import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd' import { getIsNuxt } from './context' export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: string[]) => { @@ -67,6 +67,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s tooManyProps: () => checkTooManyProps(script, filePath), vForWithIndexKey: () => isVueFile && checkVForWithIndexKey(descriptor.template, filePath), zeroLengthComparison: () => checkZeroLengthComparison(script, filePath), + noInlineStyles: () => checkNoInlineStyles(descriptor.template, filePath), } // Run the checks for each applied rule or ruleset diff --git a/src/rulesReport.ts b/src/rulesReport.ts index d213caff..baa1a803 100644 --- a/src/rulesReport.ts +++ b/src/rulesReport.ts @@ -5,7 +5,7 @@ import { reportElementSelectorsWithScoped, reportImplicitParentChildCommunicatio import { reportGlobalStyle, reportSimpleProp, reportSingleNameComponent, reportVforNoKey, reportVifWithVfor } from './rules/vue-essential' import { reportElementAttributeOrder, reportTopLevelElementOrder } from './rules/vue-recommended' import { reportComponentFilenameCasing, reportComponentFiles, reportDirectiveShorthands, reportFullWordComponentName, reportMultiAttributeElements, reportPropNameCasing, reportQuotedAttributeValues, reportSelfClosingComponents, reportSimpleComputed, reportTemplateSimpleExpression } from './rules/vue-strong' -import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd' +import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd' // TODO move out to types interface OutputType { @@ -85,6 +85,7 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel processOffenses(reportTooManyProps) processOffenses(reportVForWithIndexKey) processOffenses(reportZeroLengthComparison) + processOffenses(reportNoInlineStyles) // Sort offenses grouped by key based on the `sortBy` parameter const sortedKeys = Object.keys(offensesGrouped).sort((a, b) => {