Skip to content
This repository was archived by the owner on Jan 18, 2022. It is now read-only.

Commit 51cbeae

Browse files
ktsnznck
authored andcommitted
Support scoped css (#87)
* Use hash-sum and postcss-selector-parser for scoped css * Transform css to add scope id * Inject scope id component options object * Add test for scoped css * Fix lint errors * Split processScript function to reduce complexity * Unify scope id among various environment * Comma is missing * Update Scoped CSS docs * Change warning tips for scoped styles
1 parent 9b633dd commit 51cbeae

File tree

13 files changed

+217
-13
lines changed

13 files changed

+217
-13
lines changed

config/build.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ rollup.rollup({
3030
'de-indent',
3131
'debug',
3232
'fs',
33+
'hash-sum',
3334
'html-minifier',
3435
'less',
3536
'magic-string',
@@ -39,6 +40,7 @@ rollup.rollup({
3940
'path',
4041
'postcss',
4142
'postcss-modules',
43+
'postcss-selector-parser',
4244
'posthtml',
4345
'posthtml-attrs-parser',
4446
'pug',

docs/en/2.3/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ The `css` option accepts style handling options.
5050
- `id: String` - Path of the `.vue` file.
5151
- `lang: String` - Language defined on `<style>` element (defaults to `css`).
5252
- `module: Boolean` - Is `<style>` element a CSS module?
53-
- `scoped: Boolean` - Should `<style>` element be scoped? <p class="warning">Scoped styles are not supported yet.</p>
53+
- `scoped: Boolean` - Should `<style>` element be scoped? <p class="warning">Available in `rollup-plugin-vue@^2.4+`.</p>
5454
- `map: Object` - Source map object.
5555
- `$compiled: { code: String, ?map: Object }` - If [auto styles](#auto-styles) is enabled, `<style>` is transformed to `css`.
5656
- `compile: Function` - An async compiler that takes two parameters:
@@ -195,6 +195,39 @@ You can provide `postcss-modules` configuration options by setting:
195195
cssModules: { generateScopedName: '[name]__[local]', ... }
196196
```
197197

198+
#### Scoped CSS
199+
<p class="tip">
200+
Available in `rollup-plugin-vue@^2.4+`.
201+
</p>
202+
203+
There is another option to modularize your component styles that called Scoped CSS. Scoped CSS will add a unique attribute to all HTML elements and CSS selectors instead of transform class names. To enable this, you need to add `scoped` attribute to `<style>` tag.
204+
205+
For example, if you write following CSS in your component:
206+
207+
``` vue
208+
<style scoped>
209+
.red {
210+
color: red;
211+
}
212+
213+
.container .text {
214+
font-size: 1.8rem;
215+
}
216+
</style>
217+
```
218+
219+
The output CSS will be like:
220+
221+
``` css
222+
.red[data-v-07bdddea] {
223+
color: red;
224+
}
225+
226+
.container .text[data-v-07bdddea] {
227+
font-size: 1.8rem;
228+
}
229+
```
230+
198231
### Template
199232
Templates are processed into `render` function by default. You can disable this by setting:
200233
``` js

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@
3838
"camelcase": "^4.0.0",
3939
"de-indent": "^1.0.2",
4040
"debug": "^2.6.0",
41+
"hash-sum": "^1.0.2",
4142
"html-minifier": "^3.2.3",
4243
"magic-string": "^0.19.0",
4344
"merge-options": "0.0.64",
4445
"parse5": "^2.1.0",
4546
"postcss": "^5.2.11",
4647
"postcss-modules": "^0.6.4",
48+
"postcss-selector-parser": "^2.2.3",
4749
"posthtml": "^0.9.2",
4850
"posthtml-attrs-parser": "^0.1.1",
4951
"rollup-pluginutils": "^2.0.1",

src/gen-scope-id.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// utility for generating a uid for each component file
2+
// used in scoped CSS rewriting
3+
import path from 'path'
4+
import hash from 'hash-sum'
5+
const cache = Object.create(null)
6+
7+
export default function genScopeID (file) {
8+
const modified = path.relative(process.cwd(), file)
9+
10+
if (!cache[modified]) {
11+
cache[modified] = 'data-v-' + hash(modified)
12+
}
13+
14+
return cache[modified]
15+
}

src/injections.js

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/options.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { templateJs, moduleJs, renderJs } from './injections'
1+
import { templateJs, moduleJs, scopeJs, renderJs } from './injections'
22
import { coffee } from './script/index'
33

44
export default {
@@ -80,6 +80,11 @@ export default {
8080
module: {
8181
js: moduleJs,
8282
babel: moduleJs
83+
},
84+
85+
scoped: {
86+
js: scopeJs,
87+
babel: scopeJs
8388
}
8489
},
8590

src/style/css.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import postcss from 'postcss'
22
import modules from 'postcss-modules'
3+
import selectorParser from 'postcss-selector-parser'
34
import camelcase from 'camelcase'
45
// import MagicString from 'magic-string'
6+
import genScopeID from '../gen-scope-id'
57
import debug from '../debug'
68

9+
const addScopeID = postcss.plugin('add-scope-id', options => {
10+
const selectorTransformer = selectorParser(selectors => {
11+
selectors.each(selector => {
12+
let target = null
13+
selector.each(n => {
14+
if (n.type !== 'pseudo' && n.type !== 'combinator') {
15+
target = n
16+
}
17+
})
18+
19+
selector.insertAfter(target, selectorParser.attribute({
20+
attribute: options.scopeID
21+
}))
22+
})
23+
})
24+
25+
return root => {
26+
root.walkRules(rule => {
27+
rule.selector = selectorTransformer.process(rule.selector).result
28+
})
29+
}
30+
})
31+
732
function compileModule (code, map, source, options) {
833
let style
934
debug(`CSS Modules: ${source.id}`)
@@ -24,6 +49,22 @@ function compileModule (code, map, source, options) {
2449
)
2550
}
2651

52+
function compileScopedCSS (code, map, source, options) {
53+
debug(`Scoped CSS: ${source.id}`)
54+
55+
return postcss([
56+
addScopeID({
57+
scopeID: genScopeID(source.id)
58+
})
59+
]).process(code, { map: { inline: false, prev: map }, from: source.id, to: source.id })
60+
.then(
61+
result => ({ code: result.css, map: result.map.toString() }),
62+
error => {
63+
throw error
64+
}
65+
)
66+
}
67+
2768
function escapeRegExp (str) {
2869
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
2970
}
@@ -70,6 +111,18 @@ export default async function (promise, options) {
70111
}).catch(error => debug(error))
71112
}
72113

114+
if (style.scoped === true) {
115+
return compileScopedCSS(code, map, style, options).then(compiled => {
116+
if (style.$compiled) {
117+
compiled.$prev = style.$compiled
118+
}
119+
120+
style.$compiled = compiled
121+
122+
return style
123+
})
124+
}
125+
73126
const output = { code, map, lang: 'css' }
74127

75128
if (style.$compiled) output.$prev = style.$compiled

src/vueTransform.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import templateProcessor from './template/index'
77
import { relative } from 'path'
88
import MagicString from 'magic-string'
99
import debug from './debug'
10-
import { injectModule, injectTemplate, injectRender } from './injections'
10+
import { injectModule, injectScopeID, injectTemplate, injectRender } from './injections'
11+
import genScopeID from './gen-scope-id'
1112

1213
function getNodeAttrs (node) {
1314
if (node.attrs) {
@@ -65,7 +66,7 @@ async function processTemplate (source, id, content, options, nodes, modules) {
6566
return htmlMinifier.minify(template, options.htmlMinifier)
6667
}
6768

68-
async function processScript (source, id, content, options, nodes, modules) {
69+
async function processScript (source, id, content, options, nodes, modules, scoped) {
6970
const template = await processTemplate(nodes.template[0], id, content, options, nodes, modules)
7071

7172
debug(`Process script: ${id}`)
@@ -79,19 +80,39 @@ async function processScript (source, id, content, options, nodes, modules) {
7980
source = await options.script[source.attrs.lang](source, id, content, options, nodes)
8081
}
8182

82-
const script = deIndent(padContent(content.slice(0, content.indexOf(source.code))) + source.code)
83+
let script = deIndent(padContent(content.slice(0, content.indexOf(source.code))) + source.code)
8384
const map = (new MagicString(script)).generateMap({ hires: true })
84-
const scriptWithModules = injectModule(script, modules, lang, id, options)
8585

86+
script = processScriptForStyle(script, modules, scoped, lang, id, options)
87+
88+
script = await processScriptForRender(script, template, lang, id, options)
89+
90+
return { map, code: script }
91+
}
92+
93+
function processScriptForStyle (script, modules, scoped, lang, id, options) {
94+
script = injectModule(script, modules, lang, id, options)
95+
96+
if (scoped) {
97+
const scopeID = genScopeID(id)
98+
script = injectScopeID(script, scopeID, lang, id, options)
99+
}
100+
101+
return script
102+
}
103+
104+
async function processScriptForRender (script, template, lang, id, options) {
86105
if (template && options.compileTemplate) {
87106
const render = require('vue-template-compiler').compile(template, options.compileOptions)
88107

89-
return { map, code: await injectRender(scriptWithModules, render, lang, id, options) }
90-
} else if (template) {
91-
return { map, code: await injectTemplate(scriptWithModules, template, lang, id, options) }
92-
} else {
93-
return { map, code: scriptWithModules }
108+
return await injectRender(script, render, lang, id, options)
109+
}
110+
111+
if (template) {
112+
return await injectTemplate(script, template, lang, id, options)
94113
}
114+
115+
return script
95116
}
96117

97118
// eslint-disable-next-line complexity
@@ -173,11 +194,18 @@ const getModules = function (styles) {
173194
return all
174195
}
175196

197+
const hasScoped = function (styles) {
198+
return styles.reduce((scoped, style) => {
199+
return scoped || style.scoped
200+
}, false)
201+
}
202+
176203
export default async function vueTransform (code, id, options) {
177204
const nodes = parseTemplate(code)
178205
const css = await processStyle(nodes.style, id, code, options, nodes)
179206
const modules = getModules(css)
180-
const js = await processScript(nodes.script[0], id, code, options, nodes, modules)
207+
const scoped = hasScoped(css)
208+
const js = await processScript(nodes.script[0], id, code, options, nodes, modules, scoped)
181209

182210
const isProduction = process.env.NODE_ENV === 'production'
183211
const isWithStripped = options.stripWith !== false

test/expects/scoped-css.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.test[data-v-4f57af4d] {
2+
color: red;
3+
}

test/expects/scoped-css.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
var scopedCss = { template: "<div class=\"test\">Foo</div>",_scopeId: 'data-v-4f57af4d',};
2+
3+
export default scopedCss;

test/fixtures/scoped-css.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<template>
2+
<div class="test">Foo</div>
3+
</template>
4+
5+
<script lang="babel">
6+
export default {}
7+
</script>
8+
9+
<style lang="css" scoped>
10+
.test {
11+
color: red;
12+
}
13+
</style>

test/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function test(name) {
4141
assert.equal(code.trim(), expected.trim(), 'should compile code correctly')
4242

4343
// Check css output
44-
if (['style', 'css-modules', 'css-modules-static', 'scss', 'pug', 'less', 'stylus'].indexOf(name) > -1) {
44+
if (['style', 'css-modules', 'css-modules-static', 'scoped-css', 'scss', 'pug', 'less', 'stylus'].indexOf(name) > -1) {
4545
var css = read('expects/' + name + '.css')
4646
assert.equal(css.trim(), actualCss.trim(), 'should output style tag content')
4747
} else if (['no-css-extract'].indexOf(name) > -1) {

0 commit comments

Comments
 (0)