Skip to content

Commit 250eb83

Browse files
authored
feat: add v-deep-pseudo-style, v-slotted-pseudo-style, and v-global-pseudo-style rules (#259)
* feat: add `v-deep-pseudo-style`, `v-slotted-pseudo-style`, and `v-global-pseudo-style` rules * Create large-months-count.md
1 parent 54876c6 commit 250eb83

17 files changed

+842
-37
lines changed

Diff for: .changeset/large-months-count.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-vue-scoped-css": minor
3+
---
4+
5+
feat: add `v-deep-pseudo-style`, `v-slotted-pseudo-style`, and `v-global-pseudo-style` rules

Diff for: README.md

+3
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ For example:
145145
|:--------|:------------|:---|
146146
| [vue-scoped-css/no-deprecated-v-enter-v-leave-class](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/no-deprecated-v-enter-v-leave-class.html) | disallow v-enter and v-leave classes. | |
147147
| [vue-scoped-css/require-selector-used-inside](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-selector-used-inside.html) | disallow selectors defined that is not used inside `<template>` | |
148+
| [vue-scoped-css/v-deep-pseudo-style](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/v-deep-pseudo-style.html) | enforce `:deep()`/`::v-deep()` style | :wrench: |
149+
| [vue-scoped-css/v-global-pseudo-style](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/v-global-pseudo-style.html) | enforce `:global()`/`::v-global()` style | :wrench: |
150+
| [vue-scoped-css/v-slotted-pseudo-style](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/v-slotted-pseudo-style.html) | enforce `:slotted()`/`::v-slotted()` style | :wrench: |
148151

149152
## Deprecated
150153

Diff for: docs/rules/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ For example:
6464
|:--------|:------------|:---|
6565
| [vue-scoped-css/no-deprecated-v-enter-v-leave-class](./no-deprecated-v-enter-v-leave-class.md) | disallow v-enter and v-leave classes. | |
6666
| [vue-scoped-css/require-selector-used-inside](./require-selector-used-inside.md) | disallow selectors defined that is not used inside `<template>` | |
67+
| [vue-scoped-css/v-deep-pseudo-style](./v-deep-pseudo-style.md) | enforce `:deep()`/`::v-deep()` style | :wrench: |
68+
| [vue-scoped-css/v-global-pseudo-style](./v-global-pseudo-style.md) | enforce `:global()`/`::v-global()` style | :wrench: |
69+
| [vue-scoped-css/v-slotted-pseudo-style](./v-slotted-pseudo-style.md) | enforce `:slotted()`/`::v-slotted()` style | :wrench: |
6770

6871
## Deprecated
6972

Diff for: docs/rules/enforce-style-type.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Default is set to `{ allows: ['scoped'] }`.
3737

3838
```json
3939
{
40-
"vue-scoped-css/enforce-style-type": ["error", { allows: ['scoped'] }]
40+
"vue-scoped-css/enforce-style-type": ["error", { "allows": ["scoped"] }]
4141
}
4242
```
4343

Diff for: docs/rules/v-deep-pseudo-style.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "vue-scoped-css/v-deep-pseudo-style"
5+
description: "enforce `:deep()`/`::v-deep()` style"
6+
---
7+
# vue-scoped-css/v-deep-pseudo-style
8+
9+
> enforce `:deep()`/`::v-deep()` style
10+
11+
- :gear: This rule is included in `"plugin:vue-scoped-css/all"`.
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 enforces deep pseudo style which you should use `:deep()` or `::v-deep()`.
17+
18+
<eslint-code-block fix :rules="{'vue-scoped-css/v-deep-pseudo-style': ['error']}">
19+
20+
```vue
21+
<style scoped>
22+
/* ✗ BAD */
23+
.foo ::v-deep(.bar) {}
24+
25+
/* ✓ GOOD */
26+
.foo :deep(.bar) {}
27+
</style>
28+
```
29+
30+
</eslint-code-block>
31+
32+
## :wrench: Options
33+
34+
```json
35+
{
36+
"vue-scoped-css/v-deep-pseudo-style": [
37+
"error",
38+
":deep" // or "::v-deep"
39+
]
40+
}
41+
```
42+
43+
- `":deep"` (default) ... requires using `:deep()`.
44+
- `"::v-deep"` ... requires using `::v-deep()`.
45+
46+
## :books: Further reading
47+
48+
- [Vue.js - API SFC CSS Features > Deep Selectors](https://vuejs.org/api/sfc-css-features.html#deep-selectors)
49+
50+
## Implementation
51+
52+
- [Rule source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/lib/rules/v-deep-pseudo-style.ts)
53+
- [Test source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/tests/lib/rules/v-deep-pseudo-style.js)

Diff for: docs/rules/v-global-pseudo-style.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "vue-scoped-css/v-global-pseudo-style"
5+
description: "enforce `:global()`/`::v-global()` style"
6+
---
7+
# vue-scoped-css/v-global-pseudo-style
8+
9+
> enforce `:global()`/`::v-global()` style
10+
11+
- :gear: This rule is included in `"plugin:vue-scoped-css/all"`.
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 enforces global pseudo style which you should use `:global()` or `::v-global()`.
17+
18+
<eslint-code-block fix :rules="{'vue-scoped-css/v-global-pseudo-style': ['error']}">
19+
20+
```vue
21+
<style scoped>
22+
/* ✗ BAD */
23+
.foo ::v-global(.bar) {}
24+
25+
/* ✓ GOOD */
26+
.foo :global(.bar) {}
27+
</style>
28+
```
29+
30+
</eslint-code-block>
31+
32+
## :wrench: Options
33+
34+
```json
35+
{
36+
"vue-scoped-css/v-global-pseudo-style": [
37+
"error",
38+
":global" // or "::v-global"
39+
]
40+
}
41+
```
42+
43+
- `":global"` (default) ... requires using `:global()`.
44+
- `"::v-global"` ... requires using `::v-global()`.
45+
46+
## :books: Further reading
47+
48+
- [Vue.js - API SFC CSS Features > Global Selectors](https://vuejs.org/api/sfc-css-features.html#global-selectors)
49+
50+
## Implementation
51+
52+
- [Rule source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/lib/rules/v-global-pseudo-style.ts)
53+
- [Test source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/tests/lib/rules/v-global-pseudo-style.js)

Diff for: docs/rules/v-slotted-pseudo-style.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "vue-scoped-css/v-slotted-pseudo-style"
5+
description: "enforce `:slotted()`/`::v-slotted()` style"
6+
---
7+
# vue-scoped-css/v-slotted-pseudo-style
8+
9+
> enforce `:slotted()`/`::v-slotted()` style
10+
11+
- :gear: This rule is included in `"plugin:vue-scoped-css/all"`.
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 enforces slotted pseudo style which you should use `:slotted()` or `::v-slotted()`.
17+
18+
<eslint-code-block fix :rules="{'vue-scoped-css/v-slotted-pseudo-style': ['error']}">
19+
20+
```vue
21+
<style scoped>
22+
/* ✗ BAD */
23+
.foo ::v-slotted(.bar) {}
24+
25+
/* ✓ GOOD */
26+
.foo :slotted(.bar) {}
27+
</style>
28+
```
29+
30+
</eslint-code-block>
31+
32+
## :wrench: Options
33+
34+
```json
35+
{
36+
"vue-scoped-css/v-slotted-pseudo-style": [
37+
"error",
38+
":slotted" // or "::v-slotted"
39+
]
40+
}
41+
```
42+
43+
- `":slotted"` (default) ... requires using `:slotted()`.
44+
- `"::v-slotted"` ... requires using `::v-slotted()`.
45+
46+
## :books: Further reading
47+
48+
- [Vue.js - API SFC CSS Features > Slotted Selectors](https://vuejs.org/api/sfc-css-features.html#slotted-selectors)
49+
50+
## Implementation
51+
52+
- [Rule source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/lib/rules/v-slotted-pseudo-style.ts)
53+
- [Test source](https://github.com/future-architect/eslint-plugin-vue-scoped-css/blob/master/tests/lib/rules/v-slotted-pseudo-style.js)

Diff for: lib/rules/v-deep-pseudo-style.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { ValidStyleContext } from "../styles/context";
2+
import {
3+
getStyleContexts,
4+
getCommentDirectivesReporter,
5+
isValidStyleContext,
6+
} from "../styles/context";
7+
import type { RuleContext, RuleListener } from "../types";
8+
import type { VDeepPseudo } from "../styles/utils/selectors";
9+
import {
10+
isVDeepPseudo,
11+
isPseudoEmptyArguments,
12+
} from "../styles/utils/selectors";
13+
14+
export = {
15+
meta: {
16+
docs: {
17+
description: "enforce `:deep()`/`::v-deep()` style",
18+
categories: [
19+
// TODO: enable in next major version
20+
// "vue3-recommended"
21+
],
22+
default: "warn",
23+
url: "https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/v-deep-pseudo-style.html",
24+
},
25+
fixable: "code",
26+
messages: {
27+
expectedDeep: "Expected ':deep()' instead of '::v-deep()'.",
28+
expectedVDeep: "Expected '::v-deep()' instead of ':deep()'.",
29+
},
30+
schema: [{ enum: [":deep", "::v-deep"] }],
31+
type: "suggestion",
32+
},
33+
create(context: RuleContext): RuleListener {
34+
const styles = getStyleContexts(context)
35+
.filter(isValidStyleContext)
36+
.filter((style) => style.scoped);
37+
if (!styles.length) {
38+
return {};
39+
}
40+
const expected = (context.options[0] || ":deep") as ":deep" | "::v-deep";
41+
const reporter = getCommentDirectivesReporter(context);
42+
43+
/**
44+
* Reports the given node
45+
* @param {ASTNode} node node to report
46+
*/
47+
function report(node: VDeepPseudo) {
48+
reporter.report({
49+
node,
50+
loc: node.loc,
51+
messageId: expected === ":deep" ? "expectedDeep" : "expectedVDeep",
52+
fix(fixer) {
53+
const nodeText = context.getSourceCode().text.slice(...node.range);
54+
return fixer.replaceTextRange(
55+
node.range,
56+
nodeText.replace(
57+
/^(\s*)(?::deep|::v-deep)(\s*\()/u,
58+
(_, prefix: string, suffix: string) =>
59+
`${prefix}${expected}${suffix}`
60+
)
61+
);
62+
},
63+
});
64+
}
65+
66+
/**
67+
* Verify the node
68+
*/
69+
function verifyNode(node: VDeepPseudo) {
70+
if (node.value === expected) {
71+
return;
72+
}
73+
74+
report(node);
75+
}
76+
77+
/**
78+
* Verify the style
79+
*/
80+
function verify(style: ValidStyleContext) {
81+
style.traverseSelectorNodes({
82+
enterNode(node) {
83+
if (isVDeepPseudo(node) && !isPseudoEmptyArguments(node)) {
84+
verifyNode(node);
85+
}
86+
},
87+
});
88+
}
89+
90+
return {
91+
"Program:exit"() {
92+
for (const style of styles) {
93+
verify(style);
94+
}
95+
},
96+
};
97+
},
98+
};

Diff for: lib/rules/v-global-pseudo-style.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { ValidStyleContext } from "../styles/context";
2+
import {
3+
getStyleContexts,
4+
getCommentDirectivesReporter,
5+
isValidStyleContext,
6+
} from "../styles/context";
7+
import type { RuleContext, RuleListener } from "../types";
8+
import type { VGlobalPseudo } from "../styles/utils/selectors";
9+
import {
10+
isVGlobalPseudo,
11+
isPseudoEmptyArguments,
12+
} from "../styles/utils/selectors";
13+
14+
export = {
15+
meta: {
16+
docs: {
17+
description: "enforce `:global()`/`::v-global()` style",
18+
categories: [
19+
// TODO: enable in next major version
20+
// "vue3-recommended"
21+
],
22+
default: "warn",
23+
url: "https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/v-global-pseudo-style.html",
24+
},
25+
fixable: "code",
26+
messages: {
27+
expectedGlobal: "Expected ':global()' instead of '::v-global()'.",
28+
expectedVGlobal: "Expected '::v-global()' instead of ':global()'.",
29+
},
30+
schema: [{ enum: [":global", "::v-global"] }],
31+
type: "suggestion",
32+
},
33+
create(context: RuleContext): RuleListener {
34+
const styles = getStyleContexts(context)
35+
.filter(isValidStyleContext)
36+
.filter((style) => style.scoped);
37+
if (!styles.length) {
38+
return {};
39+
}
40+
const expected = (context.options[0] || ":global") as
41+
| ":global"
42+
| "::v-global";
43+
const reporter = getCommentDirectivesReporter(context);
44+
45+
/**
46+
* Reports the given node
47+
* @param {ASTNode} node node to report
48+
*/
49+
function report(node: VGlobalPseudo) {
50+
reporter.report({
51+
node,
52+
loc: node.loc,
53+
messageId:
54+
expected === ":global" ? "expectedGlobal" : "expectedVGlobal",
55+
fix(fixer) {
56+
const nodeText = context.getSourceCode().text.slice(...node.range);
57+
return fixer.replaceTextRange(
58+
node.range,
59+
nodeText.replace(
60+
/^(\s*)(?::global|::v-global)(\s*\()/u,
61+
(_, prefix: string, suffix: string) =>
62+
`${prefix}${expected}${suffix}`
63+
)
64+
);
65+
},
66+
});
67+
}
68+
69+
/**
70+
* Verify the node
71+
*/
72+
function verifyNode(node: VGlobalPseudo) {
73+
if (node.value === expected) {
74+
return;
75+
}
76+
77+
report(node);
78+
}
79+
80+
/**
81+
* Verify the style
82+
*/
83+
function verify(style: ValidStyleContext) {
84+
style.traverseSelectorNodes({
85+
enterNode(node) {
86+
if (isVGlobalPseudo(node) && !isPseudoEmptyArguments(node)) {
87+
verifyNode(node);
88+
}
89+
},
90+
});
91+
}
92+
93+
return {
94+
"Program:exit"() {
95+
for (const style of styles) {
96+
verify(style);
97+
}
98+
},
99+
};
100+
},
101+
};

0 commit comments

Comments
 (0)