Skip to content

Add CSS modules support in Vue.js for Sass/Less/Stylus #511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 36 additions & 2 deletions fixtures/vuejs-css-modules/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div id="app" class="red" :class="$style.italic"></div>
<div id="app" class="red large justified lowercase" :class="[$css.italic, $scss.bold, $less.underline, $stylus.rtl]"></div>
</template>

<style>
Expand All @@ -8,8 +8,42 @@
}
</style>

<style module>
<style lang="scss">
.large {
font-size: 50px;
}
</style>

<style lang="less">
.justified {
text-align: justify;
}
</style>

<style lang="styl">
.lowercase
text-transform: lowercase
</style>

<style module="$css">
.italic {
font-style: italic;
}
</style>

<style lang="scss" module="$scss">
.bold {
font-weight: bold;
}
</style>

<style lang="less" module="$less">
.underline {
text-decoration: underline;
}
</style>

<style lang="styl" module="$stylus">
.rtl
direction: rtl;
</style>
30 changes: 27 additions & 3 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,21 +306,45 @@ class ConfigGenerator {
if (this.webpackConfig.useSassLoader) {
rules.push({
test: /\.s[ac]ss$/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig))
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig))
}
]
});
}

if (this.webpackConfig.useLessLoader) {
rules.push({
test: /\.less/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig))
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig))
}
]
});
}

if (this.webpackConfig.useStylusLoader) {
rules.push({
test: /\.styl/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig))
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig))
}
]
});
}

Expand Down
6 changes: 3 additions & 3 deletions lib/loaders/less.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ const applyOptionsCallback = require('../utils/apply-options-callback');

/**
* @param {WebpackConfig} webpackConfig
* @param {bool} ignorePostCssLoader If true, postcss-loader will never be added
* @param {bool} useCssModules
* @return {Array} of loaders to use for Less files
*/
module.exports = {
getLoaders(webpackConfig, ignorePostCssLoader = false) {
Copy link
Collaborator Author

@Lyrkan Lyrkan Feb 5, 2019

Choose a reason for hiding this comment

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

ignorePostCssLoader wasn't actually useful since cssLoader.getLoaders() looks into the webpackConfig object for that info.

getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('less');

const config = {
sourceMap: webpackConfig.useSourceMaps
};

return [
...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader),
...cssLoader.getLoaders(webpackConfig, useCssModules),
{
loader: 'less-loader',
options: applyOptionsCallback(webpackConfig.lessLoaderOptionsCallback, config)
Expand Down
8 changes: 4 additions & 4 deletions lib/loaders/sass.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const applyOptionsCallback = require('../utils/apply-options-callback');

/**
* @param {WebpackConfig} webpackConfig
* @param {Object} sassOption Options to pass to the loader
* @param {bool} useCssModules
* @return {Array} of loaders to use for Sass files
*/
module.exports = {
getLoaders(webpackConfig, sassOptions = {}) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No point keeping a sassOptions parameter here, the only allowed option is resolveUrlLoader which is already handled here by directly looking into the webpackConfig object.

getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('sass');

const sassLoaders = [...cssLoader.getLoaders(webpackConfig)];
const sassLoaders = [...cssLoader.getLoaders(webpackConfig, useCssModules)];
if (true === webpackConfig.sassOptions.resolveUrlLoader) {
// responsible for resolving Sass url() paths
// without this, all url() paths must be relative to the
Expand All @@ -35,7 +35,7 @@ module.exports = {
});
}

const config = Object.assign({}, sassOptions, {
const config = Object.assign({}, {
// needed by the resolve-url-loader
sourceMap: (true === webpackConfig.sassOptions.resolveUrlLoader) || webpackConfig.useSourceMaps
});
Expand Down
6 changes: 3 additions & 3 deletions lib/loaders/stylus.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ const applyOptionsCallback = require('../utils/apply-options-callback');

/**
* @param {WebpackConfig} webpackConfig
* @param {bool} ignorePostCssLoader If true, postcss-loader will never be added
* @param {bool} useCssModules
* @return {Array} of loaders to use for Stylus files
*/
module.exports = {
getLoaders(webpackConfig, ignorePostCssLoader = false) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ignorePostCssLoader wasn't needed here since cssLoader.getLoaders() look into the webpackConfig object for that info.

getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('stylus');

const config = {
sourceMap: webpackConfig.useSourceMaps
};

return [
...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader),
...cssLoader.getLoaders(webpackConfig, useCssModules),
{
loader: 'stylus-loader',
options: applyOptionsCallback(webpackConfig.stylusLoaderOptionsCallback, config)
Expand Down
42 changes: 26 additions & 16 deletions test/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -1354,7 +1354,7 @@ module.exports = {
});
});

it('Vue.js supports CSS modules', (done) => {
it('Vue.js supports CSS/Sass/Less/Stylus modules', (done) => {
const appDir = testSetup.createTestAppDir();
const config = testSetup.createWebpackConfig(appDir, 'www/build', 'dev');
config.enableSingleRuntimeChunk();
Expand All @@ -1363,6 +1363,7 @@ module.exports = {
config.enableVueLoader();
config.enableSassLoader();
config.enableLessLoader();
config.enableStylusLoader();
config.configureCssLoader(options => {
// Remove hashes from local ident names
// since they are not always the same.
Expand All @@ -1378,17 +1379,22 @@ module.exports = {
'runtime.js',
]);

// Standard CSS
webpackAssert.assertOutputFileContains(
'main.css',
'.red {'
);
const expectClassDeclaration = (className) => {
webpackAssert.assertOutputFileContains(
'main.css',
`.${className} {`
);
};

// CSS modules
webpackAssert.assertOutputFileContains(
'main.css',
'.italic_foo {'
);
expectClassDeclaration('red'); // Standard CSS
expectClassDeclaration('large'); // Standard SCSS
expectClassDeclaration('justified'); // Standard Less
expectClassDeclaration('lowercase'); // Standard Stylus

expectClassDeclaration('italic_foo'); // CSS Module
expectClassDeclaration('bold_foo'); // SCSS Module
expectClassDeclaration('underline_foo'); // Less Module
expectClassDeclaration('rtl_foo'); // Stylus Module

testSetup.requestTestPage(
path.join(config.getContext(), 'www'),
Expand All @@ -1397,11 +1403,15 @@ module.exports = {
'build/main.js'
],
(browser) => {
// Standard CSS
browser.assert.hasClass('#app', 'red');

// CSS modules
browser.assert.hasClass('#app', 'italic_foo');
browser.assert.hasClass('#app', 'red'); // Standard CSS
browser.assert.hasClass('#app', 'large'); // Standard SCSS
browser.assert.hasClass('#app', 'justified'); // Standard Less
browser.assert.hasClass('#app', 'lowercase'); // Standard Stylus

browser.assert.hasClass('#app', 'italic_foo'); // CSS module
browser.assert.hasClass('#app', 'bold_foo'); // SCSS module
browser.assert.hasClass('#app', 'underline_foo'); // Less module
browser.assert.hasClass('#app', 'rtl_foo'); // Stylus module

done();
}
Expand Down
19 changes: 18 additions & 1 deletion test/loaders/less.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ describe('loaders/less', () => {
config.enableSourceMaps(true);

// make the cssLoader return nothing
sinon.stub(cssLoader, 'getLoaders')
const cssLoaderStub = sinon.stub(cssLoader, 'getLoaders')
.callsFake(() => []);

const actualLoaders = lessLoader.getLoaders(config);
expect(actualLoaders).to.have.lengthOf(1);
expect(actualLoaders[0].options.sourceMap).to.be.true;
expect(cssLoaderStub.getCall(0).args[1]).to.be.false;

cssLoader.getLoaders.restore();
});
Expand Down Expand Up @@ -81,4 +82,20 @@ describe('loaders/less', () => {
expect(actualLoaders[0].options).to.deep.equals({ foo: true });
cssLoader.getLoaders.restore();
});

it('getLoaders() with CSS modules enabled', () => {
const config = createConfig();
config.enableSourceMaps(true);

// make the cssLoader return nothing
const cssLoaderStub = sinon.stub(cssLoader, 'getLoaders')
.callsFake(() => []);

const actualLoaders = lessLoader.getLoaders(config, true);
expect(actualLoaders).to.have.lengthOf(1);
expect(actualLoaders[0].options.sourceMap).to.be.true;
expect(cssLoaderStub.getCall(0).args[1]).to.be.true;

cssLoader.getLoaders.restore();
});
});
50 changes: 24 additions & 26 deletions test/loaders/sass.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('loaders/sass', () => {
config.enableSourceMaps(true);

// make the cssLoader return nothing
sinon.stub(cssLoader, 'getLoaders')
const cssLoaderStub = sinon.stub(cssLoader, 'getLoaders')
.callsFake(() => []);

const actualLoaders = sassLoader.getLoaders(config);
Expand All @@ -40,6 +40,7 @@ describe('loaders/sass', () => {

expect(actualLoaders[1].loader).to.equal('sass-loader');
expect(actualLoaders[1].options.sourceMap).to.be.true;
expect(cssLoaderStub.getCall(0).args[1]).to.be.false;

cssLoader.getLoaders.restore();
});
Expand Down Expand Up @@ -83,24 +84,6 @@ describe('loaders/sass', () => {
cssLoader.getLoaders.restore();
});

it('getLoaders() with extra options', () => {
const config = createConfig();

// make the cssLoader return nothing
sinon.stub(cssLoader, 'getLoaders')
.callsFake(() => []);

const actualLoaders = sassLoader.getLoaders(config, {
custom_option: 'foo'
});
// the normal option
expect(actualLoaders[1].options.sourceMap).to.be.true;
// custom option
expect(actualLoaders[1].options.custom_option).to.equal('foo');

cssLoader.getLoaders.restore();
});

it('getLoaders() with options callback', () => {
const config = createConfig();

Expand All @@ -113,16 +96,11 @@ describe('loaders/sass', () => {
sassOptions.other_option = true;
});

const actualLoaders = sassLoader.getLoaders(config, {
custom_optiona: 'foo',
custom_optionb: 'bar'
});
const actualLoaders = sassLoader.getLoaders(config);

expect(actualLoaders[1].options).to.deep.equals({
sourceMap: true,
// callback wins over passed in options
custom_optiona: 'baz',
custom_optionb: 'bar',
other_option: true
});
cssLoader.getLoaders.restore();
Expand All @@ -143,9 +121,29 @@ describe('loaders/sass', () => {
});


const actualLoaders = sassLoader.getLoaders(config, {});
const actualLoaders = sassLoader.getLoaders(config);
expect(actualLoaders[1].options).to.deep.equals({ foo: true });

cssLoader.getLoaders.restore();
});

it('getLoaders() with CSS modules enabled', () => {
const config = createConfig();
config.enableSourceMaps(true);

// make the cssLoader return nothing
const cssLoaderStub = sinon.stub(cssLoader, 'getLoaders')
.callsFake(() => []);

const actualLoaders = sassLoader.getLoaders(config, true);
expect(actualLoaders).to.have.lengthOf(2);
expect(actualLoaders[0].loader).to.equal('resolve-url-loader');
expect(actualLoaders[0].options.sourceMap).to.be.true;

expect(actualLoaders[1].loader).to.equal('sass-loader');
expect(actualLoaders[1].options.sourceMap).to.be.true;
expect(cssLoaderStub.getCall(0).args[1]).to.be.true;

cssLoader.getLoaders.restore();
});
});
Loading