Repository: vuejs/vue-loader
Pull request #321 (2016-08-23 09:29:32)

Update documentation link anchor to deploying for production


Patch:
diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md
index 1c4d10f..c8ed4b7 100644
--- a/docs/en/SUMMARY.md
+++ b/docs/en/SUMMARY.md
@@ -4,6 +4,7 @@
 - Features
   - [ES2015 and Babel](features/es2015.md)
   - [Scoped CSS](features/scoped-css.md)
+  - [CSS Modules](features/css-modules.md)
   - [PostCSS and Autoprefixer](features/postcss.md)
   - [Hot Reload](features/hot-reload.md)
 - Configurations
diff --git a/docs/en/features/css-modules.md b/docs/en/features/css-modules.md
new file mode 100644
index 0000000..b69b742
--- /dev/null
+++ b/docs/en/features/css-modules.md
@@ -0,0 +1,56 @@
+# CSS Modules
+
+[CSS Modules](https://github.com/css-modules/css-modules) aims to solve class & animation name conflicts. It replaces all the local names with unique hashes and provides a name-to-hash map. So you can write short and general names without worrying any conflict!
+
+With vue-loader, you can simply use CSS Modules with `<style module>`.
+
+The name-to-hash map `$style` will be injected as a computed property.
+
+Example:
+
+```html
+<style module>
+  .red { color: red; }
+  /*
+    becomes
+    ._8x_KsHmyrocTNd7akA_LL { color: red; }
+  */
+</style>
+
+<template>
+  <h2 v-bind:class="$style.red"></h2>
+</template>
+
+<script>
+  export default {
+    ready() {
+      console.log(this.$style.red)
+      // => _8x_KsHmyrocTNd7akA_LL
+    }
+  }
+</script>
+```
+
+If you need mutiple `<style>` tags with `module` (or you hate `$style` being injected), you can specify the module name with `<style module="moduleName">`. `moduleName` will get injected instead.
+
+Example:
+
+```html
+<style module="foo" src="..."></style>
+<style module="bar" src="..."></style>
+
+<template>
+  <h2 v-bind:class="foo.red"></h2>
+  <h2 v-bind:class="bar.red"></h2>
+</template>
+```
+
+## Tips
+
+1. Animation names also get transformed. So, it's recommended to use animations with CSS modules.
+
+2. You can use `scoped` and `module` together to avoid problems in descendant selectors.
+
+3. Use `module` only (without `scoped`), you are able to style `<slot>`s and children components. But styling children components breaks the principle of components. You can put `<slot>` in a classed wrapper and style it under that class.
+
+4. You can expose the class name of component's root element for theming.
diff --git a/docs/en/features/scoped-css.md b/docs/en/features/scoped-css.md
index 4a1f197..f2bb9d1 100644
--- a/docs/en/features/scoped-css.md
+++ b/docs/en/features/scoped-css.md
@@ -48,4 +48,4 @@ Into the following:
 
 4. **Scoped styles do not eliminate the need for classes**. Due to the way browsers render various CSS selectors, `p { color: red }` will be many times slower when scoped (i.e. when combined with an attribute selector). If you use classes or ids instead, such as in `.example { color: red }`, then you virtually eliminate that performance hit. [Here's a playground](http://stevesouders.com/efws/css-selectors/csscreate.php) where you can test the differences yourself.
 
-5. **Be careful with descendant selectors in recursive components!** For a CSS rule with the selector `.a .b`, if the element that matches `.a` contains a recursive child component, then all `.b` in that child component will be matched by the rule.
+5. **Be careful with descendant selectors in recursive components!** For a CSS rule with the selector `.a .b`, if the element that matches `.a` contains a recursive child component, then all `.b` in that child component will be matched by the rule. To avoid class name conflicts, you can use [CSS Modules](features/css-modules.md).
diff --git a/docs/en/workflow/production.md b/docs/en/workflow/production.md
index fe25b55..ece6eda 100644
--- a/docs/en/workflow/production.md
+++ b/docs/en/workflow/production.md
@@ -3,7 +3,7 @@
 There are two things to do when building our bundle for production:
 
 1. Minify our application code;
-2. Use the [setup described in the Vue.js guide](http://vuejs.org/guide/application.html#Deploying_for_Production) to strip all the warnings from Vue.js source code.
+2. Use the [setup described in the Vue.js guide](http://vuejs.org/guide/application.html#Deploying-for-Production) to strip all the warnings from Vue.js source code.
 
 Here's an example config:
 
diff --git a/lib/loader.js b/lib/loader.js
index f6e479a..b41a09a 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -64,7 +64,7 @@ module.exports = function (content) {
       // disable all configuration loaders
       '!!' +
       // get loader string for pre-processors
-      getLoaderString(type, part, scoped) +
+      getLoaderString(type, part, index, scoped) +
       // select the corresponding part from the vue file
       getSelectorString(type, index || 0) +
       // the url to the actual vuefile
@@ -81,17 +81,40 @@ module.exports = function (content) {
   function getRequireForImportString (type, impt, scoped) {
     return loaderUtils.stringifyRequest(loaderContext,
       '!!' +
-      getLoaderString(type, impt, scoped) +
+      getLoaderString(type, impt, -1, scoped) +
       impt.src
     )
   }
 
-  function getLoaderString (type, part, scoped) {
+  function addCssModulesToLoader (loader, part, index) {
+    if (!part.module) return loader
+    return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
+      // $1: !css-loader
+      // $2: ?a=b
+      var option = loaderUtils.parseQuery($2)
+      option.modules = true
+      option.importLoaders = true
+      option.localIdentName = '[hash:base64]'
+      if (index !== -1) {
+        // Note:
+        //   Class name is generated according to its filename.
+        //   Different <style> tags in the same .vue file may generate same names.
+        //   Append `_[index]` to class name to avoid this.
+        option.localIdentName += '_' + index
+      }
+      return $1 + '?' + JSON.stringify(option)
+    })
+  }
+
+  function getLoaderString (type, part, index, scoped) {
     var lang = part.lang || defaultLang[type]
     var loader = loaders[lang]
     var rewriter = getRewriter(type, scoped)
     var injectString = (type === 'script' && query.inject) ? 'inject!' : ''
     if (loader !== undefined) {
+      if (type === 'style') {
+        loader = addCssModulesToLoader(loader, part, index)
+      }
       // inject rewriter before css/html loader for
       // extractTextPlugin use cases
       if (rewriterInjectRE.test(loader)) {
@@ -108,7 +131,8 @@ module.exports = function (content) {
         case 'template':
           return defaultLoaders.html + '!' + rewriter + templateLoader + '?raw&engine=' + lang + '!'
         case 'style':
-          return defaultLoaders.css + '!' + rewriter + lang + '!'
+          loader = addCssModulesToLoader(defaultLoaders.css, part, index)
+          return loader + '!' + rewriter + lang + '!'
         case 'script':
           return injectString + lang + '!'
       }
@@ -143,7 +167,8 @@ module.exports = function (content) {
 
   var parts = parse(content, fileName, this.sourceMap)
   var hasLocalStyles = false
-  var output = 'var __vue_script__, __vue_template__\n'
+  var output = 'var __vue_script__, __vue_template__\n' +
+               'var __vue_styles__ = {}\n'
 
   // check if there are any template syntax errors
   var templateWarnings = parts.template.length && parts.template[0].warnings
@@ -151,16 +176,31 @@ module.exports = function (content) {
     templateWarnings.forEach(this.emitError)
   }
 
+  var cssModules = {}
+  function setCssModule (style, require) {
+    if (!style.module) return require
+    if (style.module in cssModules) {
+      loaderContext.emitError('CSS module name "' + style.module + '" is not unique!')
+      return require
+    }
+    cssModules[style.module] = true
+    return '__vue_styles__["' + style.module + '"] = ' + require + '\n'
+  }
+
   // add requires for src imports
   parts.styleImports.forEach(function (impt) {
     if (impt.scoped) hasLocalStyles = true
-    output += getRequireForImport('style', impt, impt.scoped)
+    if (impt.module === '') impt.module = '$style'
+    var requireString = getRequireForImport('style', impt, impt.scoped, impt.module)
+    output += setCssModule(impt, requireString)
   })
 
   // add requires for styles
   parts.style.forEach(function (style, i) {
     if (style.scoped) hasLocalStyles = true
-    output += getRequire('style', style, i, style.scoped)
+    if (style.module === '') style.module = '$style'
+    var requireString = getRequire('style', style, i, style.scoped, style.module)
+    output += setCssModule(style, requireString)
   })
 
   // add require for script
@@ -203,11 +243,18 @@ module.exports = function (content) {
     output +=
       'module.exports = __vue_script__ || {}\n' +
       'if (module.exports.__esModule) module.exports = module.exports.default\n' +
+      'var __vue_options__ = typeof module.exports === "function" ' +
+        '? (module.exports.options || (module.exports.options = {})) ' +
+        ': module.exports\n' +
       'if (__vue_template__) {\n' +
-        '(typeof module.exports === "function" ' +
-          '? (module.exports.options || (module.exports.options = {})) ' +
-          ': module.exports).template = __vue_template__\n' +
-      '}\n'
+        '__vue_options__.template = __vue_template__\n' +
+      '}\n' +
+      // inject style modules as computed properties
+      'if (!__vue_options__.computed) __vue_options__.computed = {}\n' +
+      'Object.keys(__vue_styles__).forEach(function (key) {\n' +
+        'var module = __vue_styles__[key]\n' +
+        '__vue_options__.computed[key] = function () { return module }\n' +
+      '})\n'
     // hot reload
     if (
       !this.minimize &&
diff --git a/lib/parser.js b/lib/parser.js
index a353635..03d4514 100644
--- a/lib/parser.js
+++ b/lib/parser.js
@@ -40,6 +40,7 @@ module.exports = function (content, filename, needMap) {
     var lang = getAttribute(node, 'lang')
     var src = getAttribute(node, 'src')
     var scoped = getAttribute(node, 'scoped') != null
+    var module = getAttribute(node, 'module')
     var warnings = null
     var map = null
 
@@ -64,7 +65,8 @@ module.exports = function (content, filename, needMap) {
         output.styleImports.push({
           src: src,
           lang: lang,
-          scoped: scoped
+          scoped: scoped,
+          module: module
         })
       } else if (type === 'template') {
         output.template.push({
@@ -151,6 +153,7 @@ module.exports = function (content, filename, needMap) {
     output[type].push({
       lang: lang,
       scoped: scoped,
+      module: module,
       content: result,
       map: map && map.toJSON(),
       warnings: warnings


Test patch:
diff --git a/test/fixtures/css-modules.vue b/test/fixtures/css-modules.vue
new file mode 100644
index 0000000..8646cb8
--- /dev/null
+++ b/test/fixtures/css-modules.vue
@@ -0,0 +1,20 @@
+<style module="style">
+.red {
+  color: red;
+}
+@keyframes fade {
+  from { opacity: 1; } to { opacity: 0; }
+}
+.animate {
+  animation: fade 1s;
+}
+</style>
+
+<style scoped lang="stylus" module>
+.red
+  color: red
+</style>
+
+<script>
+module.exports = {}
+</script>
diff --git a/test/test.js b/test/test.js
index 8644b3e..2abbc05 100644
--- a/test/test.js
+++ b/test/test.js
@@ -34,7 +34,7 @@ describe('vue-loader', function () {
 
   function getFile (file, cb) {
     fs.readFile(path.resolve(outputDir, file), 'utf-8', function (err, data) {
-      expect(err).to.be.not.exist
+      expect(err).to.not.exist
       cb(data)
     })
   }
@@ -276,4 +276,37 @@ describe('vue-loader', function () {
       done()
     })
   })
+
+  it('css-modules', function (done) {
+    test({
+      entry: './test/fixtures/css-modules.vue'
+    }, function (window) {
+      var module = window.vueModule
+
+      // get local class name
+      var className = module.computed.style().red
+      expect(className).to.match(/^_/)
+
+      // class name in style
+      var style = [].slice.call(window.document.querySelectorAll('style')).map(function (style) {
+        return style.textContent
+      }).join('\n')
+      expect(style).to.contain('.' + className + ' {\n  color: red;\n}')
+
+      // animation name
+      var match = style.match(/@keyframes\s+(\S+)\s+{/)
+      expect(match).to.have.length(2)
+      var animationName = match[1]
+      expect(animationName).to.not.equal('fade')
+      expect(style).to.contain('animation: ' + animationName + ' 1s;')
+
+      // default module + pre-processor + scoped
+      var anotherClassName = module.computed.$style().red
+      expect(anotherClassName).to.match(/^_/).and.not.equal(className)
+      var id = '_v-' + hash(require.resolve('./fixtures/css-modules.vue'))
+      expect(style).to.contain('.' + anotherClassName + '[' + id + ']')
+
+      done()
+    })
+  })
 })