Skip to content

Commit a634e3c

Browse files
authored
Updated to detect Vue3 components. (#1073)
1 parent 2c92d3d commit a634e3c

10 files changed

+312
-64
lines changed

docs/user-guide/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ All component-related rules are applied to code that passes any of the following
8585
* `Vue.component()` expression
8686
* `Vue.extend()` expression
8787
* `Vue.mixin()` expression
88+
* `app.component()` expression
89+
* `app.mixin()` expression
90+
* `createApp()` expression
8891
* `export default {}` in `.vue` or `.jsx` file
8992

9093
However, if you want to take advantage of the rules in any of your custom objects that are Vue components, you might need to use the special comment `// @vue/component` that marks an object in the next line as a Vue component in any file, e.g.:

lib/utils/index.js

+100-62
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ module.exports = {
264264
return componentsNode.value.properties
265265
.filter(p => p.type === 'Property')
266266
.map(node => {
267-
const name = this.getStaticPropertyName(node)
267+
const name = getStaticPropertyName(node)
268268
return name ? { node, name } : null
269269
})
270270
.filter(comp => comp != null)
@@ -402,42 +402,7 @@ module.exports = {
402402
* @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
403403
* @return {string|null} The property name if static. Otherwise, null.
404404
*/
405-
getStaticPropertyName (node) {
406-
let prop
407-
switch (node && node.type) {
408-
case 'Property':
409-
case 'MethodDefinition':
410-
prop = node.key
411-
break
412-
case 'MemberExpression':
413-
prop = node.property
414-
break
415-
case 'Literal':
416-
case 'TemplateLiteral':
417-
case 'Identifier':
418-
prop = node
419-
break
420-
// no default
421-
}
422-
423-
switch (prop && prop.type) {
424-
case 'Literal':
425-
return String(prop.value)
426-
case 'TemplateLiteral':
427-
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
428-
return prop.quasis[0].value.cooked
429-
}
430-
break
431-
case 'Identifier':
432-
if (!node.computed) {
433-
return prop.name
434-
}
435-
break
436-
// no default
437-
}
438-
439-
return null
440-
},
405+
getStaticPropertyName,
441406

442407
/**
443408
* Get all props by looking at all component's properties
@@ -464,8 +429,8 @@ module.exports = {
464429
.filter(prop => prop.type === 'Property')
465430
.map(prop => {
466431
return {
467-
key: prop.key, value: this.unwrapTypes(prop.value), node: prop,
468-
propName: this.getStaticPropertyName(prop)
432+
key: prop.key, value: unwrapTypes(prop.value), node: prop,
433+
propName: getStaticPropertyName(prop)
469434
}
470435
})
471436
} else {
@@ -548,28 +513,52 @@ module.exports = {
548513
const callee = node.callee
549514

550515
if (callee.type === 'MemberExpression') {
551-
const calleeObject = this.unwrapTypes(callee.object)
516+
const calleeObject = unwrapTypes(callee.object)
517+
518+
if (calleeObject.type === 'Identifier') {
519+
const propName = getStaticPropertyName(callee.property)
520+
if (calleeObject.name === 'Vue') {
521+
// for Vue.js 2.x
522+
// Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
523+
const isFullVueComponentForVue2 =
524+
['component', 'mixin', 'extend'].includes(propName) &&
525+
isObjectArgument(node)
526+
527+
return isFullVueComponentForVue2
528+
}
552529

553-
const isFullVueComponent = calleeObject.type === 'Identifier' &&
554-
calleeObject.name === 'Vue' &&
555-
callee.property.type === 'Identifier' &&
556-
['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
557-
node.arguments.length >= 1 &&
558-
node.arguments.slice(-1)[0].type === 'ObjectExpression'
530+
// for Vue.js 3.x
531+
// app.component('xxx', {}) || app.mixin({})
532+
const isFullVueComponent =
533+
['component', 'mixin'].includes(propName) &&
534+
isObjectArgument(node)
559535

560-
return isFullVueComponent
536+
return isFullVueComponent
537+
}
561538
}
562539

563540
if (callee.type === 'Identifier') {
564-
const isDestructedVueComponent = callee.name === 'component' &&
565-
node.arguments.length >= 1 &&
566-
node.arguments.slice(-1)[0].type === 'ObjectExpression'
567-
568-
return isDestructedVueComponent
541+
if (callee.name === 'component') {
542+
// for Vue.js 2.x
543+
// component('xxx', {})
544+
const isDestructedVueComponent = isObjectArgument(node)
545+
return isDestructedVueComponent
546+
}
547+
if (callee.name === 'createApp') {
548+
// for Vue.js 3.x
549+
// createApp({})
550+
const isAppVueComponent = isObjectArgument(node)
551+
return isAppVueComponent
552+
}
569553
}
570554
}
571555

572556
return false
557+
558+
function isObjectArgument (node) {
559+
return node.arguments.length > 0 &&
560+
unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
561+
}
573562
},
574563

575564
/**
@@ -584,7 +573,7 @@ module.exports = {
584573
callee.type === 'Identifier' &&
585574
callee.name === 'Vue' &&
586575
node.arguments.length &&
587-
node.arguments[0].type === 'ObjectExpression'
576+
unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
588577
},
589578

590579
/**
@@ -647,7 +636,7 @@ module.exports = {
647636
'CallExpression:exit' (node) {
648637
// Vue.component('xxx', {}) || component('xxx', {})
649638
if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
650-
cb(node.arguments.slice(-1)[0])
639+
cb(unwrapTypes(node.arguments.slice(-1)[0]))
651640
}
652641
}
653642
},
@@ -664,10 +653,10 @@ module.exports = {
664653
const callee = callExpr.callee
665654

666655
if (callee.type === 'MemberExpression') {
667-
const calleeObject = this.unwrapTypes(callee.object)
656+
const calleeObject = unwrapTypes(callee.object)
668657

669658
if (calleeObject.type === 'Identifier' &&
670-
calleeObject.name === 'Vue' &&
659+
// calleeObject.name === 'Vue' && // Any names can be used in Vue.js 3.x. e.g. app.component()
671660
callee.property === node &&
672661
callExpr.arguments.length >= 1) {
673662
cb(callExpr)
@@ -682,9 +671,9 @@ module.exports = {
682671
* @param {Set} groups Name of parent group
683672
*/
684673
* iterateProperties (node, groups) {
685-
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
674+
const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(getStaticPropertyName(p.key)))
686675
for (const item of nodes) {
687-
const name = this.getStaticPropertyName(item.key)
676+
const name = getStaticPropertyName(item.key)
688677
if (!name) continue
689678

690679
if (item.value.type === 'ArrayExpression') {
@@ -705,7 +694,7 @@ module.exports = {
705694
* iterateArrayExpression (node, groupName) {
706695
assert(node.type === 'ArrayExpression')
707696
for (const item of node.elements) {
708-
const name = this.getStaticPropertyName(item)
697+
const name = getStaticPropertyName(item)
709698
if (name) {
710699
const obj = { name, groupName, node: item }
711700
yield obj
@@ -721,7 +710,7 @@ module.exports = {
721710
* iterateObjectExpression (node, groupName) {
722711
assert(node.type === 'ObjectExpression')
723712
for (const item of node.properties) {
724-
const name = this.getStaticPropertyName(item)
713+
const name = getStaticPropertyName(item)
725714
if (name) {
726715
const obj = { name, groupName, node: item.key }
727716
yield obj
@@ -865,7 +854,56 @@ module.exports = {
865854
* @param {T} node
866855
* @return {T}
867856
*/
868-
unwrapTypes (node) {
869-
return node.type === 'TSAsExpression' ? node.expression : node
857+
unwrapTypes
858+
}
859+
/**
860+
* Unwrap typescript types like "X as F"
861+
* @template T
862+
* @param {T} node
863+
* @return {T}
864+
*/
865+
function unwrapTypes (node) {
866+
return node.type === 'TSAsExpression' ? node.expression : node
867+
}
868+
869+
/**
870+
* Gets the property name of a given node.
871+
* @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
872+
* @return {string|null} The property name if static. Otherwise, null.
873+
*/
874+
function getStaticPropertyName (node) {
875+
let prop
876+
switch (node && node.type) {
877+
case 'Property':
878+
case 'MethodDefinition':
879+
prop = node.key
880+
break
881+
case 'MemberExpression':
882+
prop = node.property
883+
break
884+
case 'Literal':
885+
case 'TemplateLiteral':
886+
case 'Identifier':
887+
prop = node
888+
break
889+
// no default
870890
}
891+
892+
switch (prop && prop.type) {
893+
case 'Literal':
894+
return String(prop.value)
895+
case 'TemplateLiteral':
896+
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
897+
return prop.quasis[0].value.cooked
898+
}
899+
break
900+
case 'Identifier':
901+
if (!node.computed) {
902+
return prop.name
903+
}
904+
break
905+
// no default
906+
}
907+
908+
return null
871909
}

tests/lib/rules/component-definition-name-casing.js

+34
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ ruleTester.run('component-definition-name-casing', rule, {
116116
options: ['kebab-case'],
117117
parserOptions
118118
},
119+
{
120+
filename: 'test.vue',
121+
code: `app.component('FooBar', component)`,
122+
options: ['PascalCase'],
123+
parserOptions
124+
},
119125
{
120126
filename: 'test.vue',
121127
code: `Vue.mixin({})`,
@@ -137,6 +143,12 @@ ruleTester.run('component-definition-name-casing', rule, {
137143
options: ['kebab-case'],
138144
parserOptions
139145
},
146+
{
147+
filename: 'test.vue',
148+
code: `app.component(\`fooBar\${foo}\`, component)`,
149+
options: ['kebab-case'],
150+
parserOptions
151+
},
140152
// https://github.com/vuejs/eslint-plugin-vue/issues/1018
141153
{
142154
filename: 'test.js',
@@ -292,6 +304,17 @@ ruleTester.run('component-definition-name-casing', rule, {
292304
line: 1
293305
}]
294306
},
307+
{
308+
filename: 'test.vue',
309+
code: `app.component('foo-bar', component)`,
310+
output: `app.component('FooBar', component)`,
311+
parserOptions,
312+
errors: [{
313+
message: 'Property name "foo-bar" is not PascalCase.',
314+
type: 'Literal',
315+
line: 1
316+
}]
317+
},
295318
{
296319
filename: 'test.vue',
297320
code: `(Vue as VueConstructor<Vue>).component('foo-bar', component)`,
@@ -315,6 +338,17 @@ ruleTester.run('component-definition-name-casing', rule, {
315338
line: 1
316339
}]
317340
},
341+
{
342+
filename: 'test.vue',
343+
code: `app.component('foo-bar', {})`,
344+
output: `app.component('FooBar', {})`,
345+
parserOptions,
346+
errors: [{
347+
message: 'Property name "foo-bar" is not PascalCase.',
348+
type: 'Literal',
349+
line: 1
350+
}]
351+
},
318352
{
319353
filename: 'test.js',
320354
code: `Vue.component('foo_bar', {})`,

tests/lib/rules/match-component-file-name.js

+23
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,16 @@ ruleTester.run('match-component-file-name', rule, {
429429
options: [{ extensions: ['js'] }],
430430
parserOptions
431431
},
432+
{
433+
filename: 'MyComponent.js',
434+
code: `
435+
app.component('MyComponent', {
436+
template: '<div />'
437+
})
438+
`,
439+
options: [{ extensions: ['js'] }],
440+
parserOptions
441+
},
432442
{
433443
filename: 'MyComponent.js',
434444
code: `
@@ -701,6 +711,19 @@ ruleTester.run('match-component-file-name', rule, {
701711
message: 'Component name `MComponent` should match file name `MyComponent`.'
702712
}]
703713
},
714+
{
715+
filename: 'MyComponent.js',
716+
code: `
717+
app.component(\`MComponent\`, {
718+
template: '<div />'
719+
})
720+
`,
721+
options: [{ extensions: ['js'] }],
722+
parserOptions,
723+
errors: [{
724+
message: 'Component name `MComponent` should match file name `MyComponent`.'
725+
}]
726+
},
704727

705728
// casing
706729
{

0 commit comments

Comments
 (0)