Created
March 5, 2021 13:08
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
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!
Moved to a package:
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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