Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 73 additions & 7 deletions lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module.exports = function (content) {
var bubleOptions = hasBuble && options.buble ? '?' + JSON.stringify(options.buble) : ''
var defaultLoaders = {
html: templateCompilerPath + '?id=' + moduleId,
css: styleLoaderPath + '!css-loader' + (needCssSourceMap ? '?sourceMap' : ''),
css: (isServer ? '' : styleLoaderPath + '!') + 'css-loader' + (needCssSourceMap ? '?sourceMap' : ''),
js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? 'babel-loader' : ''
}

Expand All @@ -77,7 +77,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
Expand All @@ -94,17 +94,42 @@ 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
var option = options.cssModules || {}
return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
// $1: !css-loader
// $2: ?a=b
var query = loaderUtils.parseQuery($2)
query.modules = true
query.importLoaders = true
query.localIdentName = 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.
query.localIdentName += '_' + index
}
return $1 + '?' + JSON.stringify(query)
})
}

function getLoaderString (type, part, index, scoped) {
var lang = part.lang || defaultLang[type]
var loader = loaders[lang]
var rewriter = type === 'styles' ? styleRewriter + (scoped ? '&scoped=true!' : '!') : ''
var injectString = (type === 'script' && query.inject) ? 'inject!' : ''
if (loader !== undefined) {
// add css modules
if (type === 'styles') {
loader = addCssModulesToLoader(loader, part, index)
}
// inject rewriter before css/html loader for
// extractTextPlugin use cases
if (rewriterInjectRE.test(loader)) {
Expand All @@ -121,7 +146,8 @@ module.exports = function (content) {
case 'template':
return defaultLoaders.html + '!' + templateLoaderPath + '?raw&engine=' + lang + '!'
case 'styles':
return defaultLoaders.css + '!' + rewriter + lang + '!'
loader = addCssModulesToLoader(defaultLoaders.css, part, index)
return loader + '!' + rewriter + lang + '!'
case 'script':
return injectString + lang + '!'
}
Expand All @@ -146,13 +172,43 @@ module.exports = function (content) {
var hasScoped = parts.styles.some(function (s) { return s.scoped })
var output = 'var __vue_exports__, __vue_options__\n'

// css modules
output += 'var __vue_styles__ = {}\n'
var cssModules = {}

// add requires for styles
if (!isServer && parts.styles.length) {
if (parts.styles.length) {
output += '\n/* styles */\n'
parts.styles.forEach(function (style, i) {
output += style.src
/* !HACK! */
style.module = i === 0 ? 'style' : '$style'

// require style
if (isServer && !style.module) return
var requireString = style.src
? getRequireForImport('styles', style, style.scoped)
: getRequire('styles', style, i, style.scoped)

// setCssModule
if (style.module) {
if (style.module in cssModules) {
loaderContext.emitError('CSS module name "' + style.module + '" is not unique!')
output += requireString
} else {
cssModules[style.module] = true

// `style-loader` exposes the name-to-hash map directly
// `css-loader` exposes it in `.locals`
// We drop `style-loader` in SSR, and add `.locals` here.
if (isServer) {
requireString += '.locals'
}

output += '__vue_styles__["' + style.module + '"] = ' + requireString + '\n'
}
} else {
output += requireString
}
})
}

Expand Down Expand Up @@ -205,6 +261,16 @@ module.exports = function (content) {
exports += '__vue_options__._scopeId = "' + moduleId + '"\n'
}

if (Object.keys(cssModules).length) {
// inject style modules as computed properties
exports +=
'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'
}

if (!query.inject) {
output += exports
// hot reload
Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/css-modules.vue
Original file line number Diff line number Diff line change
@@ -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>
70 changes: 70 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function bundle (options, cb) {
})
}
expect(stats.compilation.errors).to.be.empty
require('fs').writeFileSync('./test.build.js', mfs.readFileSync('/test.build.js').toString())
cb(mfs.readFileSync('/test.build.js').toString())
})
}
Expand Down Expand Up @@ -377,4 +378,73 @@ describe('vue-loader', function () {
done()
})
})

it('css-modules', function (done) {
function testWithIdent (localIdentName, regexToMatch, cb) {
test({
entry: './test/fixtures/css-modules.vue',
vue: {
cssModules: {
localIdentName: localIdentName
}
}
}, function (window) {
var module = window.vueModule

// get local class name
var className = module.computed.style().red
expect(className).to.match(regexToMatch)

// 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(regexToMatch).and.not.equal(className)
var id = 'data-v-' + genId(require.resolve('./fixtures/css-modules.vue'))
expect(style).to.contain('.' + anotherClassName + '[' + id + ']')

cb()
})
}
// default localIdentName
testWithIdent(undefined, /^_\w{22}/, function () {
// specified localIdentName
var ident = '[path][name]---[local]---[hash:base64:5]'
var regex = /^test-fixtures-css-modules---red---\w{5}/
testWithIdent(ident, regex, done)
})
})

it.only('css-modules in SSR', function (done) {
bundle({
entry: './test/fixtures/css-modules.vue',
target: 'node',
output: Object.assign({}, globalConfig.output, {
libraryTarget: 'commonjs2'
})
}, function (code) {
// http://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky here. Anyway better to test a webpack generated node module?

function requireFromString(src, filename) {
var Module = module.constructor;
var m = new Module();
m._compile(src, filename);
return m.exports;
}

var output = requireFromString(code, './test.build.js')
expect(output.computed.style().red).to.match(/^_/)
done()
})
})
})