diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7bb646536..80e1f88fd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,7 +10,7 @@
"json",
"jsonc"
],
- "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.tsdk": "./node_modules/typescript/lib",
"vetur.validation.script": false,
"[typescript]": {
"editor.formatOnSave": true,
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 5bcb7913b..68aec1af9 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -184,7 +184,8 @@ module.exports = {
},
algolia: {
- apiKey: 'b2b69365da747a9a9635cda391317c36',
+ appId: '2L4MGZSULB',
+ apiKey: 'fdf57932b27a6c230d01a890492ab76d',
indexName: 'eslint-plugin-vue'
}
}
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 5794cc3ed..735dc87d0 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -311,6 +311,7 @@ For example:
| [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
| [vue/component-api-style](./component-api-style.md) | enforce component API style | |
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
+| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
@@ -350,6 +351,7 @@ For example:
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
+| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: |
| [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: |
diff --git a/docs/rules/component-definition-name-casing.md b/docs/rules/component-definition-name-casing.md
index 5e89ace75..615b876f9 100644
--- a/docs/rules/component-definition-name-casing.md
+++ b/docs/rules/component-definition-name-casing.md
@@ -31,7 +31,7 @@ Default casing is set to `PascalCase`.
- `"PascalCase"` (default) ... enforce component definition names to pascal case.
- `"kebab-case"` ... enforce component definition names to kebab case.
-### `"PascalCase" (default)
+### `"PascalCase"` (default)
@@ -64,18 +64,18 @@ export default {
```js
/* ✓ GOOD */
Vue.component('MyComponent', {
-
+
})
/* ✗ BAD */
Vue.component('my-component', {
-
+
})
```
-### `"kebab-case"
+### `"kebab-case"`
@@ -108,12 +108,12 @@ export default {
```js
/* ✓ GOOD */
Vue.component('my-component', {
-
+
})
/* ✗ BAD */
Vue.component('MyComponent', {
-
+
})
```
diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md
new file mode 100644
index 000000000..d37ee9fce
--- /dev/null
+++ b/docs/rules/component-options-name-casing.md
@@ -0,0 +1,169 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/component-options-name-casing
+description: enforce the casing of component name in `components` options
+since: v8.2.0
+---
+# vue/component-options-name-casing
+
+> enforce the casing of component name in `components` options
+
+- :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.
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+This rule aims to enforce casing of the component names in `components` options.
+
+## :wrench: Options
+
+```json
+{
+ "vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"]
+}
+```
+
+This rule has an option which can be one of these values:
+
+- `"PascalCase"` (default) ... enforce component names to pascal case.
+- `"kebab-case"` ... enforce component names to kebab case.
+- `"camelCase"` ... enforce component names to camel case.
+
+Please note that if you use kebab case in `components` options,
+you can **only** use kebab case in template;
+and if you use camel case in `components` options,
+you **can't** use pascal case in template.
+
+For demonstration, the code example is invalid:
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+### `"PascalCase"` (default)
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+### `"kebab-case"`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+### `"camelCase"`
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.2.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js)
diff --git a/docs/rules/prefer-separate-static-class.md b/docs/rules/prefer-separate-static-class.md
new file mode 100644
index 000000000..22df25e19
--- /dev/null
+++ b/docs/rules/prefer-separate-static-class.md
@@ -0,0 +1,47 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-separate-static-class
+description: require static class names in template to be in a separate `class` attribute
+since: v8.2.0
+---
+# vue/prefer-separate-static-class
+
+> require static class names in template to be in a separate `class` attribute
+
+- :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.
+
+## :book: Rule Details
+
+This rule reports static class names in dynamic class attributes.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-vue v8.2.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-separate-static-class.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-separate-static-class.js)
diff --git a/lib/index.js b/lib/index.js
index c9969917c..3c628c559 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -24,6 +24,7 @@ module.exports = {
'component-api-style': require('./rules/component-api-style'),
'component-definition-name-casing': require('./rules/component-definition-name-casing'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
+ 'component-options-name-casing': require('./rules/component-options-name-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
'dot-location': require('./rules/dot-location'),
@@ -153,6 +154,7 @@ module.exports = {
'operator-linebreak': require('./rules/operator-linebreak'),
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
+ 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
'prefer-template': require('./rules/prefer-template'),
'prop-name-casing': require('./rules/prop-name-casing'),
'require-component-is': require('./rules/require-component-is'),
diff --git a/lib/rules/component-options-name-casing.js b/lib/rules/component-options-name-casing.js
new file mode 100644
index 000000000..ad70c1546
--- /dev/null
+++ b/lib/rules/component-options-name-casing.js
@@ -0,0 +1,115 @@
+/**
+ * @author Pig Fang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node
+ * @returns {string | null}
+ */
+function getOptionsComponentName(node) {
+ if (node.type === 'Identifier') {
+ return node.name
+ }
+ if (node.type === 'Literal') {
+ return typeof node.value === 'string' ? node.value : null
+ }
+ return null
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'enforce the casing of component name in `components` options',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html'
+ },
+ fixable: 'code',
+ hasSuggestions: true,
+ schema: [{ enum: casing.allowedCaseOptions }],
+ messages: {
+ caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.',
+ possibleRenaming: 'Rename component name to be in {{caseType}}.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const caseType = context.options[0] || 'PascalCase'
+
+ const canAutoFix = caseType === 'PascalCase'
+ const checkCase = casing.getChecker(caseType)
+ const convert = casing.getConverter(caseType)
+
+ return utils.executeOnVue(context, (obj) => {
+ const node = utils.findProperty(obj, 'components')
+ if (!node || node.value.type !== 'ObjectExpression') {
+ return
+ }
+
+ node.value.properties.forEach((property) => {
+ if (property.type !== 'Property') {
+ return
+ }
+
+ const name = getOptionsComponentName(property.key)
+ if (!name || checkCase(name)) {
+ return
+ }
+
+ context.report({
+ node: property.key,
+ messageId: 'caseNotMatched',
+ data: {
+ component: name,
+ caseType
+ },
+ fix: canAutoFix
+ ? (fixer) => {
+ const converted = convert(name)
+ return property.shorthand
+ ? fixer.replaceText(property, `${converted}: ${name}`)
+ : fixer.replaceText(property.key, converted)
+ }
+ : undefined,
+ suggest: canAutoFix
+ ? undefined
+ : [
+ {
+ messageId: 'possibleRenaming',
+ data: { caseType },
+ fix: (fixer) => {
+ const converted = convert(name)
+ if (caseType === 'kebab-case') {
+ return property.shorthand
+ ? fixer.replaceText(property, `'${converted}': ${name}`)
+ : fixer.replaceText(property.key, `'${converted}'`)
+ }
+ return property.shorthand
+ ? fixer.replaceText(property, `${converted}: ${name}`)
+ : fixer.replaceText(property.key, converted)
+ }
+ }
+ ]
+ })
+ })
+ })
+ }
+}
diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js
index 61ffda40e..8b5e8e54c 100644
--- a/lib/rules/no-dupe-keys.js
+++ b/lib/rules/no-dupe-keys.js
@@ -14,14 +14,7 @@ const utils = require('../utils')
// Rule Definition
// ------------------------------------------------------------------------------
/** @type {GroupName[]} */
-const GROUP_NAMES = [
- 'props',
- 'computed',
- 'data',
- 'asyncData',
- 'methods',
- 'setup'
-]
+const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup']
module.exports = {
meta: {
diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js
new file mode 100644
index 000000000..504d95d60
--- /dev/null
+++ b/lib/rules/prefer-separate-static-class.js
@@ -0,0 +1,231 @@
+/**
+ * @author Flo Edelmann
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const { defineTemplateBodyVisitor, getStringLiteralValue } = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {ASTNode} node
+ * @returns {node is Literal | TemplateLiteral}
+ */
+function isStringLiteral(node) {
+ return (
+ (node.type === 'Literal' && typeof node.value === 'string') ||
+ (node.type === 'TemplateLiteral' && node.expressions.length === 0)
+ )
+}
+
+/**
+ * @param {Expression | VForExpression | VOnExpression | VSlotScopeExpression | VFilterSequenceExpression} expressionNode
+ * @returns {(Literal | TemplateLiteral | Identifier)[]}
+ */
+function findStaticClasses(expressionNode) {
+ if (isStringLiteral(expressionNode)) {
+ return [expressionNode]
+ }
+
+ if (expressionNode.type === 'ArrayExpression') {
+ return expressionNode.elements.flatMap((element) => {
+ if (element === null || element.type === 'SpreadElement') {
+ return []
+ }
+ return findStaticClasses(element)
+ })
+ }
+
+ if (expressionNode.type === 'ObjectExpression') {
+ return expressionNode.properties.flatMap((property) => {
+ if (
+ property.type === 'Property' &&
+ property.value.type === 'Literal' &&
+ property.value.value === true &&
+ (isStringLiteral(property.key) ||
+ (property.key.type === 'Identifier' && !property.computed))
+ ) {
+ return [property.key]
+ }
+ return []
+ })
+ }
+
+ return []
+}
+
+/**
+ * @param {VAttribute | VDirective} attributeNode
+ * @returns {attributeNode is VAttribute & { value: VLiteral }}
+ */
+function isStaticClassAttribute(attributeNode) {
+ return (
+ !attributeNode.directive &&
+ attributeNode.key.name === 'class' &&
+ attributeNode.value !== null
+ )
+}
+
+/**
+ * Removes the node together with the comma before or after the node.
+ * @param {RuleFixer} fixer
+ * @param {ParserServices.TokenStore} tokenStore
+ * @param {ASTNode} node
+ */
+function* removeNodeWithComma(fixer, tokenStore, node) {
+ const prevToken = tokenStore.getTokenBefore(node)
+ if (prevToken.type === 'Punctuator' && prevToken.value === ',') {
+ yield fixer.removeRange([prevToken.range[0], node.range[1]])
+ return
+ }
+
+ const [nextToken, nextNextToken] = tokenStore.getTokensAfter(node, {
+ count: 2
+ })
+ if (
+ nextToken.type === 'Punctuator' &&
+ nextToken.value === ',' &&
+ (nextNextToken.type !== 'Punctuator' ||
+ (nextNextToken.value !== ']' && nextNextToken.value !== '}'))
+ ) {
+ yield fixer.removeRange([node.range[0], nextNextToken.range[0]])
+ return
+ }
+
+ yield fixer.remove(node)
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'require static class names in template to be in a separate `class` attribute',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/prefer-separate-static-class.html'
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ preferSeparateStaticClass:
+ 'Static class "{{className}}" should be in a static `class` attribute.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return defineTemplateBodyVisitor(context, {
+ /** @param {VDirectiveKey} directiveKeyNode */
+ "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][argument.name='class']"(
+ directiveKeyNode
+ ) {
+ const attributeNode = directiveKeyNode.parent
+ if (!attributeNode.value || !attributeNode.value.expression) {
+ return
+ }
+
+ const expressionNode = attributeNode.value.expression
+ const staticClassNameNodes = findStaticClasses(expressionNode)
+
+ for (const staticClassNameNode of staticClassNameNodes) {
+ const className =
+ staticClassNameNode.type === 'Identifier'
+ ? staticClassNameNode.name
+ : getStringLiteralValue(staticClassNameNode, true)
+
+ if (className === null) {
+ continue
+ }
+
+ context.report({
+ node: staticClassNameNode,
+ messageId: 'preferSeparateStaticClass',
+ data: { className },
+ *fix(fixer) {
+ let dynamicClassDirectiveRemoved = false
+
+ yield* removeFromClassDirective()
+ yield* addToClassAttribute()
+
+ /**
+ * Remove class from dynamic `:class` directive.
+ */
+ function* removeFromClassDirective() {
+ if (isStringLiteral(expressionNode)) {
+ yield fixer.remove(attributeNode)
+ dynamicClassDirectiveRemoved = true
+ return
+ }
+
+ const listElement =
+ staticClassNameNode.parent.type === 'Property'
+ ? staticClassNameNode.parent
+ : staticClassNameNode
+
+ const listNode = listElement.parent
+ if (
+ listNode.type === 'ArrayExpression' ||
+ listNode.type === 'ObjectExpression'
+ ) {
+ const elements =
+ listNode.type === 'ObjectExpression'
+ ? listNode.properties
+ : listNode.elements
+
+ if (elements.length === 1 && listNode === expressionNode) {
+ yield fixer.remove(attributeNode)
+ dynamicClassDirectiveRemoved = true
+ return
+ }
+
+ const tokenStore =
+ context.parserServices.getTemplateBodyTokenStore()
+
+ if (elements.length === 1) {
+ yield* removeNodeWithComma(fixer, tokenStore, listNode)
+ return
+ }
+
+ yield* removeNodeWithComma(fixer, tokenStore, listElement)
+ }
+ }
+
+ /**
+ * Add class to static `class` attribute.
+ */
+ function* addToClassAttribute() {
+ const existingStaticClassAttribute =
+ attributeNode.parent.attributes.find(isStaticClassAttribute)
+ if (existingStaticClassAttribute) {
+ const literalNode = existingStaticClassAttribute.value
+ yield fixer.replaceText(
+ literalNode,
+ `"${literalNode.value} ${className}"`
+ )
+ return
+ }
+
+ // new static `class` attribute
+ const separator = dynamicClassDirectiveRemoved ? '' : ' '
+ yield fixer.insertTextBefore(
+ attributeNode,
+ `class="${className}"${separator}`
+ )
+ }
+ }
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
index 6f433a835..d7480d538 100644
--- a/lib/rules/valid-define-emits.js
+++ b/lib/rules/valid-define-emits.js
@@ -79,6 +79,7 @@ module.exports = {
variable.defs.length &&
variable.defs.every(
(def) =>
+ def.type !== 'ImportBinding' &&
utils.inRange(scriptSetup.range, def.name) &&
!utils.inRange(defineEmits.range, def.name)
)
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
index 3a084dd68..849437b1e 100644
--- a/lib/rules/valid-define-props.js
+++ b/lib/rules/valid-define-props.js
@@ -80,6 +80,7 @@ module.exports = {
variable.defs.length &&
variable.defs.every(
(def) =>
+ def.type !== 'ImportBinding' &&
utils.inRange(scriptSetup.range, def.name) &&
!utils.inRange(defineProps.range, def.name)
)
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 39ef58670..d208d5a08 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -19,7 +19,8 @@ const {
isNotOpeningBraceToken,
isOpeningBracketToken,
isClosingBracketToken,
- isSemicolonToken
+ isSemicolonToken,
+ isNotSemicolonToken
} = require('eslint-utils')
const {
isComment,
@@ -1288,13 +1289,13 @@ module.exports.defineVisitor = function create(
},
/** @param {ExportAllDeclaration} node */
ExportAllDeclaration(node) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
- if (isSemicolonToken(tokens[tokens.length - 1])) {
- tokens.pop()
- }
+ const exportToken = tokenStore.getFirstToken(node)
+ const tokens = [
+ ...tokenStore.getTokensBetween(exportToken, node.source),
+ tokenStore.getFirstToken(node.source)
+ ]
if (!node.exported) {
- setOffset(tokens, 1, firstToken)
+ setOffset(tokens, 1, exportToken)
} else {
// export * as foo from "mod"
const starToken = /** @type {Token} */ (tokens.find(isWildcard))
@@ -1302,10 +1303,28 @@ module.exports.defineVisitor = function create(
const exportedToken = tokenStore.getTokenAfter(asToken)
const afterTokens = tokens.slice(tokens.indexOf(exportedToken) + 1)
- setOffset(starToken, 1, firstToken)
+ setOffset(starToken, 1, exportToken)
setOffset(asToken, 1, starToken)
setOffset(exportedToken, 1, starToken)
- setOffset(afterTokens, 1, firstToken)
+ setOffset(afterTokens, 1, exportToken)
+ }
+
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, exportToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
}
},
/** @param {ExportDefaultDeclaration} node */
@@ -1328,28 +1347,66 @@ module.exports.defineVisitor = function create(
const firstSpecifier = node.specifiers[0]
if (!firstSpecifier || firstSpecifier.type === 'ExportSpecifier') {
// export {foo, bar}; or export {foo, bar} from "mod";
- const leftParenToken = tokenStore.getFirstToken(node, 1)
- const rightParenToken = /** @type {Token} */ (
- tokenStore.getLastToken(node, isClosingBraceToken)
+ const leftBraceTokens = firstSpecifier
+ ? tokenStore.getTokensBetween(exportToken, firstSpecifier)
+ : [tokenStore.getTokenAfter(exportToken)]
+ const rightBraceToken = /** @type {Token} */ (
+ node.source
+ ? tokenStore.getTokenBefore(node.source, isClosingBraceToken)
+ : tokenStore.getLastToken(node, isClosingBraceToken)
+ )
+ setOffset(leftBraceTokens, 0, exportToken)
+ processNodeList(
+ node.specifiers,
+ /** @type {Token} */ (last(leftBraceTokens)),
+ rightBraceToken,
+ 1
)
- setOffset(leftParenToken, 0, exportToken)
- processNodeList(node.specifiers, leftParenToken, rightParenToken, 1)
-
- const maybeFromToken = tokenStore.getTokenAfter(rightParenToken)
- if (maybeFromToken != null && maybeFromToken.value === 'from') {
- const fromToken = maybeFromToken
- const nameToken = tokenStore.getTokenAfter(fromToken)
- setOffset([fromToken, nameToken], 1, exportToken)
+
+ if (node.source) {
+ const tokens = tokenStore.getTokensBetween(
+ rightBraceToken,
+ node.source
+ )
+ setOffset(
+ [...tokens, sourceCode.getFirstToken(node.source)],
+ 1,
+ exportToken
+ )
+
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, exportToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
+ }
}
} else {
// maybe babel parser
}
}
},
- /** @param {ExportSpecifier} node */
- ExportSpecifier(node) {
+ /** @param {ExportSpecifier | ImportSpecifier} node */
+ 'ExportSpecifier, ImportSpecifier'(node) {
const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
+ let firstToken = /** @type {Token} */ (tokens.shift())
+ if (firstToken.value === 'type') {
+ const typeToken = firstToken
+ firstToken = /** @type {Token} */ (tokens.shift())
+ setOffset(firstToken, 0, typeToken)
+ }
+
setOffset(tokens, 1, firstToken)
},
/** @param {ForInStatement | ForOfStatement} node */
@@ -1540,13 +1597,23 @@ module.exports.defineVisitor = function create(
setOffset(fromToken, 1, importToken)
setOffset(afterTokens, 0, fromToken)
}
- },
- /** @param {ImportSpecifier} node */
- ImportSpecifier(node) {
- if (node.local.range[0] !== node.imported.range[0]) {
- const tokens = tokenStore.getTokens(node)
- const firstToken = /** @type {Token} */ (tokens.shift())
- setOffset(tokens, 1, firstToken)
+
+ // assertions
+ const lastToken = /** @type {Token} */ (
+ tokenStore.getLastToken(node, isNotSemicolonToken)
+ )
+ const assertionTokens = tokenStore.getTokensBetween(
+ node.source,
+ lastToken
+ )
+ if (assertionTokens.length) {
+ const assertToken = /** @type {Token} */ (assertionTokens.shift())
+ setOffset(assertToken, 0, importToken)
+ const assertionOpen = assertionTokens.shift()
+ if (assertionOpen) {
+ setOffset(assertionOpen, 1, assertToken)
+ processNodeList(assertionTokens, assertionOpen, lastToken, 1)
+ }
}
},
/** @param {ImportNamespaceSpecifier} node */
diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js
index b98e9d105..496e892cd 100644
--- a/lib/utils/indent-ts.js
+++ b/lib/utils/indent-ts.js
@@ -1302,6 +1302,27 @@ function defineVisitor({
setOffset(atToken, 0, tokenStore.getFirstToken(decorators[0]))
}
},
+ ImportAttribute(node) {
+ const firstToken = tokenStore.getFirstToken(node)
+ const keyTokens = getFirstAndLastTokens(node.key)
+ const prefixTokens = tokenStore.getTokensBetween(
+ firstToken,
+ keyTokens.firstToken
+ )
+ setOffset(prefixTokens, 0, firstToken)
+
+ setOffset(keyTokens.firstToken, 0, firstToken)
+
+ const initToken = tokenStore.getFirstToken(node.value)
+ setOffset(
+ [
+ ...tokenStore.getTokensBetween(keyTokens.lastToken, initToken),
+ initToken
+ ],
+ 1,
+ keyTokens.lastToken
+ )
+ },
// ----------------------------------------------------------------------
// DEPRECATED NODES
diff --git a/package.json b/package.json
index 2c9bcbf18..bcc6bfcdd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-vue",
- "version": "8.1.1",
+ "version": "8.2.0",
"description": "Official ESLint plugin for Vue.js",
"main": "lib/index.js",
"scripts": {
@@ -65,7 +65,7 @@
"@types/natural-compare": "^1.4.0",
"@types/node": "^13.13.5",
"@types/semver": "^7.2.0",
- "@typescript-eslint/parser": "^5.4.0",
+ "@typescript-eslint/parser": "^5.5.0",
"@vuepress/plugin-pwa": "^1.4.1",
"acorn": "^8.5.0",
"env-cmd": "^10.1.0",
@@ -82,7 +82,7 @@
"mocha": "^7.1.2",
"nyc": "^15.1.0",
"prettier": "^2.4.1",
- "typescript": "^4.5.0-0",
+ "typescript": "^4.5.0",
"vue-eslint-editor": "^1.1.0",
"vuepress": "^1.8.2"
}
diff --git a/tests/fixtures/script-indent/ts-import-assertion-01.vue b/tests/fixtures/script-indent/ts-import-assertion-01.vue
new file mode 100644
index 000000000..f14761841
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-01.vue
@@ -0,0 +1,9 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-02.vue b/tests/fixtures/script-indent/ts-import-assertion-02.vue
new file mode 100644
index 000000000..86fe80b4e
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-02.vue
@@ -0,0 +1,15 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-03.vue b/tests/fixtures/script-indent/ts-import-assertion-03.vue
new file mode 100644
index 000000000..ee88e92ce
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-03.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-import-assertion-04.vue b/tests/fixtures/script-indent/ts-import-assertion-04.vue
new file mode 100644
index 000000000..76d2f2860
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-import-assertion-04.vue
@@ -0,0 +1,10 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-01.vue b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue
new file mode 100644
index 000000000..97c6dc192
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-02.vue b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue
new file mode 100644
index 000000000..0ef5f1106
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue
@@ -0,0 +1,23 @@
+
+
diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-03.vue b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue
new file mode 100644
index 000000000..ca280f03c
--- /dev/null
+++ b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue
@@ -0,0 +1,26 @@
+
+
diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js
index baeb66433..4c4d9f5cb 100644
--- a/tests/lib/rules/component-name-in-template-casing.js
+++ b/tests/lib/rules/component-name-in-template-casing.js
@@ -69,6 +69,10 @@ tester.run('component-name-in-template-casing', rule, {
code: '
',
options: ['PascalCase', { registeredComponentsOnly: false }]
},
+ {
+ code: 'bar
',
+ options: ['PascalCase', { registeredComponentsOnly: false }]
+ },
{
code: 'Title
',
options: ['PascalCase', { registeredComponentsOnly: false }]
@@ -151,6 +155,19 @@ tester.run('component-name-in-template-casing', rule, {
{
code: '