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: "