From 8502e81771d425296a7008586c4a159c74393186 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:29:38 +0900 Subject: [PATCH 01/10] Use hash-sum and postcss-selector-parser for scoped css --- config/build.js | 2 ++ package.json | 2 ++ yarn.lock | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) 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/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/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" From 5e6fac0733c3394c21983c7bec6eca5524578788 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:31:23 +0900 Subject: [PATCH 02/10] Transform css to add scope id --- src/gen-scope-id.js | 11 ++++++++++ src/style/css.js | 53 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/gen-scope-id.js diff --git a/src/gen-scope-id.js b/src/gen-scope-id.js new file mode 100644 index 0000000..c925825 --- /dev/null +++ b/src/gen-scope-id.js @@ -0,0 +1,11 @@ +// utility for generating a uid for each component file +// used in scoped CSS rewriting +import hash from 'hash-sum' +const cache = Object.create(null) + +export default function genScopeID (file) { + if (!cache[file]) { + cache[file] = 'data-v-' + hash(file) + } + return cache[file] +} diff --git a/src/style/css.js b/src/style/css.js index 5492048..1e9b2ec 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 From 945f9dd35bc91fd5f1abcf629346a2106d58ce1d Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:32:26 +0900 Subject: [PATCH 03/10] Inject scope id component options object --- src/injections.js | 23 +++++++++++++++++++++++ src/options.js | 7 ++++++- src/vueTransform.js | 29 +++++++++++++++++++++-------- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/injections.js b/src/injections.js index 18bfda3..2ca00a8 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/vueTransform.js b/src/vueTransform.js index 86ad2e7..e2d4623 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,18 +80,23 @@ 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 = injectModule(script, modules, lang, id, options) + + if (scoped) { + const scopeID = genScopeID(id) + script = injectScopeID(script, scopeID, 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) } + return { map, code: await injectRender(script, render, lang, id, options) } } else if (template) { - return { map, code: await injectTemplate(scriptWithModules, template, lang, id, options) } + return { map, code: await injectTemplate(script, template, lang, id, options) } } else { - return { map, code: scriptWithModules } + return { map, code: script } } } @@ -173,11 +179,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 From 92a3c266f84d6b4ebae252f7748b1b8e09d29ca9 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:32:50 +0900 Subject: [PATCH 04/10] Add test for scoped css --- test/expects/scoped-css.css | 3 +++ test/expects/scoped-css.js | 3 +++ test/fixtures/scoped-css.vue | 13 +++++++++++++ test/test.js | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 test/expects/scoped-css.css create mode 100644 test/expects/scoped-css.js create mode 100644 test/fixtures/scoped-css.vue diff --git a/test/expects/scoped-css.css b/test/expects/scoped-css.css new file mode 100644 index 0000000..5f106d4 --- /dev/null +++ b/test/expects/scoped-css.css @@ -0,0 +1,3 @@ +.test[data-v-6e7113ce] { + color: red; +} diff --git a/test/expects/scoped-css.js b/test/expects/scoped-css.js new file mode 100644 index 0000000..c10ef40 --- /dev/null +++ b/test/expects/scoped-css.js @@ -0,0 +1,3 @@ +var scopedCss = { template: "
Foo
",_scopeId: 'data-v-6e7113ce'}; + +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) { From d1e42069998dc4ab019d3f7fa6375cc25789540c Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:34:37 +0900 Subject: [PATCH 05/10] Fix lint errors --- src/gen-scope-id.js | 8 ++++---- src/style/css.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gen-scope-id.js b/src/gen-scope-id.js index c925825..c9ce98c 100644 --- a/src/gen-scope-id.js +++ b/src/gen-scope-id.js @@ -4,8 +4,8 @@ import hash from 'hash-sum' const cache = Object.create(null) export default function genScopeID (file) { - if (!cache[file]) { - cache[file] = 'data-v-' + hash(file) - } - return cache[file] + if (!cache[file]) { + cache[file] = 'data-v-' + hash(file) + } + return cache[file] } diff --git a/src/style/css.js b/src/style/css.js index 1e9b2ec..89f7540 100644 --- a/src/style/css.js +++ b/src/style/css.js @@ -7,7 +7,7 @@ import genScopeID from '../gen-scope-id' import debug from '../debug' const addScopeID = postcss.plugin('add-scope-id', options => { - const selectorTransformer = selectorParser(selectors => { + const selectorTransformer = selectorParser(selectors => { selectors.each(selector => { let target = null selector.each(n => { From 51b12eba0243fabbc4906794787b6e45f18d973f Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 00:42:13 +0900 Subject: [PATCH 06/10] Split processScript function to reduce complexity --- src/vueTransform.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/vueTransform.js b/src/vueTransform.js index e2d4623..e58c826 100644 --- a/src/vueTransform.js +++ b/src/vueTransform.js @@ -82,6 +82,15 @@ async function processScript (source, id, content, options, nodes, modules, scop let script = deIndent(padContent(content.slice(0, content.indexOf(source.code))) + source.code) const map = (new MagicString(script)).generateMap({ hires: true }) + + 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) { @@ -89,15 +98,21 @@ async function processScript (source, id, content, options, nodes, modules, scop 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(script, render, lang, id, options) } - } else if (template) { - return { map, code: await injectTemplate(script, template, lang, id, options) } - } else { - return { map, code: script } + 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 From 1236c316ee70a103aad719fcc18bbea802bfc98f Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 2 May 2017 09:19:48 +0900 Subject: [PATCH 07/10] Unify scope id among various environment --- src/gen-scope-id.js | 10 +++++++--- test/expects/scoped-css.css | 2 +- test/expects/scoped-css.js | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/gen-scope-id.js b/src/gen-scope-id.js index c9ce98c..eb3a31c 100644 --- a/src/gen-scope-id.js +++ b/src/gen-scope-id.js @@ -1,11 +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) { - if (!cache[file]) { - cache[file] = 'data-v-' + hash(file) + const modified = path.relative(process.cwd(), file) + + if (!cache[modified]) { + cache[modified] = 'data-v-' + hash(modified) } - return cache[file] + + return cache[modified] } diff --git a/test/expects/scoped-css.css b/test/expects/scoped-css.css index 5f106d4..2cc2645 100644 --- a/test/expects/scoped-css.css +++ b/test/expects/scoped-css.css @@ -1,3 +1,3 @@ -.test[data-v-6e7113ce] { +.test[data-v-4f57af4d] { color: red; } diff --git a/test/expects/scoped-css.js b/test/expects/scoped-css.js index c10ef40..8b53e48 100644 --- a/test/expects/scoped-css.js +++ b/test/expects/scoped-css.js @@ -1,3 +1,3 @@ -var scopedCss = { template: "
Foo
",_scopeId: 'data-v-6e7113ce'}; +var scopedCss = { template: "
Foo
",_scopeId: 'data-v-4f57af4d'}; export default scopedCss; From f77cbc28498e238d869c7bf67e15671e3a9db681 Mon Sep 17 00:00:00 2001 From: ktsn Date: Wed, 3 May 2017 14:13:20 +0900 Subject: [PATCH 08/10] Comma is missing --- src/injections.js | 2 +- test/expects/scoped-css.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/injections.js b/src/injections.js index 2ca00a8..a29ba77 100644 --- a/src/injections.js +++ b/src/injections.js @@ -75,7 +75,7 @@ export function scopeJs (script, scopeID, lang, id, options) { const matches = findInjectionPosition(script) if (matches && matches.length) { - const scopeScript = `${matches[1]}_scopeId: '${scopeID}'` + const scopeScript = `${matches[1]}_scopeId: '${scopeID}',` return script.split(matches[1]).join(scopeScript) } diff --git a/test/expects/scoped-css.js b/test/expects/scoped-css.js index 8b53e48..d5618f7 100644 --- a/test/expects/scoped-css.js +++ b/test/expects/scoped-css.js @@ -1,3 +1,3 @@ -var scopedCss = { template: "
Foo
",_scopeId: 'data-v-4f57af4d'}; +var scopedCss = { template: "
Foo
",_scopeId: 'data-v-4f57af4d',}; export default scopedCss; From 7de3e9ae94a04c8a7e53cd9930c4a20155ff2724 Mon Sep 17 00:00:00 2001 From: ktsn Date: Wed, 3 May 2017 14:19:10 +0900 Subject: [PATCH 09/10] Update Scoped CSS docs --- docs/en/2.3/README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/en/2.3/README.md b/docs/en/2.3/README.md index f1b1cfb..9008a01 100644 --- a/docs/en/2.3/README.md +++ b/docs/en/2.3/README.md @@ -195,6 +195,39 @@ You can provide `postcss-modules` configuration options by setting: cssModules: { generateScopedName: '[name]__[local]', ... } ``` +#### Scoped CSS +

+`rollup-plugin-vue@^2.3` does not support Scoped CSS yet. +

+ +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 ` +``` + +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 From 39f0b6c834197a01cf553f293bad5f80bfabd4d3 Mon Sep 17 00:00:00 2001 From: Rahul Kadyan Date: Wed, 3 May 2017 12:26:39 +0530 Subject: [PATCH 10/10] Change warning tips for scoped styles --- docs/en/2.3/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/2.3/README.md b/docs/en/2.3/README.md index 9008a01..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 `