I'm working on a Vue app with Rails backend. I'm using Vuetify and I want to customize the SCSS variables. Sadly, I can't use Vue-CLI because we're bundling everything with webpacker, the rails implementation for webpack. Thus, I try to implement it with the base webpack configuration option.
I haven't been able to do this directly, as webpacker has their own css/sass/scss loader configurations. But, I can hook into the existing loaders and modify the options in a function that sets them later:
// config/webpack/environment.js
const updateStyleLoaders = (arr) => {
arr.forEach((item) => {
const loader = environment.loaders.get(item);
// Use vue-style-loader instead of default to parse Vue SFCs
const styleConfig = loader.use.find((el) => el.loader === 'style-loader');
if (styleConfig !== undefined) {
styleConfig.loader = 'vue-style-loader';
}
// VUETIFY: Use Dart-Sass and Fibers for Sass loaders
const sassConfig = loader.use.find((el) => el.loader === 'sass-loader');
if (sassConfig !== undefined) {
const opts = sassConfig.options;
opts.implementation = require('sass'); // Use dart-sass instead of node-sass
opts.fiber = require('fibers'); // improves compilation speed
opts.data = "@import '@/assets/sass/variables.scss';"; // Import custom variables
}
});
};
// Call fuction for all css-related loaders
updateStyleLoaders(['sass', 'css', 'moduleCss', 'moduleSass']);
// @/assets/sass/variables.scss
$body-font-family: "Comic Sans MS", "Comic Sans", cursive; // just wanna have fun
$border-radius-root: 20px;
Now here's the problem:
The sass-loader rule matches both 'sass' and 'scss'. In vuetify's example the matching for sass and scss is done seperately. When I add the semicolon, I get this error during compilation:
./node_modules/vuetify/src/components/VAlert/VAlert.sass
Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js):
ModuleBuildError: Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
// Imports
^
Semicolons aren't allowed in the indented syntax.
╷
1 │ @import '@/assets/sass/variables.scss';
│ ^
╵
stdin 1:39 root stylesheet
This tells me the line is in fact correctly added by sass-loader to the vuetify components that are added in. But, when I remove the semicolon from the import statement to support sass' indented syntax, I see no style changes.
How would I customize my vuetify components in this scenario? Webpacker uses sass-loader v7.3.1.
Thanks to the help of this GitHub comment, I've got it working.
The default loaders can be removed with environment.loaders.delete('sass') // same for 'moduleSass', 'moduleCss', 'css'
.
Then, they can be replaced by new ones. I've separated the scss and sass loaders to their own files:
// config/webpack/loaders/sass.js
const { config } = require('@rails/webpacker');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
test: /\.sass$/,
use: [
config.extract_css === false
? 'vue-style-loader'
: MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
implementation: require('sass'),
fiber: require('fibers'),
data: '@import "app/frontend/src/assets/styles/variables.scss"',
},
},
],
};
// config/webpack/loaders/scss.js
const { config } = require('@rails/webpacker');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
test: /\.scss$/,
use: [
config.extract_css === false
? 'vue-style-loader'
: MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
implementation: require('sass'),
fiber: require('fibers'),
data: '@import "app/frontend/src/assets/styles/variables.scss";',
},
},
],
};
These re-implement the basic webpacker integrations with CSS extraction.
Then, I add them to the main config like so:
// config/webpack/environment.js
const { environment } = require('@rails/webpacker');
const path = require('path');
// Plugins
const { VueLoaderPlugin } = require('vue-loader');
const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin');
// Loaders
const erb = require('./loaders/erb');
const sass = require('./loaders/sass');
const scss = require('./loaders/scss');
const vue = require('./loaders/vue');
const yml = require('./loaders/yml');
// Remove webpacker's conflicting loaders
environment.loaders.delete('moduleSass');
environment.loaders.delete('moduleCss');
environment.loaders.delete('sass');
// Modify base css loader to support vue SFC style tags
environment.loaders.get('css').use.find((el) => el.loader === 'style-loader').loader = 'vue-style-loader';
// Apply plugins
environment.plugins.prepend('VuetifyLoaderPlugin', new VuetifyLoaderPlugin());
environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin());
// Apply custom loaders
environment.loaders.append('erb', erb);
environment.loaders.append('yml', yml);
environment.loaders.append('vue', vue);
environment.loaders.append('sass', sass);
environment.loaders.append('scss', scss);
// Shorthands for import statements
environment.config.resolve.alias = {
// Use the same vue package everywhere
vue: 'vue/dist/vue.esm',
// use '@' as absolute path from /src
'@': path.resolve(__dirname, '../../app/frontend/src/'),
};
module.exports = environment;
One important part here as well is where I still modify the default css-loader to use 'vue-style-loader' over 'style-loader'. But for larger changes (like vuetify) I'm able to define my own defined loaders for SASS / SCSS syntax.
Maybe it can be optimized further to remove the duplication, but as long as it's only two configurations, I'm just happy it works :)