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

Support scoped css #87

Merged
merged 10 commits into from
May 3, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ rollup.rollup({
'de-indent',
'debug',
'fs',
'hash-sum',
'html-minifier',
'less',
'magic-string',
Expand All @@ -39,6 +40,7 @@ rollup.rollup({
'path',
'postcss',
'postcss-modules',
'postcss-selector-parser',
'posthtml',
'posthtml-attrs-parser',
'pug',
Expand Down
35 changes: 34 additions & 1 deletion docs/en/2.3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The `css` option accepts style handling options.
- `id: String` - Path of the `.vue` file.
- `lang: String` - Language defined on `<style>` element (defaults to `css`).
- `module: Boolean` - Is `<style>` element a CSS module?
- `scoped: Boolean` - Should `<style>` element be scoped? <p class="warning">Scoped styles are not supported yet.</p>
- `scoped: Boolean` - Should `<style>` element be scoped? <p class="warning">Available in `rollup-plugin-vue@^2.4+`.</p>
- `map: Object` - Source map object.
- `$compiled: { code: String, ?map: Object }` - If [auto styles](#auto-styles) is enabled, `<style>` is transformed to `css`.
- `compile: Function` - An async compiler that takes two parameters:
Expand Down Expand Up @@ -195,6 +195,39 @@ You can provide `postcss-modules` configuration options by setting:
cssModules: { generateScopedName: '[name]__[local]', ... }
```

#### Scoped CSS
<p class="tip">
Available in `rollup-plugin-vue@^2.4+`.
</p>

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.

For example, if you write following CSS in your component:

``` vue
<style scoped>
.red {
color: red;
}

.container .text {
font-size: 1.8rem;
}
</style>
```

The output CSS will be like:

``` css
.red[data-v-07bdddea] {
color: red;
}

.container .text[data-v-07bdddea] {
font-size: 1.8rem;
}
```

### Template
Templates are processed into `render` function by default. You can disable this by setting:
``` js
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
"camelcase": "^4.0.0",
"de-indent": "^1.0.2",
"debug": "^2.6.0",
"hash-sum": "^1.0.2",
"html-minifier": "^3.2.3",
"magic-string": "^0.19.0",
"merge-options": "0.0.64",
"parse5": "^2.1.0",
"postcss": "^5.2.11",
"postcss-modules": "^0.6.4",
"postcss-selector-parser": "^2.2.3",
"posthtml": "^0.9.2",
"posthtml-attrs-parser": "^0.1.1",
"rollup-pluginutils": "^2.0.1",
Expand Down
15 changes: 15 additions & 0 deletions src/gen-scope-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// utility for generating a uid for each component file
// used in scoped CSS rewriting
import path from 'path'
import hash from 'hash-sum'
const cache = Object.create(null)

export default function genScopeID (file) {
const modified = path.relative(process.cwd(), file)

if (!cache[modified]) {
cache[modified] = 'data-v-' + hash(modified)
}

return cache[modified]
}
23 changes: 23 additions & 0 deletions src/injections.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/options.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { templateJs, moduleJs, renderJs } from './injections'
import { templateJs, moduleJs, scopeJs, renderJs } from './injections'
import { coffee } from './script/index'

export default {
Expand Down Expand Up @@ -80,6 +80,11 @@ export default {
module: {
js: moduleJs,
babel: moduleJs
},

scoped: {
js: scopeJs,
babel: scopeJs
}
},

Expand Down
53 changes: 53 additions & 0 deletions src/style/css.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import postcss from 'postcss'
import modules from 'postcss-modules'
import selectorParser from 'postcss-selector-parser'
import camelcase from 'camelcase'
// import MagicString from 'magic-string'
import genScopeID from '../gen-scope-id'
import debug from '../debug'

const addScopeID = postcss.plugin('add-scope-id', options => {
const selectorTransformer = selectorParser(selectors => {
selectors.each(selector => {
let target = null
selector.each(n => {
if (n.type !== 'pseudo' && n.type !== 'combinator') {
target = n
}
})

selector.insertAfter(target, selectorParser.attribute({
attribute: options.scopeID
}))
})
})

return root => {
root.walkRules(rule => {
rule.selector = selectorTransformer.process(rule.selector).result
})
}
})

function compileModule (code, map, source, options) {
let style
debug(`CSS Modules: ${source.id}`)
Expand All @@ -24,6 +49,22 @@ function compileModule (code, map, source, options) {
)
}

function compileScopedCSS (code, map, source, options) {
debug(`Scoped CSS: ${source.id}`)

return postcss([
addScopeID({
scopeID: genScopeID(source.id)
})
]).process(code, { map: { inline: false, prev: map }, from: source.id, to: source.id })
.then(
result => ({ code: result.css, map: result.map.toString() }),
error => {
throw error
}
)
}

function escapeRegExp (str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
}
Expand Down Expand Up @@ -70,6 +111,18 @@ export default async function (promise, options) {
}).catch(error => debug(error))
}

if (style.scoped === true) {
return compileScopedCSS(code, map, style, options).then(compiled => {
if (style.$compiled) {
compiled.$prev = style.$compiled
}

style.$compiled = compiled

return style
})
}

const output = { code, map, lang: 'css' }

if (style.$compiled) output.$prev = style.$compiled
Expand Down
48 changes: 38 additions & 10 deletions src/vueTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import templateProcessor from './template/index'
import { relative } from 'path'
import MagicString from 'magic-string'
import debug from './debug'
import { injectModule, injectTemplate, injectRender } from './injections'
import { injectModule, injectScopeID, injectTemplate, injectRender } from './injections'
import genScopeID from './gen-scope-id'

function getNodeAttrs (node) {
if (node.attrs) {
Expand Down Expand Up @@ -65,7 +66,7 @@ async function processTemplate (source, id, content, options, nodes, modules) {
return htmlMinifier.minify(template, options.htmlMinifier)
}

async function processScript (source, id, content, options, nodes, modules) {
async function processScript (source, id, content, options, nodes, modules, scoped) {
const template = await processTemplate(nodes.template[0], id, content, options, nodes, modules)

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

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

script = processScriptForStyle(script, modules, scoped, lang, id, options)

script = await processScriptForRender(script, template, lang, id, options)

return { map, code: script }
}

function processScriptForStyle (script, modules, scoped, lang, id, options) {
script = injectModule(script, modules, lang, id, options)

if (scoped) {
const scopeID = genScopeID(id)
script = injectScopeID(script, scopeID, lang, id, options)
}

return script
}

async function processScriptForRender (script, template, lang, id, options) {
if (template && options.compileTemplate) {
const render = require('vue-template-compiler').compile(template, options.compileOptions)

return { map, code: await injectRender(scriptWithModules, render, lang, id, options) }
} else if (template) {
return { map, code: await injectTemplate(scriptWithModules, template, lang, id, options) }
} else {
return { map, code: scriptWithModules }
return await injectRender(script, render, lang, id, options)
}

if (template) {
return await injectTemplate(script, template, lang, id, options)
}

return script
}

// eslint-disable-next-line complexity
Expand Down Expand Up @@ -173,11 +194,18 @@ const getModules = function (styles) {
return all
}

const hasScoped = function (styles) {
return styles.reduce((scoped, style) => {
return scoped || style.scoped
}, false)
}

export default async function vueTransform (code, id, options) {
const nodes = parseTemplate(code)
const css = await processStyle(nodes.style, id, code, options, nodes)
const modules = getModules(css)
const js = await processScript(nodes.script[0], id, code, options, nodes, modules)
const scoped = hasScoped(css)
const js = await processScript(nodes.script[0], id, code, options, nodes, modules, scoped)

const isProduction = process.env.NODE_ENV === 'production'
const isWithStripped = options.stripWith !== false
Expand Down
3 changes: 3 additions & 0 deletions test/expects/scoped-css.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.test[data-v-4f57af4d] {
color: red;
}
3 changes: 3 additions & 0 deletions test/expects/scoped-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var scopedCss = { template: "<div class=\"test\">Foo</div>",_scopeId: 'data-v-4f57af4d',};

export default scopedCss;
13 changes: 13 additions & 0 deletions test/fixtures/scoped-css.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<div class="test">Foo</div>
</template>

<script lang="babel">
export default {}
</script>

<style lang="css" scoped>
.test {
color: red;
}
</style>
2 changes: 1 addition & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function test(name) {
assert.equal(code.trim(), expected.trim(), 'should compile code correctly')

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