Location>code7788 >text

Mysterious Arco Styles Emerge to Solve Unexpected Referencing Problems with Webpack

Popularity:745 ℃/2024-08-06 10:35:45

Mysterious Arco Styles Emerge to Solve Unexpected Referencing Problems with Webpack

WebpackIs 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 .WebpackProvides 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 ResolvebeWebpackThe configuration item for resolving module paths in theWebpackHow to find and locate modules, the core functionality is configured to define the finding mechanism and prioritization of modules, thus ensuring that theWebpackAbility 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.0Usually, 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 newpackageis implemented in theStoryBookThe 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.SDKpackage 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 theWebpackas well asLessIn 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 theWebpackThe 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 theWebpacks ability to address third-party dependencies. Taken together, the following three main issues are addressed in the article.

  • less-loaderStyle Citation Problem: Since this is a minimum five year old project, for theWebpackmanagement is still using an older version of theUmiscaffolding, andless-loaderbe5.0.0version, 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-loaderThe 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 theNpmpackages 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 theWebpackmodule 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 useWebpackRealized relevantDEMOThe relevant code is in the/WindrunnerMax/webpack-simple-environment/tree/master/packages/webpack-resolverCenter.

LessLoader

So let's take a look.less-loaderThe problem is that when we open theNpmlocate[email protected](used form a nominal expression)READMEdocument, you can see that thewebpack resolverThe section makes it clear that if there is a need to start fromnode_modulesIf you are referencing a style in a file, you need to prepend the reference path with the~of the symbols so that theless-loaderBeing able to correctly select fromnode_modulesreferences 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.lessThe 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-loaderThe 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-loaderversion, but unfortunately when upgrading to the latest12After 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-loaderAfter 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-loaderof the problem, and theloaderessentially by processing the raw content of the various resource files, so can't we do that in a direct implementation ofloader(coll.) come inless-loaderpretreatment.lessfile, add all relevant style references to the~logo so that it can be used in theless-loaderBefore placing the correct.lessThe file is handled. So the idea here is that after parsing to the reference.lessdocumentation.jsfile, 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.lessInternal style references in the file are made by theless-loaderhandled, and we wrote theloaderJust for the entrance..lessThe file was processed, and the deep.lessThe 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 thelessThe misconception that if we are in.lessIf the style is referenced randomly in the file, it will be repackaged even if it's not used, because the separate.lessentries end up generating a single.csshand over to a successorloaderProcessing.

/*  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-loaderIt's not easy to see how this can happen, but in practice, it's only possible with complex business component library references or multilevelUIThe 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-loaderitself supports pluginization, and we can do this by implementing theless-loader(used form a nominal expression)loaderto 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-sass2lessto achieve that.less-loaderplugin is actually an object in which we can define theinstallmethod, where the second argument is the manager instance of the plugin, by calling here theaddPreProcessormethod to join our preprocessor object, which implements theprocessmethod will suffice, so that we can implement ourless-loader(used form a nominal expression)loader. And forprocessThe idea of the function is a bit simpler, where we can follow the\ncuts to determine if a string is relevant to a third-party library when processing it@importstatement, and if so add it to the~marking, and since this is in theless-loaderIf 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-loaderIt's a good idea to configure it in the project, but in fact, because of the project timeUmiscaffolding 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-loaderJust 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)loadersolves the problem of resolving style references, and in fact, if we don't resort to theless-loaderThe words can still be continuedwebpack-loaderidea to solve the problem, when we find the problem with style references, we can implement theloaderAvoiding 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-loaderThe 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 theArcoDesignused as a component library, and a unified design specification was introduced later internally, this new specification was introduced in theArcoDesignWe'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-prointernally has helped us to actually reference theweb-reactHowever, 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 overallUIIt 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-reactstyle isn't going to solve this problem, except for controlling theimportIn addition to the order of thelazy-loadThe 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-reactstyle, 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-reactIf you continue to follow this pattern you will need to continuallylazycomponent. So to switch things around, can we just add theWebpacklevel to deal with these issues directly, and if we can directly transfer theweb-reactpatternresolveto an empty file, then that solves the problem.

In fact, due to the prevalence of this problem, there is an internalWebpackplugin to take care of this, but referencing it in our project will have a negative effect on themini-css-extract-pluginThe 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-pluginThis plugin, then we can similarly implement thebabelplugin 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 withimportstatement corresponding to theImportDeclarationas well asrequirestatementCallExpressionWhen 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-reactstyle references as a way to solve the style override problem. And adding this plugin to thebabelIt is also only necessary to add the.babelrcConfigure it in the filepluginA 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 thebabelInstead, it's throughESBuildorSWCto compilejsfile, then what to do with it. The essence of this, according to our current thinking, is that the target of the.lessfile reference redirection to an empty style file, then we can absolutely continue to use theloaderto deal with the idea that actuallybabel-loaderIt also just helps us to compile the plain text asASTGetting structured data facilitates us to adjust the output using plugins.

Then if, in accordance with something likebabel-loaderWe still need to parse theimportand other statements, it would still be more cumbersome, whereas if you think differently and deal directly with the.lessfile, if the absolute path to this file is from theweb-reactintroduced in the.lessfile to introduce styles, we can similarly take theless-loader(used form a nominal expression)loaderGo 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.loaderThe 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 theloaderConfigure towebpackin the center can be used, since we are directly configuring the, it can be relatively easy to add rules if thewebpack-chainetc. 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)ResolveThe 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 theWebpackhit the nail on the headResolveIs there any more general solution to the problem that is actually in theWebpackIt is provided in theThis configuration item, by which we can define theResolveplugin so that it can be used in theWebpack(used form a nominal expression)ResolveStage 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 mainlyAPIThere 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.aliasFor 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-installedcommoncap (a poem)overseaversion dependencies, and then add theWebpack(used form a nominal expression)file to dynamically modify the relevant dependencies in thealias

 = {
  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 ofaliasfor the unified version, which I encountered last yearYarn+rounding differenceThe problem of conflict of dependence has, for the most part, been3version, and some of the dependencies are those that require2The version was instead incorrectlyresolveuntil (a time)3version, in which case it is necessary to control certain modules of theresolvebehavior to address such issues.

unfamiliarviteThe students of the program know that based on the@rollup/plugin-aliasThe plug-in is available in thevitehit the nail on the headaliasMore advanced configurations are also available, which can support our dynamic handling of thealiasBehavior, exceptfind/replaceThis regularity-based parsing is in addition to the support for passing theResolveFunction/ResolveObjectform is used to handle theRollupparseableHookBehavior.

// 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 factWebpackThere is also a built-inNormalModuleReplacementPluginplugin to handle the replacement of module references more flexibly, by directly calling thenew (resourceRegExp, newResource)That's all, it's important to note thatnewResourceis supported in function form, so if you need to modify its behavior it is straightforward to modify it in place.contextparameter object is sufficient, and thecontextParameters 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));
}

NormalModuleReplacementPluginattributableNormalModuleFactory(used form a nominal expression)beforeResolveis 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 theloaderleveragewebpackability to fulfill its own file parsing needs, while theNormalModuleReplacementPluginis 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 theresolveis treated entirely as a separate module, and is of course itself based on theenhanced-resolveand the plugin we've implemented here is the equivalent of implementing the parsing behavior for theHook, so even if something likeless-loaderThis independently scheduled plugin also schedules properly, and this configuration is not available on thewebpack2It's already implemented in the Then we can build on this capability inbefore-hookhooks 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-loaderwill 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)loaderto 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 therequesttogether withresolveContextparameter 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/