Webpack源码深入-webpack和webpack-cli

webpack源码深入-webpack和webpack-cli

webpack命令工作原理如下

webpack指令

javascript 复制代码
// webpack/package.json
{
	...
	"mian":"lib/index.js",
	"bin": {
		"webpack": "bin/webpack.js"
	},
	...
}

webpack指令的入口是webpack.js。

  1. 首先脚本内部创建cli对象
javascript 复制代码
const cli = {
	name: "webpack-cli",
	package: "webpack-cli",
	binName: "webpack-cli",
	installed: isInstalled("webpack-cli"),
	url: "https://github.com/webpack/webpack-cli"
};
  1. 检查isInstalled方法检查安装情况,原理是:fs.statSync获取stat对象,在通过stat.isDierectory()判断webpack-cli目录是否存在
javascript 复制代码
const isInstalled = packageName => {
	if (process.versions.pnp) {
		return true;
	}
	const path = require("path");
	const fs = require("graceful-fs");
	let dir = __dirname;
	do {
		try {
			if (
				fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()
			) {
				return true;
			}
		} catch (_error) {
			// Nothing
		}
	} while (dir !== (dir = path.dirname(dir)));
	for (const internalPath of require("module").globalPaths) {
		try {
			if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) {
				return true;
			}
		} catch (_error) {
			// Nothing
		}
	}
	return false;
};

while循环从node_modules/webpack/bin下面这个目录向上查找,一直找到根目录下面的node_modules的过程,直到找到根目录,如果没有找到,则认定为没有。这个对应的node.js查找依赖包的规则。

  1. 如果没有cli.installed,可以得出webpack-cli的安装情况,如果安装则调用cli,未安装引导安装
javascript 复制代码
if(!cli.installed) {
    // 引导安装
} else {
    // 调用
    runCli(cli)
}
  1. 已经安装
javascript 复制代码
runCli(cli)
const runCli = cli => {
	const path = require("path");
	const pkgPath = require.resolve(`${cli.package}/package.json`);
	const pkg = require(pkgPath);

	if (pkg.type === "module" || /\.mjs/i.test(pkg.bin[cli.binName])) {
		import(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName])).catch(
			error => {
				console.error(error);
				process.exitCode = 1;
			}
		);
	} else {
		require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
	}
};

进入require(path.resolve(path.dirname(pkgPath),pkg.bin[cli.binName]))这段函数会进入cli.js文件,然后进入lib下面的bootstrap.js

javascript 复制代码
 "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// eslint-disable-next-line @typescript-eslint/no-var-requires
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
    // Create a new instance of the CLI object
    const cli = new WebpackCLI();
    try {
        await cli.run(args);
    }
    catch (error) {
        cli.logger.error(error);
        process.exit(2);
    }
};
module.exports = runCLI;

实例化了个WebpackCLI(),这个实例的对象就是webpack-cli.js文件中的。这个webpack-cli是处理命令行参数的,然后调用webpack进行打包,不论是什么类型的cli,最后都是调用webpack,执行webpack(config)

  1. 引导调用

包管理检查: 根据yarn.lockjk判定yarn,根据pnpm-lock.yaml判定pnpm,否则使用npm

javascript 复制代码
let packageManager;  
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {  
packageManager = "yarn";  
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {  
packageManager = "pnpm";  
} else {  
packageManager = "npm";  
}  

接下来就是通过交互式命令行界面,完成webpack-cli的剩余安装引导。

webpack-cli指令

webpack-cli/bin/cli.js => 导入bootstrap模块,执行该模块,然后传入process.argv进程参数。
webpack-cli/lib/bootstrap.js 导出一个runCLI,在这个函数内部中,创建了一个WebpackCLI实例cli,然后调用cli.run()方法,run方法是WebpackCLI类型的入口方法。

webpack-cli/lib/webpack-cli.js

javascript 复制代码
clsaa WebpackCLI {
    constructor() {
    
    },
    async run(args, parseOptions) {
    
    }
}
module.exports = WebpackCLI

run中有build, watch, version, help

  • build: 运行webpack
  • watch: 运行webpack并且监听文件变化
  • version: 显示已经安装的package以及已经安装的子package的版本
  • help: 列出命令行可以使用的基础命令喝flag
    externalbBuiltInCommandsInfo中有外置内建命令,包括
  • serve: 运行webpack开发服务器
  • info: 输入系统信息
  • init: 用于初始化一个新的webpack项目
  • loader: 初始化一个loader
  • plugin: 初始化一个插件
  • migrate: 这个命令文档未列出[npm]
  • configtest: 校验webpack配置。
contrutor

构造函数内部通过commander创建了program对象并挂载在webpackcli实例上。

javascript 复制代码
constructor() {
    this.colors = this.createColors();
    this.logger = this.getLogger();
    // Initialize program
    this.program = program;
    this.program.name("webpack");
    this.program.configureOutput({
        writeErr: this.logger.error,
        outputError: (str, write) => write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`),
    });
}
run方法

run方法是webpackcli的主入口

exitOverride改写退出
javascript 复制代码
this.program.exitOverride(async (error) => {
    var _a;
    if (error.exitCode === 0) {
        process.exit(0);
    }
    if (error.code === "executeSubCommandAsync") {
        process.exit(2);
    }
    if (error.code === "commander.help") {
        process.exit(0);
    }
    if (error.code === "commander.unknownOption") {
        let name = error.message.match(/'(.+)'/);
        if (name) {
            name = name[1].slice(2);
            if (name.includes("=")) {
                name = name.split("=")[0];
            }
            const { operands } = this.program.parseOptions(this.program.args);
            const operand = typeof operands[0] !== "undefined"
                ? operands[0]
                : getCommandName(buildCommandOptions.name);
            if (operand) {
                const command = findCommandByName(operand);
                if (!command) {
                    this.logger.error(`Can't find and load command '${operand}'`);
                    this.logger.error("Run 'webpack --help' to see available commands and options");
                    process.exit(2);
                }
                const levenshtein = require("fastest-levenshtein");
                for (const option of command.options) {
                    if (!option.hidden && levenshtein.distance(name, (_a = option.long) === null || _a === void 0 ? void 0 : _a.slice(2)) < 3) {
                        this.logger.error(`Did you mean '--${option.name()}'?`);
                    }
                }
            }
        }
    }
    this.logger.error("Run 'webpack --help' to see available commands and options");
    process.exit(2);
});

这是由于comander在声明式的命令行中有一些默认的退出规则。这里做了一些拦截动作,然后自定义退出过程

注册color/no-color options
javascript 复制代码
this.program.option("--color", "Enable colors on console.");
this.program.on("option:color", function () {
    // @ts-expect-error shadowing 'this' is intended
    const { color } = this.opts();
    cli.isColorSupportChanged = color;
    cli.colors = cli.createColors(color);
});
this.program.option("--no-color", "Disable colors on console.");
this.program.on("option:no-color", function () {
    // @ts-expect-error shadowing 'this' is intended
    const { color } = this.opts();
    cli.isColorSupportChanged = color;
    cli.colors = cli.createColors(color);
});

颜色设置

注册version option
javascript 复制代码
this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.");
处理help option
javascript 复制代码
this.program.helpOption(false);
this.program.addHelpCommand(false);
this.program.option("-h, --help [verbose]", "Display help for commands and options.");

生成式命令中,webpack-cli自己处理help的命令具体动作

action handler
javascript 复制代码
this.program.action(async (options, program) => {
    if (!isInternalActionCalled) {
        isInternalActionCalled = true;
    }
    else {
        this.logger.error("No commands found to run");
        process.exit(2);
    }
    // Command and options
    const { operands, unknown } = this.program.parseOptions(program.args);
    const defaultCommandToRun = getCommandName(buildCommandOptions.name);
    const hasOperand = typeof operands[0] !== "undefined";
    const operand = hasOperand ? operands[0] : defaultCommandToRun;
    const isHelpOption = typeof options.help !== "undefined";
    const isHelpCommandSyntax = isCommand(operand, helpCommandOptions);
    if (isHelpOption || isHelpCommandSyntax) {
        let isVerbose = false;
        if (isHelpOption) {
            if (typeof options.help === "string") {
                if (options.help !== "verbose") {
                    this.logger.error("Unknown value for '--help' option, please use '--help=verbose'");
                    process.exit(2);
                }
                isVerbose = true;
            }
        }
        this.program.forHelp = true;
        const optionsForHelp = []
            .concat(isHelpOption && hasOperand ? [operand] : [])
            // Syntax `webpack help [command]`
            .concat(operands.slice(1))
            // Syntax `webpack help [option]`
            .concat(unknown)
            .concat(isHelpCommandSyntax && typeof options.color !== "undefined"
            ? [options.color ? "--color" : "--no-color"]
            : [])
            .concat(isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : []);
        await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program);
    }
    const isVersionOption = typeof options.version !== "undefined";
    if (isVersionOption) {
        const info = await this.getInfoOutput({ output: "", additionalPackage: [] });
        this.logger.raw(info);
        process.exit(0);
    }
    let commandToRun = operand;
    let commandOperands = operands.slice(1);
    if (isKnownCommand(commandToRun)) {
        await loadCommandByName(commandToRun, true);
    }
    else {
        const isEntrySyntax = fs.existsSync(operand);
        if (isEntrySyntax) {
            commandToRun = defaultCommandToRun;
            commandOperands = operands;
            await loadCommandByName(commandToRun);
        }
        else {
            this.logger.error(`Unknown command or entry '${operand}'`);
            const levenshtein = require("fastest-levenshtein");
            const found = knownCommands.find((commandOptions) => levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3);
            if (found) {
                this.logger.error(`Did you mean '${getCommandName(found.name)}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`);
            }
            this.logger.error("Run 'webpack --help' to see available commands and options");
            process.exit(2);
        }
    }
    await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
        from: "user",
    });
});

主要功能就是:

  • 解析进程参数获取operands, options
  • 判断是否为help
  • 判断是否为version
  • 处理非help或version的语法
  • operand在前面判断过,如果没有传递则默认使用build命令
判断commandToRun是否为已知命令

如果是,则直接进行加载并执行的动作。

javascript 复制代码
if (isKnownCommand(commandToRun)) {
    await loadCommandByName(commandToRun, true);
}
javascript 复制代码
const loadCommandByName = async (commandName, allowToInstall = false) => {
const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
const isWatchCommandUsed = isCommand(commandName, watchCommandOptions);
if (isBuildCommandUsed || isWatchCommandUsed) {
    await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
        this.webpack = await this.loadWebpack();
        return this.getBuiltInOptions();
    }, async (entries, options) => {
        if (entries.length > 0) {
            options.entry = [...entries, ...(options.entry || [])];
        }
        await this.runWebpack(options, isWatchCommandUsed);
    });
}
else if (isCommand(commandName, helpCommandOptions)) {
    // Stub for the `help` command
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    this.makeCommand(helpCommandOptions, [], () => { });
}
else if (isCommand(commandName, versionCommandOptions)) {
    // Stub for the `version` command
    this.makeCommand(versionCommandOptions, this.getInfoOptions(), async (options) => {
        const info = await cli.getInfoOutput(options);
        cli.logger.raw(info);
    });
}
else {
    const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find((externalBuiltInCommandInfo) => getCommandName(externalBuiltInCommandInfo.name) === commandName ||
        (Array.isArray(externalBuiltInCommandInfo.alias)
            ? externalBuiltInCommandInfo.alias.includes(commandName)
            : externalBuiltInCommandInfo.alias === commandName));
    let pkg;
    if (builtInExternalCommandInfo) {
        ({ pkg } = builtInExternalCommandInfo);
    }
    else {
        pkg = commandName;
    }
    if (pkg !== "webpack-cli" && !this.checkPackageExists(pkg)) {
        if (!allowToInstall) {
            return;
        }
        pkg = await this.doInstall(pkg, {
            preMessage: () => {
                this.logger.error(`For using this command you need to install: '${this.colors.green(pkg)}' package.`);
            },
        });
    }
    let loadedCommand;
    try {
        loadedCommand = await this.tryRequireThenImport(pkg, false);
    }
    catch (error) {
        // Ignore, command is not installed
        return;
    }
    let command;
    try {
        command = new loadedCommand();
        await command.apply(this);
    }
    catch (error) {
        this.logger.error(`Unable to load '${pkg}' command`);
        this.logger.error(error);
        process.exit(2);
    }
}
};

commandToRun => build / watch

commandToRun => help

commandToRun => version

commandToRun => externalBuiltIn命令

未知命令
entry命令

webpack-CLI中支持entry语法

javascript 复制代码
$ npx webpack <entry> --output-path <output-path>  
错误命令

如果为止命令不是入口语法的情况下,webpackcli认为我们的输入有无,cli会查找和输入单词命令最接近的命令并提示到命令行。

javascript 复制代码
this.logger.error(`Unknown command or entry '${operand}'`);  
const levenshtein = require("fastest-levenshtein"); // 这个库用于计算两个词之间的差别  
const found = knownCommands.find((commandOptions) => levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3);  
if (found) {  
this.logger.error(`Did you mean '${getCommandName(found.name)}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`);  
}  
this.logger.error("Run 'webpack --help' to see available commands and options");  
process.exit(2);  
调用program.parseAsyanc执行新创建的命令
makeCommand
签名
  1. commandOptions: 创建命令所需要的option
  2. options: 命令执行所需要的options
  3. action: 处理命令的action handler
函数工作流
  1. 判断是否已经加载过的命令,如果是加载过,则不在使用make
  2. 判断program.comman()注册新的子命令
  3. 注册command.description()描述星系
  4. 注册command.usage()用法信息
  5. 注册command.alias()别名信息
  6. 检查命令的依赖包的安装信息
  7. 为新增的command注册传入的options
  8. 最后为新的command注册action handler
javascript 复制代码
async makeCommand(commandOptions, options, action) {
    const alreadyLoaded = this.program.commands.find((command) => command.name() === commandOptions.name.split(" ")[0] ||
        command.aliases().includes(commandOptions.alias));
    if (alreadyLoaded) {
        return;
    }
    const command = this.program.command(commandOptions.name, {
        hidden: commandOptions.hidden,
        isDefault: commandOptions.isDefault,
    });
    if (commandOptions.description) {
        command.description(commandOptions.description, commandOptions.argsDescription);
    }
    if (commandOptions.usage) {
        command.usage(commandOptions.usage);
    }
    if (Array.isArray(commandOptions.alias)) {
        command.aliases(commandOptions.alias);
    }
    else {
        command.alias(commandOptions.alias);
    }
    if (commandOptions.pkg) {
        command.pkg = commandOptions.pkg;
    }
    else {
        command.pkg = "webpack-cli";
    }
    const { forHelp } = this.program;
    let allDependenciesInstalled = true;
    if (commandOptions.dependencies && commandOptions.dependencies.length > 0) {
        for (const dependency of commandOptions.dependencies) {
            const isPkgExist = this.checkPackageExists(dependency);
            if (isPkgExist) {
                continue;
            }
            else if (!isPkgExist && forHelp) {
                allDependenciesInstalled = false;
                continue;
            }
            let skipInstallation = false;
            // Allow to use `./path/to/webpack.js` outside `node_modules`
            if (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) {
                skipInstallation = true;
            }
            // Allow to use `./path/to/webpack-dev-server.js` outside `node_modules`
            if (dependency === WEBPACK_DEV_SERVER_PACKAGE && WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM) {
                skipInstallation = true;
            }
            if (skipInstallation) {
                continue;
            }
            await this.doInstall(dependency, {
                preMessage: () => {
                    this.logger.error(`For using '${this.colors.green(commandOptions.name.split(" ")[0])}' command you need to install: '${this.colors.green(dependency)}' package.`);
                },
            });
        }
    }
    if (options) {
        if (typeof options === "function") {
            if (forHelp && !allDependenciesInstalled && commandOptions.dependencies) {
                command.description(`${commandOptions.description} To see all available options you need to install ${commandOptions.dependencies
                    .map((dependency) => `'${dependency}'`)
                    .join(", ")}.`);
                options = [];
            }
            else {
                options = await options();
            }
        }
        for (const option of options) {
            this.makeOption(command, option);
        }
    }
    command.action(action);
    return command;
}
doInstall
  1. 获取包管理工具
  2. 创建REPL引导用户输入
  3. 创建子进程执行安装命令
javascript 复制代码
async  
doInstall(packageName, options = {})  
{  
// 获取包管理器i  
const packageManager = this.getDefaultPackageManager();  
if (!packageManager) {  
this.logger.error("Can't find package manager");  
process.exit(2);  
}  
if (options.preMessage) {  
options.preMessage();  
}  
// 创建 REPL  
const prompt = ({ message, defaultResponse, stream }) => {  
const readline = require("readline");  
const rl = readline.createInterface({  
input: process.stdin,  
output: stream,  
});  
return new Promise((resolve) => {  
rl.question(`${message} `, (answer) => {  
// Close the stream  
rl.close();  
const response = (answer || defaultResponse).toLowerCase();  
// Resolve with the input response  
if (response === "y" || response === "yes") {  
resolve(true);  
} else {  
resolve(false);  
}  
});  
});  
};  
// yarn uses 'add' command, rest npm and pnpm both use 'install'  
const commandArguments = [packageManager === "yarn" ? "add" : "install", "-D", packageName];  
const commandToBeRun = `${packageManager} ${commandArguments.join(" ")}`;  
let needInstall;  
try {  
needInstall = await prompt({  
message: `[webpack-cli] Would you like to install '${this.colors.green(packageName)}' package? (That will run '${this.colors.green(commandToBeRun)}') (${this.colors.yellow("Y/n")})`,  
defaultResponse: "Y",  
stream: process.stderr,  
});  
} catch (error) {  
this.logger.error(error);  
process.exit(error);  
}  
if (needInstall) {  
// 子进程执行安装命令  
const { sync } = require("cross-spawn");  
try {  
sync(packageManager, commandArguments, { stdio: "inherit" });  
} catch (error) {  
this.logger.error(error);  
process.exit(2);  
}  
return packageName;  
}  
process.exit(2);  
}  
相关推荐
高山我梦口香糖41 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_7482352444 分钟前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing3 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600953 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js