Next-plugins: How to import sass with css modules AND without?

Created on 8 Apr 2018  路  15Comments  路  Source: vercel/next-plugins

I'd like to import this global.styles.scss file in every page

/**
  Bootstrap
 */
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/utilities/align";
@import "~bootstrap/scss/utilities/flex";
@import "~bootstrap/scss/utilities/display";
@import "~bootstrap/scss/utilities/spacing";
@import "~bootstrap/scss/utilities/sizing";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/_buttons.scss";
@import "~bootstrap/scss/_dropdown.scss";
@import "~bootstrap/scss/_card.scss";
@import "~bootstrap/scss/_nav.scss";

And theme bootstrap while I'm at it.

But also I'd like to use CSS modules in my components.

If I try to wrap the above imports in :global, it doesn't work.

ModuleBuildError: Module build failed: ModuleBuildError: Module build failed: Missing whitespace before :global (3014:5)

  3012 |       background-color: #007bff;
  3013 |       border-color: #007bff; }
> 3014 |     :global .btn-primary:not(:disabled):not(.disabled):active, :global .btn-primary:not(:disabled):not(.disabled).active,
       |     ^
  3015 |     .show > :global .btn-primary.dropdown-toggle {
  3016 |       color: #fff;
  3017 |       background-color: #0062cc;

Most helpful comment

Thanks @astenmies and @fatlinesofcode. Solution works, just wondering if it is possible to use the default mangled names that are generated by default for files that don't match.

i.e ftuojk123 instead of login___loginBox

EDIT:
I ended up doing the following:

const withCSS = require('@zeit/next-css');
const defaultGetLocalIdent = require('css-loader/lib/getLocalIdent');

module.exports = withCSS({
  cssModules: true,
  cssLoaderOptions: {
    getLocalIdent: (loaderContext, localIdentName, localName, options) => {
      if (loaderContext.resourcePath.includes('node_modules')) {
        return localName
      } else {
        return defaultGetLocalIdent(loaderContext, localIdentName, localName, options);
      }
    }
  }
});

All 15 comments

i'm in a similar situation where i want SASS _with_ modules and also have some global CSS files/rules that I don't want affected by the modular behavior. is this possible without having to put the global files in /static?

I am in a similar situation as well. None of the docs that are provided here work

This has to be resolved or please provide an update

I went with CSS modules using next-css for app styles. And gulp.watch with node-sass for global styles. The gulp task builds to a destination folder and I reference that file in _document, also. If it's urgent, I would suggest this course of action.

Did anyone find a solution for this?

We are running into this issue as well (only we use react-md instead of bootstrap). Is there any solution besides adding an extra build step to generate the global CSS file from the react-md SCSS sources?

Okay, we figured something out. If we include our style.scss file (the one that includes the react-md styles via html { :global { @import '~react-md/src/scss/react-md'; } }) not in the pages/_document.js but in our main Layout.js component, which is used by all our pages, then everything somehow works.

I have no idea why that is, and since I spent already over 16 hours on this, I am right now not willing to spend any more time to figure this out.

you as custom getLocalIdent function to stop rewriting classnames in files named *global

e.g.

module.exports = withCSS(withSass({
  cssModules: true,
cssLoaderOptions: {
    // localIdentName: "[name]__[local]",
    getLocalIdent: (loaderContext, localIdentName, localName, options) => {
      const fileName = path.basename(loaderContext.resourcePath)
      if(fileName.indexOf('global.scss') !== -1){
        return localName
      }else{
        const name = fileName.replace(/\.[^/.]+$/, "")
        return `${name}__${localName}`
      }
    }
  },
}))

Some fine-tuning based on fatlinesofcode's answer...

  • in src/comps -> not transformed unless suffix is _module
  • if you want it to be transformed and not transformed, inside src/comps just create mystyle.scss and mystyle_module.scss where the last one imports the first one @import './mystyle.scss'
  • in avoidPaths -> never transformed
const avoidPaths = ['src/styles/css-new', 'node_modules', 'src/pages'].map(d => path.join(__dirname, d));

function canBeTransformed(pathToCheck) {
    return !avoidPaths.some(function(v) {
        const path = pathToCheck.substr(0, pathToCheck.lastIndexOf('/') + 1);
        return v.includes(path);
    });
}

// prettier-ignore
module.exports = withSass({
    cssModules: true,
    cssLoaderOptions: {
        getLocalIdent: (loaderContext, localIdentName, localName, options) => {
            const fileName = path.basename(loaderContext.resourcePath)
            const shoudTransform = canBeTransformed(loaderContext.resourcePath)

            if(!shoudTransform){
                return localName
            }else{
                const name = fileName.replace(/\.[^/.]+$/, "")
                const suffix = name.substring(name.lastIndexOf("_") + 1);
                if(suffix === "module") {
                    return `${name}___${localName}`
                } else {
                    return localName
                }
            }
        }
    },
})

Thanks @astenmies and @fatlinesofcode. Solution works, just wondering if it is possible to use the default mangled names that are generated by default for files that don't match.

i.e ftuojk123 instead of login___loginBox

EDIT:
I ended up doing the following:

const withCSS = require('@zeit/next-css');
const defaultGetLocalIdent = require('css-loader/lib/getLocalIdent');

module.exports = withCSS({
  cssModules: true,
  cssLoaderOptions: {
    getLocalIdent: (loaderContext, localIdentName, localName, options) => {
      if (loaderContext.resourcePath.includes('node_modules')) {
        return localName
      } else {
        return defaultGetLocalIdent(loaderContext, localIdentName, localName, options);
      }
    }
  }
});

@affanshahid Only work when import bootstrap.scss in js file, it will failed when import in scss file like this:

@import '~bootstrap/scss/bootstrap.scss';

@affanshahid Only work when import bootstrap.scss in js file, it will failed when import in scss file like this:

@import '~bootstrap/scss/bootstrap.scss';

@yeluoqiuzhi You should include this:

sassLoaderOptions: {
  includePaths: ["node_modules", "./node_modules"],
},

in your next.config.js file. It works for me.

I end up using CSS for global and SCSS for modules.

Current config is:

const withSass = require("@zeit/next-sass");
const withCSS = require("@zeit/next-css");

module.exports = withSass({
    ...withCSS({ cssModules: false }),
    cssModules: true
});

while global.css is

@import "~normalize.css/normalize.css";
@import "~@blueprintjs/core/lib/css/blueprint.css";
@import "~bootstrap/dist/css/bootstrap-grid.css";

imported as import ./global.css

For the SCSS modules files you can do it as usual with import style from "./file.scss"

But I have a problem. The SCSS modules are always written before the global styles in the generated css on build. This is the contrary with dev, which is what is desired because you want to overwrite global style. This is confusing and boring because it is unreliable.

@affanshahid Only work when import bootstrap.scss in js file, it will failed when import in scss file like this:

@import '~bootstrap/scss/bootstrap.scss';

@yeluoqiuzhi You should include this:

sassLoaderOptions: {
  includePaths: ["node_modules", "./node_modules"],
},

in your next.config.js file. It works for me.

@huycuongdao I believe the reason that @affanshahid 's solution did not work for @yeluoqiuzhi was because the loaderContext.resourcePath is always (for me at least) the name of the SCSS file that was imported by the JavaScript. It is never the name of an SCSS file that is imported by another SCSS file.

Because of that, I don't understand how @affanshahid's solution works. @fatlinesofcode's solution works because it's checking for the name of the single global SCSS file that is imported by JavaScript, and not for the names of the files which that file imports.

Next.js          v9.1.4
@zeit/next-sass  v1.0.1
css-loader       v3.2.0
sass-loader      v6.0.6

Anyway...I ended up with:

const withSass = require('@zeit/next-sass')
const defaultGetLocalIdent = require('css-loader/dist/utils').getLocalIdent

module.exports = withSass({
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: '[local]___[hash:base64:5]',
    getLocalIdent: (loaderContext, localIdentName, localName, options) => {
      let filePath = loaderContext.resourcePath
      let isFromGlobal = filePath.includes('global.scss')
      if (isFromGlobal) return localName

      return defaultGetLocalIdent(
        loaderContext,
        localIdentName,
        localName,
        options
      )
    }
  },
  sassLoaderOptions: {
    includePaths: ['./node_modules']
  }
})

@zposten
This seems to be the solution! Many thanks. Using [email protected] however, it seems they removed the export of getLocalIdent (at least I couldn't manage to import it).
However, I just copied the relevant bits out, modified the imports to make it work, and moved it to next.config.js.
Here's my modified next.config.js: (the helper could also be put into another file)

const withSass = require('@zeit/next-sass');

module.exports = withSass({
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: '[local]___[hash:base64:5]',
    // allow both CSS modules AND global stylesheets (https://github.com/zeit/next-plugins/issues/135#issuecomment-563415934)
    getLocalIdent: (loaderContext, localIdentName, localName, options) => {
      let filePath = loaderContext.resourcePath;
      let isFromGlobal = filePath.includes('global.scss');
      if (isFromGlobal) return localName;

      return defaultGetLocalIdent(
        loaderContext,
        localIdentName,
        localName,
        options
      );
    }
  }
});

// hijacked from css-loader/dist/utils and slightly modified...
const _path = require("path");
const _loaderUtils = require("loader-utils");
const _normalizePath = require("normalize-path");
const _cssesc = require("cssesc");
const filenameReservedRegex = /[<>:"/\\|?*\x00-\x1F]/g; // eslint-disable-next-line no-control-regex
const reControlChars = /[\u0000-\u001f\u0080-\u009f]/g;
const reRelativePath = /^\.+/;
const defaultGetLocalIdent = (loaderContext, localIdentName, localName, options) => {
  if (!options.context) {
    // eslint-disable-next-line no-param-reassign
    options.context = loaderContext.rootContext;
  }

  const request = (0, _normalizePath)(_path.relative(options.context || '', loaderContext.resourcePath)); // eslint-disable-next-line no-param-reassign

  options.content = `${options.hashPrefix + request}+${unescape(localName)}`; // Using `[path]` placeholder outputs `/` we need escape their
  // Also directories can contains invalid characters for css we need escape their too

  return (0, _cssesc)(_loaderUtils.interpolateName(loaderContext, localIdentName, options) // For `[hash]` placeholder
  .replace(/^((-?[0-9])|--)/, '_$1').replace(filenameReservedRegex, '-').replace(reControlChars, '-').replace(reRelativePath, '-').replace(/\./g, '-'), {
    isIdentifier: true
  }).replace(/\\\[local\\\]/gi, localName);
};

Solved? Closing.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

romainquellec picture romainquellec  路  12Comments

Jauny picture Jauny  路  20Comments

Splendith picture Splendith  路  18Comments

selique picture selique  路  12Comments

vijayst picture vijayst  路  13Comments