Author: Zhao Yiming

preface

Webpack uses enhanced- Resolve for path resolution. Resolve resolves the string introduced in the require/import statement to the absolute path to the imported file.

// Absolute path
const moduleA = require('/Users/didi/Desktop/github/test-enhanced-resolve/src/moduleA.js')

// Relative path
const moduleB = require('.. /moduleB.js')

// Module path, NPM package name, or alias configured through alias
const moduleC = require('moduleC')
Copy the code

In its official documentation, it is described as highly configurable, thanks to its complete plug-in system. In fact, all of the built-in functionality of Enhanced – Resolve is implemented through plug-ins.

How does WebPack integrate Enhanced – Resolve

In webPackOptionsapply.js, merge the resolve option in webpack.config.js:

compiler.resolverFactory.hooks.resolveOptions
  .for("normal")
  .tap("WebpackOptionsApply".resolveOptions= > {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem
      },
      cachedCleverMerge(options.resolve, resolveOptions)
    );
  });
  
compiler.resolverFactory.hooks.resolveOptions
  .for("context")
  .tap("WebpackOptionsApply".resolveOptions= > {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem,
        resolveToContext: true
      },
      cachedCleverMerge(options.resolve, resolveOptions)
    );
  });
  
compiler.resolverFactory.hooks.resolveOptions
  .for("loader")
  .tap("WebpackOptionsApply".resolveOptions= > {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem
      },
      cachedCleverMerge(options.resolveLoader, resolveOptions)
    );
  });
Copy the code

NormalModuleFactory and contextModuleFactory are passed to resolverFactory. NormalModuleFactory and contextModuleFactory can use the enhanced resolve functionality when resolving paths.

createNormalModuleFactory() {
  const normalModuleFactory = new NormalModuleFactory(
    this.options.context,
    this.resolverFactory,
    this.options.module || {}
  );
  this.hooks.normalModuleFactory.call(normalModuleFactory);
  return normalModuleFactory;
}

createContextModuleFactory() {
  const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
  this.hooks.contextModuleFactory.call(contextModuleFactory);
  return contextModuleFactory;
}
Copy the code

Use a diagram to illustrate this part of the process:

The core function

1. Obtain the absolute path of the module through synchronous or asynchronous mode, and determine whether the module exists.

The create method allows us to pass in options for custom parsing rules.

2. Inherit Tapable, expose the custom plug-in function externally, and realize more flexible module parsing rules.

3. Flexible and customized file systems: Enhanced – Resolve provides NodeJsInputFileSystem and CachedInputFileSystem.

Path Resolution Process

Using version 5.4.0 as an example, the principle of enhanced resolve can be simply interpreted as a pipeline to resolve, pass in the path to resolve from the original place, go through plug-in resolution, and finally return the file path or error.

In resolver.js, enhance-resolve has only four hooks by default:

class Resolver {
  constructor(fileSystem, options) {
    this.fileSystem = fileSystem;
    this.options = options;
    this.hooks = {
      // this is called every time a plug-in is executed
      resolveStep: new SyncHook(["hook"."request"]."resolveStep"),
      // No specific file or directory was found
      noResolve: new SyncHook(["request"."error"]."noResolve"),
      // Start parsing
      resolve: new AsyncSeriesBailHook(
        ["request"."resolveContext"]."resolve"
      ),
      // Parsing is complete
      result: new AsyncSeriesHook(["result"."resolveContext"]."result")}}}Copy the code

As you can see, only the start resolve and end result hooks are related to the resolution process. The rest are added manually in resolverFactory.js.

resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInteralResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("internal");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("resolveAsModule");
resolver.ensureHook("undescribedResolveInPackage");
resolver.ensureHook("resolveInPackage");
resolver.ensureHook("resolveInExistingDirectory");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("undescribedExistingDirectory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("finalFile");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
Copy the code

Enhanced -resolve allows very flexible custom path resolution in the form of passing in configuration and writing plugins. Resolve and result hooks are fixed at start and end.

For the following demo, the order of hook calls is fixed:

const { CachedInputFileSystem, ResolverFactory } = require('enhanced-resolve')
const path = require('path')

const myResolver = ResolverFactory.createResolver({
  fileSystem: new CachedInputFileSystem(fs, 4000),
  extensions: ['.json'.'.js'.'.ts']./ /... More configuration
})

const context = {}
const resolveContext = {}
const lookupStartPath = path.resolve(__dirname)
const request= './a'
myResolver.resolve(context, lookupStartPath, request, resolveContext, (err, path, result) = > {
	if (err) {
    console.log('createResolve err: ', err)
  } else {
    console.log('createResolve path: ', path)
  }
});
Copy the code

See which hooks are called in the demo during path parsing:

When the demo above called myresolver.resolve, it actively called the doResolve method inside the resolve method and used the resolve hook.

class Resolver {
  resolve (context, path, request, resolveContext, callback) {
    // ...
    if (resolveContext.log) {
      const parentLog = resolveContext.log;
      const log = [];
      return this.doResolve(
        // ----------- here -----------
        this.hooks.resolve,
        obj,
        message,
        {
          log: msg= > {
            parentLog(msg);
            log.push(msg);
          },
          fileDependencies: resolveContext.fileDependencies,
          contextDependencies: resolveContext.contextDependencies,
          missingDependencies: resolveContext.missingDependencies,
          stack: resolveContext.stack
        },
        (err, result) = > {
          if (err) return callback(err);

          if (result) return finishResolved(result);

          returnfinishWithoutResolve(log); }); }else {
      // ...}}}Copy the code

Enhanced – Resolve initializes different plug-ins with different configurations, registers a hook inside the plug-in, and then invokes the next hook using the doResolve method to string together the entire parsing process.

Plug-in writing method:

A plug-in depends on three pieces of information:

1. Upstream hook: After the last hook has processed the information, it is my turn to continue processing, so we need to register a hook tap.

2. Configuration information: parameters used when the plugin processes logic.

3. Downstream Hook: When the plugin is finished processing the logic, it notifies the downstream hook to call its registered TAP.

class ResolvePlugin {
  constructor (source, option, target) {
    this.source = source // Which hook the current plug-in hangs on
    this.target = target // Triggers the next hook
    this.option = option
  }
  
  apply (resolver) {
    const target = resolver.ensureHook(this.target)
    
    resolver.getHook(this.source).tapAsync('ResolvePlugin'.(request, resolveContext, callback) = > {
      const resource = request.request
      const resourceExt = path.extname(request.request)
      const obj = Object.assign({}, request, {})
      const message = null

      // Triggers the next hook
      resolver.doResolve(target, obj, message, resolveContext, callback)
    })
  }
}
Copy the code

The next hooks to fire can be customized in the plug-in, so hooks may not be executed in a fixed order.

Mpx implements conditional compilation of file dimensions through the enhanced-resolve plug-in

There are also applications for Enhanced resolve in the team’s own enhanced cross-end applets framework, Mpx.

Based on the syntax of wechat applet, Mpx can build and output the applet code of other platforms by reading the mode and srcMode passed in by users. However, some components or apis on different platforms may differ greatly and cannot be smoothed out with a simple if/else. For example, there is a business map component map. MPX in the project of transferring wechat from Alipay in Didi Chuxing mini program. Since the standard of the native map component in wechat and Alipay is very different, it cannot be directly exported across platforms by means of framework translation, so we can create a new map.Ali. MPX in the same position. The compilation system will load corresponding modules according to the mode currently compiled. When mode is Ali, map.ali. MPX will be loaded first, otherwise, map.mpx will be loaded.

The principle is to achieve the priority matching loading of different mode files through the customized AddModePlugin.

conclusion

Webpack uses the enhanced-resolve module for path resolution, a highly configurable require.resolve path resolver that implements custom path lookup rules using exposed options and plug-ins.