I have a lot of questions about Webpack… This article will briefly take a look at how WebPack handles ESM, and try to understand the general implementation of WebPack’s ESM ass we Can

The Webpack version for this analysis is 4.41.2

Warm up

  1. Tapable can be used like EventEmitter (if you want to use tapable, it can be used to sync, Async, waterfall… , I guess for debugging convenience)
  2. Learn about the Webpack process (Webpack 3.x)

    Webpack is the process it packages through “hooks” of several key classes, divided into “core stages”

  3. Understand that WebPack-Sources provides several types of CachedSource, PrefixSource, ConcatSource, and ReplaceSource that can be used in combination, It is easy to add, replace, and connect code, and it also has some source-Map related apis, such as updateHash, for internal invocation of WebPack

The sample

How does WebPack build bundle.js from the following example (a single entry ESM module)

src/index.js

// Introduce the hello function
import sayHello from './hello.js'

let str = sayHello('1')
console.log(str)
Copy the code

The above code has three statements

src/hello.js

export default function sayHelloFn(. args) {
  return 'Hello, parameter value:' + args[0]}Copy the code

The above code has 1 statement and the webpack configuration is the most basic

module.exports = {
  mode: 'development'.devtool: 'source-map'.entry: {
    app: path.resolve(__dirname, './src/index.js')},output: {
    filename: '[name].bundle.js'.path: path.resolve(__dirname, 'dist')}}Copy the code

After analysis, it can be roughly divided into two processes

The parse process

Parse occurs programmatically after the Compilation buildModule hook. The code is found in the doBuild callback of the NormalModule class parser. parse, which calls the Parser static method. In this method, you can see that WebPack uses the Acorn module to parse into ast

Now comes the core

if (this.hooks.program.call(ast, comments) === undefined) {
  this.detectStrictMode(ast.body);
  // Prewalking iterates the scope for variable declarations
  this.prewalkStatements(ast.body);
  // Block-Prewalking iterates the scope for block variable declarations
  this.blockPrewalkStatements(ast.body);
  // Walking iterates the statements and expressions and processes them
  this.walkStatements(ast.body);
}
Copy the code

The main job here is to iterate through statement after statement in the ast module, do some processing when it encounters some types, and possibly call handlers for various “expressions “tap on the evaluate hook (HookMap defined in the Parser constructor). These expressions are “Literal”, “LogicalExpression”, “BinaryExpression”, “UnaryExpression”… At the same time will call plug-ins (HarmonyImportDependencyParserPlugin HarmonyExportDependencyParserPlugin…) Prewalk, blockPrewalk, and walk all parse each statement and operate on scope objects before parsing, so handling import and export here is a bit like js scope raising

this.scope = {
  topLevelScope: true.inTry: false.inShorthand: false.isStrict: false.definitions: new StackedSetMap(),
  renames: new StackedSetMap()
};
Copy the code

So even if I write it this way, it’s ok

let str = sayHello('1')
console.log(str)
import sayHello from './hello.js'
Copy the code

So here’s the main analysis

index.js

index ast

First statement

Second statement

The third statement

In prewalk, the first statement is preparsed, namely, statement.type is ImportDeclaration. Get source = statement.source.value, ‘./hello.js, ‘by calling the prewalkImportDeclaration method. this.hooks.import.call(statement, source)

Calling the Import hook

Parser. State. LastHarmonyImportOrder = (parser. State. LastHarmonyImportOrder | | 0) + 1 ConstDependency is added to the module relies on HarmonyImportSideEffectDependency is added to the module relies on

Traversal, specifiers, namely preliminary analytical indicator enclosing scope. Renames. Set (” sayHello “, null) enclosing scope. Definitions. The add (‘ sayHello) type, Namely specifiers. Type ImportDefaultSpecifier, can call this. Hooks. ImportSpecifier. Call (statement, source, ‘default’ name)

Call the importSpecifier hook
    parser.scope.definitions.delete('sayHello')
    parser.scope.renames.set('sayHello'.'imported var')
    parser.state.harmonySpecifier.set('sayHello', {
      source: './hello.js'.id: 'default'./ / 1
      sourceOrder: parser.state.lastHarmonyImportOrder  
    })
Copy the code

The second statement of the blockPrewalk procedure is the pre-parsed-statement type, i.e. Statement. type is VariableDeclaration. Will call blockPrewalkVariableDeclaration method traversal. Declarations, here is a case, according to the declarator enclosing scope. Renames. Set (” STR “, null); this.scope.definitions.add(‘str’)

Call callHook = this.imports.call. get(‘imported var’); callHook.call(expression)

Calling the Call hook

Parser. State. HarmonySpecifier. Get (” sayHello “), its set in importSpecifier hook, As a parameter to HarmonyImportSpecifierDependency HarmonyImportSpecifierDependency is added to the module to rely on

hello.js

hello.js ast

A statement

Prewalk process type, in the process of the parse the statement function declarations that statement. The declaration, the type of FunctionDeclartion, This. Will call hook hooks. ExportSpecifier. Call (the statement, ‘sayHelloFn’, ‘default’)

Call the exportSpecifier hook

HarmonyExportSpecifierDependency is added to the module dependencies

Walk process parse the statement types, namely the statement. The type of ExportDefaultDeclaration, calls the hook enclosing hooks. Export. Call (statement)

Call the export hook

HarmonyExportHeaderDependency is added to the module dependencies

Declarator.type = functiontion This will call hook. Hooks. ExportDeclaration. Call (the statement, the statement. The declaration)

Call the exportDeclaration hook

The empty hook function is not currently processed

After these hook calls, we add scope and module dependencies to the property Dependencies list of the current module object, which will be dealt with later. The generate procedure calls the template class corresponding to the Dependecy class in Dependencies

The generate process

Genernate occurs after the beforeChunkAssets hook of the Compilation process before the chunkAsset hook, In MainTemplate’s renderManifest hook and modules hook, hello.js should be handled first. In buildChunkGraph, hello.js is the module that index.js depends on. We’ll talk about that later

Next the paper template calls (note: oh, found a “bug”, HarmonyCompatibilityDependency corresponding template class called HarmonyExportDependencyTemplate, HarmonyExportHeaderDependency corresponding template class also called HarmonyExportDependencyTemplate, what is the international seek bug engineer? Tactical backward)

hello.js


/ / call runtimeTemplate defineEsModuleFlagStatement which parameters exportsArgument as "__webpack_exports__"
/ / get the content = "__webpack_require__. R (__webpack_exports__)"
const content = runtime.defineEsModuleFlagStatement({
  exportsArgument: dep.originModule.exportsArgument
});

source.insert(- 10, content)  // -10 indicates the priority. The smaller the priority is, the higher the priority is
Copy the code

(2) HarmonyInitDependency it corresponds to the Template class HarmonyInitDependencyTemplate calls to apply for the module. The dependencies traversal, GetHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder = getHarmonyInitOrder Call the Template harmonyInit method in turn

(2.1) HarmonyExportSpecifierDependency it corresponds to the Template class invokes HarmonyExportSpecifierDependencyTemplate getHarmonyInitOrder Direct return 0 returned to invoke harmonyInit HarmonyInitDependencyTemplate

const content = `/* harmony export (binding) */ webpack_require.d({exportsName}, {JSON.stringify(
  used
)}, function() { return ${dep.id}; }); \n`

source.insert(- 1, content);  // -1 indicates the priority. The smaller the priority is, the higher the priority is
Copy the code

(3) HarmonyExportHeaderDependency it corresponds to the Template class for HarmonyExportDependencyTemplate call the apply (x)

source.replace(dep.rangeStatement[0], replaceUntil, content);
Copy the code

The effect is to replace “export default” with an empty string

Processed code

__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default".function() { return sayHelloFn; });
function sayHelloFn(. args) {
  return 'Hello, parameter value:' + args[0]}Copy the code

Line 5 of index.js (Dependency – means none) is not in module Dependency, only shown here






hello.js

(2) HarmonyInitDependency it corresponds to the Template class call the apply for HarmonyInitDependencyTemplate with hello. Js (2) call getHarmonyInitOrder Template, HarmonyInit method

(2.1) HarmonyImportSideEffectDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from getHarmonyInitOrder HarmonyImportDependencyTemplate calls Return dep. SourceOrder, this property is to add module based on new HarmonyImportSideEffectDependency incoming, its value for the parser. State. LastHarmonyImportOrder, Here is 1 to call getHarmonyInit

let sourceInfo = importEmittedMap.get(source);
if(! sourceInfo) { importEmittedMap.set( source, (sourceInfo = {emittedImports: new Map()})); }const key = dep._module || dep.request;
if (key && sourceInfo.emittedImports.get(key)) return;
sourceInfo.emittedImports.set(key, true);
/ / dep for HarmonyImportSideEffectDependency instance, getImportStatement method is defined in HarmonyImportSideEffectDependency the superclass
const content = dep.getImportStatement(false, runtime);
source.insert(- 1, content);
Copy the code

The content is /* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello.js */ “./analysis/harmonymodule-analysis/hello.js”);

(2.2) HarmonyImportSpecifierDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from HarmonyImportDependencyTemplate call getHarmonyInitOrder with (2.1) Call getHarmonyInit

/ / due to (2.1)
// This condition is true and no processing is performed
if (key && sourceInfo.emittedImports.get(key)) return;
Copy the code

(3) ConstDependency: the Template class is ConstDependencyTemplate and calls Apply

// dep is an instance of ConstDependency
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
Copy the code

It replaces positions 13 to 46(import sayHello from ‘./hello.js’) with an empty string

(4) HarmonyImportSpecifierDependency inherited from HarmonyImportDependency, It corresponds to the Template class HarmonyImportSideEffectDependencyTemplate it inherited from HarmonyImportDependencyTemplate

HarmonyImportSpecifierDependency (2.2) is not processed? Didn’t see it wrong template class inherits from HarmonyImportDependencyTemplate its apply method is empty function, getHarmonyInitOrder, getHarmonyInit are defined, So here is the apply of HarmonyImportSpecifierDependencyTemplate apply call method calls the apply

/ / dep is HarmonyImportSpecifierDependency instance
const content = this.getContent(dep, runtime);
source.replace(dep.range[0], dep.range[1] - 1, content)
Copy the code

It replaces positions 58 to 65(sayHello) with Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__[“default”])

Processed code

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/ *! ./hello.js */ "./analysis/harmonymodule-analysis/hello.js");
// Introduce the hello function

let str = Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__["default"]) ('1')
console.log(str)
Copy the code

Finally, they are spliced together with the bootstrap code into the entire bundle in the Render hook of the MainTemplate

A question

Didn’t it say there were a lot of questions? What questions do you have when you see this? come in… No? All right, let me ask you a question

Q: WebPack will parse ESM, and Babel-Loader will let Babel convert ES6 to ES5, so don’t they “conflict” when converting ESM? Webpack adds configuration

module: {
  rules: [{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          '@babel/preset-env'
        ]
      }
    }
  }]
}
Copy the code

{caller: object. assign({name: “babel-loader”, supportsStaticESM: true, supportsDynamicImport: True}, opts.caller)} This is the caller metadata feature provided by babel7, so that @babel/core is passed to presets/plugins. Here @babel/preset-env doesn’t use @babel/transform-modules-commonjs to transform the import export code, and the parse is after runLoader.

conclusion

Contact our daily work, like a project module, analyze the requirements (parse) to determine the Dependency needs, such as design, front end and back end, each with its own responsibilities to generate the project. Some projects might only need the front end and the back end, and some of the front end might even help the back end, and the back end might say, you help me, you lose your job, Backend quickly to finish the task in hand (HarmonyImportSideEffectDependency and HarmonyImportSpecifierDependency), and of course project (hello. Js) and project (index. Js) is a dependent.

Dig a hole for the rest and explore it later.