diff --git a/config/build.js b/config/build.js index 4aefa7c..c930f5b 100644 --- a/config/build.js +++ b/config/build.js @@ -30,6 +30,7 @@ rollup.rollup({ 'de-indent', 'debug', 'fs', + 'hash-sum', 'html-minifier', 'less', 'magic-string', @@ -39,6 +40,7 @@ rollup.rollup({ 'path', 'postcss', 'postcss-modules', + 'postcss-selector-parser', 'posthtml', 'posthtml-attrs-parser', 'pug', diff --git a/docs/en/2.3/README.md b/docs/en/2.3/README.md index f1b1cfb..bf35c54 100644 --- a/docs/en/2.3/README.md +++ b/docs/en/2.3/README.md @@ -50,7 +50,7 @@ The `css` option accepts style handling options. - `id: String` - Path of the `.vue` file. - `lang: String` - Language defined on ` +``` + +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 diff --git a/package.json b/package.json index 00dc0bc..424d52e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/gen-scope-id.js b/src/gen-scope-id.js new file mode 100644 index 0000000..eb3a31c --- /dev/null +++ b/src/gen-scope-id.js @@ -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] +} diff --git a/src/injections.js b/src/injections.js index 18bfda3..a29ba77 100644 --- a/src/injections.js +++ b/src/injections.js @@ -71,6 +71,19 @@ export function moduleJs (script, modules, lang, id, options) { `[rollup-plugin-vue] CSS modules are injected in the default export of .vue file (lang: ${lang}). In ${id}, it cannot find 'export defaults'.` ) } +export function scopeJs (script, scopeID, lang, id, options) { + const matches = findInjectionPosition(script) + + if (matches && matches.length) { + const scopeScript = `${matches[1]}_scopeId: '${scopeID}',` + + return script.split(matches[1]).join(scopeScript) + } + + throw new Error( + `[rollup-plugin-vue] Scope ID is injected in the default export of .vue file (lang: ${lang}). In ${id}, it cannot find 'export defaults'.` + ) +} export function injectTemplate (script, template, lang, id, options) { if (lang in options.inject.template) { @@ -100,3 +113,13 @@ export function injectModule (script, modules, lang, id, options) { `[rollup-plugin-vue] CSS modules are injected in the default export of .vue file. In ${id}, it cannot find 'export defaults'.` ) } + +export function injectScopeID (script, scopeID, lang, id, options) { + if (lang in options.inject.scoped) { + return options.inject.scoped[lang](script, scopeID, lang, id, options) + } + + throw new Error( + `[rollup-plugin-vue] Scope ID is injected in the default export of .vue file. In ${id}, it cannot find 'export defaults'.` + ) +} diff --git a/src/options.js b/src/options.js index 8704691..b7aba4f 100644 --- a/src/options.js +++ b/src/options.js @@ -1,4 +1,4 @@ -import { templateJs, moduleJs, renderJs } from './injections' +import { templateJs, moduleJs, scopeJs, renderJs } from './injections' import { coffee } from './script/index' export default { @@ -80,6 +80,11 @@ export default { module: { js: moduleJs, babel: moduleJs + }, + + scoped: { + js: scopeJs, + babel: scopeJs } }, diff --git a/src/style/css.js b/src/style/css.js index 5492048..89f7540 100644 --- a/src/style/css.js +++ b/src/style/css.js @@ -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}`) @@ -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, '\\$&') } @@ -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 diff --git a/src/vueTransform.js b/src/vueTransform.js index 86ad2e7..e58c826 100644 --- a/src/vueTransform.js +++ b/src/vueTransform.js @@ -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) { @@ -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}`) @@ -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 @@ -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 diff --git a/test/expects/scoped-css.css b/test/expects/scoped-css.css new file mode 100644 index 0000000..2cc2645 --- /dev/null +++ b/test/expects/scoped-css.css @@ -0,0 +1,3 @@ +.test[data-v-4f57af4d] { + color: red; +} diff --git a/test/expects/scoped-css.js b/test/expects/scoped-css.js new file mode 100644 index 0000000..d5618f7 --- /dev/null +++ b/test/expects/scoped-css.js @@ -0,0 +1,3 @@ +var scopedCss = { template: "
Foo
",_scopeId: 'data-v-4f57af4d',}; + +export default scopedCss; diff --git a/test/fixtures/scoped-css.vue b/test/fixtures/scoped-css.vue new file mode 100644 index 0000000..9c058da --- /dev/null +++ b/test/fixtures/scoped-css.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/test/test.js b/test/test.js index 71610df..f4d7203 100644 --- a/test/test.js +++ b/test/test.js @@ -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) { diff --git a/yarn.lock b/yarn.lock index 22ad619..853f0a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1408,6 +1408,10 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -1606,6 +1610,10 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -1692,6 +1700,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2534,6 +2546,14 @@ postcss-modules@^0.6.4: postcss "^5.2.8" string-hash "^1.1.1" +postcss-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss@5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.1.2.tgz#bd84886a66bcad489afaf7c673eed5ef639551e2" @@ -3236,6 +3256,10 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + upper-case@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"