Skip to content

Commit 1bb4edd

Browse files
authored
Add vue/prefer-import-from-vue rule (#1804)
1 parent 86b3b3f commit 1bb4edd

File tree

9 files changed

+776
-3
lines changed

9 files changed

+776
-3
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ For example:
352352
| [vue/no-v-text-v-html-on-component](./no-v-text-v-html-on-component.md) | disallow v-text / v-html on component | |
353353
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | |
354354
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
355+
| [vue/prefer-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: |
355356
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: |
356357
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: |
357358
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |

docs/rules/prefer-import-from-vue.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-import-from-vue
5+
description: enforce import from 'vue' instead of import from '@vue/*'
6+
---
7+
# vue/prefer-import-from-vue
8+
9+
> enforce import from 'vue' instead of import from '@vue/*'
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
This rule aims to use imports from `'vue'` instead of imports from `'@vue/*'`.
17+
18+
Imports from the following modules are almost always wrong. You should import from `vue` instead.
19+
20+
- `@vue/runtime-dom`
21+
- `@vue/runtime-core`
22+
- `@vue/reactivity`
23+
- `@vue/shared`
24+
25+
<eslint-code-block fix :rules="{'vue/prefer-import-from-vue': ['error']}" filename="example.js" language="javascript">
26+
27+
```js
28+
/* ✓ GOOD */
29+
import { createApp, ref, Component } from 'vue'
30+
```
31+
32+
</eslint-code-block>
33+
34+
<eslint-code-block fix :rules="{'vue/prefer-import-from-vue': ['error']}" filename="example.js" language="javascript">
35+
36+
```js
37+
/* ✗ BAD */
38+
import { createApp } from '@vue/runtime-dom'
39+
import { Component } from '@vue/runtime-core'
40+
import { ref } from '@vue/reactivity'
41+
```
42+
43+
</eslint-code-block>
44+
45+
## :wrench: Options
46+
47+
Nothing.
48+
49+
## :mag: Implementation
50+
51+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-import-from-vue.js)
52+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-import-from-vue.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ module.exports = {
157157
'operator-linebreak': require('./rules/operator-linebreak'),
158158
'order-in-components': require('./rules/order-in-components'),
159159
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
160+
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
160161
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
161162
'prefer-template': require('./rules/prefer-template'),
162163
'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),

lib/rules/prefer-import-from-vue.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const vue3ExportNames = new Set(require('../utils/vue3-export-names.json'))
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
const TARGET_AT_VUE_MODULES = new Set([
18+
'@vue/runtime-dom',
19+
'@vue/runtime-core',
20+
'@vue/reactivity',
21+
'@vue/shared'
22+
])
23+
// Modules with the names of a subset of vue.
24+
const SUBSET_AT_VUE_MODULES = new Set(['@vue/runtime-dom', '@vue/runtime-core'])
25+
26+
/**
27+
* @param {ImportDeclaration} node
28+
*/
29+
function* extractImportNames(node) {
30+
for (const specifier of node.specifiers) {
31+
if (specifier.type === 'ImportDefaultSpecifier') {
32+
yield 'default'
33+
} else if (specifier.type === 'ImportNamespaceSpecifier') {
34+
yield null // all
35+
} else if (specifier.type === 'ImportSpecifier') {
36+
yield specifier.imported.name
37+
}
38+
}
39+
}
40+
41+
// ------------------------------------------------------------------------------
42+
// Rule Definition
43+
// ------------------------------------------------------------------------------
44+
45+
module.exports = {
46+
meta: {
47+
type: 'suggestion',
48+
docs: {
49+
description: "enforce import from 'vue' instead of import from '@vue/*'",
50+
// TODO We will change it in the next major version.
51+
// categories: ['vue3-essential'],
52+
categories: undefined,
53+
url: 'https://eslint.vuejs.org/rules/prefer-import-from-vue.html'
54+
},
55+
fixable: 'code',
56+
schema: [],
57+
messages: {
58+
importedAtVue: "Import from 'vue' instead of '{{source}}'."
59+
}
60+
},
61+
/**
62+
* @param {RuleContext} context
63+
* @returns {RuleListener}
64+
*/
65+
create(context) {
66+
/**
67+
*
68+
* @param {Literal & { value: string }} source
69+
* @param { () => boolean } fixable
70+
*/
71+
function verifySource(source, fixable) {
72+
if (!TARGET_AT_VUE_MODULES.has(source.value)) {
73+
return
74+
}
75+
76+
context.report({
77+
node: source,
78+
messageId: 'importedAtVue',
79+
data: { source: source.value },
80+
fix: fixable()
81+
? (fixer) =>
82+
fixer.replaceTextRange(
83+
[source.range[0] + 1, source.range[1] - 1],
84+
'vue'
85+
)
86+
: null
87+
})
88+
}
89+
90+
return {
91+
ImportDeclaration(node) {
92+
verifySource(node.source, () => {
93+
if (SUBSET_AT_VUE_MODULES.has(node.source.value)) {
94+
// If the module is a subset of 'vue', we can safely change it to 'vue'.
95+
return true
96+
}
97+
for (const name of extractImportNames(node)) {
98+
if (name == null) {
99+
return false // import all
100+
}
101+
if (!vue3ExportNames.has(name)) {
102+
// If there is a name that is not exported from 'vue', it will not be auto-fixed.
103+
return false
104+
}
105+
}
106+
return true
107+
})
108+
},
109+
ExportNamedDeclaration(node) {
110+
if (node.source) {
111+
verifySource(node.source, () => {
112+
for (const specifier of node.specifiers) {
113+
if (!vue3ExportNames.has(specifier.local.name)) {
114+
// If there is a name that is not exported from 'vue', it will not be auto-fixed.
115+
return false
116+
}
117+
}
118+
return true
119+
})
120+
}
121+
},
122+
ExportAllDeclaration(node) {
123+
verifySource(
124+
node.source,
125+
// If we change it to `from 'vue'`, it will export more, so it will not be auto-fixed.
126+
() => false
127+
)
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)