Repository: rrd108/vue-mess-detector
Pull request #68 (2024-07-21 16:19:31)

feat: add vue-strong component-files rule


Patch:
diff --git a/src/analyzer.ts b/src/analyzer.ts
index 1a81d4d..3a50fa2 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.ts b/src/rules/vue-strong/componentFiles.ts
new file mode 100644
index 0000000..64a0fe4
--- /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>(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


Test patch:
diff --git a/src/rules/vue-strong/componentFiles.test.ts b/src/rules/vue-strong/componentFiles.test.ts
new file mode 100644
index 0000000..6bbf7a5
--- /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: `
+        <script setup>
+        console.log('This is just fine');
+        </script>
+      `
+    } 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: `
+        <script setup>
+        app.component('TodoList', {
+          // ...
+        })
+
+        app.component('TodoItem', {
+          // ...
+        })
+        </script>
+      `
+    } 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