Skip to content

Commit 75ca421

Browse files
YiSiWangyyx990803
authored andcommitted
[WIP]CSS modules (#422)
* add test * add css module support with parser hack * add localIdentName support * add SSR support with test case * use vue-template-compiler#dev and remove parse hack * use vue#2.0.4
1 parent 73d82a4 commit 75ca421

File tree

4 files changed

+164
-9
lines changed

4 files changed

+164
-9
lines changed

lib/loader.js

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ module.exports = function (content) {
5858
var bubleOptions = hasBuble && options.buble ? '?' + JSON.stringify(options.buble) : ''
5959
var defaultLoaders = {
6060
html: templateCompilerPath + '?id=' + moduleId,
61-
css: styleLoaderPath + '!css-loader' + (needCssSourceMap ? '?sourceMap' : ''),
61+
css: (isServer ? '' : styleLoaderPath + '!') + 'css-loader' + (needCssSourceMap ? '?sourceMap' : ''),
6262
js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? 'babel-loader' : ''
6363
}
6464

@@ -77,7 +77,7 @@ module.exports = function (content) {
7777
// disable all configuration loaders
7878
'!!' +
7979
// get loader string for pre-processors
80-
getLoaderString(type, part, scoped) +
80+
getLoaderString(type, part, index, scoped) +
8181
// select the corresponding part from the vue file
8282
getSelectorString(type, index || 0) +
8383
// the url to the actual vuefile
@@ -94,17 +94,42 @@ module.exports = function (content) {
9494
function getRequireForImportString (type, impt, scoped) {
9595
return loaderUtils.stringifyRequest(loaderContext,
9696
'!!' +
97-
getLoaderString(type, impt, scoped) +
97+
getLoaderString(type, impt, -1, scoped) +
9898
impt.src
9999
)
100100
}
101101

102-
function getLoaderString (type, part, scoped) {
102+
function addCssModulesToLoader (loader, part, index) {
103+
if (!part.module) return loader
104+
var option = options.cssModules || {}
105+
return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
106+
// $1: !css-loader
107+
// $2: ?a=b
108+
var query = loaderUtils.parseQuery($2)
109+
query.modules = true
110+
query.importLoaders = true
111+
query.localIdentName = option.localIdentName || '[hash:base64]'
112+
if (index !== -1) {
113+
// Note:
114+
// Class name is generated according to its filename.
115+
// Different <style> tags in the same .vue file may generate same names.
116+
// Append `_[index]` to class name to avoid this.
117+
query.localIdentName += '_' + index
118+
}
119+
return $1 + '?' + JSON.stringify(query)
120+
})
121+
}
122+
123+
function getLoaderString (type, part, index, scoped) {
103124
var lang = part.lang || defaultLang[type]
104125
var loader = loaders[lang]
105126
var rewriter = type === 'styles' ? styleRewriter + (scoped ? '&scoped=true!' : '!') : ''
106127
var injectString = (type === 'script' && query.inject) ? 'inject!' : ''
107128
if (loader !== undefined) {
129+
// add css modules
130+
if (type === 'styles') {
131+
loader = addCssModulesToLoader(loader, part, index)
132+
}
108133
// inject rewriter before css/html loader for
109134
// extractTextPlugin use cases
110135
if (rewriterInjectRE.test(loader)) {
@@ -121,7 +146,8 @@ module.exports = function (content) {
121146
case 'template':
122147
return defaultLoaders.html + '!' + templateLoaderPath + '?raw&engine=' + lang + '!'
123148
case 'styles':
124-
return defaultLoaders.css + '!' + rewriter + lang + '!'
149+
loader = addCssModulesToLoader(defaultLoaders.css, part, index)
150+
return loader + '!' + rewriter + lang + '!'
125151
case 'script':
126152
return injectString + lang + '!'
127153
}
@@ -146,13 +172,42 @@ module.exports = function (content) {
146172
var hasScoped = parts.styles.some(function (s) { return s.scoped })
147173
var output = 'var __vue_exports__, __vue_options__\n'
148174

175+
// css modules
176+
output += 'var __vue_styles__ = {}\n'
177+
var cssModules = {}
178+
149179
// add requires for styles
150-
if (!isServer && parts.styles.length) {
180+
if (parts.styles.length) {
151181
output += '\n/* styles */\n'
152182
parts.styles.forEach(function (style, i) {
153-
output += style.src
183+
var moduleName = (style.module === true) ? '$style' : style.module
184+
185+
// require style
186+
if (isServer && !moduleName) return
187+
var requireString = style.src
154188
? getRequireForImport('styles', style, style.scoped)
155189
: getRequire('styles', style, i, style.scoped)
190+
191+
// setCssModule
192+
if (moduleName) {
193+
if (moduleName in cssModules) {
194+
loaderContext.emitError('CSS module name "' + moduleName + '" is not unique!')
195+
output += requireString
196+
} else {
197+
cssModules[moduleName] = true
198+
199+
// `style-loader` exposes the name-to-hash map directly
200+
// `css-loader` exposes it in `.locals`
201+
// We drop `style-loader` in SSR, and add `.locals` here.
202+
if (isServer) {
203+
requireString += '.locals'
204+
}
205+
206+
output += '__vue_styles__["' + moduleName + '"] = ' + requireString + '\n'
207+
}
208+
} else {
209+
output += requireString
210+
}
156211
})
157212
}
158213

@@ -205,6 +260,16 @@ module.exports = function (content) {
205260
exports += '__vue_options__._scopeId = "' + moduleId + '"\n'
206261
}
207262

263+
if (Object.keys(cssModules).length) {
264+
// inject style modules as computed properties
265+
exports +=
266+
'if (!__vue_options__.computed) __vue_options__.computed = {}\n' +
267+
'Object.keys(__vue_styles__).forEach(function (key) {\n' +
268+
'var module = __vue_styles__[key]\n' +
269+
'__vue_options__.computed[key] = function () { return module }\n' +
270+
'})\n'
271+
}
272+
208273
if (!query.inject) {
209274
output += exports
210275
// hot reload

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"source-map": "^0.5.6",
4141
"vue-hot-reload-api": "^2.0.1",
4242
"vue-style-loader": "^1.0.0",
43-
"vue-template-compiler": "^2.0.0-rc.3",
43+
"vue-template-compiler": "^2.0.4",
4444
"vue-template-es2015-compiler": "^1.0.0"
4545
},
4646
"peerDependencies": {
@@ -72,7 +72,7 @@
7272
"stylus": "^0.54.5",
7373
"stylus-loader": "^2.0.0",
7474
"sugarss": "^0.1.3",
75-
"vue": "^2.0.0-rc.4",
75+
"vue": "^2.0.4",
7676
"webpack": "^1.12.2"
7777
}
7878
}

test/fixtures/css-modules.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<style module="style">
2+
.red {
3+
color: red;
4+
}
5+
@keyframes fade {
6+
from { opacity: 1; } to { opacity: 0; }
7+
}
8+
.animate {
9+
animation: fade 1s;
10+
}
11+
</style>
12+
13+
<style scoped lang="stylus" module>
14+
.red
15+
color: red
16+
</style>
17+
18+
<script>
19+
module.exports = {}
20+
</script>

test/test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,74 @@ describe('vue-loader', function () {
377377
done()
378378
})
379379
})
380+
381+
it('css-modules', function (done) {
382+
function testWithIdent (localIdentName, regexToMatch, cb) {
383+
test({
384+
entry: './test/fixtures/css-modules.vue',
385+
vue: {
386+
cssModules: {
387+
localIdentName: localIdentName
388+
}
389+
}
390+
}, function (window) {
391+
var module = window.vueModule
392+
393+
// get local class name
394+
var className = module.computed.style().red
395+
expect(className).to.match(regexToMatch)
396+
397+
// class name in style
398+
var style = [].slice.call(window.document.querySelectorAll('style')).map(function (style) {
399+
return style.textContent
400+
}).join('\n')
401+
expect(style).to.contain('.' + className + ' {\n color: red;\n}')
402+
403+
// animation name
404+
var match = style.match(/@keyframes\s+(\S+)\s+{/)
405+
expect(match).to.have.length(2)
406+
var animationName = match[1]
407+
expect(animationName).to.not.equal('fade')
408+
expect(style).to.contain('animation: ' + animationName + ' 1s;')
409+
410+
// default module + pre-processor + scoped
411+
var anotherClassName = module.computed.$style().red
412+
expect(anotherClassName).to.match(regexToMatch).and.not.equal(className)
413+
var id = 'data-v-' + genId(require.resolve('./fixtures/css-modules.vue'))
414+
expect(style).to.contain('.' + anotherClassName + '[' + id + ']')
415+
416+
cb()
417+
})
418+
}
419+
// default localIdentName
420+
testWithIdent(undefined, /^_\w{22}/, function () {
421+
// specified localIdentName
422+
var ident = '[path][name]---[local]---[hash:base64:5]'
423+
var regex = /^test-fixtures-css-modules---red---\w{5}/
424+
testWithIdent(ident, regex, done)
425+
})
426+
})
427+
428+
it('css-modules in SSR', function (done) {
429+
bundle({
430+
entry: './test/fixtures/css-modules.vue',
431+
target: 'node',
432+
output: Object.assign({}, globalConfig.output, {
433+
libraryTarget: 'commonjs2'
434+
})
435+
}, function (code) {
436+
// http://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory
437+
function requireFromString(src, filename) {
438+
var Module = module.constructor;
439+
var m = new Module();
440+
m._compile(src, filename);
441+
return m.exports;
442+
}
443+
444+
var output = requireFromString(code, './test.build.js')
445+
expect(output.computed.style().red).to.exist
446+
447+
done()
448+
})
449+
})
380450
})

0 commit comments

Comments
 (0)