Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update some rules to support style CSS vars injection #1574

Merged
merged 5 commits into from Jul 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/rules/no-unsupported-features.md
Expand Up @@ -32,6 +32,7 @@ The `"ignores"` option accepts an array of the following strings.
- Vue.js 3.1.0+
- `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is)
- Vue.js 3.0.0+
- `"style-css-vars-injection"` ... [SFC CSS variable injection][Vue RFCs - 0043-sfc-style-variables]
- `"script-setup"` ... [`<script setup>`][Vue RFCs - 0040-script-setup]
- `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
- `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
Expand Down Expand Up @@ -107,6 +108,7 @@ The `"ignores"` option accepts an array of the following strings.
- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
- [Vue RFCs - 0011-v-model-api-change]
- [Vue RFCs - 0040-script-setup]
- [Vue RFCs - 0043-sfc-style-variables]
- [Vue RFCs - v-bind .prop shorthand proposal]

[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
Expand All @@ -115,6 +117,7 @@ The `"ignores"` option accepts an array of the following strings.
[Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md
[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
[Vue RFCs - 0040-script-setup]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
[Vue RFCs - 0043-sfc-style-variables]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md

[Vue RFCs - v-bind .prop shorthand proposal]: https://github.com/vuejs/rfcs/pull/18

Expand Down
3 changes: 3 additions & 0 deletions lib/rules/no-unsupported-features.js
Expand Up @@ -27,6 +27,7 @@ const FEATURES = {
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
'v-is': require('./syntaxes/v-is'),
'script-setup': require('./syntaxes/script-setup'),
'style-css-vars-injection': require('./syntaxes/style-css-vars-injection'),
// Vue.js 3.1.0+
'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix')
}
Expand Down Expand Up @@ -103,6 +104,8 @@ module.exports = {
forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".',
forbiddenScriptSetup:
'`<script setup>` are not supported until Vue.js "3.0.0".',
forbiddenStyleCssVarsInjection:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
// Vue.js 3.1.0+
forbiddenIsAttributeWithVuePrefix:
'`is="vue:"` are not supported until Vue.js "3.1.0".'
Expand Down
10 changes: 10 additions & 0 deletions lib/rules/no-unused-properties.js
Expand Up @@ -10,6 +10,7 @@

const utils = require('../utils')
const eslintUtils = require('eslint-utils')
const { getStyleVariablesContext } = require('../utils/style-variables')

/**
* @typedef {import('../utils').GroupName} GroupName
Expand Down Expand Up @@ -1056,6 +1057,15 @@ module.exports = {
{
/** @param {Program} node */
'Program:exit'(node) {
const styleVars = getStyleVariablesContext(context)
if (styleVars) {
for (const { id } of styleVars.references) {
templatePropertiesContainer.usedProperties.addUsed(
id.name,
(context) => extractPatternOrThisProperties(id, context, true)
)
}
}
if (!node.templateBody) {
reportUnusedProperties()
}
Expand Down
12 changes: 11 additions & 1 deletion lib/rules/script-setup-uses-vars.js
Expand Up @@ -4,6 +4,7 @@
*/
'use strict'

const { getStyleVariablesContext } = require('../utils/style-variables')
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -112,7 +113,16 @@ module.exports = {
}
}
},
undefined,
{
Program() {
const styleVars = getStyleVariablesContext(context)
if (styleVars) {
for (const ref of styleVars.references) {
context.markVariableAsUsed(ref.id.name)
}
}
}
},
{
templateBodyTriggerSelector: 'Program'
}
Expand Down
28 changes: 28 additions & 0 deletions lib/rules/syntaxes/style-css-vars-injection.js
@@ -0,0 +1,28 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const { getStyleVariablesContext } = require('../../utils/style-variables')

module.exports = {
supported: '>=3.0.3',
/** @param {RuleContext} context @returns {TemplateListener} */
createScriptVisitor(context) {
const styleVars = getStyleVariablesContext(context)
if (!styleVars) {
return {}
}
return {
Program() {
for (const vBind of styleVars.vBinds) {
context.report({
node: vBind,
messageId: 'forbiddenStyleCssVarsInjection'
})
}
}
}
}
}
61 changes: 61 additions & 0 deletions lib/utils/style-variables/index.js
@@ -0,0 +1,61 @@
const { isVElement } = require('..')

module.exports = {
getStyleVariablesContext
}

class StyleVariablesContext {
/**
* @param {RuleContext} context
* @param {VElement[]} styles
*/
constructor(context, styles) {
this.context = context
this.styles = styles
/** @type {VReference[]} */
this.references = []
/** @type {VExpressionContainer[]} */
this.vBinds = []
for (const style of styles) {
for (const node of style.children) {
if (node.type === 'VExpressionContainer') {
this.vBinds.push(node)
for (const ref of node.references.filter(
(ref) => ref.variable == null
)) {
this.references.push(ref)
}
}
}
}
}
}

/** @type {Map<VElement, StyleVariablesContext} */
const cache = new Map()
/**
* Get the style vars context
* @param {RuleContext} context
* @returns {StyleVariablesContext | null}
*/
function getStyleVariablesContext(context) {
const df =
context.parserServices.getDocumentFragment &&
context.parserServices.getDocumentFragment()
if (!df) {
return null
}
const styles = df.children
.filter(isVElement)
.filter((e) => e.name === 'style')
if (!styles.length) {
return null
}
let ctx = cache.get(styles[0])
if (ctx) {
return ctx
}
ctx = new StyleVariablesContext(context, styles)
cache.set(styles[0], ctx)
return ctx
}
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -56,7 +56,7 @@
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0",
"semver": "^6.3.0",
"vue-eslint-parser": "^7.8.0"
"vue-eslint-parser": "^7.9.0"
},
"devDependencies": {
"@types/eslint": "^7.2.0",
Expand Down
26 changes: 26 additions & 0 deletions tests/lib/rules/no-parsing-error.js
Expand Up @@ -615,6 +615,32 @@ tester.run('no-parsing-error', rule, {
{
code: '<template><div xmlns=""></template>',
errors: ['Parsing error: x-invalid-namespace.']
},

//style vars
{
filename: 'test.vue',
code: `
<template></template>
<style>
.text {
color: v-bind(color.);
font-size: v-bind('font size');
}
</style>
`,
errors: [
{
message: 'Parsing error: Unexpected end of expression.',
line: 5,
column: 33
},
{
message: 'Parsing error: Unexpected token size.',
line: 6,
column: 37
}
]
}
]
})
118 changes: 118 additions & 0 deletions tests/lib/rules/no-unsupported-features/style-css-vars-injection.js
@@ -0,0 +1,118 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../../lib/rules/no-unsupported-features')
const utils = require('./utils')

const buildOptions = utils.optionsBuilder('style-css-vars-injection', '^3.0.3')
const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2019,
sourceType: 'module'
}
})

tester.run('no-unsupported-features/style-css-vars-injection', rule, {
valid: [
{
code: `
<template>
<div class="text">hello</div>
</template>

<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>

<style>
.text {
color: v-bind(color);

/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>`,
options: buildOptions()
},
{
code: `
<template>
<div class="text">hello</div>
</template>

<script>
</script>

<style>
.text {
color: red;
font-size: 2em;
}
</style>`,
options: buildOptions({ version: '^2.6.0' })
}
],
invalid: [
{
code: `
<template>
<div class="text">hello</div>
</template>

<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>

<style>
.text {
color: v-bind(color);

/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>`,
options: buildOptions({ version: '^3.0.0' }),
errors: [
{
message:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
line: 21,
column: 18,
endLine: 21,
endColumn: 31
},
{
message:
'SFC CSS variable injection is not supported until Vue.js "3.0.3".',
line: 24,
column: 22,
endLine: 24,
endColumn: 41
}
]
}
]
})
32 changes: 32 additions & 0 deletions tests/lib/rules/no-unused-properties.js
Expand Up @@ -1562,6 +1562,38 @@ tester.run('no-unused-properties', rule, {
}
</script>`,
options: allOptions
},

//style vars
{
filename: 'test.vue',
code: `
<template>
<div class="text">hello</div>
</template>

<script>
export default {
data() {
return {
color: 'red',
font: {
size: '2em',
},
}
},
}
</script>

<style>
.text {
color: v-bind(color);

/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style>
`
}
],

Expand Down