Repository: vuejs/vue-loader
Pull request #697 (2017-03-07 23:33:55)

[feature]About the hard-coded list of excluded loaders chained before vue-loader
here is the [issue](https://github.com/vuejs/vue-loader/issues/696)


Patch:
diff --git a/docs/en/configurations/advanced.md b/docs/en/configurations/advanced.md
index 5c48d64..17aa63d 100644
--- a/docs/en/configurations/advanced.md
+++ b/docs/en/configurations/advanced.md
@@ -48,7 +48,10 @@ module.exports = {
           //   plugin will be a better option.
           postLoaders: {
             html: 'babel-loader'
-          }
+          },
+          
+          // `excludedPreLoaders` should be regex
+          excludedPreLoaders: /(eslint-loader)/
         }
       }
     ]
diff --git a/docs/en/configurations/custom-blocks.md b/docs/en/configurations/custom-blocks.md
index 2066b59..ce833e0 100644
--- a/docs/en/configurations/custom-blocks.md
+++ b/docs/en/configurations/custom-blocks.md
@@ -57,11 +57,11 @@ module.exports = {
           }
         }
       }
-    ],
-    plugins: [
-      // output all docs into a single file
-      new ExtractTextPlugin('docs.md')
     ]
-  }
+  },
+  plugins: [
+    // output all docs into a single file
+    new ExtractTextPlugin('docs.md')
+  ]
 }
 ```
diff --git a/docs/pt_BR/configurations/custom-blocks.md b/docs/pt_BR/configurations/custom-blocks.md
index 518a419..f3a01db 100644
--- a/docs/pt_BR/configurations/custom-blocks.md
+++ b/docs/pt_BR/configurations/custom-blocks.md
@@ -57,11 +57,11 @@ module.exports = {
           }
         }
       }
-    ],
-    plugins: [
-      // saída de todos os docs em um único arquivo
-      new ExtractTextPlugin('docs.md')
     ]
-  }
+  },
+  plugins: [
+    // saída de todos os docs em um único arquivo
+    new ExtractTextPlugin('docs.md')
+  ]
 }
 ```
diff --git a/docs/ru/configurations/custom-blocks.md b/docs/ru/configurations/custom-blocks.md
index aea3c64..31e3a55 100644
--- a/docs/ru/configurations/custom-blocks.md
+++ b/docs/ru/configurations/custom-blocks.md
@@ -57,11 +57,11 @@ module.exports = {
           }
         }
       }
-    ],
-    plugins: [
-      // вывести всю документацию в отдельный файл
-      new ExtractTextPlugin('docs.md')
     ]
-  }
+  },
+  plugins: [
+    // вывести всю документацию в отдельный файл
+    new ExtractTextPlugin('docs.md')
+  ]
 }
 ```
diff --git a/lib/component-normalizer.js b/lib/component-normalizer.js
index d1b8a19..2a72a28 100644
--- a/lib/component-normalizer.js
+++ b/lib/component-normalizer.js
@@ -1,3 +1,6 @@
+// this module is a runtime utility for cleaner component module output and will
+// be included in the final webpack user bundle
+
 module.exports = function normalizeComponent (
   rawScriptExports,
   compiledTemplate,
diff --git a/lib/loader.js b/lib/loader.js
index 7778a38..006e658 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -1,15 +1,15 @@
 var path = require('path')
 var parse = require('./parser')
-var genId = require('./gen-id')
-var normalize = require('./normalize')
+var genId = require('./utils/gen-id')
+var normalize = require('./utils/normalize')
 var loaderUtils = require('loader-utils')
 var querystring = require('querystring')
 
 // internal lib loaders
 var selectorPath = normalize.lib('selector')
-var styleRewriterPath = normalize.lib('style-rewriter')
-var templateLoaderPath = normalize.lib('template-loader')
-var templateCompilerPath = normalize.lib('template-compiler')
+var styleCompilerPath = normalize.lib('style-compiler/index')
+var templateCompilerPath = normalize.lib('template-compiler/index')
+var templatePreprocessorPath = normalize.lib('template-compiler/preprocessor')
 var componentNormalizerPath = normalize.lib('component-normalizer')
 
 // dep loaders
@@ -36,15 +36,13 @@ var defaultLang = {
 
 // When extracting parts from the source vue file, we want to apply the
 // loaders chained before vue-loader, but exclude some loaders that simply
-// produces side effects such as linting. This is a hard-coded list and
-// hopefully eslint-loader is the only one.
-var excludedPreLoadersRE = /eslint-loader/
-
-function getRawRequest (context) {
+// produces side effects such as linting.
+function getRawRequest (context, excludedPreLoaders) {
+  excludedPreLoaders = excludedPreLoaders || /eslint-loader/
   return loaderUtils.getRemainingRequest({
     resource: context.resource,
     loaderIndex: context.loaderIndex,
-    loaders: context.loaders.filter(loader => !excludedPreLoadersRE.test(loader.path))
+    loaders: context.loaders.filter(loader => !excludedPreLoaders.test(loader.path))
   })
 }
 
@@ -55,8 +53,8 @@ module.exports = function (content) {
 
   var loaderContext = this
   var query = loaderUtils.getOptions(this) || {}
-  var rawRequest = getRawRequest(this)
   var options = this.options.__vueOptions__ = Object.assign({}, this.options.vue, this.vue, query)
+  var rawRequest = getRawRequest(this, options.excludedPreLoaders)
 
   var filePath = this.resourcePath
   var fileName = path.basename(filePath)
@@ -80,7 +78,11 @@ module.exports = function (content) {
     id: moduleId,
     transformToRequire: options.transformToRequire,
     preserveWhitespace: options.preserveWhitespace,
-    buble: options.buble
+    buble: options.buble,
+    // only pass compilerModules if it's a path string
+    compilerModules: typeof options.compilerModules === 'string'
+      ? options.compilerModules
+      : undefined
   })
 
   var defaultLoaders = {
@@ -186,9 +188,9 @@ module.exports = function (content) {
     var lang = part.lang || defaultLang[type]
     var loader = loaders[lang]
 
-    var styleRewriter = ''
+    var styleCompiler = ''
     if (type === 'styles') {
-      styleRewriter = styleRewriterPath + '?' + JSON.stringify({
+      styleCompiler = styleCompilerPath + '?' + JSON.stringify({
         id: moduleId,
         scoped: !!scoped,
         hasInlineConfig: !!query.postcss
@@ -211,10 +213,10 @@ module.exports = function (content) {
         // inject rewriter before css loader for extractTextPlugin use cases
         if (rewriterInjectRE.test(loader)) {
           loader = loader.replace(rewriterInjectRE, function (m, $1) {
-            return ensureBang($1) + styleRewriter
+            return ensureBang($1) + styleCompiler
           })
         } else {
-          loader = ensureBang(loader) + styleRewriter
+          loader = ensureBang(loader) + styleCompiler
         }
       }
       // if user defines custom loaders for html, add template compiler to it
@@ -226,10 +228,10 @@ module.exports = function (content) {
       // unknown lang, infer the loader to be used
       switch (type) {
         case 'template':
-          return defaultLoaders.html + '!' + templateLoaderPath + '?raw&engine=' + lang + '!'
+          return defaultLoaders.html + '!' + templatePreprocessorPath + '?raw&engine=' + lang + '!'
         case 'styles':
           loader = addCssModulesToLoader(defaultLoaders.css, part, index)
-          return loader + '!' + styleRewriter + ensureBang(ensureLoader(lang))
+          return loader + '!' + styleCompiler + ensureBang(ensureLoader(lang))
         case 'script':
           return injectString + ensureBang(ensureLoader(lang))
         default:
diff --git a/lib/selector.js b/lib/selector.js
index 7779b8d..e2f8751 100644
--- a/lib/selector.js
+++ b/lib/selector.js
@@ -1,3 +1,7 @@
+// this is a utility loader that takes a *.vue file, parses it and returns
+// the requested language block, e.g. the content inside <template>, for
+// further processing.
+
 var path = require('path')
 var parse = require('./parser')
 var loaderUtils = require('loader-utils')
diff --git a/lib/style-compiler/index.js b/lib/style-compiler/index.js
new file mode 100644
index 0000000..a3d14b9
--- /dev/null
+++ b/lib/style-compiler/index.js
@@ -0,0 +1,68 @@
+var postcss = require('postcss')
+var loaderUtils = require('loader-utils')
+var loadPostcssConfig = require('./load-postcss-config')
+
+var trim = require('./plugins/trim')
+var scopeId = require('./plugins/scope-id')
+
+module.exports = function (css, map) {
+  this.cacheable()
+  var cb = this.async()
+
+  var query = loaderUtils.getOptions(this) || {}
+  var vueOptions = this.options.__vueOptions__
+
+  if (!vueOptions) {
+    if (query.hasInlineConfig) {
+      this.emitError(
+        `\n  [vue-loader] It seems you are using HappyPack with inline postcss ` +
+        `options for vue-loader. This is not supported because loaders running ` +
+        `in different threads cannot share non-serializable options. ` +
+        `It is recommended to use a postcss config file instead.\n` +
+        `\n  See http://vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details.\n`
+      )
+    }
+    vueOptions = Object.assign({}, this.options.vue, this.vue)
+  }
+
+  // use the same config loading interface as postcss-loader
+  loadPostcssConfig(vueOptions.postcss).then(({ plugins, options }) => {
+    plugins = [trim].concat(plugins)
+    options = Object.assign({
+      to: this.resourcePath,
+      from: this.resourcePath,
+      map: false
+    }, options)
+
+    // add plugin for vue-loader scoped css rewrite
+    if (query.scoped) {
+      plugins.push(scopeId({ id: query.id }))
+    }
+
+    // source map
+    if (
+      this.sourceMap &&
+      !this.minimize &&
+      vueOptions.cssSourceMap !== false &&
+      process.env.NODE_ENV !== 'production' &&
+      !options.map
+    ) {
+      options.map = {
+        inline: false,
+        annotation: false,
+        prev: map
+      }
+    }
+
+    return postcss(plugins)
+      .process(css, options)
+      .then(function (result) {
+        var map = result.map && result.map.toJSON()
+        cb(null, result.css, map)
+        return null // silence bluebird warning
+      })
+  }).catch(e => {
+    console.log(e)
+    cb(e)
+  })
+}
diff --git a/lib/style-compiler/load-postcss-config.js b/lib/style-compiler/load-postcss-config.js
new file mode 100644
index 0000000..95bac6a
--- /dev/null
+++ b/lib/style-compiler/load-postcss-config.js
@@ -0,0 +1,45 @@
+var load = require('postcss-load-config')
+
+let loaded
+
+function isObject (val) {
+  return val && typeof val === 'object'
+}
+
+module.exports = function loadPostcssConfig (inlineConfig) {
+  if (process.env.VUE_LOADER_TEST || !loaded) {
+    loaded = load({}, null, { argv: false }).catch(() => {
+      // postcss-load-config throws error when no config file is found,
+      // but for us it's optional.
+    })
+  }
+
+  return loaded.then(config => {
+    var plugins = []
+    var options = {}
+
+    // inline postcss options for vue-loader
+    if (typeof inlineConfig === 'function') {
+      inlineConfig = inlineConfig.call(this, this)
+    }
+    if (Array.isArray(inlineConfig)) {
+      plugins = inlineConfig
+    } else if (isObject(inlineConfig)) {
+      plugins = inlineConfig.plugins || []
+      options = inlineConfig.options || {}
+    }
+
+    // merge postcss config file
+    if (config && config.plugins) {
+      plugins = plugins.concat(config.plugins)
+    }
+    if (config && config.options) {
+      options = Object.assign({}, config.options, options)
+    }
+
+    return {
+      plugins,
+      options
+    }
+  })
+}
diff --git a/lib/style-compiler/plugins/scope-id.js b/lib/style-compiler/plugins/scope-id.js
new file mode 100644
index 0000000..dbcad9f
--- /dev/null
+++ b/lib/style-compiler/plugins/scope-id.js
@@ -0,0 +1,27 @@
+var postcss = require('postcss')
+var selectorParser = require('postcss-selector-parser')
+
+module.exports = postcss.plugin('add-id', function (opts) {
+  return function (root) {
+    root.each(function rewriteSelector (node) {
+      if (!node.selector) {
+        // handle media queries
+        if (node.type === 'atrule' && node.name === 'media') {
+          node.each(rewriteSelector)
+        }
+        return
+      }
+      node.selector = selectorParser(function (selectors) {
+        selectors.each(function (selector) {
+          var node = null
+          selector.each(function (n) {
+            if (n.type !== 'pseudo') node = n
+          })
+          selector.insertAfter(node, selectorParser.attribute({
+            attribute: opts.id
+          }))
+        })
+      }).process(node.selector).result
+    })
+  }
+})
diff --git a/lib/style-compiler/plugins/trim.js b/lib/style-compiler/plugins/trim.js
new file mode 100644
index 0000000..57163d8
--- /dev/null
+++ b/lib/style-compiler/plugins/trim.js
@@ -0,0 +1,11 @@
+var postcss = require('postcss')
+
+module.exports = postcss.plugin('trim', function (opts) {
+  return function (css) {
+    css.walk(function (node) {
+      if (node.type === 'rule' || node.type === 'atrule') {
+        node.raws.before = node.raws.after = '\n'
+      }
+    })
+  }
+})
diff --git a/lib/style-rewriter.js b/lib/style-rewriter.js
deleted file mode 100644
index 964ff85..0000000
--- a/lib/style-rewriter.js
+++ /dev/null
@@ -1,132 +0,0 @@
-var postcss = require('postcss')
-var loaderUtils = require('loader-utils')
-var loadPostcssConfig = require('postcss-load-config')
-var selectorParser = require('postcss-selector-parser')
-
-var addId = postcss.plugin('add-id', function (opts) {
-  return function (root) {
-    root.each(function rewriteSelector (node) {
-      if (!node.selector) {
-        // handle media queries
-        if (node.type === 'atrule' && node.name === 'media') {
-          node.each(rewriteSelector)
-        }
-        return
-      }
-      node.selector = selectorParser(function (selectors) {
-        selectors.each(function (selector) {
-          var node = null
-          selector.each(function (n) {
-            if (n.type !== 'pseudo') node = n
-          })
-          selector.insertAfter(node, selectorParser.attribute({
-            attribute: opts.id
-          }))
-        })
-      }).process(node.selector).result
-    })
-  }
-})
-
-var trim = postcss.plugin('trim', function (opts) {
-  return function (css) {
-    css.walk(function (node) {
-      if (node.type === 'rule' || node.type === 'atrule') {
-        node.raws.before = node.raws.after = '\n'
-      }
-    })
-  }
-})
-
-module.exports = function (css, map) {
-  this.cacheable()
-  var cb = this.async()
-
-  var query = loaderUtils.getOptions(this) || {}
-  var vueOptions = this.options.__vueOptions__
-
-  if (!vueOptions) {
-    if (query.hasInlineConfig) {
-      this.emitError(
-        `\n  [vue-loader] It seems you are using HappyPack with inline postcss ` +
-        `options for vue-loader. This is not supported because loaders running ` +
-        `in different threads cannot share non-serializable options. ` +
-        `It is recommended to use a postcss config file instead.\n` +
-        `\n  See http://vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details.\n`
-      )
-    }
-    vueOptions = Object.assign({}, this.options.vue, this.vue)
-  }
-
-  // use the same config loading interface as postcss-loader
-  loadPostcssConfig({ webpack: this }, null, { argv: false }).catch(() => {
-    // postcss-load-config throws error when no config file is found,
-    // but for us it's optional.
-  }).then(config => {
-    var plugins
-    var options
-
-    // inline postcss options for vue-loader
-    var rawInlineOptions = vueOptions.postcss
-    if (typeof rawInlineOptions === 'function') {
-      rawInlineOptions = rawInlineOptions.call(this, this)
-    }
-    if (Array.isArray(rawInlineOptions)) {
-      plugins = rawInlineOptions
-    } else if (isObject(rawInlineOptions)) {
-      plugins = rawInlineOptions.plugins
-      options = rawInlineOptions.options
-    }
-
-    plugins = [trim].concat(plugins || [])
-    options = Object.assign({
-      to: this.resourcePath,
-      from: this.resourcePath,
-      map: false
-    }, options)
-
-    // merge postcss config file
-    if (config && config.plugins) {
-      plugins = plugins.concat(config.plugins)
-    }
-    if (config && config.options) {
-      options = Object.assign({}, config.options, options)
-    }
-
-    // add plugin for vue-loader scoped css rewrite
-    if (query.scoped) {
-      plugins.push(addId({ id: query.id }))
-    }
-
-    // source map
-    if (
-      this.sourceMap &&
-      !this.minimize &&
-      vueOptions.cssSourceMap !== false &&
-      process.env.NODE_ENV !== 'production' &&
-      !options.map
-    ) {
-      options.map = {
-        inline: false,
-        annotation: false,
-        prev: map
-      }
-    }
-
-    postcss(plugins)
-      .process(css, options)
-      .then(function (result) {
-        var map = result.map && result.map.toJSON()
-        cb(null, result.css, map)
-        return null // silence bluebird warning
-      })
-      .catch(function (e) {
-        console.log(e)
-        cb(e)
-      })
-  })
-}
-
-function isObject (val) {
-  return val && typeof val === 'object'
-}
diff --git a/lib/template-compiler.js b/lib/template-compiler/index.js
similarity index 57%
rename from lib/template-compiler.js
rename to lib/template-compiler/index.js
index b9d0d92..e3b3092 100644
--- a/lib/template-compiler.js
+++ b/lib/template-compiler/index.js
@@ -1,67 +1,31 @@
-var compiler = require('vue-template-compiler')
-var transpile = require('vue-template-es2015-compiler')
 var loaderUtils = require('loader-utils')
+var normalize = require('../utils/normalize')
+var compiler = require('vue-template-compiler')
 var beautify = require('js-beautify').js_beautify
-var normalize = require('./normalize')
+var transpile = require('vue-template-es2015-compiler')
 var hotReloadAPIPath = normalize.dep('vue-hot-reload-api')
-
-// vue compiler module for using transforming `<tag>:<attribute>` to `require`
-var defaultTransformToRequire = {
-  img: 'src',
-  image: 'xlink:href'
-}
-var transformToRequire = defaultTransformToRequire
-var defaultCompileOptions = {
-  modules: [{
-    postTransformNode (el) {
-      for (var tag in transformToRequire) {
-        if (el.tag === tag && el.attrs) {
-          var attributes = transformToRequire[tag]
-          if (typeof attributes === 'string') {
-            el.attrs.some(attr => rewrite(attr, attributes))
-          } else if (Array.isArray(attributes)) {
-            attributes.forEach(item => el.attrs.some(attr => rewrite(attr, item)))
-          }
-        }
-      }
-    }
-  }]
-}
-
-function rewrite (attr, name) {
-  if (attr.name === name) {
-    var value = attr.value
-    var isStatic = value.charAt(0) === '"' && value.charAt(value.length - 1) === '"'
-    if (!isStatic) {
-      return
-    }
-    var firstChar = value.charAt(1)
-    if (firstChar === '.' || firstChar === '~') {
-      if (firstChar === '~') {
-        value = '"' + value.slice(2)
-      }
-      attr.value = `require(${value})`
-    }
-    return true
-  }
-}
+var transformRequire = require('./modules/transform-require')
 
 module.exports = function (html) {
   this.cacheable()
   var isServer = this.target === 'node'
   var isProduction = this.minimize || process.env.NODE_ENV === 'production'
+  var vueOptions = this.options.__vueOptions__ || {}
   var options = loaderUtils.getOptions(this) || {}
-  if (options.transformToRequire) {
-    transformToRequire = Object.assign(
-      {},
-      defaultTransformToRequire,
-      options.transformToRequire
-    )
+
+  var defaultModules = [transformRequire(options.transformToRequire)]
+  var userModules = vueOptions.compilerModules || options.compilerModules
+  // for HappyPack cross-process use cases
+  if (typeof userModules === 'string') {
+    userModules = require(userModules)
+  }
+
+  var compilerOptions = {
+    preserveWhitespace: options.preserveWhitespace,
+    modules: defaultModules.concat(userModules || [])
   }
 
-  var compiled = compiler.compile(html, Object.assign({
-    preserveWhitespace: options.preserveWhitespace
-  }, defaultCompileOptions))
+  var compiled = compiler.compile(html, compilerOptions)
 
   // tips
   if (compiled.tips && compiled.tips.length) {
@@ -102,6 +66,7 @@ module.exports = function (html) {
       '  }\n' +
       '}'
   }
+
   return code
 }
 
diff --git a/lib/template-compiler/modules/transform-require.js b/lib/template-compiler/modules/transform-require.js
new file mode 100644
index 0000000..d347670
--- /dev/null
+++ b/lib/template-compiler/modules/transform-require.js
@@ -0,0 +1,49 @@
+// vue compiler module for transforming `<tag>:<attribute>` to `require`
+
+var defaultOptions = {
+  img: 'src',
+  image: 'xlink:href'
+}
+
+module.exports = userOptions => {
+  var options = userOptions
+    ? Object.assign({}, defaultOptions, userOptions)
+    : defaultOptions
+
+  return {
+    postTransformNode: node => {
+      transform(node, options)
+    }
+  }
+}
+
+function transform (node, options) {
+  for (var tag in options) {
+    if (node.tag === tag && node.attrs) {
+      var attributes = options[tag]
+      if (typeof attributes === 'string') {
+        node.attrs.some(attr => rewrite(attr, attributes))
+      } else if (Array.isArray(attributes)) {
+        attributes.forEach(item => node.attrs.some(attr => rewrite(attr, item)))
+      }
+    }
+  }
+}
+
+function rewrite (attr, name) {
+  if (attr.name === name) {
+    var value = attr.value
+    var isStatic = value.charAt(0) === '"' && value.charAt(value.length - 1) === '"'
+    if (!isStatic) {
+      return
+    }
+    var firstChar = value.charAt(1)
+    if (firstChar === '.' || firstChar === '~') {
+      if (firstChar === '~') {
+        value = '"' + value.slice(2)
+      }
+      attr.value = `require(${value})`
+    }
+    return true
+  }
+}
diff --git a/lib/template-loader.js b/lib/template-compiler/preprocessor.js
similarity index 95%
rename from lib/template-loader.js
rename to lib/template-compiler/preprocessor.js
index e0690c8..860b099 100644
--- a/lib/template-loader.js
+++ b/lib/template-compiler/preprocessor.js
@@ -1,3 +1,5 @@
+// loader for pre-processing templates with e.g. pug
+
 var cons = require('consolidate')
 var loaderUtils = require('loader-utils')
 var extname = require('path').extname
diff --git a/lib/gen-id.js b/lib/utils/gen-id.js
similarity index 100%
rename from lib/gen-id.js
rename to lib/utils/gen-id.js
diff --git a/lib/normalize.js b/lib/utils/normalize.js
similarity index 70%
rename from lib/normalize.js
rename to lib/utils/normalize.js
index 4664486..dda06ce 100644
--- a/lib/normalize.js
+++ b/lib/utils/normalize.js
@@ -3,13 +3,13 @@ var fs = require('fs')
 var path = require('path')
 
 exports.lib = function (file) {
-  return path.resolve(__dirname, file)
+  return path.resolve(__dirname, '../', file)
 }
 
 exports.dep = function (dep) {
   if (IS_TEST) {
     return dep
-  } else if (fs.existsSync(path.resolve(__dirname, '../node_modules', dep))) {
+  } else if (fs.existsSync(path.resolve(__dirname, '../../node_modules', dep))) {
     // npm 2 or npm linked
     return 'vue-loader/node_modules/' + dep
   } else {
diff --git a/package.json b/package.json
index b09702a..71da958 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "vue-loader",
-  "version": "11.1.4",
+  "version": "11.2.0",
   "description": "Vue single-file component loader for Webpack",
   "main": "index.js",
   "repository": {
@@ -34,7 +34,7 @@
     "js-beautify": "^1.6.3",
     "loader-utils": "^1.0.2",
     "lru-cache": "^4.0.1",
-    "postcss": "^5.0.10",
+    "postcss": "^5.0.21",
     "postcss-load-config": "^1.1.0",
     "postcss-selector-parser": "^2.0.0",
     "source-map": "^0.5.6",
@@ -69,7 +69,6 @@
     "node-libs-browser": "^2.0.0",
     "normalize-newline": "^3.0.0",
     "null-loader": "^0.1.1",
-    "postcss": "^5.0.21",
     "pug": "^2.0.0-beta6",
     "raw-loader": "^0.5.1",
     "skeleton-loader": "0.0.5",


Test patch:
diff --git a/test/fixtures/custom-module.js b/test/fixtures/custom-module.js
new file mode 100644
index 0000000..ff11834
--- /dev/null
+++ b/test/fixtures/custom-module.js
@@ -0,0 +1,9 @@
+module.exports = [
+  {
+    postTransformNode: el => {
+      if (el.staticClass) {
+        el.staticClass = '"red blue"'
+      }
+    }
+  }
+]
diff --git a/test/fixtures/custom-module.vue b/test/fixtures/custom-module.vue
new file mode 100644
index 0000000..fd9dbe5
--- /dev/null
+++ b/test/fixtures/custom-module.vue
@@ -0,0 +1,5 @@
+<template>
+  <div style="flex-direction: row">
+    <span :style="{ transform }"></span>
+  </div>
+</template>
diff --git a/test/test.js b/test/test.js
index 6153927..00b3938 100644
--- a/test/test.js
+++ b/test/test.js
@@ -700,4 +700,50 @@ describe('vue-loader', function () {
       done()
     })
   })
+
+  it('custom compiler modules', done => {
+    test({
+      entry: './test/fixtures/custom-module.vue',
+      vue: {
+        compilerModules: [
+          {
+            postTransformNode: el => {
+              if (el.staticStyle) {
+                el.staticStyle = `$processStyle(${el.staticStyle})`
+              }
+              if (el.styleBinding) {
+                el.styleBinding = `$processStyle(${el.styleBinding})`
+              }
+            }
+          }
+        ]
+      }
+    }, (window, module) => {
+      var results = []
+      var vnode = mockRender(module, {
+        $processStyle: style => results.push(style),
+        transform: 'translateX(10px)'
+      })
+      expect(results).to.deep.equal([
+        { 'flex-direction': 'row' },
+        { 'transform': 'translateX(10px)' }
+      ])
+      done()
+    })
+  })
+
+  it('custom compiler modules (string)', done => {
+    test({
+      entry: './test/fixtures/basic.vue',
+      vue: {
+        compilerModules: require.resolve('./fixtures/custom-module')
+      }
+    }, (window, module) => {
+      var vnode = mockRender(module, {
+        msg: 'hi'
+      })
+      expect(vnode.data.staticClass).to.equal('red blue')
+      done()
+    })
+  })
 })