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
resolveContext
Context objects throughout the processcallback
Function that passes information to the next plugin after the current plugin completes execution.result
This 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:
- A hook is listened on
described-resolve
Event, passing the result toresolve
Events. - 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 - If it is a non-relative path, it passes
matchPath
Function 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 toresolve
Hook 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
- Configure ‘compileroptions. baseUrl = “./ SRC”
- To build a
src/lib/axios
filesrc/index
In 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
- We are in
index.ts
Import axios, and the axios entry filenode_modules/axios/index.js
There 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
package.json
add"postinstall": "patch-package"
npm i -D patch-package
Copy the code
- Directly to the
node_modules/tsconfig-paths-webpack-plugin/lib/plugin.js
Change the corresponding position to the correct code - perform
npx patch-package tsconfig-paths-webpack-plugin
- At this point, a patch file is generated and submitted to the Git repository
- Run the code, there is no error as shown at the beginning, the bug is removed