background

Recently, I encountered a weird runtime error when upgrading the configuration of the business project

After further debugging, the cause is found in the business code lib/axios.ts import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from ‘axios’; This line of code is imported into the business code lib/ ‘ ‘axios”. Ts instead of axios in node_modules

Since the project used a secondary packaging webpack framework in the department, I found a bug in tsconfig-Paths-webpack-plugin after checking with relevant students. It can be seen that some people put forward this issue online, and some people also put forward a MR to fix this problem. However, since the guy who proposed the MR fix doesn’t know why this is good, the author also says that we need to wait for enough unit tests before we can merge this

Github.com/dividab/tsc… Github.com/dividab/tsc…

Now that we have all come, why not study how this bug happened and how to fix it

Principles of the Webpack Resolve Plugin

What is the tsconfig-paths-webpack-plugin plugin for?

Use this to load modules whose location is specified in the paths section of tsconfig.json when using webpack. This package provides the functionality of the tsconfig-paths package but as a webpack plug-in.

Using this plugin means that you should no longer need to add alias entries in your webpack.config.js which correspond to the paths entries in your tsconfig.json. This plugin creates those alias entries for you, so you don’t have to!

In a nutshell, is what we use the ts development project usually need to configure the tsconfig.com pilerOptions. Paths. By default, WebPack does not recognize this configuration. By configuring the WebPack plug-in, you can enable webPack to find the corresponding file according to tsConfig’s Paths when packaging without having to configure resolve. Alias in webpack. It relies on tsconfig-Path’s createMatchPathAsync function to find the module’s path

It is also important to note that tsconfig-paths-webpack-plugin is the resolve plugin of Webpack and not the regular Webpack plugin. The difference between the two is that the Webpack Plugin is configured in the config.plugins field of webpack, while the Webpack resolve plugin is configured in the config.resolve. Plugins field. The apis of the two plugins are also different. The WebPack Plugin listens to the whole webPack packaging process through complier/compilation. The Resolve Plugin focuses on the process of handling the resolve module. The config.resolve field of the WebPack configuration file is basically passed to the enhanced resolve library, which instantiates resolver. The path handling of module resolution packaging within WebPack is implemented by enhanced-resolve.

The createResolver shown below from the WebPack source is provided by Enhanced resolve

Before looking at the implementation of tsconfig-Paths-webpack-Plugin, understanding the architecture of enhanced resolver will help you find bugs better.

Enhanced – Resolver is a runtime mechanism based on Core + Plugin. Enhanced -resolver mainly provides a basic resolver object to handle module path lookup through its resolve method. Provide plugin mechanism to implement event communication based on tapable to bridge the relationship between webPack and resolver plugin. Resolver calls resolve/doResolve to concatenate all plugins to achieve module lookup.

Resolver: github.com/webpack/enh…

Resolver plugin mechanism:

The resolver object internally registers four hook objects during its instantiation. Hooks are all instances of Tabable, and the hook types used are as follows

Resolve AsyncSeriesBailHook tapAsync/tapPromise/callAsync/registering callback returns a during the execution of a promiseundefinedThe function in callAsync or promise is executed directly, and subsequent callbacks registered will not execute the resolveStep SyncHook synchronization hook. Call/TAP noResolve SyncHook Synchronization hook. Call/tap result AsyncSeriesHook tapAsync/tapPromise/callAsync/promise order execution registered an asynchronous callbackCopy the code

The Resolver plugin follows the execution flow from source to target. Each Plugin listens for the trigger of source event through hook, and triggers the target event to the next corresponding hook after executing the logic of this Plugin. (Request, resolveContext, callback: (Err? : any, result: any) => void) => void Signature format

  • Request: A request object from the resolver module that contains information about the file to be searched

  • resolveContextContext objects throughout the process
  • callbackFunction that passes information to the next plugin after the current plugin completes execution.resultThis is what the next plugin receivesrequest

As a simplest NextPlugin, the code follows. By convention, each resolve Plugin has a source and target attribute. Source indicates that the current plugin executes after the source event is triggered; Target: the target event is triggered after the current plugin executes

// https://github.com/webpack/enhanced-resolve/blob/main/lib/NextPlugin.js

module.exports = class NextPlugin {
    / * * *@param {string | ResolveStepHook} source source
     * @param {string | ResolveStepHook} target target
     */
    constructor(source, target) {
        this.source = source;
        this.target = target;
    }
    / * * *@param {Resolver} resolver the resolver

     * @returns {void}* /
    apply(resolver) {
        const target = resolver.ensureHook(this.target);
        resolver
            .getHook(this.source)
            .tapAsync("NextPlugin".(request, resolveContext, callback) = > {
                resolver.doResolve(target, request, null, resolveContext, callback); }); }};Copy the code

The plugin logic is to listen for the source event, execute the resolver object’s doResolve method to find the module, and pass the result to the plugin that listens for the target event

// https://github.com/webpack/enhanced-resolve/blob/main/lib/ResolverFactory.js
/ /...
    plugins.push(
        new NextPlugin("after-undescribed-resolve-in-package"."resolve-in-package"));Copy the code

The above code indicates that the resolve-in-package event will be triggered after doResolve is executed after listening on undescribed-resolve-in-package trigger is complete. The basic functionality of Enhanced – Resolve itself is implemented by several built-in plugins

The overall resolve architecture is shown below

Tsconfig – paths – webpack – plugin principle

After the above analysis, this is a Resolve Plugin, and the overall flow of the plugin is as follows:

  1. A hook is listened ondescribed-resolveEvent, passing the result toresolveEvents.
  2. If the request file path is.or.Prefix, is a relative path, plugin does not handle. Skip the process and apply the WebPack default resolve process
  3. If it is a non-relative path, it passesmatchPathFunction to find the actual module path, if the actual module path does not exist, directly skip the processing process, apply webpack default resolve process; If so, pass the result toresolveHook to find the module

The main logic pseudocode after simplification is as follows

// https://github.com/dividab/tsconfig-paths-webpack-plugin/blob/master/src/plugin.ts#L230
function createPluginCallback(
  matchPath: TsconfigPaths.MatchPathAsync,
  resolver: Resolver,
  absoluteBaseUrl: string,
  hook: Tapable,
  extensions: ReadonlyArray<string>
) :TapAsyncCallback {
  return (request: ResolveRequest, resolveContext: ResolveContext, callback: TapAsyncInnerCallback) = > {
    / / anchor point 1
    const innerRequest = getInnerRequest(resolver, request);
    / / anchor point 2
    if (
      !innerRequest ||
      innerRequest.startsWith(".") ||
      innerRequest.startsWith("..")) {return callback();
    }
    / / anchor point 3
    matchPath(
      innerRequest,
      readJsonAsync,
      fileExistAsync,
      extensions,
      (err, foundMatch) = > {
        if (err) {
          return callback(err);
        }
        if(! foundMatch) {return callback();
        }
        constnewRequest = { ... request,request: foundMatch,
          path: absoluteBaseUrl,
        };
        return resolver.doResolve(
          hook,
          newRequest,
          {},
          {},
          (err2: Error.result2: ResolveRequest): void= > {
            callback(undefined, result2); }); }); }; }Copy the code

The cause of the bug

The simplest reproduction of demo

  1. Configure ‘compileroptions. baseUrl = “./ SRC”
  2. To build asrc/lib/axiosfile
  3. src/indexIn theimport axios from 'axios'

The import axios from index is lib/axios! Instead of node_modules, it causes an error.

Through breakpoint debugging and the code logic interpretation can find the cause of the bug

  1. We are inindex.tsImport axios, and the axios entry filenode_modules/axios/index.jsThere is the following code
module.exports = require('./lib/axios');
Copy the code

At this point, it’s time to find the./lib/axios flow. Go to anchor 1’s getInnerRequest function, which does a processing and returns. Request. relativePath is., innerRequest is./lib/axios. The result of their join is lib/axios.

innerRequest = resolver.join(request.relativePath, innerRequest);
Copy the code

This is done by passing in the lib/axios parameter to anchor 3’s matchPath function. The result is the SRC /lib/axios file, which already exists in the project. Thus, import axios ends up importing SRC /lib/axios, which leads to the bug mentioned earlier. The root cause here is that./lib/axios is handled incorrectly inside the axios entry file, which logically should not be handled by the Alias plug-in. We can see that anchor 2 determines whether innerRequest is relative to a path, but the result of getInnerRequest must be non-relative to a path.

According to the above analysis, tsconfig-paths-webpack-plugin is triggered by the described-resolve hook event. You can find DescriptionFilePlugin triggering described-resolve in enhanced-resolve

RelativePath is the path of the requested file relative to the package, in the case of node_modules/axios’ index.js.

Therefore, anchor 2 uses the return value of getInnerRequest to determine if the relative path is a bug. GetInnerRequest joins the request from a relativePath with a relativePath, causing the preceding relativePath prefix to be lost

The correct solution is request. Request. Plugin skips the relative path. Request. Request corresponds to the part of the source code that introduces the path

To solve

The PR solution is already there. But the maintainer hasn’t joined in yet. Modifying the file name by force can solve this problem temporarily, but it will inevitably lead to another pit in the future. In this case, node_modules can be resolved by patch-package mode

  1. package.jsonadd"postinstall": "patch-package"
npm i -D patch-package
Copy the code
  1. Directly to thenode_modules/tsconfig-paths-webpack-plugin/lib/plugin.jsChange the corresponding position to the correct code
  2. performnpx patch-package tsconfig-paths-webpack-plugin
  3. At this point, a patch file is generated and submitted to the Git repository

  1. Run the code, there is no error as shown at the beginning, the bug is removed