Mysterious Arco Styles Emerge to Solve Unexpected Referencing Problems with Webpack
Webpack
Is a modern static resource modular management and packaging tools , it can be configured through the plug-in processing and packaging of a variety of file formats , to generate optimized static resources , the core principle is that a variety of resource files as a module , through the configuration file to define the dependencies between the module and the processing rules , so as to achieve modular development .Webpack
Provides a powerful plug-in and loader system that supports efficient build capabilities such as code splitting, hot loading and code compression, significantly improving development efficiency and performance.Webpack Resolve
beWebpack
The configuration item for resolving module paths in theWebpack
How to find and locate modules, the core functionality is configured to define the finding mechanism and prioritization of modules, thus ensuring that theWebpack
Ability to find and load dependent modules correctly.
descriptive
First of all, let's talk about the background of the story, in some time next door to the old man needs to be about five years ago the project gradually development refactoring new version2.0
Usually, if we develop a new version, we may start a new project from scratch and reuse the component modules in the new project, but due to the time constraints of the new project and the complexity of the project structure, the modules that need to be modified and added in the initial version of the planning is not the majority of the comprehensive assessment of the cost of developing a new version from scratch is too high, so the finalized plan is to gradually transition to a new version of the old version of the project. Therefore, the finalized plan is to gradually transition to the new version on the old version.
Of course, if you just modify and add new modules on the original project can not be called refactoring the new version, the details of the program is to gradually introduce new components on the original project, and these new components are in the newpackage
is implemented in theStoryBook
The first reason to do this is to ensure the independence of the new component so that it can gradually replace the existing component module. The reason for this is firstly to ensure the independence of the new component, so that it can gradually replace the existing component module, and also because the new component still needs to be released here.SDK
package for external access, which makes it easier for us to reuse these components.
Then this thing could have been carried out in an orderly manner, in the process of the development of new components also went very smoothly, however, when we need to introduce the new components into the original project, there is a problem here, in fact, in retrospect this problem is not very complex, but in the unfamiliar with theWebpack
as well asLess
In this case, it does take some time to deal with it. Coinciding with Friday, was a happy weekend, but the problem was successfully solved in a little more than two days, after solving the problem there will be this article, of course, the solution is not just the method mentioned in the article, but also hope to be able to encounter similar problems for the students to bring some reference.
This problem mainly occurs in the style of processing, five years ago, the project for some of the configuration is really not suitable for the current component module. So after the discovery of the problem, we have to enter the classic troubleshooting phase, after retrieving the cause of the exception thrown, elimination method to locate the problem components, and constantly generate and locate the problem configurations, and ultimately decided to use theWebpack
The problem is that we can't force changes on the third-party dependencies referenced by the component. In fact, if it's entirely our own code, we can just modify it if it doesn't fit, but the problem is in the third-party dependencies referenced by the component, which we can't force to be modified, so we have to use theWebpack
s ability to address third-party dependencies. Taken together, the following three main issues are addressed in the article.
-
less-loader
Style Citation Problem: Since this is a minimum five year old project, for theWebpack
management is still using an older version of theUmi
scaffolding, andless-loader
be5.0.0
version, and the latest version is now up to12.2.0
, in which the configuration and handling has changed considerably, so here's what needs to be addressedless-loader
The problem of handling styles for the - Component style override problem: often use the component library students should know, in the company's internal standardization of style specification, is not able to re-introduce the original style content, otherwise there will be style override problems, but released to the
Npm
packages do not always adhere to this specification, they may still refer to older style files, so we need to avoid the resulting style overrides. - Dependencies dynamically introduced: In fact, after we solved the above problem, the problem with the style section is over, and here too, a new problem is derived, where we are essentially dealing with the
Webpack
module references, then in other scenarios, such as when we need to introduce specialized dependencies for services deployed offshore, or compilation issues caused by ghost dependencies, the problem of dynamically introducing dependencies needs to be addressed.
For each of the three questions useWebpack
Realized relevantDEMO
The relevant code is in the/WindrunnerMax/webpack-simple-environment/tree/master/packages/webpack-resolver
Center.
LessLoader
So let's take a look.less-loader
The problem is that when we open theNpm
locate[email protected]
(used form a nominal expression)README
document, you can see that thewebpack resolver
The section makes it clear that if there is a need to start fromnode_modules
If you are referencing a style in a file, you need to prepend the reference path with the~
of the symbols so that theless-loader
Being able to correctly select fromnode_modules
references style files in it, otherwise they are all considered relative path imports.
@import "~@arco-design/web-react/es/style/";
In our project, its own dependencies are fine, and since it compiles and passes then it must be in the.less
The files are all introduced carrying the~
flag, but the current style file introduced in our new component does not carry the~
logo, which leads to theless-loader
The location of the style file could not be resolved correctly, thus throwing a module not found exception. If it was simply the case that the styles in our new component didn't carry the logo, we could have added it manually, however after troubleshooting this part is caused by the newly introduced component and is still a dependency of the dependency, which leads to the fact that we can't directly modify the style introduction to solve this problem.
The first thing that comes to mind when dealing with this type of problem is definitely an upgrade!less-loader
version, but unfortunately when upgrading to the latest12
After the release, the project also didn't run, the problem was probably a conflict with some of the root dependencies, which threw some very strange exceptions, after retrieving this error message for a while, I finally gave up on the upgradeless-loader
After all, if we take the bull by the horns we need to keep trying various dependency versions, which takes a lot of time to test and doesn't always solve the problem.
At this point we need to think differently, since it's essentially stillless-loader
of the problem, and theloader
essentially by processing the raw content of the various resource files, so can't we do that in a direct implementation ofloader
(coll.) come inless-loader
pretreatment.less
file, add all relevant style references to the~
logo so that it can be used in theless-loader
Before placing the correct.less
The file is handled. So the idea here is that after parsing to the reference.less
documentation.js
file, match it and add it to the~
The markup here is just a simple representation of regular matching, and the actual situation to be considered will be a bit more complex.
/**
* @typedef {Record<string, unknown>} OptionsType
* @this {import("webpack").LoaderContext<OptionsType>}
* @param {string} source
* @returns {string}
*/
= function (source) {
const regexp = /@import\s+"@arco-design\/web-react\/(.*)\/index\.less";/g;
const next = (regexp, '@import "~@arco-design/web-react/$1/";');
return next;
};
Theoretically, there is no problem in this way, but in the actual use of the process found that there is still the case of reporting errors, only that the error file has changed. After analyzing this, I found that this is because the.less
Internal style references in the file are made by theless-loader
handled, and we wrote theloader
Just for the entrance..less
The file was processed, and the deep.less
The file is not preprocessed by us and still throws the module not found exception. In fact, it was also found here that the previous use of theless
The misconception that if we are in.less
If the style is referenced randomly in the file, it will be repackaged even if it's not used, because the separate.less
entries end up generating a single.css
hand over to a successorloader
Processing.
/* ok */
/* import "./"; */
/* ok */
@import "@arco-design/web-react-pro/es/style/";
/* @arco-design/web-react-pro/es/style/ error */
@import "@arco-design/web-react/es/Button/style/";
In this case where there are multiple levels of style references, it seems that all we can do to deal with it is to focus on theless-loader
It's not easy to see how this can happen, but in practice, it's only possible with complex business component library references or multilevelUI
The specification is only possible in the case of a specification. But since it's already in our program it has to be fixed, and fortunatelyless-loader
itself supports pluginization, and we can do this by implementing theless-loader
(used form a nominal expression)loader
to deal with this problem, except that since the documentation is not perfect, we can only refer to the source code of other plugins to implement.
Here we'll refer to theless-plugin-sass2less
to achieve that.less-loader
plugin is actually an object in which we can define theinstall
method, where the second argument is the manager instance of the plugin, by calling here theaddPreProcessor
method to join our preprocessor object, which implements theprocess
method will suffice, so that we can implement ourless-loader
(used form a nominal expression)loader
. And forprocess
The idea of the function is a bit simpler, where we can follow the\n
cuts to determine if a string is relevant to a third-party library when processing it@import
statement, and if so add it to the~
marking, and since this is in theless-loader
If the object is processed in a style file, the reference path must be the style file, and there is no need to consider non-style content references. At the same time in order to increase the generality, we can also need to deal with the name of the component library in the instantiation of the object passed in, of course, because it is biased towards business data processing, the generality can not be necessary very high.
// packages/webpack-resolver/src/less/
= class LessImportPrefixPlugin {
constructor(prefixModules) {
= prefixModules || [];
= [2, 7, 1];
}
/**
* @param {string} source
* @param {object} extra
* @returns {string}
*/
process(source) {
const lines = ("\n");
const next = (line => {
const text = ();
if (!("@import")) return line;
const result = /@import ['"](.+)['"];?/.exec(text);
if (!result || !result[1]) return line;
const uri = result[1];
for (const it of ) {
if ((it)) return `@import "~${uri}";`;
}
return line;
});
return ("\n");
}
install(less, pluginManager) {
({ process: (this) }, 3000);
}
};
plugin has been implemented, we need to do the same in theless-loader
It's a good idea to configure it in the project, but in fact, because of the project timeUmi
scaffolding is built, modifications to the configuration must be made with the help of thewebpack-chain
, it's still a bit of a hassle if you're not familiar with it, so we're going to go straight for it here atrules
(used form a nominal expression)less-loader
Just configure the plugin in the
// packages/webpack-resolver/
= {
// ...
module: {
rules: [
{
test: /\.less$/,
use: [
,
"css-loader",
{
loader: "less-loader",
options: {
plugins: [new LessImportPrefix(["@arco-design/web-react"])],
},
},
],
},
// ...
],
},
// ...
};
So far we've used theless-loader
(used form a nominal expression)loader
solves the problem of resolving style references, and in fact, if we don't resort to theless-loader
The words can still be continuedwebpack-loader
idea to solve the problem, when we find the problem with style references, we can implement theloader
Avoiding deep internal calls and handing them over to the project's root directory to re-reference the styles will also solve the problem, but requires us to manually analyze the dependencies and introduce them, which takes some time and cost.
WebpackLoader
When the solution to theless-loader
The project was up and running successfully after the adaptation issues, but during the debugging process we found a new problem. Usually, our projects are usually built directly into theArcoDesign
used as a component library, and a unified design specification was introduced later internally, this new specification was introduced in theArcoDesign
We'll just name it for now.web-react-pro
, and introduces a new set of styling, then this creates new problems with style overrides if referenced in the project in the wrong order.
// ok
import "@arco-design/web-react/es/style/";
import "@arco-design/web-react-pro/es/style/";
// error
import "@arco-design/web-react-pro/es/style/";
import "@arco-design/web-react/es/style/";
as a matter of factweb-react-pro
internally has helped us to actually reference theweb-react
However, due to the fact that not all projects follow the new design specification as mentioned earlier, especially the style references of many historical tripartite libraries, this leads to an uncontrollable sequence of introductions, which leads to style overriding issues, especially since our projects are usually configured to refer to on-demand references, which results in some component design specifications being new, some component styles being old, and the main page still having the new style. The style of some components is old, in the main page or the new style, after opening the form you will find that the style of the component has obviously changed, the overallUI
It will seem more confusing.
Here at the beginning of the idea is to find out exactly which three-way library caused the problem, however, due to the complexity of the project reference relationship, the scanning of the conventional route will also lead to the actual components not referenced are still being compiled, the dichotomous exclusion method to find the process of finding a lot of time, but of course ultimately located to the problem of the form engine components. Then continue to imagine, now the problem is nothing more than the style loading order of the problem, if we take the initiative to control the reference to theweb-react
style isn't going to solve this problem, except for controlling theimport
In addition to the order of thelazy-load
The form of the relevant component libraries will be referenced to the project, that is, the original components are loaded first, and then the new components are loaded afterward to avoid the new style overrides.
import App from "...";
import React, { Suspense, lazy } from "react";
const Next = lazy(() => import("./component/next"));
export const LazyNextMain = (
<Suspense fallback={<></>}>
<Next />
</Suspense>
);
However, it is clear that this will only solve the problem temporarily, and if you need to add new components directly into theweb-react
style, such as the need to continue to extend functionality based on the form engine, or the introduction of a document preview component, will require the indirect introduction of theweb-react
If you continue to follow this pattern you will need to continuallylazy
component. So to switch things around, can we just add theWebpack
level to deal with these issues directly, and if we can directly transfer theweb-react
patternresolve
to an empty file, then that solves the problem.
In fact, due to the prevalence of this problem, there is an internalWebpack
plugin to take care of this, but referencing it in our project will have a negative effect on themini-css-extract-plugin
The same program was abandoned after a period of fruitless troubleshooting, causing a very strange exception to be thrown. When it comes to handling references, we probably first think of thebabel-import-plugin
This plugin, then we can similarly implement thebabel
plugin to handle this, and because of the simplicity of the scenario, it doesn't require too much complex processing logic.
// packages/webpack-resolver/src/loader/
/**
* @param {import("@babel/core") babel}
* @returns {import("@babel/core").PluginObj<{}>}
*/
= function (babel) {
const { types: t } = babel;
return {
visitor: {
ImportDeclaration(path) {
const { node } = path;
if (!node) return;
if ( === "@arco-design/web-react/dist/css/") {
= (("./"));
}
},
CallExpression(path) {
if (
=== "require" &&
=== 1 &&
([0]) &&
[0].value === "@arco-design/web-react/dist/css/"
) {
[0] = (("./"));
}
},
},
};
};
Here we only need to deal withimport
statement corresponding to theImportDeclaration
as well asrequire
statementCallExpression
When we match the plugin in question, we can just replace it with the empty style file of the target, which is equivalent to wiping out all theweb-react
style references as a way to solve the style override problem. And adding this plugin to thebabel
It is also only necessary to add the.babelrc
Configure it in the fileplugin
A citation will suffice.
// packages/webpack-resolver/.babelrc
{
"plugins": ["./src/loader/"]
}
So is there any other way we can think about solving a similar problem, if our project is not using thebabel
Instead, it's throughESBuild
orSWC
to compilejs
file, then what to do with it. The essence of this, according to our current thinking, is that the target of the.less
file reference redirection to an empty style file, then we can absolutely continue to use theloader
to deal with the idea that actuallybabel-loader
It also just helps us to compile the plain text asAST
Getting structured data facilitates us to adjust the output using plugins.
Then if, in accordance with something likebabel-loader
We still need to parse theimport
and other statements, it would still be more cumbersome, whereas if you think differently and deal directly with the.less
file, if the absolute path to this file is from theweb-react
introduced in the.less
file to introduce styles, we can similarly take theless-loader
(used form a nominal expression)loader
Go deal with this.
// packages/webpack-resolver/src/loader/
/**
* @typedef {Record<string, unknown>} OptionsType
* @this {import("webpack").LoaderContext<OptionsType>}
* @param {string} source
* @returns {string}
*/
= function (source) {
const regexp = /@arco-design\/web-react\/.+\.less/;
if (()) {
return "@empty: 1px;";
}
return source;
};
Don't look at this.loader
The implementation is simple, but it does help us with style coverage, and high-end ingredients often require only the simplest of cooking methods. So right after that we just need to set theloader
Configure towebpack
in the center can be used, since we are directly configuring the, it can be relatively easy to add rules if the
webpack-chain
etc. is still more efficient to create a new rule.
// packages/webpack-resolver/
/**
* @typedef {import("webpack").Configuration} WebpackConfig
* @typedef {import("webpack-dev-server").Configuration} WebpackDevServerConfig
* @type {WebpackConfig & {devServer?: WebpackDevServerConfig}}
*/
= {
// ...
module: {
rules: [
{
test: /\.less$/,
use: [
,
"css-loader",
"less-loader",
("./src/loader/import-loader"),
],
},
// ...
],
},
// ...
};
WebpackResolver
Here we have our style introduction problem solved, and to summarize we are actually dealing with it in various waysWebpack
(used form a nominal expression)Resolve
The problem, as mentioned at the very beginning, is not a complex problem, but just a part of our unfamiliarity with this capability that leads to the need to explore the problem and the solution. So in theWebpack
hit the nail on the headResolve
Is there any more general solution to the problem that is actually in theWebpack
It is provided in theThis configuration item, by which we can define the
Resolve
plugin so that it can be used in theWebpack
(used form a nominal expression)Resolve
Stage processing module lookup and parsing.
Let's first envision a scenario when our project requires a proprietary deployment service, for example, we need to introduce a proprietary version of a dependency offshore that is mainlyAPI
There is no difference between the proprietary version and the generic version, mainly some compliant data reporting interfaces, etc. However, the problem is that the package names of the proprietary version and the generic version are not the same, so if we want to deal with this issue directly at compile time instead of having to manually maintain the version, a feasible solution would be to script the relevant dependencies before compiling.alias
For overseas package versions, if there are deeper dependencies, you also need to lock the version through the package manager, so that you can solve the problem of maintaining multiple versions.
//
{
"dependencies": {
// common
"package-common": "1.0.0",
// oversea
"package-common": "npm:[email protected]",
}
}
However, through scripts that are constantly modifiedconfiguration is still a bit cumbersome, and you still need to reinstall the dependency every time you change it, which is obviously not friendly enough, so we can consider a more elegant way.
pre-installed
common
cap (a poem)oversea
version dependencies, and then add theWebpack
(used form a nominal expression)file to dynamically modify the relevant dependencies in the
alias
。
= {
resolve: {
alias: {
"package-common": ? "package-oversea" : "package-common",
},
},
}
Then if we need more granular control, for example due to ghost dependencies we can't have all the package versions ofalias
for the unified version, which I encountered last yearYarn+
rounding differenceThe problem of conflict of dependence has, for the most part, been
3
version, and some of the dependencies are those that require2
The version was instead incorrectlyresolve
until (a time)3
version, in which case it is necessary to control certain modules of theresolve
behavior to address such issues.
unfamiliarvite
The students of the program know that based on the@rollup/plugin-alias
The plug-in is available in thevite
hit the nail on the headalias
More advanced configurations are also available, which can support our dynamic handling of thealias
Behavior, exceptfind/replace
This regularity-based parsing is in addition to the support for passing theResolveFunction/ResolveObject
form is used to handle theRollup
parseableHook
Behavior.
// rollup/dist/
export type ResolveIdResult = string | NullValue | false | PartialResolvedId;
export type ResolveIdHook = (
this: PluginContext,
source: string,
importer: string | undefined,
options: { attributes: Record<string, string>; custom?: CustomPluginOptions; isEntry: boolean }
) => ResolveIdResult;
// vite/dist/node/
interface ResolverObject {
buildStart?: PluginHooks['buildStart']
resolveId: ResolverFunction
}
interface Alias {
find: string | RegExp
replacement: string
customResolver?: ResolverFunction | ResolverObject | null
}
type AliasOptions = readonly Alias[] | { [find: string]: string }
as a matter of factWebpack
There is also a built-inNormalModuleReplacementPlugin
plugin to handle the replacement of module references more flexibly, by directly calling thenew (resourceRegExp, newResource)
That's all, it's important to note thatnewResource
is supported in function form, so if you need to modify its behavior it is straightforward to modify it in place.context
parameter object is sufficient, and thecontext
Parameters carry a lot of information, and it is entirely possible to determine the source of parsing with the information they carry.
// webpack/
// /webpack/webpack/blob/main/lib/
declare interface ModuleFactoryCreateDataContextInfo {
issuer: string;
issuerLayer?: null | string;
compiler: string;
}
declare interface ResolveData {
contextInfo: ModuleFactoryCreateDataContextInfo;
resolveOptions?: ResolveOptions;
context: string;
request: string;
assertions?: Record<string, any>;
dependencies: ModuleDependency[];
dependencyType: string;
createData: Partial<NormalModuleCreateData & { settings: ModuleSettings }>;
fileDependencies: LazySet<string>;
missingDependencies: LazySet<string>;
contextDependencies: LazySet<string>;
/**
* allow to use the unsafe cache
*/
cacheable: boolean;
}
declare class NormalModuleReplacementPlugin {
constructor(resourceRegExp: RegExp, newResource: string | ((arg0: ResolveData) => void));
}
NormalModuleReplacementPlugin
attributableNormalModuleFactory
(used form a nominal expression)beforeResolve
is implemented, however, there is a limitation in that it can only handle dependency resolution for our application itself, whereas, for example, in our first problem, the[email protected]
It's active dispatch.method to perform file parsing, i.e. this is the
loader
leveragewebpack
ability to fulfill its own file parsing needs, while theNormalModuleReplacementPlugin
is unable to deal with this situation.
// [email protected]/dist/
const resolve = pify((loaderContext));
loadFile(filename, currentDirectory, options) {
// ...
const moduleRequest = (url, (0) === '/' ? '' : null);
const context = (trailingSlash, '');
let resolvedFilename;
return resolve(context, moduleRequest).then(f => {
resolvedFilename = f;
(resolvedFilename);
if ((resolvedFilename)) {
return readFile(resolvedFilename).then(contents => ('utf8'));
}
return loadModule([stringifyLoader, resolvedFilename].join('!')).then();
// ...
})
// ...
}
Then it's time to use ourup, and we can put the
resolve
is treated entirely as a separate module, and is of course itself based on theenhanced-resolve
and the plugin we've implemented here is the equivalent of implementing the parsing behavior for theHook
, so even if something likeless-loader
This independently scheduled plugin also schedules properly, and this configuration is not available on thewebpack2
It's already implemented in the Then we can build on this capability inbefore-hook
hooks to solve the second problem we mentioned earlier, that of style overrides.
// packages/webpack-resolver/src/resolver/
= class ImportResolver {
constructor() {}
/**
* @typedef {Required<import("webpack").Configuration>["resolve"]} ResolveOptionsWebpackOptions
* @typedef {Exclude<Required<ResolveOptionsWebpackOptions>["plugins"]["0"], "...">} ResolvePluginInstance
* @typedef {Parameters<ResolvePluginInstance["apply"]>["0"]} Resolver
* @param {Resolver} resolver
*/
apply(resolver) {
const target = ("resolve");
resolver
.getHook("before-resolve")
.tapAsync("ImportResolverPlugin", (request, resolveContext, callback) => {
const regexp = /@arco-design\/web-react\/.+\.less/;
const prev = ;
const next = ("./");
if ((prev)) {
const newRequest = { ...request, request: next };
return (
target,
newRequest,
`Resolved ${prev} to ${next}`,
resolveContext,
callback
);
}
return callback();
});
}
};
// packages/webpack-resolver/
= {
// ...
resolve: {
plugins: [new ImportResolver()],
},
// ...
}
Because of its impact onless-loader
will also take effect, and we can similarly match parse the content and process it to the correct reference address, so that we don't have to implement theless-loader
(used form a nominal expression)loader
to deal with this problem, that is, we can solve two problems at the same time through a plugin. And the differential parsing problem mentioned earlier can also be handled by therequest
together withresolveContext
parameter to determine the source, and thus deal with compilation problems caused by references or phantom dependencies under certain conditions, and so on.
// => @import "@arco-design/web-react/es/style/"
{
context: {},
path: '/xxx/webpack-simple-environment/packages/webpack-resolver/src/less',
request: './@arco-design/web-react/es/style/'
}
// => import "./"
{
context: {
issuer: '/xxx/webpack-simple-environment/packages/webpack-resolver/src/less/',
issuerLayer: null,
compiler: undefined
},
path: '/xxx/webpack-simple-environment/packages/webpack-resolver/src/less',
request: './'
}
question of the day
/WindrunnerMax/EveryDay
consultation
/api/loaders
/configuration/resolve/#resolveplugins
/webpack/enhanced-resolve?tab=readme-ov-file#plugins
/less/less-docs/blob/master/content/tools/
/less/less-docs/blob/master/content/features/
/jamiebuilds/babel-handbook/blob/master/translations/en/