Skip to content

Instantly share code, notes, and snippets.

@haggen
Created March 5, 2021 13:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save haggen/05dc0d9a9d1e30334ce94fcabf193fe4 to your computer and use it in GitHub Desktop.
Save haggen/05dc0d9a9d1e30334ce94fcabf193fe4 to your computer and use it in GitHub Desktop.
Next.js v9/v10 integration for LESS leveraging built-in support for SASS
const cloneDeep = require("clone-deep");
const { readFileSync, existsSync } = require("fs");
const path = require("path");
// next-less (official plugin to integrate Less) is now
// deprecated so we have to provide our own solution.
//
// Next.js already provides built-in integration with CSS and Sass, but it
// relies on a dynamic WebPack config. generation to do it optimally.
//
// Since I don't to reimplement it I thought I could leverage the existing
// code to do the heavy lifting and then adapat the result to my desired goal.
//
// Below is the code responsible to replicating Next.js's Sass support for Less.
//
function withLess(nextConfig) {
return Object.assign({}, nextConfig, {
webpack(config, opts) {
// Next.js have two sets of rules. One for scoped CSS,
// via CSS Modules, and one for global stylesheets.
let copyOfModuleSassRule;
let copyOfGlobalSassRule;
// We're going to dive in the resulting WebPack config,
// find the rules related to CSS and Sass support,
// duplicate and modify them to support Less instead.
config.module.rules.forEach((rule) => {
// The Sass rule looks like this:
//
// { oneOf: [ { test: /\.module\.(scss|sass)$/, ... }, ... ] }
//
// There're also several other rules for .css/.scss files,
// but they're only for error handling.
//
// Watch out! Potentially bad heuristics. Might break in the future!
//
if (rule.oneOf) {
rule.oneOf.forEach((rule) => {
if (rule.test) {
if (rule.test.source === "\\.module\\.(scss|sass)$") {
copyOfModuleSassRule = cloneDeep(rule);
}
if (rule.test.source === "(?<!\\.module)\\.(scss|sass)$") {
copyOfGlobalSassRule = cloneDeep(rule);
}
}
// Also we need to fix the file-loader rule, so
// assets imported from our .less files work.
//
// Watch out! Potentially bad heuristics. Might break in the future!
//
if (rule.issuer && rule.issuer.test) {
if (rule.issuer.test.source === "\\.(css|scss|sass)$") {
rule.issuer.test = /\.(css|scss|sass|less)$/;
}
}
});
// Push our new rules here since that's where Next.js
// expects stylesheet loaders to be found.
if (copyOfGlobalSassRule) {
rule.oneOf.push(copyOfGlobalSassRule);
}
rule.oneOf.push(copyOfModuleSassRule);
}
});
// Next.js skips .scss files coming from node_modules, but we
// need that since we load a bunch of .less files from antd.
// They do that by pattern matching the issuer of
// the import. We can simply delete the key.
delete copyOfModuleSassRule.issuer;
// Fix the module test pattern.
copyOfModuleSassRule.test = /\.less$/;
if (copyOfGlobalSassRule) {
copyOfGlobalSassRule.test = /\.less$/;
}
const lessLoader = {
loader: "less-loader",
options: {
// We use appendData to be able to override antd's styles.
// Our antd.less file is attached at the end of
// every .less file being imported from antd.
additionalData: (content, context) => {
// Match the name of the component being loaded from antd.
const match = context.resource.match(
/node_modules\/antd\/lib\/(style|(.+?)\/style)/
);
if (!match) {
return content;
}
// Override variables and mixins.
const theme = path.resolve("./src/styles/theme.less");
if (existsSync(theme)) {
context.addDependency(theme);
content += readFileSync(theme, "utf-8");
}
// Override component style.
const component = path.resolve(
`./src/styles/antd/${match[2] || match[1]}.less`
);
if (existsSync(component)) {
context.addDependency(component);
content += readFileSync(component, "utf-8");
}
return content;
},
lessOptions: {
paths: [path.resolve("./src/styles")],
// Antd's requirement albeit dangerous.
// ⚠ Watch out for user provided style being parsed by LESS.
javascriptEnabled: true,
},
},
};
// Switch the sass-loader (last entry) with our less-loader.
if (copyOfGlobalSassRule) {
copyOfGlobalSassRule.use.splice(-1, 1, lessLoader);
}
copyOfModuleSassRule.use.splice(-1, 1, lessLoader);
// Next.js use different rules for scoped and global CSS.
// We're using a single rule, but we can still only apply scope to
// files suffixed with `.module.less` using the `auto: true` option.
// @see https://github.com/webpack-contrib/css-loader/blob/bc7b18db5a46cf7446e45a11e13a8aeb83312c20/README.md#auto
copyOfModuleSassRule.use.find((loader) => {
if (loader.loader.indexOf("css-loader") > -1) {
loader.options.modules.auto = true;
return true;
}
});
// Next.js disables built-in Sass integration when it detects custom CSS loaders configured.
// Since we're emulating this built-in mechanism we need to disable this safety.
// Fortunately they make it quite easy.
// @see https://github.com/vercel/next.js/blob/96c3b087011f8395c887666c7593b48e2a85d4a6/packages/next/build/webpack-config.ts#L1130
config.plugins.find((plugin) => {
if (plugin.__next_css_remove) {
delete plugin.__next_css_remove;
return true;
}
});
config.optimization.minimizer.find((minimizer) => {
if (minimizer.__next_css_remove) {
delete minimizer.__next_css_remove;
return true;
}
});
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, opts);
}
return config;
},
});
}
module.exports = withLess;
@elado
Copy link

elado commented Apr 20, 2021

Thanks for that! I created an updated version that works with webpack5, fixed file-loader integration, server rendering, and keeps the original error messages for global css not in _app etc:

https://gist.github.com/elado/5891c94c1072cb9f18749761131a7a36

@haggen
Copy link
Author

haggen commented Apr 20, 2021

Thanks for that! I created an updated version that works with webpack5, fixed file-loader integration, server rendering, and keeps the original error messages for global css not in _app etc:

https://gist.github.com/elado/5891c94c1072cb9f18749761131a7a36

This is awesome! Thanks @elado!

@elado
Copy link

elado commented Apr 20, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment