diff --git a/src/analyzer.ts b/src/analyzer.ts index 1a81d4d1..3a50fa29 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -22,6 +22,7 @@ import { checkFunctionSize, reportFunctionSize } from './rules/rrd/functionSize' import { checkParameterCount, reportParameterCount } from './rules/rrd/parameterCount' import { checkShortVariableName, reportShortVariableName } from './rules/rrd/shortVariableName' import { checkSimpleComputed, reportSimpleComputed } from './rules/vue-strong/simpleComputed' +import { checkComponentFiles, reportComponentFiles } from './rules/vue-strong/componentFiles' import { checkImplicitParentChildCommunication, reportImplicitParentChildCommunication } from './rules/vue-caution/implicitParentChildCommunication' let filesCount = 0 @@ -82,6 +83,7 @@ export const analyze = (dir: string) => { checkSimpleProp(script, filePath) checkPropNameCasing(script, filePath) + checkComponentFiles(script, filePath) checkScriptLength(script, filePath) checkCyclomaticComplexity(script, filePath) @@ -126,6 +128,7 @@ export const analyze = (dir: string) => { errors += reportQuotedAttributeValues() errors += reportDirectiveShorthands() errors += reportSimpleComputed() + errors += reportComponentFiles() // vue-reccomended rules diff --git a/src/rules/vue-strong/componentFiles.test.ts b/src/rules/vue-strong/componentFiles.test.ts new file mode 100644 index 00000000..6bbf7a56 --- /dev/null +++ b/src/rules/vue-strong/componentFiles.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from 'vitest'; +import { SFCScriptBlock } from '@vue/compiler-sfc'; +import { BG_ERR, BG_RESET, BG_WARN } from '../asceeCodes'; +import { checkComponentFiles, reportComponentFiles } from './componentFiles'; + +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +describe('checkComponentFiles', () => { + it('should not report files where each component is its own file', () => { + const script = { + content: ` + + ` + } as SFCScriptBlock + const filename = 'component.vue' + checkComponentFiles(script, filename) + expect(reportComponentFiles()).toBe(0) + expect(mockConsoleLog).not.toHaveBeenCalled() + }) + + it('should report files each component is not its own file', () => { + const script = { + content: ` + + ` + } as SFCScriptBlock + const filename = 'component.vue' + const lineNumber = 6; + checkComponentFiles(script, filename) + expect(reportComponentFiles()).toBe(2) + expect(mockConsoleLog).toHaveBeenCalled() + expect(mockConsoleLog).toHaveBeenLastCalledWith( + `- ${filename}#${lineNumber} ${BG_WARN}(TodoItem)${BG_RESET} 🚨` + ) + }) +}) \ No newline at end of file diff --git a/src/rules/vue-strong/componentFiles.ts b/src/rules/vue-strong/componentFiles.ts new file mode 100644 index 00000000..64a0fe46 --- /dev/null +++ b/src/rules/vue-strong/componentFiles.ts @@ -0,0 +1,39 @@ +import { SFCScriptBlock } from "@vue/compiler-sfc"; +import { BG_RESET, BG_WARN, TEXT_WARN, TEXT_RESET, TEXT_INFO, BG_ERR } from '../asceeCodes' +import { getUniqueFilenameCount } from "../../helpers"; +import getLineNumber from "../getLineNumber"; + +type ComponentFiles = { filename: string, message: string }; + +const componentFiles: ComponentFiles[] = []; + +const checkComponentFiles = (script: SFCScriptBlock, filePath: string) => { + // regular expression to match `.component('anyString', { ... })` pattern + const regex = /app\.component\('([^']+)',\s*\{[^}]*\}\)/g; + + const matches = [...script.content.matchAll(regex)].map(match => match[1].trim()); + + matches.forEach(match => { + const lineNumber = getLineNumber(script.content.trim(), match); + const firstPart = match.split('\n').at(0)?.trim() || '' + componentFiles.push({ filename: filePath, message: `${filePath}#${lineNumber} ${BG_WARN}(${firstPart})${BG_RESET}` }) + }) +} + +const reportComponentFiles = () => { + if (componentFiles.length > 0) { + // count only non duplicated objects (by its `filename` property) + const fileCount = getUniqueFilenameCount(componentFiles, 'filename'); + + console.log( + `\n${TEXT_INFO}vue-strong${TEXT_RESET} ${BG_ERR}component files${BG_RESET} detected in ${fileCount} files.` + ) + console.log(`👉 ${TEXT_WARN}Whenever a build system is available to concatenate files, each component should be in its own file.${TEXT_RESET} See: https://vuejs.org/style-guide/rules-strongly-recommended.html#component-files`) + componentFiles.forEach(file => { + console.log(`- ${file.message} 🚨`) + }) + } + return componentFiles.length; +} + +export { checkComponentFiles, reportComponentFiles }; \ No newline at end of file