Skip to content
83 changes: 83 additions & 0 deletions docs/rules/enforce-style-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/enforce-style-attribute
description: enforce either the `scoped` or `module` attribute in SFC top level style tags
---

# vue/enforce-style-attribute

> enfore either the `scoped` or `module` attribute in SFC top level style tags

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>

## :wrench: Options

```json
{
"vue/attribute-hyphenation": ["error", "either" | "scoped" | "module"]
}
```

## :book: Rule Details

This rule warns you about top level style tags that are missing either the `scoped` or `module` attribute.

- `"either"` (default) ... Warn if a style tag doesn't have neither `scoped` nor `module` attributes.
- `"scoped"` ... Warn if a style tag doesn't have the `scoped` attribute.
- `"module"` ... Warn if a style tag doesn't have the `module` attribute.

### `"either"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', 'either']}">

```vue
<!-- ✓ GOOD -->
<style scoped></style>
<style lang="scss" src="../path/to/style.scss" scoped></style>

<!-- ✓ GOOD -->
<style module></style>

<!-- ✗ BAD -->
<style></style>
```

</eslint-code-block>

### `"scoped"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', 'scoped']}">

```vue
<!-- ✓ GOOD -->
<style scoped></style>
<style lang="scss" src="../path/to/style.scss" scoped></style>

<!-- ✗ BAD -->
<style></style>
<style module></style>
```

</eslint-code-block>

### `"module"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', 'module']}">

```vue
<!-- ✓ GOOD -->
<style module></style>

<!-- ✗ BAD -->
<style></style>
<style scoped></style>
<style lang="scss" src="../path/to/style.scss" scoped></style>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/enforce-style-attribute.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/enforce-style-attribute.js)
102 changes: 102 additions & 0 deletions lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @author Mussin Benarbia
* See LICENSE file in root directory for full license.
*/
'use strict'

const { isVElement } = require('../utils')

/**
* check whether a tag has the `scoped` attribute
* @param {VElement} componentBlock
*/
function isScoped(componentBlock) {
return componentBlock.startTag.attributes.some(
(attribute) => !attribute.directive && attribute.key.name === 'scoped'
)
}

/**
* check whether a tag has the `module` attribute
* @param {VElement} componentBlock
*/
function isModule(componentBlock) {
return componentBlock.startTag.attributes.some(
(attribute) => !attribute.directive && attribute.key.name === 'module'
)
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce either the `scoped` or `module` attribute in SFC top level style tags',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/enforce-style-attribute.html'
},
fixable: 'code',
schema: [{ enum: ['scoped', 'module', 'either'] }],
messages: {
needsScoped: 'The <style> tag should have the scoped attribute.',
needsModule: 'The <style> tag should have the module attribute.',
needsEither:
'The <style> tag should have either the scoped or module attribute.'
}
},

/** @param {RuleContext} context */
create(context) {
if (!context.parserServices.getDocumentFragment) {
return {}
}
const documentFragment = context.parserServices.getDocumentFragment()
if (!documentFragment) {
return {}
}

const topLevelElements = documentFragment.children.filter(isVElement)
const topLevelStyleTags = topLevelElements.filter(
(element) => element.rawName === 'style'
)

if (topLevelStyleTags.length === 0) {
return {}
}

const mode = context.options[0] || 'either'
const needsScoped = mode === 'scoped'
const needsModule = mode === 'module'
const needsEither = mode === 'either'

return {
Program() {
for (const styleTag of topLevelStyleTags) {
if (needsScoped && !isScoped(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsScoped'
})
return
}

if (needsModule && !isModule(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsModule'
})
return
}

if (needsEither && !isScoped(styleTag) && !isModule(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsEither'
})
return
}
}
}
}
}
}
101 changes: 101 additions & 0 deletions tests/lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @author Mussin Benarbia
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/enforce-style-attribute')

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
}
})

tester.run('enforce-style-attribute', rule, {
valid: [
// With default options
{
filename: 'test.vue',
code: '<template></template><script></script><style scoped></style>'
},
{
filename: 'test.vue',
code: '<template></template><script></script><style module></style>'
},
{
filename: 'test.vue',
code: '<template></template><script></script><style lang=scss" "src="../path/to/style.scss" scoped></style>'
},
// With scoped option
{
filename: 'test.vue',
code: '<template></template><script></script><style scoped></style>',
options: ['scoped']
},
{
filename: 'test.vue',
code: '<template></template><script></script><style lang=scss" "src="../path/to/style.scss" scoped></style>',
options: ['scoped']
},
// With module option
{
filename: 'test.vue',
code: '<template></template><script></script><style module></style>',
options: ['module']
}
],
invalid: [
// With default options
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message:
'The <style> tag should have either the scoped or module attribute.'
}
]
},
// With scoped option
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message: 'The <style> tag should have the scoped attribute.'
}
],
options: ['scoped']
},
{
code: `<template></template><script></script><style module></style>`,
errors: [
{
message: 'The <style> tag should have the scoped attribute.'
}
],
options: ['scoped']
},
// With module option
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message: 'The <style> tag should have the module attribute.'
}
],
options: ['module']
},
{
code: `<template></template><script></script><style scoped></style>`,
errors: [
{
message: 'The <style> tag should have the module attribute.'
}
],
options: ['module']
}
]
})