next-plugin-antd-less
Version:
Use Antd (with Less) with Next.js, Zero Dependency on other Next-Plugins.
540 lines (469 loc) • 15.6 kB
JavaScript
/* eslint-disable no-param-reassign, consistent-return, no-restricted-syntax */
const clone = require('clone');
const fs = require('fs');
const path = require('path');
const {
getCssModuleLocalIdentForNextJs,
loaderUtils,
} = require('./getCssModuleLocalIdent');
// const util = require('util'); // for debugInfo()
// fix: prevents error when .less files are required by node
if (require && require.extensions) {
require.extensions['.less'] = () => {};
}
/*
* 🌈 这里简要说一下基本原理,Next.js 和 CRA 通用
*
* 大概就是找到 sass-loader,然后 clone,最后 replace loader 的一些参数变成 less-loader
*
* */
/**
* checkIsNextJs
*
* @param webpackConfig
* @returns {boolean}
*/
function checkIsNextJs(webpackConfig) {
return Boolean(
webpackConfig &&
webpackConfig.resolveLoader &&
webpackConfig.resolveLoader.alias &&
(webpackConfig.resolveLoader.alias['next-babel-loader'] ||
webpackConfig.resolveLoader.alias['next-swc-loader']),
);
}
/**
* debugInfo
*
* @param nextConfig
* @param colorEmoji
* @param log
* @returns {string}
*/
function debugInfo(nextConfig, colorEmoji, log) {
const envText = nextConfig && nextConfig.isServer ? 'Server' : 'Client';
return `\n\n\n\n${colorEmoji} -------- ${envText} --------\n ${log}`;
}
/**
* overrideWebpackConfig
*
* @param webpackConfig
* @param nextConfig
* @param pluginOptions
* @returns {*}
*/
function overrideWebpackConfig({ webpackConfig, nextConfig, pluginOptions }) {
const isNextJs = checkIsNextJs(webpackConfig);
if (isNextJs && !nextConfig.defaultLoaders) {
throw new Error(
// eslint-disable-next-line max-len
'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade',
);
}
let __DEV__;
if (isNextJs) __DEV__ = nextConfig.dev;
else __DEV__ = webpackConfig.mode !== 'production';
const { rules } = webpackConfig.module;
// compatible w/ webpack 4 and 5
const ruleIndex = rules.findIndex((rule) => Array.isArray(rule.oneOf));
const rule = rules[ruleIndex];
// default localIdentName
let localIdentName = __DEV__ ? '[local]--[hash:base64:4]' : '[hash:base64:8]';
if (
pluginOptions &&
pluginOptions.cssLoaderOptions &&
pluginOptions.cssLoaderOptions.modules &&
pluginOptions.cssLoaderOptions.modules.localIdentName
) {
localIdentName = pluginOptions.cssLoaderOptions.modules.localIdentName;
}
let localIdentNameFollowDev = false;
if (
pluginOptions &&
pluginOptions.nextjs &&
pluginOptions.nextjs.localIdentNameFollowDev
) {
localIdentNameFollowDev = pluginOptions.nextjs.localIdentNameFollowDev;
}
/*
|--------------------------------------------------------------------------
| cssModule
|--------------------------------------------------------------------------
|
| delete default `getLocalIdent` and set `localIdentName`
|
*/
const cssModuleRegx = '/\\.module\\.css$/';
const cssModuleIndex = rule.oneOf.findIndex(
(item) => `${item.test}` === cssModuleRegx,
);
const cssModule = rule.oneOf[cssModuleIndex];
const cssLoaderInCssModule = cssModule.use.find((item) =>
`${item.loader}`.includes('css-loader'),
);
if (pluginOptions.cssLoaderOptions) {
cssLoaderInCssModule.options = {
...cssLoaderInCssModule.options,
...pluginOptions.cssLoaderOptions,
};
}
if (
pluginOptions.cssLoaderOptions &&
pluginOptions.cssLoaderOptions.modules
) {
cssLoaderInCssModule.options.modules = {
...cssLoaderInCssModule.options.modules,
...pluginOptions.cssLoaderOptions.modules,
};
}
/*
|--------------------------------------------------------------------------
| lessLoader (from the sassLoader clone)
|--------------------------------------------------------------------------
|
| Tips:
| sass has test `module` and `non-module` loader,
| but `less-loader` has `auto: true`, so just copy onec.
|
*/
// find
const sassLoaderIndex = rule.oneOf.findIndex(
(item) => item.test.toString() === /\.module\.(scss|sass)$/.toString(),
);
const sassLoader = rule.oneOf[sassLoaderIndex];
// clone
const lessLoader = clone(sassLoader);
lessLoader.test = /\.less$/;
delete lessLoader.issuer;
// overwrite
const lessLoaderIndex = lessLoader.use.findIndex((item) =>
`${item.loader}`.includes('sass-loader'),
);
// merge lessLoader options
const lessLoaderOptions = {
lessOptions: {
javascriptEnabled: true,
},
...pluginOptions.lessLoaderOptions,
};
/*
|--------------------------------------------------------------------------
| file-loader supported *.less
|--------------------------------------------------------------------------
|
| url()s fail to load files
| https://github.com/SolidZORO/next-plugin-antd-less/issues/39
|
*/
const fileLoaderIndex = rule.oneOf.findIndex((item) => {
if (
item.use &&
item.use.loader &&
item.use.loader.includes('/file-loader/')
) {
return item;
}
});
const fileLoader = rule.oneOf[fileLoaderIndex];
if (fileLoader) {
// RAW ---> issuer: /\.(css|scss|sass)$/,
fileLoader.issuer = /\.(css|scss|sass|less)$/;
}
/*
|--------------------------------------------------------------------------
| noop-loader supported *.less (Next.js ONLY)
|--------------------------------------------------------------------------
|
*/
if (isNextJs) {
const noopLoaderIndex = rule.oneOf.findIndex((item) => {
if (
item &&
item.test &&
item.test.toString() ===
// RAW test
/\.(css|scss|sass)(\.webpack\[javascript\/auto\])?$/.toString()
) {
return item;
}
});
const noopLoader = rule.oneOf[noopLoaderIndex];
if (noopLoader) {
noopLoader.test =
/\.(css|scss|sass|less)(\.webpack\[javascript\/auto\])?$/;
}
}
/*
|--------------------------------------------------------------------------
| next-image-loader supported *.less (Next.js ONLY)
|--------------------------------------------------------------------------
|
| TODO:
|
| Modify this to enable *.less to use background-image.
| The
| But I don't know why it only takes effect in dev, when prod will prompt `Error: Module parse failed: Unexpected character '�' (1:0)`.
| This should actually be a less-loader problem, but considering that the CRA is still wp4 over there, it's too late to upgrade to wp5.
| I'm not sure if this is a problem with CRA, but I'm not sure if it's a problem with Next.js, so I'll just leave it alone.
| I don't know what to do. This Next.js my head is big, with the black box, just a patch version upgrade, you can make a bunch of plug-ins crash, my mind also collapsed, tired ah ......
|
| 修改这里就可以实现 *.less 使用 background-image 了。
|
| 但是不知道为什么只在 dev 生效,prod 时会提示 `Error: Module parse failed: Unexpected character '�' (1:0)`
| 这个其实应该是 less-loader 的问题,但是考虑到 CRA 那边还是 wp4 迟迟没有升级到 wp5,
| 且反正 Next.js 这边 prod 也会挂掉,所以干脆不处理了。
| 妈的!弄这 Next.js 我头都大,跟黑盒似的,随便来一个 patch 版本升级,就能让一堆插件崩掉,我的心态也崩了,累啊……
|
*/
// if (isNextJs) {
// const nextImageLoaderIndex = rules.findIndex(
// (item) => item && item.loader && item.loader === 'next-image-loader',
// );
//
// const nextImageLoader = rules[nextImageLoaderIndex];
//
// if (nextImageLoader) {
// // RAW ---> issuer: { not: /\.(css|scss|sass)(\.webpack\[javascript\/auto\])?$/ },
// nextImageLoader.issuer = {
// not: /\.(css|scss|sass|less)(\.webpack\[javascript\/auto\])?$/,
// };
// }
// }
/*
|--------------------------------------------------------------------------
| ignore-loader supported *.less (Next.js Server ONLY)
|--------------------------------------------------------------------------
|
*/
if (isNextJs) {
const ignoreLoaderIndex = rule.oneOf.findIndex(
(item) =>
item &&
item.use &&
item.use.includes &&
item.use.includes('ignore-loader'),
);
const ignoreLoader = rule.oneOf[ignoreLoaderIndex];
if (ignoreLoader) {
// RAW ---> test: [ /(?<!\.module)\.css$/, /(?<!\.module)\.(scss|sass)$/ ],
ignoreLoader.test = [
/(?<!\.module)\.css$/,
/(?<!\.module)\.(scss|sass|less)$/,
];
}
}
//
//
//
//
//
//
//
//
/*
|--------------------------------------------------------------------------
| modifyVars (Hot Reload is **NOT Supported**, NEED restart webpack)
|--------------------------------------------------------------------------
|
| CONSTANTS --> e.g. `@THEME--DARK: 'theme-dark';`
| `:global(.@{THEME--DARK}) { color: red }`
|
*/
let modifyVars = undefined;
if (pluginOptions.modifyVars) {
modifyVars = pluginOptions.modifyVars;
}
if (pluginOptions.modifyVars) {
lessLoaderOptions.lessOptions.modifyVars = modifyVars;
}
/*
|--------------------------------------------------------------------------
| lessVarsFilePath (Hot Reload is **Supported**, can overwrite `antd` vars)
|--------------------------------------------------------------------------
|
| variables file --> e.g. `./styles/variables.less`
| `@primary-color: #04f;`
|
*/
if (pluginOptions.lessVarsFilePath) {
lessLoaderOptions.additionalData = (content) => {
const lessVarsFileResolvePath = path.resolve(
pluginOptions.lessVarsFilePath,
);
if (fs.existsSync(lessVarsFileResolvePath)) {
const importLessLine = `@import '${lessVarsFileResolvePath}';`;
// https://github.com/SolidZORO/next-plugin-antd-less/issues/40
if (pluginOptions.lessVarsFilePathAppendToEndOfContent) {
content = `${content}\n\n${importLessLine};`;
} else {
content = `${importLessLine};\n\n${content}`;
}
// console.log(content);
}
return content;
};
}
// console.log(debugInfo(nextConfig, '🟡', 'lessLoaderOptions'));
// console.log(util.inspect(lessLoaderOptions, false, null, true));
lessLoader.use.splice(lessLoaderIndex, 1, {
// https://github.com/webpack-contrib/less-loader#options
loader: 'less-loader',
options: lessLoaderOptions,
});
//
//
//
// ---- cssLoader In LessModule ----
// find
const cssLoaderInLessLoaderIndex = lessLoader.use.findIndex((item) =>
`${item.loader}`.includes('css-loader'),
);
const cssLoaderInLessLoader = lessLoader.use.find((item) =>
`${item.loader}`.includes('css-loader'),
);
// 🔰 Compatibility CRA v5.0
//
// find and delete `resolve-url-loader`
//
// in CRA v5.0, `sass-loader` uses `resolve-url-loader` by default,
// but `less-loader` doesn't need it and will throw an ERROR if it does
const resolveUrlLoaderInLessLoaderIndex = lessLoader.use.findIndex((item) =>
`${item.loader}`.includes('resolve-url-loader'),
);
lessLoader.use.splice(resolveUrlLoaderInLessLoaderIndex, 1);
// clone
const cssLoaderClone = clone(cssLoaderInLessLoader);
let getLocalIdentFn = (context, _, exportName, options) =>
getCssModuleLocalIdentForNextJs(
context,
_,
exportName,
options,
__DEV__,
localIdentNameFollowDev,
);
if (
pluginOptions &&
pluginOptions.cssLoaderOptions &&
pluginOptions.cssLoaderOptions.modules &&
pluginOptions.cssLoaderOptions.modules.getLocalIdent
) {
getLocalIdentFn = pluginOptions.cssLoaderOptions.modules.getLocalIdent;
}
// merge CssModule options
cssLoaderClone.options = {
...cssLoaderClone.options,
sourceMap: Boolean(__DEV__),
...pluginOptions.cssLoaderOptions,
//
modules: {
localIdentName,
// Inherited from Raw NextJs cssModule
...cssLoaderClone.options.modules,
//
// if enable `local` mode, you can write this less
//
// ```styles.module.less
// .abc { <---- is local, match class='abc--nx3xc2'
// color: red;
//
// :global {
// .xyz { <---- is global, match class='xyz'
// color: blue;
// }
// }
// }
//
mode: 'local', // local, global, and pure, next.js default is `pure`
//
// Inherited from pluginOptions
...(pluginOptions.cssLoaderOptions || {}).modules,
//
// recommended to keep `true`!
auto: true,
// Next.js need getLocalIdent (non-full-featured localIdentName 😂)
// CRA Don't need it (full-featured localIdentName)
getLocalIdent: isNextJs ? getLocalIdentFn : undefined,
},
};
// console.log(debugInfo(nextConfig, '🟢', 'cssModuleOptions'));
// console.log(util.inspect(cssLoaderClone.options, false, null, true));
// overwrite
lessLoader.use.splice(cssLoaderInLessLoaderIndex, 1, cssLoaderClone);
//
//
//
// ---- append lessLoader to webpack modules ----
rule.oneOf.splice(sassLoaderIndex, 0, lessLoader);
webpackConfig.module.rules[ruleIndex] = rule;
//
//
// ---- handleAntdInServer (ONLY Next.js) ----
if (isNextJs) {
webpackConfig = handleAntdInServer(webpackConfig, nextConfig);
if (typeof pluginOptions.webpack === 'function')
return pluginOptions.webpack(webpackConfig, nextConfig);
}
// console.log(debugInfo(nextConfig, '🟣', 'webpackConfig.module.rules'));
// console.log(util.inspect(webpackConfig.module.rules, false, null, true));
return webpackConfig;
}
/**
* isWebpack5
*
* @param nextConfig
* @returns {boolean}
*/
function isWebpack5(nextConfig) {
return (
typeof nextConfig.webpack.version === 'string' &&
nextConfig.webpack.version.startsWith('5')
);
}
/**
* handleAntdInServer
*
* @param webpackConfig
* @param nextConfig
* @returns {*}
*/
function handleAntdInServer(webpackConfig, nextConfig) {
if (!nextConfig.isServer) return webpackConfig;
const ANTD_STYLE_REGX = /(antd\/.*?\/style|@ant-design).*(?<![.]js)$/;
const exts = [...webpackConfig.externals];
webpackConfig.externals = isWebpack5(nextConfig)
? [
// ctx and cb are both webpack5's params
// ctx eqauls { context, request, contextInfo, getResolve }
// https://webpack.js.org/configuration/externals/#function
(ctx, cb) => {
if (ctx.request.match(ANTD_STYLE_REGX)) return cb();
// next's params are different when webpack5 enable
// https://github.com/vercel/next.js/blob/0425763ed6a90f4ff99ab2ff37821da61d895e09/packages/next/build/webpack-config.ts#L770
if (typeof exts[0] === 'function') return exts[0](ctx, cb);
else return cb();
},
...(typeof exts[0] === 'function' ? [] : exts),
]
: [
// webpack4
(ctx, req, cb) => {
if (req.match(ANTD_STYLE_REGX)) return cb();
if (typeof exts[0] === 'function') return exts[0](ctx, req, cb);
else return cb();
},
...(typeof exts[0] === 'function' ? [] : exts),
];
webpackConfig.module.rules.unshift({
test: ANTD_STYLE_REGX,
use: 'null-loader',
});
return webpackConfig;
}
module.exports = {
overrideWebpackConfig,
handleAntdInServer,
//
loaderUtils,
getCssModuleLocalIdentForNextJs,
};