欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 产业 > Webpack源码深入-webpack和webpack-cli

Webpack源码深入-webpack和webpack-cli

2024/10/26 7:18:02 来源:https://blog.csdn.net/m0_73280507/article/details/139813937  浏览:    关键词:Webpack源码深入-webpack和webpack-cli

webpack源码深入-webpack和webpack-cli

webpack命令工作原理如下
在这里插入图片描述

webpack指令

// webpack/package.json
{..."mian":"lib/index.js","bin": {"webpack": "bin/webpack.js"},...
}

webpack指令的入口是webpack.js。

  1. 首先脚本内部创建cli对象
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目录是否存在
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查找依赖包的规则。
3. 如果没有cli.installed,可以得出webpack-cli的安装情况,如果安装则调用cli,未安装引导安装

if(!cli.installed) {// 引导安装
} else {// 调用runCli(cli)
}
  1. 已经安装
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

 "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 objectconst 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)
5. 引导调用
包管理检查: 根据yarn.lockjk判定yarn,根据pnpm-lock.yaml判定pnpm,否则使用npm

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

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实例上。

constructor() {this.colors = this.createColors();this.logger = this.getLogger();// Initialize programthis.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改写退出
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
this.program.option("--color", "Enable colors on console.");
this.program.on("option:color", function () {// @ts-expect-error shadowing 'this' is intendedconst { 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 intendedconst { color } = this.opts();cli.isColorSupportChanged = color;cli.colors = cli.createColors(color);
});

颜色设置

注册version option
this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.");
处理help option
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
this.program.action(async (options, program) => {if (!isInternalActionCalled) {isInternalActionCalled = true;}else {this.logger.error("No commands found to run");process.exit(2);}// Command and optionsconst { 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是否为已知命令

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

if (isKnownCommand(commandToRun)) {await loadCommandByName(commandToRun, true);
}
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-functionthis.makeCommand(helpCommandOptions, [], () => { });
}
else if (isCommand(commandName, versionCommandOptions)) {// Stub for the `version` commandthis.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 installedreturn;}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语法

$ npx webpack <entry> --output-path <output-path>  
错误命令

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

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
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. 创建子进程执行安装命令
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);  
}  

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com