Skip to content

Commit fd75893

Browse files
committed
feat: add prefer-define-component rule
1 parent 7dec48d commit fd75893

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed

lib/rules/prefer-define-component.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @author Kamogelo Moalusi <github.com/thesheppard>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// @ts-nocheck
8+
const utils = require('../utils')
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'require components to be defined using `defineComponent`',
15+
categories: ['vue3-recommended', 'vue2-recommended'],
16+
url: 'https://eslint.vuejs.org/rules/prefer-define-component.html'
17+
},
18+
fixable: null,
19+
schema: [],
20+
messages: {
21+
'prefer-define-component': 'Use `defineComponent` to define a component.'
22+
}
23+
},
24+
/** @param {RuleContext} context */
25+
create(context) {
26+
const filePath = context.getFilename()
27+
if (!utils.isVueFile(filePath)) return {}
28+
29+
const sourceCode = context.getSourceCode()
30+
const documentFragment = sourceCode.parserServices.getDocumentFragment?.()
31+
32+
// Check if there's a non-setup script tag
33+
const hasNormalScript =
34+
documentFragment &&
35+
documentFragment.children.some(
36+
(e) =>
37+
utils.isVElement(e) &&
38+
e.name === 'script' &&
39+
(!e.startTag.attributes ||
40+
!e.startTag.attributes.some((attr) => attr.key.name === 'setup'))
41+
)
42+
43+
// If no regular script tag, we don't need to check
44+
if (!hasNormalScript) return {}
45+
46+
// Skip checking if there's only a setup script (no normal script)
47+
if (utils.isScriptSetup(context) && !hasNormalScript) return {}
48+
49+
let hasDefineComponent = false
50+
/** @type {ExportDefaultDeclaration | null} */
51+
let exportDefaultNode = null
52+
let hasVueExtend = false
53+
54+
return utils.compositingVisitors(utils.defineVueVisitor(context, {}), {
55+
/** @param {ExportDefaultDeclaration} node */
56+
'Program > ExportDefaultDeclaration'(node) {
57+
exportDefaultNode = node
58+
},
59+
60+
/** @param {CallExpression} node */
61+
'Program > ExportDefaultDeclaration > CallExpression'(node) {
62+
if (
63+
node.callee.type === 'Identifier' &&
64+
node.callee.name === 'defineComponent'
65+
) {
66+
hasDefineComponent = true
67+
return
68+
}
69+
70+
// Support aliased imports
71+
if (node.callee.type === 'Identifier') {
72+
const variable = utils.findVariableByIdentifier(context, node.callee)
73+
if (
74+
variable &&
75+
variable.defs &&
76+
variable.defs.length > 0 &&
77+
variable.defs[0].node.type === 'ImportSpecifier' &&
78+
variable.defs[0].node.imported &&
79+
variable.defs[0].node.imported.name === 'defineComponent'
80+
) {
81+
hasDefineComponent = true
82+
return
83+
}
84+
}
85+
86+
// Check for Vue.extend case
87+
if (
88+
node.callee.type === 'MemberExpression' &&
89+
node.callee.object &&
90+
node.callee.object.type === 'Identifier' &&
91+
node.callee.object.name === 'Vue' &&
92+
node.callee.property &&
93+
node.callee.property.type === 'Identifier' &&
94+
node.callee.property.name === 'extend'
95+
) {
96+
hasVueExtend = true
97+
}
98+
},
99+
100+
'Program > ExportDefaultDeclaration > ObjectExpression'() {
101+
hasDefineComponent = false
102+
},
103+
104+
'Program:exit'() {
105+
if (exportDefaultNode && (hasVueExtend || !hasDefineComponent)) {
106+
context.report({
107+
node: exportDefaultNode,
108+
messageId: 'prefer-define-component'
109+
})
110+
}
111+
}
112+
})
113+
}
114+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* @author Kamogelo Moalusi <github.com/thesheppard>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('../../eslint-compat').RuleTester
8+
const rule = require('../../../lib/rules/prefer-define-component')
9+
10+
const tester = new RuleTester({
11+
languageOptions: {
12+
parser: require('vue-eslint-parser'),
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('prefer-define-component', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: `
23+
<script lang="ts">
24+
export default defineComponent({
25+
name: 'Test',
26+
})
27+
</script>
28+
`
29+
},
30+
{
31+
filename: 'test.vue',
32+
code: `
33+
<script setup>
34+
</script>
35+
`
36+
},
37+
{
38+
filename: 'test.vue',
39+
code: `
40+
<script setup lang="ts">
41+
</script>
42+
`
43+
},
44+
{
45+
filename: 'test.vue',
46+
code: `
47+
<script>
48+
import { defineComponent } from 'vue'
49+
export default defineComponent({
50+
name: 'Test',
51+
inheritAttrs: false
52+
})
53+
</script>
54+
<script setup>
55+
import { ref } from 'vue'
56+
const count = ref(0)
57+
</script>
58+
`
59+
},
60+
{
61+
filename: 'test.vue',
62+
code: `
63+
<script lang="ts">
64+
import { defineComponent } from 'vue'
65+
export default defineComponent({
66+
name: 'Test',
67+
inheritAttrs: false,
68+
props: {
69+
message: {
70+
type: String,
71+
required: true
72+
}
73+
}
74+
})
75+
</script>
76+
<script setup lang="ts">
77+
import { ref, computed } from 'vue'
78+
const count = ref<number>(0)
79+
const doubled = computed<number>(() => count.value * 2)
80+
</script>
81+
`
82+
},
83+
{
84+
filename: 'test.vue',
85+
code: `
86+
<script>
87+
import { defineComponent as createComponent } from 'vue'
88+
export default createComponent({
89+
name: 'Test',
90+
})
91+
</script>
92+
`
93+
},
94+
{
95+
filename: 'test.vue',
96+
code: `
97+
<script lang="ts">
98+
export default defineComponent({
99+
name: 'GloballyAvailable',
100+
setup(props) {
101+
return {
102+
message: 'Hello World'
103+
}
104+
}
105+
})
106+
</script>
107+
`
108+
}
109+
],
110+
invalid: [
111+
{
112+
filename: 'test.vue',
113+
code: `
114+
<script lang="ts">
115+
export default {
116+
name: 'Test',
117+
}
118+
</script>
119+
`,
120+
errors: [
121+
{
122+
message: 'Use `defineComponent` to define a component.',
123+
line: 3,
124+
column: 7
125+
}
126+
]
127+
},
128+
{
129+
filename: 'test.vue',
130+
code: `
131+
<script>
132+
export default {
133+
name: 'Test',
134+
}
135+
</script>
136+
`,
137+
errors: [
138+
{
139+
message: 'Use `defineComponent` to define a component.',
140+
line: 3,
141+
column: 7
142+
}
143+
]
144+
},
145+
{
146+
filename: 'test.vue',
147+
code: `
148+
<script>
149+
export default Vue.extend({
150+
name: 'Test',
151+
})
152+
</script>
153+
`,
154+
errors: [
155+
{
156+
message: 'Use `defineComponent` to define a component.',
157+
line: 3,
158+
column: 7
159+
}
160+
]
161+
},
162+
{
163+
filename: 'test.vue',
164+
code: `
165+
<script>
166+
export default {
167+
name: 'Test',
168+
inheritAttrs: false
169+
}
170+
</script>
171+
<script setup>
172+
import { ref } from 'vue'
173+
const count = ref(0)
174+
</script>
175+
`,
176+
errors: [
177+
{
178+
message: 'Use `defineComponent` to define a component.',
179+
line: 3,
180+
column: 7
181+
}
182+
]
183+
},
184+
{
185+
filename: 'test.vue',
186+
code: `
187+
<script>
188+
const obj = { foo: 'bar' }
189+
export const helpers = {
190+
method() { return 'helper' }
191+
}
192+
export default {
193+
name: 'Test',
194+
}
195+
</script>
196+
`,
197+
errors: [
198+
{
199+
message: 'Use `defineComponent` to define a component.',
200+
line: 7,
201+
column: 7
202+
}
203+
]
204+
},
205+
{
206+
filename: 'test.vue',
207+
code: `
208+
<script>
209+
export default Vue.extend({
210+
name: 'Test',
211+
data() {
212+
return { count: 0 }
213+
}
214+
})
215+
</script>
216+
<script setup>
217+
import { computed } from 'vue'
218+
</script>
219+
`,
220+
errors: [
221+
{
222+
message: 'Use `defineComponent` to define a component.',
223+
line: 3,
224+
column: 7
225+
}
226+
]
227+
}
228+
]
229+
})

0 commit comments

Comments
 (0)