Search code examples
javascriptruby-on-railswebpackvuetify.jswebpacker

Using vuetify custom sass variables in a vue app using rails/webpacker


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.


Solution

  • 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 :)