定位 webpack 打包入口

  1. const webpack = require('webpack');
  2. const options = require('./webpack.config.js');
  3. let compiler = webpack(options);
  4. compiler.run(function (err, stats) {
  5. console.log(err);
  6. console.log(stats.toJson());
  7. });

定义 webpack.config.js 文件,执行 npx webpack 和手动引入会产生一样的效果。

执行 npx webpack 会找 node_modules 下 bin 目录下的 webpack 命令。

webpack.cmd

  1. #!/bin/sh
  2. basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
  3. case `uname` in
  4. *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
  5. esac
  6. if [ -x "$basedir/node" ]; then
  7. "$basedir/node" "$basedir/../webpack/bin/webpack.js" "$@"
  8. ret=$?
  9. else
  10. node "$basedir/../webpack/bin/webpack.js" "$@"
  11. ret=$?
  12. fi
  13. exit $ret

=> node webpack/bin/webpack.js

cmd 文件核心作用就是使用 node 命令执行 webpack/bin/webpack.js 文件。

webpack/bin/webpack.js

  1. #!/usr/bin/env node
  2. // @ts-ignore
  3. process.exitCode = 0;
  4. /**
  5. * @param {string} command process to run
  6. * @param {string[]} args commandline arguments
  7. * @returns {Promise<void>} promise
  8. */
  9. const runCommand = (command, args) => {
  10. const cp = require("child_process");
  11. return new Promise((resolve, reject) => {
  12. const executedCommand = cp.spawn(command, args, {
  13. stdio: "inherit",
  14. shell: true
  15. });
  16. executedCommand.on("error", error => {
  17. reject(error);
  18. });
  19. executedCommand.on("exit", code => {
  20. if (code === 0) {
  21. resolve();
  22. } else {
  23. reject();
  24. }
  25. });
  26. });
  27. };
  28. /**
  29. * @param {string} packageName name of the package
  30. * @returns {boolean} is the package installed?
  31. */
  32. const isInstalled = packageName => {
  33. try {
  34. require.resolve(packageName);
  35. return true;
  36. } catch (err) {
  37. return false;
  38. }
  39. };
  40. /**
  41. * @typedef {Object} CliOption
  42. * @property {string} name display name
  43. * @property {string} package npm package name
  44. * @property {string} binName name of the executable file
  45. * @property {string} alias shortcut for choice
  46. * @property {boolean} installed currently installed?
  47. * @property {boolean} recommended is recommended
  48. * @property {string} url homepage
  49. * @property {string} description description
  50. */
  51. /** @type {CliOption[]} */
  52. const CLIs = [
  53. {
  54. name: "webpack-cli",
  55. package: "webpack-cli",
  56. binName: "webpack-cli",
  57. alias: "cli",
  58. installed: isInstalled("webpack-cli"),
  59. recommended: true,
  60. url: "https://github.com/webpack/webpack-cli",
  61. description: "The original webpack full-featured CLI."
  62. },
  63. {
  64. name: "webpack-command",
  65. package: "webpack-command",
  66. binName: "webpack-command",
  67. alias: "command",
  68. installed: isInstalled("webpack-command"),
  69. recommended: false,
  70. url: "https://github.com/webpack-contrib/webpack-command",
  71. description: "A lightweight, opinionated webpack CLI."
  72. }
  73. ];
  74. const installedClis = CLIs.filter(cli => cli.installed);
  75. if (installedClis.length === 0) {
  76. const path = require("path");
  77. const fs = require("fs");
  78. const readLine = require("readline");
  79. let notify =
  80. "One CLI for webpack must be installed. These are recommended choices, delivered as separate packages:";
  81. for (const item of CLIs) {
  82. if (item.recommended) {
  83. notify += `\n - ${item.name} (${item.url})\n ${item.description}`;
  84. }
  85. }
  86. console.error(notify);
  87. const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));
  88. const packageManager = isYarn ? "yarn" : "npm";
  89. const installOptions = [isYarn ? "add" : "install", "-D"];
  90. console.error(
  91. `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
  92. " "
  93. )}".`
  94. );
  95. const question = `Do you want to install 'webpack-cli' (yes/no): `;
  96. const questionInterface = readLine.createInterface({
  97. input: process.stdin,
  98. output: process.stderr
  99. });
  100. questionInterface.question(question, answer => {
  101. questionInterface.close();
  102. const normalizedAnswer = answer.toLowerCase().startsWith("y");
  103. if (!normalizedAnswer) {
  104. console.error(
  105. "You need to install 'webpack-cli' to use webpack via CLI.\n" +
  106. "You can also install the CLI manually."
  107. );
  108. process.exitCode = 1;
  109. return;
  110. }
  111. const packageName = "webpack-cli";
  112. console.log(
  113. `Installing '${packageName}' (running '${packageManager} ${installOptions.join(
  114. " "
  115. )} ${packageName}')...`
  116. );
  117. runCommand(packageManager, installOptions.concat(packageName))
  118. .then(() => {
  119. require(packageName); //eslint-disable-line
  120. })
  121. .catch(error => {
  122. console.error(error);
  123. process.exitCode = 1;
  124. });
  125. });
  126. } else if (installedClis.length === 1) {
  127. const path = require("path");
  128. // 取出数据第一项,即 webpack-cli/package.json
  129. const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
  130. // eslint-disable-next-line node/no-missing-require
  131. const pkg = require(pkgPath);
  132. // eslint-disable-next-line node/no-missing-require
  133. require(path.resolve(
  134. path.dirname(pkgPath), // webpack-cli
  135. pkg.bin[installedClis[0].binName] // bin/cli.js
  136. ));
  137. } else {
  138. console.warn(
  139. `You have installed ${installedClis
  140. .map(item => item.name)
  141. .join(
  142. " and "
  143. )} together. To work with the "webpack" command you need only one CLI package, please remove one of them or use them directly via their binary.`
  144. );
  145. // @ts-ignore
  146. process.exitCode = 1;
  147. }

webpack.js 核心作用就是 require 了 node_modules/webapck-cli/bin/cli.js。

webpack-cli/bin/cli.js

  1. #!/usr/bin/env node
  2. /*
  3. MIT License http://www.opensource.org/licenses/mit-license.php
  4. Author Tobias Koppers @sokra
  5. */
  6. const { NON_COMPILATION_ARGS } = require("./utils/constants");
  7. (function() {
  8. // wrap in IIFE to be able to use return
  9. const importLocal = require("import-local");
  10. // Prefer the local installation of webpack-cli
  11. if (importLocal(__filename)) {
  12. return;
  13. }
  14. require("v8-compile-cache");
  15. const ErrorHelpers = require("./utils/errorHelpers");
  16. const NON_COMPILATION_CMD = process.argv.find(arg => {
  17. if (arg === "serve") {
  18. global.process.argv = global.process.argv.filter(a => a !== "serve");
  19. process.argv = global.process.argv;
  20. }
  21. return NON_COMPILATION_ARGS.find(a => a === arg);
  22. });
  23. if (NON_COMPILATION_CMD) {
  24. return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
  25. }
  26. const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version}
  27. Usage: webpack-cli [options]
  28. webpack-cli [options] --entry <entry> --output <output>
  29. webpack-cli [options] <entries...> --output <output>
  30. webpack-cli <command> [options]
  31. For more information, see https://webpack.js.org/api/cli/.`);
  32. require("./config/config-yargs")(yargs);
  33. // yargs will terminate the process early when the user uses help or version.
  34. // This causes large help outputs to be cut short (https://github.com/nodejs/node/wiki/API-changes-between-v0.10-and-v4#process).
  35. // To prevent this we use the yargs.parse API and exit the process normally
  36. yargs.parse(process.argv.slice(2), (err, argv, output) => {
  37. Error.stackTraceLimit = 30;
  38. // arguments validation failed
  39. if (err && output) {
  40. console.error(output);
  41. process.exitCode = 1;
  42. return;
  43. }
  44. // help or version info
  45. if (output) {
  46. console.log(output);
  47. return;
  48. }
  49. if (argv.verbose) {
  50. argv["display"] = "verbose";
  51. }
  52. let options;
  53. try {
  54. options = require("./utils/convert-argv")(argv);
  55. } catch (err) {
  56. if (err.code === "MODULE_NOT_FOUND") {
  57. const moduleName = err.message.split("'")[1];
  58. let instructions = "";
  59. let errorMessage = "";
  60. if (moduleName === "webpack") {
  61. errorMessage = `\n${moduleName} not installed`;
  62. instructions = `Install webpack to start bundling: \u001b[32m\n $ npm install --save-dev ${moduleName}\n`;
  63. if (process.env.npm_execpath !== undefined && process.env.npm_execpath.includes("yarn")) {
  64. instructions = `Install webpack to start bundling: \u001b[32m\n $ yarn add ${moduleName} --dev\n`;
  65. }
  66. Error.stackTraceLimit = 1;
  67. console.error(`${errorMessage}\n\n${instructions}`);
  68. process.exitCode = 1;
  69. return;
  70. }
  71. }
  72. if (err.name !== "ValidationError") {
  73. throw err;
  74. }
  75. const stack = ErrorHelpers.cleanUpWebpackOptions(err.stack, err.message);
  76. const message = err.message + "\n" + stack;
  77. if (argv.color) {
  78. console.error(`\u001b[1m\u001b[31m${message}\u001b[39m\u001b[22m`);
  79. } else {
  80. console.error(message);
  81. }
  82. process.exitCode = 1;
  83. return;
  84. }
  85. /**
  86. * When --silent flag is present, an object with a no-op write method is
  87. * used in place of process.stout
  88. */
  89. const stdout = argv.silent ? { write: () => {} } : process.stdout;
  90. function ifArg(name, fn, init) {
  91. if (Array.isArray(argv[name])) {
  92. if (init) init();
  93. argv[name].forEach(fn);
  94. } else if (typeof argv[name] !== "undefined") {
  95. if (init) init();
  96. fn(argv[name], -1);
  97. }
  98. }
  99. function processOptions(options) {
  100. // process Promise
  101. if (typeof options.then === "function") {
  102. options.then(processOptions).catch(function(err) {
  103. console.error(err.stack || err);
  104. // eslint-disable-next-line no-process-exit
  105. process.exit(1);
  106. });
  107. return;
  108. }
  109. const firstOptions = [].concat(options)[0];
  110. const statsPresetToOptions = require("webpack").Stats.presetToOptions;
  111. let outputOptions = options.stats;
  112. if (typeof outputOptions === "boolean" || typeof outputOptions === "string") {
  113. outputOptions = statsPresetToOptions(outputOptions);
  114. } else if (!outputOptions) {
  115. outputOptions = {};
  116. }
  117. ifArg("display", function(preset) {
  118. outputOptions = statsPresetToOptions(preset);
  119. });
  120. outputOptions = Object.create(outputOptions);
  121. if (Array.isArray(options) && !outputOptions.children) {
  122. outputOptions.children = options.map(o => o.stats);
  123. }
  124. if (typeof outputOptions.context === "undefined") outputOptions.context = firstOptions.context;
  125. // ...
  126. // 引入 webpack
  127. const webpack = require("webpack");
  128. let lastHash = null;
  129. let compiler;
  130. try {
  131. compiler = webpack(options);
  132. } catch (err) {
  133. if (err.name === "WebpackOptionsValidationError") {
  134. if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
  135. else console.error(err.message);
  136. // eslint-disable-next-line no-process-exit
  137. process.exit(1);
  138. }
  139. throw err;
  140. }
  141. if (argv.progress) {
  142. const ProgressPlugin = require("webpack").ProgressPlugin;
  143. new ProgressPlugin({
  144. profile: argv.profile
  145. }).apply(compiler);
  146. }
  147. if (outputOptions.infoVerbosity === "verbose") {
  148. if (argv.w) {
  149. compiler.hooks.watchRun.tap("WebpackInfo", compilation => {
  150. const compilationName = compilation.name ? compilation.name : "";
  151. console.error("\nCompilation " + compilationName + " starting…\n");
  152. });
  153. } else {
  154. compiler.hooks.beforeRun.tap("WebpackInfo", compilation => {
  155. const compilationName = compilation.name ? compilation.name : "";
  156. console.error("\nCompilation " + compilationName + " starting…\n");
  157. });
  158. }
  159. compiler.hooks.done.tap("WebpackInfo", compilation => {
  160. const compilationName = compilation.name ? compilation.name : "";
  161. console.error("\nCompilation " + compilationName + " finished\n");
  162. });
  163. }
  164. function compilerCallback(err, stats) {
  165. if (!options.watch || err) {
  166. // Do not keep cache anymore
  167. compiler.purgeInputFileSystem();
  168. }
  169. if (err) {
  170. lastHash = null;
  171. console.error(err.stack || err);
  172. if (err.details) console.error(err.details);
  173. process.exitCode = 1;
  174. return;
  175. }
  176. if (outputOptions.json) {
  177. stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + "\n");
  178. } else if (stats.hash !== lastHash) {
  179. lastHash = stats.hash;
  180. if (stats.compilation && stats.compilation.errors.length !== 0) {
  181. const errors = stats.compilation.errors;
  182. if (errors[0].name === "EntryModuleNotFoundError") {
  183. console.error("\n\u001b[1m\u001b[31mInsufficient number of arguments or no entry found.");
  184. console.error(
  185. "\u001b[1m\u001b[31mAlternatively, run 'webpack(-cli) --help' for usage info.\u001b[39m\u001b[22m\n"
  186. );
  187. }
  188. }
  189. const statsString = stats.toString(outputOptions);
  190. const delimiter = outputOptions.buildDelimiter ? `${outputOptions.buildDelimiter}\n` : "";
  191. if (statsString) stdout.write(`${statsString}\n${delimiter}`);
  192. }
  193. if (!options.watch && stats.hasErrors()) {
  194. process.exitCode = 2;
  195. }
  196. }
  197. if (firstOptions.watch || options.watch) {
  198. const watchOptions =
  199. firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
  200. if (watchOptions.stdin) {
  201. process.stdin.on("end", function(_) {
  202. process.exit(); // eslint-disable-line
  203. });
  204. process.stdin.resume();
  205. }
  206. compiler.watch(watchOptions, compilerCallback);
  207. if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
  208. } else {
  209. // compiler.run
  210. compiler.run((err, stats) => {
  211. if (compiler.close) {
  212. compiler.close(err2 => {
  213. compilerCallback(err || err2, stats);
  214. });
  215. } else {
  216. compilerCallback(err, stats);
  217. }
  218. });
  219. }
  220. }
  221. processOptions(options);
  222. });
  223. })();

cli.js

  • 当前文件一般存在两个操作,处理参数,将参数交给不同的逻辑(业务分发)
  • options 处理 options
  • compiler 加载 webpack 配置
  • compiler.run() 执行

webpack 主流程分析

测试代码

const webpack = require('webpack');
const options = require('./webpack.config.js');

let compiler = webpack(options);

compiler.run(function (err, stats) {
  console.log(err);
  console.log(stats.toJson());
});

node_modules/webpack/lib/webpack.js

/*
    MIT License http://www.opensource.org/licenses/mit-license.php
    Author Tobias Koppers @sokra
*/
"use strict";

const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin");
const WebpackOptionsApply = require("./WebpackOptionsApply");
const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter");
const validateSchema = require("./validateSchema");
const WebpackOptionsValidationError = require("./WebpackOptionsValidationError");
const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
const RemovedPluginError = require("./RemovedPluginError");
const version = require("../package.json").version;

/** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */

/**
 * @param {WebpackOptions} options options object
 * @param {function(Error=, Stats=): void=} callback callback
 * @returns {Compiler | MultiCompiler} the compiler object
 */
const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }

    let compiler; // 定义 compiler 变量

  // 用户传入的 options
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(
            Array.from(options).map(options => webpack(options))
        );
    } else if (typeof options === "object") {
    // 通过 process 方法合并默认配置
        options = new WebpackOptionsDefaulter().process(options);
        // 实例化 Compiler
        compiler = new Compiler(options.context);
    // 缓存 options
        compiler.options = options;

    // 使用插件,调用 apply 方法
    // 经过 NodeEnvironmentPlugin 处理后,compiler 具备文件读写能力
        new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);

    // 获取用户自定义配置的 plugins 
        if (options.plugins && Array.isArray(options.plugins)) {
      // 循环执行 plugin
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
    // 触发事件监听
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
    // 对默认插件进行挂载,同时可以确认打包入口
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }

    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }

  // 返回 compiler 对象
    return compiler;
};

exports = module.exports = webpack;
exports.version = version;

webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;
webpack.WebpackOptionsApply = WebpackOptionsApply;
webpack.Compiler = Compiler;
webpack.MultiCompiler = MultiCompiler;
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
// @ts-ignore Global @this directive is not supported
webpack.validate = validateSchema.bind(this, webpackOptionsSchema);
webpack.validateSchema = validateSchema;
webpack.WebpackOptionsValidationError = WebpackOptionsValidationError;

const exportPlugins = (obj, mappings) => {
    for (const name of Object.keys(mappings)) {
        Object.defineProperty(obj, name, {
            configurable: false,
            enumerable: true,
            get: mappings[name]
        });
    }
};

exportPlugins(exports, {
    AutomaticPrefetchPlugin: () => require("./AutomaticPrefetchPlugin"),
    BannerPlugin: () => require("./BannerPlugin"),
    CachePlugin: () => require("./CachePlugin"),
    ContextExclusionPlugin: () => require("./ContextExclusionPlugin"),
    ContextReplacementPlugin: () => require("./ContextReplacementPlugin"),
    DefinePlugin: () => require("./DefinePlugin"),
    Dependency: () => require("./Dependency"),
    DllPlugin: () => require("./DllPlugin"),
    DllReferencePlugin: () => require("./DllReferencePlugin"),
    EnvironmentPlugin: () => require("./EnvironmentPlugin"),
    EvalDevToolModulePlugin: () => require("./EvalDevToolModulePlugin"),
    EvalSourceMapDevToolPlugin: () => require("./EvalSourceMapDevToolPlugin"),
    ExtendedAPIPlugin: () => require("./ExtendedAPIPlugin"),
    ExternalsPlugin: () => require("./ExternalsPlugin"),
    HashedModuleIdsPlugin: () => require("./HashedModuleIdsPlugin"),
    HotModuleReplacementPlugin: () => require("./HotModuleReplacementPlugin"),
    IgnorePlugin: () => require("./IgnorePlugin"),
    LibraryTemplatePlugin: () => require("./LibraryTemplatePlugin"),
    LoaderOptionsPlugin: () => require("./LoaderOptionsPlugin"),
    LoaderTargetPlugin: () => require("./LoaderTargetPlugin"),
    MemoryOutputFileSystem: () => require("./MemoryOutputFileSystem"),
    Module: () => require("./Module"),
    ModuleFilenameHelpers: () => require("./ModuleFilenameHelpers"),
    NamedChunksPlugin: () => require("./NamedChunksPlugin"),
    NamedModulesPlugin: () => require("./NamedModulesPlugin"),
    NoEmitOnErrorsPlugin: () => require("./NoEmitOnErrorsPlugin"),
    NormalModuleReplacementPlugin: () =>
        require("./NormalModuleReplacementPlugin"),
    PrefetchPlugin: () => require("./PrefetchPlugin"),
    ProgressPlugin: () => require("./ProgressPlugin"),
    ProvidePlugin: () => require("./ProvidePlugin"),
    SetVarMainTemplatePlugin: () => require("./SetVarMainTemplatePlugin"),
    SingleEntryPlugin: () => require("./SingleEntryPlugin"),
    SourceMapDevToolPlugin: () => require("./SourceMapDevToolPlugin"),
    Stats: () => require("./Stats"),
    Template: () => require("./Template"),
    UmdMainTemplatePlugin: () => require("./UmdMainTemplatePlugin"),
    WatchIgnorePlugin: () => require("./WatchIgnorePlugin")
});
exportPlugins((exports.dependencies = {}), {
    DependencyReference: () => require("./dependencies/DependencyReference")
});
exportPlugins((exports.optimize = {}), {
    AggressiveMergingPlugin: () => require("./optimize/AggressiveMergingPlugin"),
    AggressiveSplittingPlugin: () =>
        require("./optimize/AggressiveSplittingPlugin"),
    ChunkModuleIdRangePlugin: () =>
        require("./optimize/ChunkModuleIdRangePlugin"),
    LimitChunkCountPlugin: () => require("./optimize/LimitChunkCountPlugin"),
    MinChunkSizePlugin: () => require("./optimize/MinChunkSizePlugin"),
    ModuleConcatenationPlugin: () =>
        require("./optimize/ModuleConcatenationPlugin"),
    OccurrenceOrderPlugin: () => require("./optimize/OccurrenceOrderPlugin"),
    OccurrenceModuleOrderPlugin: () =>
        require("./optimize/OccurrenceModuleOrderPlugin"),
    OccurrenceChunkOrderPlugin: () =>
        require("./optimize/OccurrenceChunkOrderPlugin"),
    RuntimeChunkPlugin: () => require("./optimize/RuntimeChunkPlugin"),
    SideEffectsFlagPlugin: () => require("./optimize/SideEffectsFlagPlugin"),
    SplitChunksPlugin: () => require("./optimize/SplitChunksPlugin")
});
exportPlugins((exports.web = {}), {
    FetchCompileWasmTemplatePlugin: () =>
        require("./web/FetchCompileWasmTemplatePlugin"),
    JsonpTemplatePlugin: () => require("./web/JsonpTemplatePlugin")
});
exportPlugins((exports.webworker = {}), {
    WebWorkerTemplatePlugin: () => require("./webworker/WebWorkerTemplatePlugin")
});
exportPlugins((exports.node = {}), {
    NodeTemplatePlugin: () => require("./node/NodeTemplatePlugin"),
    ReadFileCompileWasmTemplatePlugin: () =>
        require("./node/ReadFileCompileWasmTemplatePlugin")
});
exportPlugins((exports.debug = {}), {
    ProfilingPlugin: () => require("./debug/ProfilingPlugin")
});
exportPlugins((exports.util = {}), {
    createHash: () => require("./util/createHash")
});

const defineMissingPluginError = (namespace, pluginName, errorMessage) => {
    Object.defineProperty(namespace, pluginName, {
        configurable: false,
        enumerable: true,
        get() {
            throw new RemovedPluginError(errorMessage);
        }
    });
};

// TODO remove in webpack 5
defineMissingPluginError(
    exports.optimize,
    "UglifyJsPlugin",
    "webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead."
);

// TODO remove in webpack 5
defineMissingPluginError(
    exports.optimize,
    "CommonsChunkPlugin",
    "webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead."
);

node_modules/webpack/lib/Compiler.js

/*
    MIT License http://www.opensource.org/licenses/mit-license.php
    Author Tobias Koppers @sokra
*/
"use strict";

const parseJson = require("json-parse-better-errors");
const asyncLib = require("neo-async");
const path = require("path");
const { Source } = require("webpack-sources");
const util = require("util");
const {
    Tapable,
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

const Compilation = require("./Compilation");
const Stats = require("./Stats");
const Watching = require("./Watching");
const NormalModuleFactory = require("./NormalModuleFactory");
const ContextModuleFactory = require("./ContextModuleFactory");
const ResolverFactory = require("./ResolverFactory");

const RequestShortener = require("./RequestShortener");
const { makePathsRelative } = require("./util/identifier");
const ConcurrentCompilationError = require("./ConcurrentCompilationError");
const { Logger } = require("./logging/Logger");

/** @typedef {import("../declarations/WebpackOptions").Entry} Entry */
/** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */

/**
 * @typedef {Object} CompilationParams
 * @property {NormalModuleFactory} normalModuleFactory
 * @property {ContextModuleFactory} contextModuleFactory
 * @property {Set<string>} compilationDependencies
 */

class Compiler extends Tapable {
    constructor(context) {
        super();
    // compiler.hooks.
    // 默认初始化很多钩子
        this.hooks = {
            /** @type {SyncBailHook<Compilation>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {AsyncSeriesHook<>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<Compiler>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<string, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),

            /** @type {SyncHook<Compilation, CompilationParams>} */
            thisCompilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<Compilation, CompilationParams>} */
            compilation: new SyncHook(["compilation", "params"]),
            /** @type {SyncHook<NormalModuleFactory>} */
            normalModuleFactory: new SyncHook(["normalModuleFactory"]),
            /** @type {SyncHook<ContextModuleFactory>}  */
            contextModuleFactory: new SyncHook(["contextModulefactory"]),

            /** @type {AsyncSeriesHook<CompilationParams>} */
            beforeCompile: new AsyncSeriesHook(["params"]),
            /** @type {SyncHook<CompilationParams>} */
            compile: new SyncHook(["params"]),
            /** @type {AsyncParallelHook<Compilation>} */
            make: new AsyncParallelHook(["compilation"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterCompile: new AsyncSeriesHook(["compilation"]),

            /** @type {AsyncSeriesHook<Compiler>} */
            watchRun: new AsyncSeriesHook(["compiler"]),
            /** @type {SyncHook<Error>} */
            failed: new SyncHook(["error"]),
            /** @type {SyncHook<string, string>} */
            invalid: new SyncHook(["filename", "changeTime"]),
            /** @type {SyncHook} */
            watchClose: new SyncHook([]),

            /** @type {SyncBailHook<string, string, any[]>} */
            infrastructureLog: new SyncBailHook(["origin", "type", "args"]),

            // TODO the following hooks are weirdly located here
            // TODO move them for webpack 5
            /** @type {SyncHook} */
            environment: new SyncHook([]),
            /** @type {SyncHook} */
            afterEnvironment: new SyncHook([]),
            /** @type {SyncHook<Compiler>} */
            afterPlugins: new SyncHook(["compiler"]),
            /** @type {SyncHook<Compiler>} */
            afterResolvers: new SyncHook(["compiler"]),
            /** @type {SyncBailHook<string, Entry>} */
            entryOption: new SyncBailHook(["context", "entry"])
        };
        // TODO webpack 5 remove this
        this.hooks.infrastructurelog = this.hooks.infrastructureLog;

        this._pluginCompat.tap("Compiler", options => {
            switch (options.name) {
                case "additional-pass":
                case "before-run":
                case "run":
                case "emit":
                case "after-emit":
                case "before-compile":
                case "make":
                case "after-compile":
                case "watch-run":
                    options.async = true;
                    break;
            }
        });

        /** @type {string=} */
        this.name = undefined;
        /** @type {Compilation=} */
        this.parentCompilation = undefined;
        /** @type {string} */
        this.outputPath = "";

        this.outputFileSystem = null;
        this.inputFileSystem = null;

        /** @type {string|null} */
        this.recordsInputPath = null;
        /** @type {string|null} */
        this.recordsOutputPath = null;
        this.records = {};
        this.removedFiles = new Set();
        /** @type {Map<string, number>} */
        this.fileTimestamps = new Map();
        /** @type {Map<string, number>} */
        this.contextTimestamps = new Map();
        /** @type {ResolverFactory} */
        this.resolverFactory = new ResolverFactory();

        this.infrastructureLogger = undefined;

        // TODO remove in webpack 5
        this.resolvers = {
            normal: {
                plugins: util.deprecate((hook, fn) => {
                    this.resolverFactory.plugin("resolver normal", resolver => {
                        resolver.plugin(hook, fn);
                    });
                }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
                apply: util.deprecate((...args) => {
                    this.resolverFactory.plugin("resolver normal", resolver => {
                        resolver.apply(...args);
                    });
                }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n  resolver.apply(/* … */);\n}); instead.')
            },
            loader: {
                plugins: util.deprecate((hook, fn) => {
                    this.resolverFactory.plugin("resolver loader", resolver => {
                        resolver.plugin(hook, fn);
                    });
                }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
                apply: util.deprecate((...args) => {
                    this.resolverFactory.plugin("resolver loader", resolver => {
                        resolver.apply(...args);
                    });
                }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n  resolver.apply(/* … */);\n}); instead.')
            },
            context: {
                plugins: util.deprecate((hook, fn) => {
                    this.resolverFactory.plugin("resolver context", resolver => {
                        resolver.plugin(hook, fn);
                    });
                }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
                apply: util.deprecate((...args) => {
                    this.resolverFactory.plugin("resolver context", resolver => {
                        resolver.apply(...args);
                    });
                }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n  resolver.apply(/* … */);\n}); instead.')
            }
        };

        /** @type {WebpackOptions} */
        this.options = /** @type {WebpackOptions} */ ({});

        this.context = context;

        this.requestShortener = new RequestShortener(context);

        /** @type {boolean} */
        this.running = false;

        /** @type {boolean} */
        this.watchMode = false;

        /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
        this._assetEmittingSourceCache = new WeakMap();
        /** @private @type {Map<string, number>} */
        this._assetEmittingWrittenFiles = new Map();
    }

    /**
     * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name
     * @returns {Logger} a logger with that name
     */
    getInfrastructureLogger(name) {
        if (!name) {
            throw new TypeError(
                "Compiler.getInfrastructureLogger(name) called without a name"
            );
        }
        return new Logger((type, args) => {
            if (typeof name === "function") {
                name = name();
                if (!name) {
                    throw new TypeError(
                        "Compiler.getInfrastructureLogger(name) called with a function not returning a name"
                    );
                }
            }
            if (this.hooks.infrastructureLog.call(name, type, args) === undefined) {
                if (this.infrastructureLogger !== undefined) {
                    this.infrastructureLogger(name, type, args);
                }
            }
        });
    }

    watch(watchOptions, handler) {
        if (this.running) return handler(new ConcurrentCompilationError());

        this.running = true;
        this.watchMode = true;
        this.fileTimestamps = new Map();
        this.contextTimestamps = new Map();
        this.removedFiles = new Set();
        return new Watching(this, watchOptions, handler);
    }

    run(callback) {
        if (this.running) return callback(new ConcurrentCompilationError());

        const finalCallback = (err, stats) => {
            this.running = false;

            if (err) {
                this.hooks.failed.call(err);
            }

            if (callback !== undefined) return callback(err, stats);
        };

        const startTime = Date.now();

        this.running = true;

        const onCompiled = (err, compilation) => {
            if (err) return finalCallback(err);

            if (this.hooks.shouldEmit.call(compilation) === false) {
                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
                this.hooks.done.callAsync(stats, err => {
                    if (err) return finalCallback(err);
                    return finalCallback(null, stats);
                });
                return;
            }

            this.emitAssets(compilation, err => {
                if (err) return finalCallback(err);

                if (compilation.hooks.needAdditionalPass.call()) {
                    compilation.needAdditionalPass = true;

                    const stats = new Stats(compilation);
                    stats.startTime = startTime;
                    stats.endTime = Date.now();
                    this.hooks.done.callAsync(stats, err => {
                        if (err) return finalCallback(err);

                        this.hooks.additionalPass.callAsync(err => {
                            if (err) return finalCallback(err);
                            this.compile(onCompiled);
                        });
                    });
                    return;
                }

                this.emitRecords(err => {
                    if (err) return finalCallback(err);

                    const stats = new Stats(compilation);
                    stats.startTime = startTime;
                    stats.endTime = Date.now();
                    this.hooks.done.callAsync(stats, err => {
                        if (err) return finalCallback(err);
                        return finalCallback(null, stats);
                    });
                });
            });
        };

        this.hooks.beforeRun.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.readRecords(err => {
                    if (err) return finalCallback(err);

                    this.compile(onCompiled);
                });
            });
        });
    }

    runAsChild(callback) {
        this.compile((err, compilation) => {
            if (err) return callback(err);

            this.parentCompilation.children.push(compilation);
            for (const { name, source, info } of compilation.getAssets()) {
                this.parentCompilation.emitAsset(name, source, info);
            }

            const entries = Array.from(
                compilation.entrypoints.values(),
                ep => ep.chunks
            ).reduce((array, chunks) => {
                return array.concat(chunks);
            }, []);

            return callback(null, entries, compilation);
        });
    }

    purgeInputFileSystem() {
        if (this.inputFileSystem && this.inputFileSystem.purge) {
            this.inputFileSystem.purge();
        }
    }

    emitAssets(compilation, callback) {
        let outputPath;
        const emitFiles = err => {
            if (err) return callback(err);

            asyncLib.forEachLimit(
                compilation.getAssets(),
                15,
                ({ name: file, source }, callback) => {
                    let targetFile = file;
                    const queryStringIdx = targetFile.indexOf("?");
                    if (queryStringIdx >= 0) {
                        targetFile = targetFile.substr(0, queryStringIdx);
                    }

                    const writeOut = err => {
                        if (err) return callback(err);
                        const targetPath = this.outputFileSystem.join(
                            outputPath,
                            targetFile
                        );
                        // TODO webpack 5 remove futureEmitAssets option and make it on by default
                        if (this.options.output.futureEmitAssets) {
                            // check if the target file has already been written by this Compiler
                            const targetFileGeneration = this._assetEmittingWrittenFiles.get(
                                targetPath
                            );

                            // create an cache entry for this Source if not already existing
                            let cacheEntry = this._assetEmittingSourceCache.get(source);
                            if (cacheEntry === undefined) {
                                cacheEntry = {
                                    sizeOnlySource: undefined,
                                    writtenTo: new Map()
                                };
                                this._assetEmittingSourceCache.set(source, cacheEntry);
                            }

                            // if the target file has already been written
                            if (targetFileGeneration !== undefined) {
                                // check if the Source has been written to this target file
                                const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
                                if (writtenGeneration === targetFileGeneration) {
                                    // if yes, we skip writing the file
                                    // as it's already there
                                    // (we assume one doesn't remove files while the Compiler is running)

                                    compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                                        size: cacheEntry.sizeOnlySource.size()
                                    });

                                    return callback();
                                }
                            }

                            // TODO webpack 5: if info.immutable check if file already exists in output
                            // skip emitting if it's already there

                            // get the binary (Buffer) content from the Source
                            /** @type {Buffer} */
                            let content;
                            if (typeof source.buffer === "function") {
                                content = source.buffer();
                            } else {
                                const bufferOrString = source.source();
                                if (Buffer.isBuffer(bufferOrString)) {
                                    content = bufferOrString;
                                } else {
                                    content = Buffer.from(bufferOrString, "utf8");
                                }
                            }

                            // Create a replacement resource which only allows to ask for size
                            // This allows to GC all memory allocated by the Source
                            // (expect when the Source is stored in any other cache)
                            cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
                            compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
                                size: content.length
                            });

                            // Write the file to output file system
                            this.outputFileSystem.writeFile(targetPath, content, err => {
                                if (err) return callback(err);

                                // information marker that the asset has been emitted
                                compilation.emittedAssets.add(file);

                                // cache the information that the Source has been written to that location
                                const newGeneration =
                                    targetFileGeneration === undefined
                                        ? 1
                                        : targetFileGeneration + 1;
                                cacheEntry.writtenTo.set(targetPath, newGeneration);
                                this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
                                this.hooks.assetEmitted.callAsync(file, content, callback);
                            });
                        } else {
                            if (source.existsAt === targetPath) {
                                source.emitted = false;
                                return callback();
                            }
                            let content = source.source();

                            if (!Buffer.isBuffer(content)) {
                                content = Buffer.from(content, "utf8");
                            }

                            source.existsAt = targetPath;
                            source.emitted = true;
                            this.outputFileSystem.writeFile(targetPath, content, err => {
                                if (err) return callback(err);
                                this.hooks.assetEmitted.callAsync(file, content, callback);
                            });
                        }
                    };

                    if (targetFile.match(/\/|\\/)) {
                        const dir = path.dirname(targetFile);
                        this.outputFileSystem.mkdirp(
                            this.outputFileSystem.join(outputPath, dir),
                            writeOut
                        );
                    } else {
                        writeOut();
                    }
                },
                err => {
                    if (err) return callback(err);

                    this.hooks.afterEmit.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback();
                    });
                }
            );
        };

        this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
        });
    }

    emitRecords(callback) {
        if (!this.recordsOutputPath) return callback();
        const idx1 = this.recordsOutputPath.lastIndexOf("/");
        const idx2 = this.recordsOutputPath.lastIndexOf("\\");
        let recordsOutputPathDirectory = null;
        if (idx1 > idx2) {
            recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx1);
        } else if (idx1 < idx2) {
            recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx2);
        }

        const writeFile = () => {
            this.outputFileSystem.writeFile(
                this.recordsOutputPath,
                JSON.stringify(this.records, undefined, 2),
                callback
            );
        };

        if (!recordsOutputPathDirectory) {
            return writeFile();
        }
        this.outputFileSystem.mkdirp(recordsOutputPathDirectory, err => {
            if (err) return callback(err);
            writeFile();
        });
    }

    readRecords(callback) {
        if (!this.recordsInputPath) {
            this.records = {};
            return callback();
        }
        this.inputFileSystem.stat(this.recordsInputPath, err => {
            // It doesn't exist
            // We can ignore this.
            if (err) return callback();

            this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
                if (err) return callback(err);

                try {
                    this.records = parseJson(content.toString("utf-8"));
                } catch (e) {
                    e.message = "Cannot parse records: " + e.message;
                    return callback(e);
                }

                return callback();
            });
        });
    }

    createChildCompiler(
        compilation,
        compilerName,
        compilerIndex,
        outputOptions,
        plugins
    ) {
        const childCompiler = new Compiler(this.context);
        if (Array.isArray(plugins)) {
            for (const plugin of plugins) {
                plugin.apply(childCompiler);
            }
        }
        for (const name in this.hooks) {
            if (
                ![
                    "make",
                    "compile",
                    "emit",
                    "afterEmit",
                    "invalid",
                    "done",
                    "thisCompilation"
                ].includes(name)
            ) {
                if (childCompiler.hooks[name]) {
                    childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
                }
            }
        }
        childCompiler.name = compilerName;
        childCompiler.outputPath = this.outputPath;
        childCompiler.inputFileSystem = this.inputFileSystem;
        childCompiler.outputFileSystem = null;
        childCompiler.resolverFactory = this.resolverFactory;
        childCompiler.fileTimestamps = this.fileTimestamps;
        childCompiler.contextTimestamps = this.contextTimestamps;

        const relativeCompilerName = makePathsRelative(this.context, compilerName);
        if (!this.records[relativeCompilerName]) {
            this.records[relativeCompilerName] = [];
        }
        if (this.records[relativeCompilerName][compilerIndex]) {
            childCompiler.records = this.records[relativeCompilerName][compilerIndex];
        } else {
            this.records[relativeCompilerName].push((childCompiler.records = {}));
        }

        childCompiler.options = Object.create(this.options);
        childCompiler.options.output = Object.create(childCompiler.options.output);
        for (const name in outputOptions) {
            childCompiler.options.output[name] = outputOptions[name];
        }
        childCompiler.parentCompilation = compilation;

        compilation.hooks.childCompiler.call(
            childCompiler,
            compilerName,
            compilerIndex
        );

        return childCompiler;
    }

    isChild() {
        return !!this.parentCompilation;
    }

    createCompilation() {
        return new Compilation(this);
    }

    newCompilation(params) {
        const compilation = this.createCompilation();
        compilation.fileTimestamps = this.fileTimestamps;
        compilation.contextTimestamps = this.contextTimestamps;
        compilation.name = this.name;
        compilation.records = this.records;
        compilation.compilationDependencies = params.compilationDependencies;
        this.hooks.thisCompilation.call(compilation, params);
        this.hooks.compilation.call(compilation, params);
        return compilation;
    }

    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;
    }

    newCompilationParams() {
        const params = {
            normalModuleFactory: this.createNormalModuleFactory(),
            contextModuleFactory: this.createContextModuleFactory(),
            compilationDependencies: new Set()
        };
        return params;
    }

    compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);

                compilation.finish(err => {
                    if (err) return callback(err);

                    compilation.seal(err => {
                        if (err) return callback(err);

                        this.hooks.afterCompile.callAsync(compilation, err => {
                            if (err) return callback(err);

                            return callback(null, compilation);
                        });
                    });
                });
            });
        });
    }
}

module.exports = Compiler;

// ...

hooks 执行顺序

shouldEmit
done
additionalPass
beforeRun
run
emit
assetEmitted
afterEmit
thisCompilation
compilation
normalModuleFactory
contextModuleFactory
beforeCompile
compile
make
afterCompile
watchRun
failed
invalid
watchClose
infrastructureLog

webpack 初始化的时候,就已经定义好一系列钩子供我们使用。

beforeRun、run、thisCompilation、compilation、beforeCompile、compile、make、afterCompile 等。

开始 -> 配置合并 -> 实例化 compiler -> 初始化 node 文件读写能力 -> 挂载 plugins -> 处理 wbepack 内部插件(入口文件处理)
开始 -> compiler.beforeRun -> compiler.run -> compiler.beforeComile -> compiler.compile -> compiler.make

webpack.js 主流程实现

主要分为 pack 目录和测试文件 run.js 以及 webpack.config.js。

webpack.config.js

const path = require('path');

module.exports = {
  devtool: 'none',
  mode: 'development',
  entry: './src/index.js',
  context: process.cwd(),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist')
  }
}

run.js

// const webpack = require('webpack');
// const options = require('./webpack.config.js');

// let compiler = webpack(options);

// compiler.run(function (err, stats) {
//   console.log(err);
//   console.log(stats.toJson());
// }); 

const webpack = require('./pack');
const options = require('./webpack.config.js');

let compiler = webpack(options);

compiler.run(function (err, stats) {
  console.log(err);
  console.log(stats.toJson());
});

pack/package.json

{
  "name": "pack",
  "version": "1.0.0",
  "description": "",
  "main": "lib/webpack.js",
  "directories": {
    "lib": "lib"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "tapable": "1"
  }
}

pack/lib/webpack.js

const Compiler = require('./Compiler');
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin');

const webpack = function (options) {
  // 实例化 compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;

  // 初始化 NodeEnvironmentPlugin
  new NodeEnvironmentPlugin().apply(compiler);

  // 挂载所有的 plugins 插件至 compiler 对象身上
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler);
    }
  }

  // 挂载所有的 webpack 内置插件
  // compiler.options = new WebpackOptionApply().process(options, compiler);

  // 返回 compiler 对象
  return compiler;
}

module.exports = webpack;

pack/lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook
} = require('tapable');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats'])
    }
  }

  run (callback) {
    callback && callback(null, {
      toJson () {
        return {
          entries: [], // 入口信息
          chunks: [], // chunk 信息
          modules: [], // 模块信息
          assets: [], // 最终生成资源
        }
      }
    });
  }
}

module.exports = Compiler;

pack/lib/node/NodeEnvironmentPlugin.js

简单实现,了解逻辑即可。

const fs = require('fs');

class NodeEnvironmentPlugin {
  constructor (options) {
    this.options = options || {};
  }

  apply (compiler) {
    compiler.inputFileSystem = fs;
    compiler.outputFileSystem = fs;
  }
}

module.exports = NodeEnvironmentPlugin;

EntryOptionPlugin 分析

new WebpackOptionsApply().process(options, compiler),对 webpack 默认插件进行挂载

webpack/lib/WebpackOptionsApply

/*
    MIT License http://www.opensource.org/licenses/mit-license.php
    Author Tobias Koppers @sokra
*/
"use strict";

// ...

const { cachedCleverMerge } = require("./util/cleverMerge");

/** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */
/** @typedef {import("./Compiler")} Compiler */

class WebpackOptionsApply extends OptionsApply {
    constructor() {
        super();
    }

    /**
     * @param {WebpackOptions} options options object
     * @param {Compiler} compiler compiler object
     * @returns {WebpackOptions} options object
     */
    process(options, compiler) {
        let ExternalsPlugin;
        compiler.outputPath = options.output.path;
        compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
        compiler.recordsOutputPath =
            options.recordsOutputPath || options.recordsPath;
        compiler.name = options.name;
        // TODO webpack 5 refactor this to MultiCompiler.setDependencies() with a WeakMap
        // @ts-ignore TODO
        compiler.dependencies = options.dependencies;

    // ...

        let noSources;
        let legacy;
        let modern;
        let comment;
        if (
            options.devtool &&
            (options.devtool.includes("sourcemap") ||
                options.devtool.includes("source-map"))
        ) {
            const hidden = options.devtool.includes("hidden");
            const inline = options.devtool.includes("inline");
            const evalWrapped = options.devtool.includes("eval");
            const cheap = options.devtool.includes("cheap");
            const moduleMaps = options.devtool.includes("module");
            noSources = options.devtool.includes("nosources");
            legacy = options.devtool.includes("@");
            modern = options.devtool.includes("#");
            comment =
                legacy && modern
                    ? "\n/*\n//@ source" +
                      "MappingURL=[url]\n//# source" +
                      "MappingURL=[url]\n*/"
                    : legacy
                    ? "\n/*\n//@ source" + "MappingURL=[url]\n*/"
                    : modern
                    ? "\n//# source" + "MappingURL=[url]"
                    : null;
            const Plugin = evalWrapped
                ? EvalSourceMapDevToolPlugin
                : SourceMapDevToolPlugin;
            new Plugin({
                filename: inline ? null : options.output.sourceMapFilename,
                moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
                fallbackModuleFilenameTemplate:
                    options.output.devtoolFallbackModuleFilenameTemplate,
                append: hidden ? false : comment,
                module: moduleMaps ? true : cheap ? false : true,
                columns: cheap ? false : true,
                lineToLine: options.output.devtoolLineToLine,
                noSources: noSources,
                namespace: options.output.devtoolNamespace
            }).apply(compiler);
        } else if (options.devtool && options.devtool.includes("eval")) {
            legacy = options.devtool.includes("@");
            modern = options.devtool.includes("#");
            comment =
                legacy && modern
                    ? "\n//@ sourceURL=[url]\n//# sourceURL=[url]"
                    : legacy
                    ? "\n//@ sourceURL=[url]"
                    : modern
                    ? "\n//# sourceURL=[url]"
                    : null;
            new EvalDevToolModulePlugin({
                sourceUrlComment: comment,
                moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
                namespace: options.output.devtoolNamespace
            }).apply(compiler);
        }

        new JavascriptModulesPlugin().apply(compiler);
        new JsonModulesPlugin().apply(compiler);
        new WebAssemblyModulesPlugin({
            mangleImports: options.optimization.mangleWasmImports
        }).apply(compiler);

    // 
        new EntryOptionPlugin().apply(compiler);
        compiler.hooks.entryOption.call(options.context, options.entry);

     // ...
}

module.exports = WebpackOptionsApply;

webpack/lib/EntryOptionPlugin

/*
    MIT License http://www.opensource.org/licenses/mit-license.php
    Author Tobias Koppers @sokra
*/
"use strict";

const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");

/** @typedef {import("../declarations/WebpackOptions").EntryItem} EntryItem */
/** @typedef {import("./Compiler")} Compiler */

/**
 * @param {string} context context path
 * @param {EntryItem} item entry array or single path
 * @param {string} name entry key name
 * @returns {SingleEntryPlugin | MultiEntryPlugin} returns either a single or multi entry plugin
 */
const itemToPlugin = (context, item, name) => {
    if (Array.isArray(item)) {
        return new MultiEntryPlugin(context, item, name);
    }
  // 返回实例对象
    return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
    /**
     * @param {Compiler} compiler the compiler instance one is tapping into
     * @returns {void}
     */
    apply(compiler) {
    // tap EntryOptionPlugin,注册事件监听
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            if (typeof entry === "string" || Array.isArray(entry)) {
                itemToPlugin(context, entry, "main").apply(compiler);
            } else if (typeof entry === "object") {
                for (const name of Object.keys(entry)) {
                    itemToPlugin(context, entry[name], name).apply(compiler);
                }
            } else if (typeof entry === "function") {
                new DynamicEntryPlugin(context, entry).apply(compiler);
            }
            return true;
        });
    }
};

webpack/lib/SingleEntryPlugin

"use strict";
const SingleEntryDependency = require("./dependencies/SingleEntryDependency");

/** @typedef {import("./Compiler")} Compiler */

class SingleEntryPlugin {
    /**
     * An entry plugin which will handle
     * creation of the SingleEntryDependency
     *
     * @param {string} context context path
     * @param {string} entry entry path
     * @param {string} name entry key name
     */
    constructor(context, entry, name) {
        this.context = context;
        this.entry = entry;
        this.name = name;
    }

    /**
     * @param {Compiler} compiler the compiler instance
     * @returns {void}
     */
    apply(compiler) {
        compiler.hooks.compilation.tap(
            "SingleEntryPlugin",
            (compilation, { normalModuleFactory }) => {
                compilation.dependencyFactories.set(
                    SingleEntryDependency,
                    normalModuleFactory
                );
            }
        );

        compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                const { entry, name, context } = this;

                const dep = SingleEntryPlugin.createDependency(entry, name);

        // 开始执行编译,交由 comilation
        // compiler 主要是做编译前的准备,比如订阅钩子
                compilation.addEntry(context, dep, name, callback);
            }
        );
    }

    /**
     * @param {string} entry entry request
     * @param {string} name entry name
     * @returns {SingleEntryDependency} the dependency
     */
    static createDependency(entry, name) {
        const dep = new SingleEntryDependency(entry);
        dep.loc = { name };
        return dep;
    }
}

module.exports = SingleEntryPlugin;

EntryOptionPlugin 实现

pack/lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation'])
    }
  }

  run (callback) {
    callback && callback(null, {
      toJson () {
        return {
          entries: [], // 入口信息
          chunks: [], // chunk 信息
          modules: [], // 模块信息
          assets: [], // 最终生成资源
        }
      }
    });
  }
}

module.exports = Compiler;

pack/lib/EntryOptionPlugin.js

const SingleEntryPlugin = require('./SingleEntryPlugin');

const itemToPlugin = function (context, item, name) {
  return new SingleEntryPlugin(context, item, name);
}

class EntryOptionPlugin {
  apply (compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      itemToPlugin(context, entry, 'main').apply(compiler);
    });
  }
}

module.exports = EntryOptionPlugin;

pack/lib/SingleEntryPlugin.js

class SingleEntryPlugin {
  constructor (context, entry, name) {
    this.context = context;
    this.entry = entry;
    this.name = name;
  }

  apply (compiler) {
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { context, entry, name } = this;

      console.log('make tap trigger.');
      // compilation.addEntry(context, entry, name, callback);
    });
  }
}

module.exports = SingleEntryPlugin;

pack/lib/webpack.js

const Compiler = require('./Compiler');
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin');
const WebpackOptionApply = require('./WebpackOptionApply');

const webpack = function (options) {
  // 实例化 compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;

  // 初始化 NodeEnvironmentPlugin
  new NodeEnvironmentPlugin().apply(compiler);

  // 挂载所有的 plugins 插件至 compiler 对象身上
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler);
    }
  }

  // 挂载所有的 webpack 内置插件
  compiler.options = new WebpackOptionApply().process(options, compiler);

  // 返回 compiler 对象
  return compiler;
}

module.exports = webpack;

pack/lib/WebpackOptionApply.js

const EntryOptionPlugin = require('./EntryOptionPlugin');

class WebpackOptionApply {
  process (options, compiler) {
    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
  }
}

module.exports = WebpackOptionApply;

run.js

const webpack = require('./pack');
const options = require('./webpack.config.js');

let compiler = webpack(options);

compiler.run(function (err, stats) {
  console.log(err);
  console.log(stats.toJson());
});

run 方法分析及实现

源码分析

run(callback) {
  if (this.running) return callback(new ConcurrentCompilationError());

  const finalCallback = (err, stats) => {
    this.running = false;

    if (err) {
      this.hooks.failed.call(err);
    }

    if (callback !== undefined) return callback(err, stats);
  };

  const startTime = Date.now();

  this.running = true;

  const onCompiled = (err, compilation) => {
    if (err) return finalCallback(err);

    if (this.hooks.shouldEmit.call(compilation) === false) {
      const stats = new Stats(compilation);
      stats.startTime = startTime;
      stats.endTime = Date.now();
      this.hooks.done.callAsync(stats, err => {
        if (err) return finalCallback(err);
        return finalCallback(null, stats);
      });
      return;
    }

    this.emitAssets(compilation, err => {
      if (err) return finalCallback(err);

      if (compilation.hooks.needAdditionalPass.call()) {
        compilation.needAdditionalPass = true;

        const stats = new Stats(compilation);
        stats.startTime = startTime;
        stats.endTime = Date.now();
        this.hooks.done.callAsync(stats, err => {
          if (err) return finalCallback(err);

          this.hooks.additionalPass.callAsync(err => {
            if (err) return finalCallback(err);
            this.compile(onCompiled);
          });
        });
        return;
      }

      this.emitRecords(err => {
        if (err) return finalCallback(err);

        const stats = new Stats(compilation);
        stats.startTime = startTime;
        stats.endTime = Date.now();
        this.hooks.done.callAsync(stats, err => {
          if (err) return finalCallback(err);
          return finalCallback(null, stats);
        });
      });
    });
  };

  this.hooks.beforeRun.callAsync(this, err => {
    if (err) return finalCallback(err);

    this.hooks.run.callAsync(this, err => {
      if (err) return finalCallback(err);

      this.readRecords(err => {
        if (err) return finalCallback(err);

        this.compile(onCompiled);
      });
    });
  });
}

代码实现

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),

      beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation'])
    }
  }

  compile () {
    console.log('compile');
  }

  run (callback) {
    const finalCallback = function (err, status) {
      callback(err, status);
    }

    const onCompiled = function (err, compilation) {
      console.log('onCompiled');

      finalCallback(err, {
        toJson () {
          return {
            entries: [],
            chunks: [],
            module: [],
            assets: []
          }
        }
      })
    }

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled);
      });
    });
  }
}

module.exports = Compiler;

compier 方法分析及实现

源码分析

Compiler.js

createCompilation() {
    return new Compilation(this);
}

newCompilation(params) {
    const compilation = this.createCompilation();
    compilation.fileTimestamps = this.fileTimestamps;
    compilation.contextTimestamps = this.contextTimestamps;
    compilation.name = this.name;
    compilation.records = this.records;
    compilation.compilationDependencies = params.compilationDependencies;
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
}

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory(),
        compilationDependencies: new Set()
    };
    return params;
}

compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);

        this.hooks.compile.call(params);

        const compilation = this.newCompilation(params);

        this.hooks.make.callAsync(compilation, err => {
            if (err) return callback(err);

            compilation.finish(err => {
                if (err) return callback(err);

                compilation.seal(err => {
                    if (err) return callback(err);

                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    });
}

调用 newCompilationParams ,返回 params

调用 beforeCompile 钩子:回调中触发 compile 钩子,调用 newCompilationfan 方法返回 compliation, 触发 make 钩子。

代码实现

lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');
const Compilation = require('./Compilation');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),

      beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation'])
    }
  }

  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params;
  }

  createCompilation () {
    return new Compilation(this);
  }

  newCompilation (params) {
    const compilation = this.createCompilation();
  }

  compile (callback) {
    const params = this.newCompilationParams();

    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params);

      const compilation = this.newCompilation(params);

      this.hooks.make.callAsync(compilation, (err) => {
        console.log('make trigger', callback)

        callback && callback();
      });
    });
  }

  run (callback) {
    const finalCallback = function (err, status) {
      callback(err, status);
    }

    const onCompiled = function (err, compilation) {
      console.log('onCompiled');

      finalCallback(err, {
        toJson () {
          return {
            entries: [],
            chunks: [],
            module: [],
            assets: []
          }
        }
      })
    }

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled);
      });
    });
  }
}

module.exports = Compiler;

make 前流程分析

  • 实例化 compiler 对象(贯穿整个 webpack 工作过程)、由 compiler 调用 run 方法
  • compiler 实例化操作
    • compiler 继承 tapable,因此它具备钩子的操作能力(监听事件、触发事件、webpack 是一个事件流)
    • 实例化 compiler 对象之后向它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作让它具备了文件读写能力
    • 具备文件读写能力之后,然后将 plugins 中的插件挂载到 compiler 对象上
    • 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 用来处理模块 ID
    • 在实例化 compiler 的时候,只是监听 make 钩子(SingleEntryPlugin)
      • SingleEntryPlugin 模块的 apply 中存在二个钩子的监听
      • 其中 compilation 钩子就是 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力,因为它就是利用一个自己创建的模块来加载需要被打包的模块
      • 其中 make 钩子在 compiler.run 时会被触发,意味着某个模块打包之前的准备工作就完成了
      • addEntry 方法调用
  • run 方法执行
    • run 方法里就是一堆钩子按照顺序触发(beforeRun、run、compile)
    • compile 方法执行
      • 准备参数(其中 normalModuleFactory 是后续用于创建模块)
      • 触发 beforeCompile
      • 将第一个参数传给一个函数,创建一个 compilation(newCompilation)
      • 在调用 newCompilation 的内部
        • 调用了 createCompilation
        • 触发 this.compilation 钩子和 compilation 的监听
      • 当创建 compilation 对象之后,触发 make 钩子
      • 当触发 make 钩子监听时,将 comilation 对象传递作为参数传递进

总结

  • 实例化 Compiler
  • 调用 compile 方法
  • newCompilation
  • 实例化 Compilation 对象(和 compiler 存在关系)
  • 触发 make 钩子,调用 addEntry 方法(将 context、name、entry 等)进行编译

addEntry 流程分析

  • make 钩子在被触发时,接收 compilation 实例,它由很多属性。
  • 从 compilation 解构三个值
    • entry:当前需要被打包的模块的相对路径(./src/index.js)
    • name:main
    • context:当前项目的根路径
  • dep 是对当前入口模块的依赖关系进行处理
  • 调用 addEntry 方法。
  • 在 compilation 实例身上存在一个 addEntry 方法,然后内部调用 _addModuleChain 方法去处理依赖
  • 在 compilation 中可以通过 NormalModuleFactory 工厂来创建一个普通的模块对象
  • 在 webpack 内部默认开启了一个 100 并发量的打包操作,我们看到的是 normalModule.create()
  • 在 beforeResolve 内部会触发一个 factory 钩子监听(这部分操作用来处理 loader,不会重点分析)
  • 上述操作完成之后,获取到一个函数存在 factory 中,然后对它进行立即调用,在这个函数调用里又触发了一个 resolver 的钩子(处理 loader,拿到 resolver 方法之后意味着所有的 loader 处理完毕)
  • 调用 resolver() 方法之后,就会进入到 afterResolve 这个钩子里,然后就会触发 new NormalModule
  • 完成上述操作之后就将 module 进行保存和一些其他属性参加
  • 调用 buildModule 方法开始编译,内部调用 build 方法,内部返回并调用 doBuild

lib/compilation.js

_addModuleChain(context, dependency, onModule, callback) {
    const start = this.profile && Date.now();
    const currentProfile = this.profile && {};

    const errorAndCallback = this.bail
        ? err => {
                callback(err);
            }
        : err => {
                err.dependencies = [dependency];
                this.errors.push(err);
                callback();
            };

    if (
        typeof dependency !== "object" ||
        dependency === null ||
        !dependency.constructor
    ) {
        throw new Error("Parameter 'dependency' must be a Dependency");
    }
    const Dep = /** @type {DepConstructor} */ (dependency.constructor);
    const moduleFactory = this.dependencyFactories.get(Dep);
    if (!moduleFactory) {
        throw new Error(
            `No dependency factory available for this dependency type: ${dependency.constructor.name}`
        );
    }

    this.semaphore.acquire(() => {
        moduleFactory.create(
            {
                contextInfo: {
                    issuer: "",
                    compiler: this.compiler.name
                },
                context: context,
                dependencies: [dependency]
            },
            (err, module) => {
                if (err) {
                    this.semaphore.release();
                    return errorAndCallback(new EntryModuleNotFoundError(err));
                }

                let afterFactory;

                if (currentProfile) {
                    afterFactory = Date.now();
                    currentProfile.factory = afterFactory - start;
                }

                const addModuleResult = this.addModule(module);
                module = addModuleResult.module;

                onModule(module);

                dependency.module = module;
                module.addReason(null, dependency);

                const afterBuild = () => {
                    if (addModuleResult.dependencies) {
                        this.processModuleDependencies(module, err => {
                            if (err) return callback(err);
                            callback(null, module);
                        });
                    } else {
                        return callback(null, module);
                    }
                };

                if (addModuleResult.issuer) {
                    if (currentProfile) {
                        module.profile = currentProfile;
                    }
                }

                if (addModuleResult.build) {
                    this.buildModule(module, false, null, null, err => {
                        if (err) {
                            this.semaphore.release();
                            return errorAndCallback(err);
                        }

                        if (currentProfile) {
                            const afterBuilding = Date.now();
                            currentProfile.building = afterBuilding - afterFactory;
                        }

                        this.semaphore.release();
                        afterBuild();
                    });
                } else {
                    this.semaphore.release();
                    this.waitForBuildingFinished(module, afterBuild);
                }
            }
        );
    });
}

addEntry(context, entry, name, callback) {
    this.hooks.addEntry.call(entry, name);

    const slot = {
        name: name,
        // TODO webpack 5 remove `request`
        request: null,
        module: null
    };

    if (entry instanceof ModuleDependency) {
        slot.request = entry.request;
    }

    // TODO webpack 5: merge modules instead when multiple entry modules are supported
    const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
    if (idx >= 0) {
        // Overwrite existing entrypoint
        this._preparedEntrypoints[idx] = slot;
    } else {
        this._preparedEntrypoints.push(slot);
    }
    // 
    this._addModuleChain(
        context,
        entry,
        module => {
            this.entries.push(module);
        },
        (err, module) => {
            if (err) {
                this.hooks.failedEntry.call(entry, name, err);
                return callback(err);
            }

            if (module) {
                slot.module = module;
            } else {
                const idx = this._preparedEntrypoints.indexOf(slot);
                if (idx >= 0) {
                    this._preparedEntrypoints.splice(idx, 1);
                }
            }
            this.hooks.succeedEntry.call(entry, name, module);
            return callback(null, module);
        }
    );
}

addEntry 实现

lib/NormalModule.js

class NormalModule {
  constructor (data) {
    this.name = data.name;
    this.entry = data.entry;
    this.rawRequest = data.rawRequest;
    this.parser = data.parser;
    this.resource = data.resource;
    this._source = undefined; // 模块源代码
    this._ast = undefined; // 模块源代码对应的 AST
  }
}

module.exports = NormalModule;

lib/NormalModuleFactory.js

const NormalModule = require('./NormalModule');

class NormalModuleFactory {
  create (data) {
    return new NormalModule(data);
  }
}

module.exports = NormalModuleFactory;

lib/Compilation

const path = require('path');
const { Tapable, SyncHook } = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');

const normalModuleFactory = new NormalModuleFactory();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.hooks = [
      successModule: new SyncHook(['module'])
    ]
  }

  _addModuleChain (context, entry, name) {
    let entryModule = normalModuleFactory.create({
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径
      // parser
    });

    const afterBuild = function (err) {
      callback(err, entryModule);
    }

    this.buildModule(entryModule, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    this.entries.push(entryModule);
    this.modules.push(entryModule);
  } 

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }
}

module.exports = Compilation;

buildModule 实现

lib/Compilation.js

const path = require('path');
const { Tapable, SyncHook } = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.hooks = {
      successModule: new SyncHook(['module'])
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err);
    });
  }

  _addModuleChain (context, entry, name, callback) {
    let entryModule = normalModuleFactory.create({
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径
      parser
    });

    const afterBuild = function (err) {
      callback(err, entryModule);
    }

    this.buildModule(entryModule, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    this.entries.push(entryModule);
    this.modules.push(entryModule);
  } 

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }
}

module.exports = Compilation;

lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');
const Stats = require('./Stats');
const NormalModuleFactory = require('./NormalModuleFactory');
const Compilation = require('./Compilation');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),

      beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation'])
    }
  }

  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params;
  }

  createCompilation () {
    return new Compilation(this);
  }

  newCompilation (params) {
    const compilation = this.createCompilation();
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
  }

  compile (callback) {
    const params = this.newCompilationParams();

    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params);

      const compilation = this.newCompilation(params);

      this.hooks.make.callAsync(compilation, (err) => {
        callback(err, compilation);
      });
    });
  }

  run (callback) {
    const finalCallback = function (err, status) {
      callback(err, status);
    }

    const onCompiled = function (err, compilation) {
      console.log('onCompiled');

      finalCallback(err, new Stats(compilation));
    }

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled);
      });
    });
  }
}

module.exports = Compiler;

lib/NormalModule

class NormalModule {
  constructor (data) {
    this.name = data.name;
    this.entry = data.entry;
    this.rawRequest = data.rawRequest;
    this.parser = data.parser;
    this.resource = data.resource;
    this._source = undefined; // 模块源代码
    this._ast = undefined; // 模块源代码对应的 AST
  }

  getSource (compilation, callback) {
    compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback);
  }

  doBuild (compilation, callback) {
    this.getSource(compilation, (err, source) => {
      this._source = source;
      callback();
    });
  }

  build (compilation, callback) {
    // 从文件中读取需要被加载的 module 内容
    // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块
    // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树
    // 当且 js 模块内部可能又引用很多其他模块,需要递归处理
    this.doBuild(compilation, (err) => {
      this._ast = this.parser.parse(this._source);
      callback(err);
    });
  }
}

module.exports = NormalModule;

lib/Parser.js

const babylon = require('babylon');
const { Tapable } = require('tapable');

class Parser extends Tapable {
  parse (source) {
    return babylon.parse(source, {
      sourceType: 'module',
      plugins: ['dynamicImport'], // 支持 import 动态导入的语法
    });
  }
}

module.exports = Parser;

lib/Stats.js

class Stats {
  constructor (compilation) {
    this.entries = compilation.entries;
    this.modules = compilation.modules;
  }

  toJson () {
    return this;
  }
}

module.exports = Stats;

依赖模块处理

yarn add @babel/core @babel/generator @babel/traverse @babel/types neo-async -D
Stats {
  entries: [
    NormalModule {
      name: 'main',
      rawRequest: './src/index.js',
      parser: [Parser],
      resource: 'D:\\workspace\\notes\\webpack\\webpack_write_plus\\webpack_write/src/index.js',
      _source: "const title = require('./title');\r\n" +
        '\r\n' +
        "console.log('index');\r\n" +
        'console.log(name);',
      _ast: [Node]
    }
  ],
  modules: [
    NormalModule {
      name: 'main',
      rawRequest: './src/index.js',
      parser: [Parser],
      resource: 'D:\\workspace\\notes\\webpack\\webpack_write_plus\\webpack_write/src/index.js',
      _source: "const title = require('./title');\r\n" +
        '\r\n' +
        "console.log('index');\r\n" +
        'console.log(name);',
      _ast: [Node]
    }
  ]
}
  • 需要将 Index.js 中的 require 方法替换成 __webpack_require__
  • 需要将 ./title 替换成 ./src/title.js
  • 实现对模块递归处理

lib/Compilation.js

const path = require('path');
const { Tapable, SyncHook } = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.hooks = {
      successModule: new SyncHook(['module'])
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err, module);
    });
  }

  processDependcies (module, callback) {
    // 当前的函数的功能就是实现一个被依赖模块的递归加载
    // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来
    // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async)

  }

  _addModuleChain (context, entry, name, callback) {
    let entryModule = normalModuleFactory.create({
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径
      parser
    });

    const afterBuild = function (err, module) {
      // 我们需要判断当前 module 存在依赖 
      if (module.dependencies.length > 0) {
        // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现
        this.processDependcies(module, (err) => {
          callback(err, module);
        });
      } else {
        callback(err, module);
      }
    }

    this.buildModule(entryModule, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    this.entries.push(entryModule);
    this.modules.push(entryModule);
  } 

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }
}

module.exports = Compilation;

lib/NormalModule.js

const path = require('path');
const types = require('@babel/types');
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;

class NormalModule {
  constructor (data) {
    this.name = data.name;
    this.context = data.context;
    this.rawRequest = data.rawRequest;
    this.parser = data.parser;
    this.resource = data.resource;
    this._source = undefined; // 模块源代码
    this._ast = undefined; // 模块源代码对应的 AST
    this.dependencies = []; // 定义空数组,用于保存被依赖加载的模块信息
  }

  getSource (compilation, callback) {
    compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback);
  }

  doBuild (compilation, callback) {
    this.getSource(compilation, (err, source) => {
      this._source = source;
      callback();
    });
  }

  build (compilation, callback) {
    // 从文件中读取需要被加载的 module 内容
    // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块
    // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树
    // 当且 js 模块内部可能又引用很多其他模块,需要递归处理
    this.doBuild(compilation, (err) => {
      this._ast = this.parser.parse(this._source);

      // _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 树转换为 code
      // https://astexplorer.net
      traverse(this._ast, {
        CallExpression: (nodePath) => {
          const node = nodePath.node;

          // 定位 require 所在的节点
          if (node.callee.name === 'require') {
            // 获取原始请求路径
            const modulePath = node.arguments[0].value; // './title'
            // 获取当前被加载的模块名称
            let moduleName = modulePath.split(path.posix.sep).pop(); // title
            // 当前只处理 js,只考虑 js 文件处理
            const extName = moduleName.indexOf('.') === -1 ? '.js' : '';
            // 拼接路径
            moduleName += extName; // title.js
            // 拼接绝对路径
            const depResource = path.posix.join(path.posix.dirname(this.resource), moduleName);
            // 将当前模块的 ID 定义 ok
            const depModuleId = './' + path.posix.relative(this.context, depResource); // ./src/title.js

            // 保存当前被依赖模块的信息,方便后续递归加载
            this.dependencies.push({
              name: this.name, // TODO
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            });

            // 替换内容
            node.callee.name = '__webpack_require__';
            node.arguments = [types.stringLiteral(depModuleId)];
          }
        }
      });

      // 利用 ast 修改代码后,然后需要将修改后的 ast 树转会可执行 code
      const { code } = generator(this._ast);

      this._source = code;

      callback(err);
    });
  }
}

module.exports = NormalModule;

抽离 createModule 方法

const path = require('path');
const { Tapable, SyncHook } = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.hooks = {
      successModule: new SyncHook(['module'])
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err, module);
    });
  }

  processDependcies (module, callback) {
    // 当前的函数的功能就是实现一个被依赖模块的递归加载
    // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来
    // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async)

  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      name,
      context,
      parser,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule);
    }, callback);
  }

  /**
   * @description 定义一个创建模块的方法,复用
   * @param {*} data 创建模块时所需要的一些配置 
   * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data);

    const afterBuild = (err, module) => {
      // 我们需要判断当前 module 存在依赖 
      if (module.dependencies.length > 0) {
        // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现
        this.processDependcies(module, (err) => {
          callback(err, module);
        });
      } else {
        callback(err, module);
      }
    }

    this.buildModule(module, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    doAddEntry && doAddEntry(module);
    this.modules.push(module);
  }

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }
}

module.exports = Compilation;

编译依赖模块

const path = require('path');
const async = require('neo-async');
const { Tapable, SyncHook } = require('tapable');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.hooks = {
      successModule: new SyncHook(['module'])
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err, module);
    });
  }

  processDependcies (module, callback) {
    // 当前的函数的功能就是实现一个被依赖模块的递归加载
    // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来
    // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async)
    const dependencies = module.dependencies;

    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done);
    }, callback);
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      name,
      context,
      parser,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule);
    }, callback);
  }

  /**
   * @description 定义一个创建模块的方法,复用
   * @param {*} data 创建模块时所需要的一些配置 
   * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data);

    const afterBuild = (err, module) => {
      // 我们需要判断当前 module 存在依赖 
      if (module.dependencies.length > 0) {
        // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现
        this.processDependcies(module, (err) => {
          callback(err, module);
        });
      } else {
        callback(err, module);
      }
    }

    this.buildModule(module, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    doAddEntry && doAddEntry(module);
    this.modules.push(module);
  }

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }
}

module.exports = Compilation;

chunk 流程分析及实现

lib/Stats.js

class Stats {
  constructor (compilation) {
    this.entries = compilation.entries;
    this.modules = compilation.modules;
    this.chunks = compilation.chunks;
  }

  toJson () {
    return this;
  }
}

module.exports = Stats;

lib/Chunk.js

class Chunk {
  constructor (entryModule) {
    this.entryModule = entryModule;
    this.name = entryModule.name;
    this.files = []; // 记录 chunk 的文件信息
    this.modules = []; // 记录 chunk 包含的模块
  }
}

module.exports = Chunk;

lib/Compilation.js

const path = require('path');
const async = require('neo-async');
const { Tapable, SyncHook } = require('tapable');
const Chunk = require('./Chunk');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.chunks = []; // 存放打包过程中产出的 chunk
    this.hooks = {
      successModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err, module);
    });
  }

  processDependcies (module, callback) {
    // 当前的函数的功能就是实现一个被依赖模块的递归加载
    // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来
    // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async)
    const dependencies = module.dependencies;

    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done);
    }, callback);
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      name,
      context,
      parser,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule);
    }, callback);
  }

  /**
   * @description 定义一个创建模块的方法,复用
   * @param {*} data 创建模块时所需要的一些配置 
   * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data);

    const afterBuild = (err, module) => {
      // 我们需要判断当前 module 存在依赖 
      if (module.dependencies.length > 0) {
        // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现
        this.processDependcies(module, (err) => {
          callback(err, module);
        });
      } else {
        callback(err, module);
      }
    }

    this.buildModule(module, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    doAddEntry && doAddEntry(module);
    this.modules.push(module);
  }

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }

  // 封装 chunk 
  seal (callback) {
    this.hooks.seal.call();
    this.hooks.beforeChunks.call();

    // 所有的入口模块都被存放在 compilation 对象的 entries 数组中
    // 封装 chunk 指的就是根据某个入口,找到它的所有依赖,将它们的源代码放到一起,之后再进行合并

    for (const entryModule of this.entries) {
      // 创建模块,加载已有模块内容,同时记录模块信息
      const chunk = new Chunk(entryModule);

      // 保存 chunk 信息
      this.chunks.push(chunk);

      // 给 chunk 属性赋值
      chunk.modules = this.modules.filter(module => module.name === chunk.name);
    }

    callback();
  }
}

module.exports = Compilation;

lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');
const Stats = require('./Stats');
const NormalModuleFactory = require('./NormalModuleFactory');
const Compilation = require('./Compilation');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),

      beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation'])
    }
  }

  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params;
  }

  createCompilation () {
    return new Compilation(this);
  }

  newCompilation (params) {
    const compilation = this.createCompilation();
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
  }

  compile (callback) {
    const params = this.newCompilationParams();

    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params);

      const compilation = this.newCompilation(params);

      this.hooks.make.callAsync(compilation, (err) => {

        // 开始处理 chunk
        compilation.seal(err => {
          this.hooks.afterCompile.callAsync(compilation, (err) => {
            callback(err, compilation);
          })
        });
      });
    });
  }

  run (callback) {
    const finalCallback = function (err, status) {
      callback(err, status);
    }

    const onCompiled = function (err, compilation) {
      console.log('onCompiled');

      // 将处理好的 chunk 写入到指定的文件,然后输入至 dist 目录

      finalCallback(err, new Stats(compilation));
    }

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled);
      });
    });
  }
}

module.exports = Compiler;

生成 chunk 代码

yarn add ejs -D

lib/temp/main.ejs

(function (modules) {
  // 定义 webpackJsonpCallback:合并模块定义、改变 Promise 状态,执行后续行为
  function webpackJsonpCallback (data) {
    // 获取需要被加载的模块 ID
    const chunkIds = data[0];
    // 获取需要被动态加载的模块依赖关系对象
    const moreModules = data[1];

    let chunkId, resolves = [];

    // 循环判断 chunkIds 里对应的模块内容是否已经完成加载
    for (let i = 0; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];

      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }

      // 更新当前 chunk 状态
      installedChunks[chunkId] = 0;
    }

    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }

    while (resolves.length) {
      resolves.shift()();
    }
  }


  // 缓存被加载的模块
  const installedModules = {};

  // 定义 installedChunks 对于用于标识某个 chunkId 对应 chunk 是否完成加载
  // 0 已加载过、promises 正在加载、null/undefiend 未加载
  var installedChunks = {
    main: 0 
  }

  // 定义 __webpack_require__ 方法替换 require
  function __webpack_require__ (moduleId) {
    // 判断当前缓存中是否存在要被加载的模块内容,如果存在,直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    // 如果当前缓存中不存在,定义对象
    const module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // 调用当前 moduleId 对应的函数,完成内容加载
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 当上述方法调用完成之后,就可以修改 l 的值用于表示当前模块内容已加载完成
    module.l = true;

    // 加载完成之后,将模块内容返回至调用位置

    return module.exports;
  }

  // 定义 m 属性保存 modules
  __webpack_require__.m = modules;

  // 定义 c 属性保存 cache
  __webpack_require__.c = installedModules;

  // 定义 o 方法用于对象身上是否存在指定属性
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  }

  // 定义 d 方法用于在对象身上添加指定属性及 getter
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  }

  // 定义 r 方法用于标识当前模块是 ES6 类型
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true })
  }

  // 定义 n 方法用于设置具体 getter
  __webpack_require__.n = function (module) {
    let getter = module && module.__esModule ? (
      function getDefault () { return module['default'] }
    ) : (
      function getModuleExports () { return module }
    );

    __webpack_require__.d(getter, 'a', getter);

    return getter;
  }

  // 定义 jsonpScriptSrc 实现 src 的处理
  function jsonpScriptSrc (chunkId) {
    return __webpack_require__.p + "" + chunkId + '.build.js';
  }

  // 定义 e 方法用于实现 jsonp 来加载内容,利用 promise 实现异步加载操作
  __webpack_require__.e = function (chunkId) {
    // 定义一个数组用于存放 promise
    let promises = [];

    // 获取 chunkId 对应的 chunk 是否已经完成加载
    let installedChunkData = installedChunks[chunkId];

    // 根据当前是否已完成加载的状态来执行后续逻辑
    if (installedChunkData !== 0) {
      if (installedChunkData) {
        promises.push(installedChunkData[2]); 
      } else {
        const promise = new Promise((resolve, reject) => {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push(installedChunkData[2] = promise);

        // 创建标签
        const script = document.createElement('script');

        // 设置 src
        script.src = jsonpScriptSrc(chunkId);

        // 写入 scrpit 标签
        document.head.appendChild(script);
      }
    }

    // 执行 promise
    return Promise.all(promises);
  }

  // 定义 t 方法用于加载指定 value 的模块内容,对内容进行处理并返回
  __webpack_require__.t = function (value, mode) {
    // 加载 value 对应的模块内容(value 通常是模块 ID)

    if (mode & 1) {
      value = __webpack_require__(value);
    }

    // commonjs
    if (mode & 8) {
      return value;
    }

    // esmodule
    if ((mode & 4 && typeof value === 'object' && value && value.__esModule)) {
      return value;
    }

    // 如果 8 和 4 都不成立,则需要自定义 ns,通过 default 属性返回内容
    const ns = Object.create(null);

    __webpack_require__.r(ns);

    Object.defineProperty(ns, 'default', { enumerable: true, value });

    if (mode & 2 && typeof value !== 'string') {
      for (const k in value) {
        __webpack_require__.d(ns, k, function (key) {
          return value[key];
        }.bind(null, k));
      }
    }

    return ns;
  }

  // 定义 p 属性用于保存资源访问路径
  __webpack_require__.p = "";

  // 定义变量存放数组
  const jsonpArray = window['webpackJsonp'] = window['webpackJsonp'] || [];

  // 保存原生的 push 方法
  const oldJsonpFunction = jsonpArray.push.bind(jsonpArray);

  // 重写原生的 push 方法
  jsonpArray.push = webpackJsonpCallback;

  // 调用 __webpack_require__ 方法执行模块导入与加载操作
  return __webpack_require__(__webpack_require__.s = '<%-entryModuleId%>');
})
({
  <% for (let module of modules) { %>
    "<%- module.moduleId %>":
    (function(module, exports, __webpack_require__) {
      <%- module._source %>      
    }),
  <% } %>
});

lib/Compilation.js

const path = require('path');
const async = require('neo-async');
const ejs = require('ejs');
const { Tapable, SyncHook } = require('tapable');
const Chunk = require('./Chunk');
const NormalModuleFactory = require('./NormalModuleFactory');
const Parser = require('./Parser');

const normalModuleFactory = new NormalModuleFactory();
const parser = new Parser();

class Compilation extends Tapable {
  constructor (compiler) {
    super();
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    this.inputFileSystem = compiler.inputFileSystem;
    this.outputFileSystem = compiler.outputFileSystem;
    this.entries = []; // 存放所有入口模块数组
    this.modules = []; // 存放所有模块数组
    this.chunks = []; // 存放打包过程中产出的 chunk
    this.assets = [];
    this.files = [];
    this.hooks = {
      successModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  // 完成具体的 build 行为
  buildModule (module, callback) {
    module.build(this, (err) => {
      // module 编译完成
      this.hooks.successModule.call(module);
      callback(err, module);
    });
  }

  processDependcies (module, callback) {
    // 当前的函数的功能就是实现一个被依赖模块的递归加载
    // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来
    // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async)
    const dependencies = module.dependencies;

    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done);
    }, callback);
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      name,
      context,
      parser,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule);
    }, callback);
  }

  /**
   * @description 定义一个创建模块的方法,复用
   * @param {*} data 创建模块时所需要的一些配置 
   * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    let module = normalModuleFactory.create(data);

    const afterBuild = (err, module) => {
      // 我们需要判断当前 module 存在依赖 
      if (module.dependencies.length > 0) {
        // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现
        this.processDependcies(module, (err) => {
          callback(err, module);
        });
      } else {
        callback(err, module);
      }
    }

    this.buildModule(module, afterBuild);

    // 完成本次 build 之后,将 Module 进行保存
    doAddEntry && doAddEntry(module);
    this.modules.push(module);
  }

  // 完成模块编译操作
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module);
    });
  }

  // 封装 chunk 
  seal (callback) {
    this.hooks.seal.call();
    this.hooks.beforeChunks.call();

    // 所有的入口模块都被存放在 compilation 对象的 entries 数组中
    // 封装 chunk 指的就是根据某个入口,找到它的所有依赖,将它们的源代码放到一起,之后再进行合并

    for (const entryModule of this.entries) {
      // 创建模块,加载已有模块内容,同时记录模块信息
      const chunk = new Chunk(entryModule);

      // 保存 chunk 信息
      this.chunks.push(chunk);

      // 给 chunk 属性赋值
      chunk.modules = this.modules.filter(module => module.name === chunk.name);
    }

    // chunk 代码处理环节(模板文件 + 模块内的源代码 => chunk.js)
    this.hooks.afterChunks.call(this.chunks);

    // 生成代码内容
    this.createChunkAssets();

    callback();
  }

  createChunkAssets () {
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i];
      const fileName = chunk.name + '.js';

      chunk.files.push(fileName);

      // 获取模板文件路径
      const tempPath = path.posix.join(__dirname, 'temp/main.ejs');
      // 读取模块文件中的内容
      const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8');
      // 获取渲染函数
      const tempRender = ejs.compile(tempCode);
      // 使用 ejs 语法渲染数据
      let source  = tempRender({
        entryModuleId: chunk.entryModule.moduleId,
        modules: chunk.modules
      });
      // 输出文件
      this.emitAssets(fileName, source);
    }
  }

  emitAssets (fileName, source) {
    this.assets[fileName] = source;
    this.files.push(fileName);
  }
}

module.exports = Compilation;

lib/NormalModule.js

const path = require('path');
const types = require('@babel/types');
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;

class NormalModule {
  constructor (data) {
    this.name = data.name;
    this.context = data.context;
    this.moduleId = data.moduleId;
    this.rawRequest = data.rawRequest;
    this.parser = data.parser;
    this.resource = data.resource;
    this._source = undefined; // 模块源代码
    this._ast = undefined; // 模块源代码对应的 AST
    this.dependencies = []; // 定义空数组,用于保存被依赖加载的模块信息
  }

  getSource (compilation, callback) {
    compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback);
  }

  doBuild (compilation, callback) {
    this.getSource(compilation, (err, source) => {
      this._source = source;
      callback();
    });
  }

  build (compilation, callback) {
    // 从文件中读取需要被加载的 module 内容
    // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块
    // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树
    // 当且 js 模块内部可能又引用很多其他模块,需要递归处理
    this.doBuild(compilation, (err) => {
      this._ast = this.parser.parse(this._source);

      // _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 树转换为 code
      // https://astexplorer.net
      traverse(this._ast, {
        CallExpression: (nodePath) => {
          const node = nodePath.node;

          // 定位 require 所在的节点
          if (node.callee.name === 'require') {
            // 获取原始请求路径
            const modulePath = node.arguments[0].value; // './title'
            // 获取当前被加载的模块名称
            let moduleName = modulePath.split(path.posix.sep).pop(); // title
            // 当前只处理 js,只考虑 js 文件处理
            const extName = moduleName.indexOf('.') === -1 ? '.js' : '';
            // 拼接路径
            moduleName += extName; // title.js
            // 拼接绝对路径
            const depResource = path.posix.join(path.posix.dirname(this.resource), moduleName);
            // 将当前模块的 ID 定义 ok
            const depModuleId = './' + path.posix.relative(this.context, depResource); // ./src/title.js

            // 保存当前被依赖模块的信息,方便后续递归加载
            this.dependencies.push({
              name: this.name, // TODO
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            });

            // 替换内容
            node.callee.name = '__webpack_require__';
            node.arguments = [types.stringLiteral(depModuleId)];
          }
        }
      });

      // 利用 ast 修改代码后,然后需要将修改后的 ast 树转会可执行 code
      const { code } = generator(this._ast);

      this._source = code;

      callback(err);
    });
  }
}

module.exports = NormalModule;

生成打包文件

src/index.js

const { name } = require('./title');

console.log('index');
console.log(name);

lib/Stat.js

class Stats {
  constructor (compilation) {
    this.entries = compilation.entries;
    this.modules = compilation.modules;
    this.chunks = compilation.chunks;
    this.files = compilation.files;
  }

  toJson () {
    return this;
  }
}

module.exports = Stats;

lib/webpack.js

const Compiler = require('./Compiler');
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin');
const WebpackOptionApply = require('./WebpackOptionApply');

const webpack = function (options) {
  // 实例化 compiler 对象
  const compiler = new Compiler(options.context);
  compiler.options = options;

  // 初始化 NodeEnvironmentPlugin
  new NodeEnvironmentPlugin().apply(compiler);

  // 挂载所有的 plugins 插件至 compiler 对象身上
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler);
    }
  }

  // 挂载所有的 webpack 内置插件
  new WebpackOptionApply().process(options, compiler);

  // 返回 compiler 对象
  return compiler;
}

module.exports = webpack;

lib/Compiler.js

const {
  Tapable,
  AsyncSeriesHook,
  SyncBailHook,
  SyncHook,
  AsyncParallelBailHook
} = require('tapable');
const path = require('path');
const mkdirp = require('mkdirp');
const Stats = require('./Stats');
const NormalModuleFactory = require('./NormalModuleFactory');
const Compilation = require('./Compilation');

class Compiler extends Tapable {
  constructor (context) {
    super();
    this.context = context;
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),

      beforeRun: new AsyncSeriesHook(["compiler"]),
            run: new AsyncSeriesHook(["compiler"]),

      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),

      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelBailHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),

      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params;
  }

  createCompilation () {
    return new Compilation(this);
  }

  newCompilation (params) {
    const compilation = this.createCompilation();
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);
    return compilation;
  }

  compile (callback) {
    const params = this.newCompilationParams();

    this.hooks.beforeRun.callAsync(params, (err) => {
      this.hooks.compile.call(params);

      const compilation = this.newCompilation(params);

      this.hooks.make.callAsync(compilation, (err) => {

        // 开始处理 chunk
        compilation.seal(err => {
          this.hooks.afterCompile.callAsync(compilation, (err) => {
            callback(err, compilation);
          })
        });
      });
    });
  }

  emitAssets (compilation, callback) {
    // 定义工具方法,用于文件生成操作
    const emitFiles = (err) => {
      const assets = compilation.assets;
      const outputPath = this.options.output.path;

      for (let file in assets) {
        const source = assets[file];
        const targetPath = path.posix.join(outputPath, file);

        this.outputFileSystem.writeFileSync(targetPath, source, 'utf8');
      }

      callback(err);
    }

    // 创建目录,准备文件写入
    this.hooks.emit.callAsync(compilation, (err) => {
      mkdirp.sync(this.options.output.path);
      emitFiles();
    });
  }

  run (callback) {
    const finalCallback = function (err, status) {
      callback(err, status);
    }

    const onCompiled = (err, compilation) => {
      // 将处理好的 chunk 写入到指定的文件,然后输入至 dist 目录
      this.emitAssets(compilation, (err) => {
        finalCallback(err, new Stats(compilation));
      });
    }

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.compile(onCompiled);
      });
    });
  }
}

module.exports = Compiler;