-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
142 lines (125 loc) · 4.39 KB
/
index.ts
File metadata and controls
142 lines (125 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'
import { relative } from 'path'
const KEBAB_SNAKE_PATTERN = /[-_](.)/g
const FIRST_LOWER_PATTERN = /^[a-z]/
function toPascal(str: string) {
return str
.replace(KEBAB_SNAKE_PATTERN, (_, c: string) => c.toUpperCase())
.replace(FIRST_LOWER_PATTERN, (c) => c.toUpperCase())
}
const createRule = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/dev-five-git/devup/tree/main/packages/eslint-plugin/src/rules/${name}`,
)
// Combined regex patterns for better performance (single regex test instead of multiple)
const EXCLUDE_PATTERN =
/[\\/]utils[\\/]|[\\/](__)?tests?(__)?[\\/]|\.(test|css|stories)\.[jt]sx$/
const INCLUDE_PATTERN =
/(src[/\\])?(app[/\\](?!.*[\\/]?(page|layout|404)\.[jt]sx$)|components[/\\]).*\.[jt]sx$/
const COMPONENT_NAME_PATTERN = /([^/\\]+)[/\\]([^/\\]+)\.[jt]sx$/i
export const component = createRule({
name: 'component',
defaultOptions: [],
meta: {
schema: [],
messages: {
componentNameShouldBeFollowDirectoryStructure:
'Component name should follow directory or file name.',
componentFileShouldExportComponent:
'Component file should export a component. (Component name: {targetComponentName})',
},
type: 'problem',
fixable: 'code',
docs: {
description: 'require component name to follow directory or file name.',
},
},
create(context) {
const filename = relative(context.cwd, context.physicalFilename)
// Early return: check exclusion first (more likely to match, faster exit)
if (EXCLUDE_PATTERN.test(filename) || !INCLUDE_PATTERN.test(filename)) {
return {}
}
// Extract component name from file path
const targetComponentRegex = COMPONENT_NAME_PATTERN.exec(filename)!
const isIndex = targetComponentRegex[2].startsWith('index')
const targetComponentName = isIndex
? toPascal(targetComponentRegex[1])
: toPascal(targetComponentRegex[2])
const exportFunc: TSESTree.Identifier[] = []
let ok = false
return {
ExportDefaultDeclaration() {
ok = true
},
ExportNamedDeclaration(namedExport) {
if (ok) return
// Pass check if there are export specifiers in index file
if (namedExport.specifiers.length && isIndex) {
ok = true
return
}
const declaration = namedExport.declaration
if (!declaration) return
switch (declaration.type) {
case 'FunctionDeclaration':
if (declaration.id) {
if (declaration.id.name === targetComponentName) {
ok = true
} else {
exportFunc.push(declaration.id)
}
}
break
case 'ClassDeclaration':
if (declaration.id?.name === targetComponentName) {
ok = true
}
break
case 'VariableDeclaration':
for (const el of declaration.declarations) {
if (el.id.type !== 'Identifier') continue
if (el.id.name === targetComponentName) {
ok = true
return
}
if (
el.init?.type === 'ArrowFunctionExpression' ||
el.init?.type === 'FunctionExpression'
) {
exportFunc.push(el.id)
}
}
break
}
},
'Program:exit'(program) {
if (ok) return
// Suggest fix when component name does not match
if (exportFunc.length) {
for (const exported of exportFunc) {
context.report({
node: exported,
messageId: 'componentNameShouldBeFollowDirectoryStructure',
fix: (fixer) => fixer.replaceText(exported, targetComponentName),
})
}
return
}
// Suggest adding default component when no component is exported
context.report({
node: program,
messageId: 'componentFileShouldExportComponent',
data: { targetComponentName },
fix: (fixer) =>
fixer.insertTextAfter(
context.sourceCode.ast,
context.sourceCode.text.trim().length
? `\nexport function ${targetComponentName}(){return <></>}`
: `export function ${targetComponentName}(){return <></>}`,
),
})
},
}
},
})