当使用命令行启动项目时,npm会让命令行进入node_modules下的.bin文件,在该文件中查找webpack.sh或者webpack.cmd文件,如果找到就执行, 如果没有找到就报错。
webpack.cmd文件:
@IF EXIST "%~dp0\node.exe"
( "%~dp0\node.exe" "%~dp0\..\webpack\bin\webpack.js" %*)
ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.JS;=;% node "%~dp0\..\webpack\bin\webpack.js" %*)
可以看到实际上是执行了node_modules/webpack/bin/webpack.js文件。
webpack.js代码分析:
函数一共有三个:
-
runCommand:创建一个异步的进程(使用nodeJS的child_process模块),使用yarn/npm/pnpm下载webpack-cli。
-
isInstalled:下载webpack-cli
-
runCli:找到node_modules文件下的入口文件,然后执行。
源码分析:
#!/usr/bin/env node/**
* @param {string} command process to run
* @param {string[]} args command line arguments
* @returns {Promise<void>} promise
*/
const runCommand = (command, args) => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
// 创建一个子进程下载webpack-cli,如果错误,直接抛出错误,如果没有错,关闭子进程,并resolve出去。
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true
});
executedCommand.on("error", error => {
reject(error);
});
executedCommand.on("exit", code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};
/**
* @param {string} packageName name of the package
* @returns {boolean} is the package installed?
*/
const isInstalled = packageName => {
// 使用开启pnp
if (process.versions.pnp) {
return true;
}
const path = require("path");
const fs = require("graceful-fs");
let dir = __dirname;
do {
try {
// 判断node_modules/webpac/package.json中的依赖是否都下载,如果下载,返回true,没有则报错。
if (
fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()
) {
return true;
}
}
catch (_error) {
// Nothing
}
} while (dir !== (dir = path.dirname(dir)));
return false;
};
/**
* @param {CliOption} cli options
* @returns {void}
*/
const runCli = cli => {
const path = require("path");
// 获取webpack-cli/package.json文件的绝对目录。
const pkgPath = require.resolve(`${cli.package}/package.json`);
// eslint-disable-next-line node/no-missing-require
const pkg = require(pkgPath);
// 执行webpack-cli/package.json文件和package.json中的bin指向的文件。
// 这里pkg.bin的值为node_modules/webpack-cli/bin/cli.js
// eslint-disable-next-line node/no-missing-require
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
/**
* @typedef {Object} CliOption
* @property {string} name display name
* @property {string} package npm package name
* @property {string} binName name of the executable file
* @property {boolean} installed currently installed?
* @property {string} url homepage
*//** @type {CliOption}
*/
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};
// 判断webpack-cli是否存在,不存在则下载再执行,如果存在,则直接执行
if (!cli.installed) {
const path = require("path");
const fs = require("graceful-fs");
const readLine = require("readline");
const notify = "CLI for webpack must be installed.\n" + ` ${cli.name} (${cli.url})\n`;
console.error(notify);
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";
}
const installOptions = [packageManager === "yarn" ? "add" : "install", "-D"];
console.error( `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join( " " )} ${cli.package}".` );
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({ input: process.stdin, output: process.stderr });
// In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be
// executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback
// function is responsible for clearing the exit code if the user wishes to install webpack-cli.
process.exitCode = 1;
questionInterface.question(question, answer => {
questionInterface.close();
const normalizedAnswer = answer.toLowerCase().startsWith("y");
if (!normalizedAnswer) {
console.error( "You need to install 'webpack-cli' to use webpack via CLI.\n" + "You can also install the CLI manually." );
return;
}
process.exitCode = 0;
console.log( `Installing '${ cli.package }' (running '${packageManager} ${installOptions.join(" ")} ${ cli.package }')...` );
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} else { runCli(cli);}