webpack源码深入-webpack和webpack-cli
webpack命令工作原理如下
webpack指令
javascript
// webpack/package.json
{
...
"mian":"lib/index.js",
"bin": {
"webpack": "bin/webpack.js"
},
...
}
webpack指令的入口是webpack.js。
- 首先脚本内部创建cli对象
javascript
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};
- 检查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查找依赖包的规则。
- 如果没有cli.installed,可以得出webpack-cli的安装情况,如果安装则调用cli,未安装引导安装
javascript
if(!cli.installed) {
// 引导安装
} else {
// 调用
runCli(cli)
}
- 已经安装
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)
- 引导调用
包管理检查: 根据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
签名
- commandOptions: 创建命令所需要的option
- options: 命令执行所需要的options
- action: 处理命令的action handler
函数工作流
- 判断是否已经加载过的命令,如果是加载过,则不在使用make
- 判断program.comman()注册新的子命令
- 注册command.description()描述星系
- 注册command.usage()用法信息
- 注册command.alias()别名信息
- 检查命令的依赖包的安装信息
- 为新增的command注册传入的options
- 最后为新的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
- 获取包管理工具
- 创建REPL引导用户输入
- 创建子进程执行安装命令
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);
}