This is the ninth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Note: The Version of WebPack used in this article is V4. X

Every time you package, you call a command like NPM run build. Today we’re going to talk about what happens after you call NPM run build.

NPM finds Webpack

We can call NPM run build because we have the build command configured in the scripts in package.json; As follows:

{
  "name": "webpack-demo"."scripts": {
    "build": "webpack"."debug": "... --watch"
  },
  "author": ""."license": "ISC"."devDependencies": {}}Copy the code

As you can see from the code above, scripts. Build corresponds to a webpack command, so where did you find the Webpack? The answer is that NPM will find node_modules/.bin/webpack; The.bin directory contains executable files (x in RWX). This is where the.bin/webpack comes from.

According to NPM rules, when the package.json of your NPM package is configured with the bin field and effectively points to a script, the executable file corresponding to the bin field will be registered on node_modules/.bin when the package is installed. For example, here is the package.json file in the WebPack;

{/ /... other fields "bin": { "webpack": "bin/webpack.js" }, "bugs": { } }Copy the code

We can open the two files, node_modules/bin/webpack and node_modules/webpack/bin/webpack, js, you will find the two files are the same. So when you run the NPM run build command on the command line, eventually NPM will find the node_modules/.bin/webpack executable and execute it, and your roller coaster ride begins.

2. Executable webpack

The entire node_modules/.bin/webpack does two things:

  1. checkwebpackThe installation of thewebpack cli
  2. Based on the results of the first step, call the Webpack CLI if it is already installed, and guide the user to install it if it is notwebpack-cli;

Personally, I think it’s a great part to guide users through the installation, which uses a few lines of code to implement an interactive CLI, and a few simple lines of code to start a child process to install webpack-CLI. This is great for those of you who are writing independent scripts, so you can say zero dependence, so let’s take a look at how both of these things work;

2.1 Checking the Installation of webPack-CLI

Webpack Check the installation of the webpack-CLI and webpack-command cli. Webpack defines an array describing the two cli constants CLIs,

const CLIs = [
   {
      name: "webpack-cli".package: "webpack-cli".binName: "webpack-cli".alias: "cli".installed: isInstalled("webpack-cli"),
      recommended: true.url: "https://github.com/webpack/webpack-cli".description: "The original webpack full-featured CLI."
   },
   {
      name: "webpack-command".package: "webpack-command".binName: "webpack-command".alias: "command".installed: isInstalled("webpack-command"),
      recommended: false.url: "https://github.com/webpack-contrib/webpack-command".description: "A lightweight, opinionated webpack CLI."}];Copy the code

There is a property in each description cli description object that describes whether it isInstalled: installed. This value is obtained by calling the isInstalled method. This method takes the package name and returns if the package is installed. The core is implemented through require.resolve();

/ * * *@param {string} packageName name of the package
 * @returns {boolean} is the package installed? * /
const isInstalled = packageName= > {
   try {
      require.resolve(packageName);

      return true;
   } catch (err) {
      return false; }};Copy the code

With the installed property describing the object, you can call the filter method on the array to get the installed CLI; InstalledClis (installedClis)

const installedClis = CLIs.filter(cli= > cli.installed);
Copy the code

2.2 Handling situations where the CLI is Installed or not Installed

The check result installedClis has been obtained through filter. To judge the installation, only the installedClis. Length value can be judged.

  1. installedClis.length0, none is installed.
  2. installed.length1Is displayed, indicating that a specific one is installed.
  3. installed.length2, indicating that both are installed;

The simplified code is as follows:

if (installedClis.length === 0) {
   // none
} else if (installedClis.length === 1) {
   // one of the CLIs
} else {
   // both
}
Copy the code

2.2.1 installedClis.length0When you install the Webpack-CLI

The following steps are used to guide the user to the installation:

  1. Select the package manager, i.eyarnornpm
  2. throughreadlineCreate an interactive command line that asks the user to accept automatic installationwebpack-cli;
  3. throughchild_process.spawnTo create a child process and perform the installationwebpack-cliIf the command is successfully executed, the loading will continuewebpack-cli;

2.2.1.1 Select the package manager, i.eyarnornpm

To check whether yarn is used, check whether the yarn. Lock lockfile exists in the current directory. If yes, check that the project is dependent on YARN management and obtain the corresponding installation command, that is, yarn add or NPM install -d. The example code is as follows:

const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));

const packageManager = isYarn ? "yarn" : "npm";
const installOptions = [isYarn ? "add" : "install"."-D"];
Copy the code

2.2.1.2. ThroughreadlineCreate an interactive command line to boot automatic installation

The core node.js package’s createInterface method readline is used to implement a simple command line. When a question needs to be asked to the user, the question method needs to be called. The callback of this method is the user’s input on the command line.

Do you want to install ‘webpack-cli’ (yes/no):, and receive the answer. If the answer begins with Y, the user agrees. After obtaining the user’s consent, the installation command can be executed.

The code is as follows:

const readLine = require("readline");

const question = `Do you want to install 'webpack-cli' (yes/no): `;

const questionInterface = readLine.createInterface({
    input: process.stdin,
    output: process.stderr
});
questionInterface.question(question, answer= > {
    questionInterface.close();

    const normalizedAnswer = answer.toLowerCase().startsWith("y");
});
Copy the code

2.2.1.3. Create a child process and perform the installationwebpack-cliThe command

This step is technically a substep of step 2 above, but it is a point worth mentioning, so it is singled out separately; After getting user input consent to automatically install webpack-CLI, execute the installation command with a known package manager; The runCommand method is used to invoke the installation command in WebPack.

questionInterface.question(question, answer= > {
    const normalizedAnswer = answer.toLowerCase().startsWith("y");
    runCommand(packageManager, installOptions.concat(packageName))
        .then(() = > {
            require(packageName); //eslint-disable-line
        })
        .catch(error= > {
            console.error(error);
            process.exitCode = 1;
        });

});
Copy the code
  • RunCommand method:

Create child processes using node.js’ child_proces and spawn them by calling the spawn method.

The first parameter to spawn is the command to be executed, namely NPM install D or yarn add, the second parameter arg is the parameter to be passed to the command to be executed, namely webpack-cli, and the third parameter controls the spawn’s behavior: Stdio inherits the stdio of the current parent process, and uses the shell to output the installation process information to the command line. For this advice, see the official documentation related to Node.js.

It’s also worth noting that wrapping the spawn call returns a Promise object for a smoother asynchronous programming experience, or making it an async function if you’re happy.

const runCommand = (command, args) = > {
   const cp = require("child_process");
   return new Promise((resolve, reject) = > {
      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(); }}); }); };Copy the code

Of course, the most important step is to load webPack-CLI after it has been successfully installed, which is in runCommand(…). . Then (after) the require (packageName)

2.2.2 installed.length1The CLI has been installed

Parse the description file (package.json) path of the corresponding CLI package, and then load the corresponding executable file according to the bin field in the description file. Notably, require loads and executes the executable file directly, allowing the dabao process to continue;

if (installedClis.length === 0) {
  // none
} else if (installedClis.length === 1) {
   const path = require("path");
   const pkgPath = require.resolve(`${installedClis[0].package}/package.json`); // Parse the description file path
   const pkg = require(pkgPath);
   // require that it also executes it
   require(path.resolve(
       path.dirname(pkgPath),
       pkg.bin[installedClis[0].binName]
   ));
} else {
    // both
}
Copy the code

2.2.3 installed.lengthFor other values, that is, both are installed

If both are installed, you need to choose between two options: tell the user to make a decision and exit the process with process.exitCode = 1:

console.warn(
      `You have installed ${installedClis
         .map(item => item.name)
         .join(
            " and "
         )} 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.`
   );

   // @ts-ignore
   process.exitCode = 1;
Copy the code