preface

It has been a long time since I posted my 2021 year-end review at the end of last year. The reasons include but are not limited to:

  • Promotion on duty (almost a month of agonizing)
  • Chinese New Year (after going home for half a month, I just want to lie down)
  • Moving bricks, writing bugs, fixing bugs, working overtime
  • Lazy (…

After working overtime over the weekend, I suddenly found that the middle of March was approaching, and my OKR progress was still 0%… Can’t can’t can’t do that, I have remedial space… When I clone a project, I automatically type NPM install.

This article was born.

Find program entry

First, let’s be clear about our goal: to see the process of NPM install through source debugging.

Our first step in debugging any program should be to find the program entry, so we need to install NPM locally first. NPM is a package manager that comes with Node.js. If you install Node.js, you will automatically install NPM. I chose the 16.x version of Node.js. Verify the successful installation after downloading and installing:

~ ➜ node -v v16.14.0 to ➜ NPM -v 8.3.1Copy the code

The installation is successful and we are ready to start debugging.

In order to perform the debugging steps, we need to find the entry to the NPM install command. I am using a MAC and can view the address of the executable file from which NPM in any path on the terminal:

➜ which NPM/usr/local/bin/NPMCopy the code

The NPM command actually executes the /usr/local/bin/npm file.

/usr/local/bin is where users put their own executable programs.

Use the ll command to view the details of the file:

➜ ll /usr/local/bin/npm
lrwxr-xr-x  1 root  wheel    38B  3 30  2021 /usr/local/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js
Copy the code

It can be found that /usr/local/bin/npm is a pointer to.. The soft link of /lib/node_modules/ NPM /bin/ NPM -cli.js is executed in the /usr/local/lib/node_modules/ NPM /bin/ NPM -cli.js file.

Soft connect is a common command in Linux. Its function is to establish a link for a file in another location. Ln -s Source file Destination file. When we need to use the same file in different directories, we do not need to put the same file in each directory. We can just use the ln command to link the file in other directories, so we do not need to occupy disk space repeatedly. Soft links are similar to shortcuts on Windows.

Debugging preparation

Go to NPM’s Git repository with the clone code: github.com/npm/cli, using the Lastest branch. After clone, start debugging preparation, we need to use VSCode for debugging.

The first is a general debug configuration:

First click on the Debug icon on the left to create a launch.json:

After clicking VSCode will let us select the environment in which the debugging will run, in this case we will select Node.js:

When selected, VSCode will generate the.vscode folder in the project root directory, which contains the launch.json configuration file.

The default generated configuration file may not meet our requirements, so some configuration changes need to be made manually here. We found that in the previous sectionnpm installThe entry file, so the command here we modify as${workspaceFolder}/bin/npm-cli.js.argsModified toinstall dayjsInstall will do anything, as long as it triggers the command.stopOnEntryConfigured totrue, you can stay in the entry while debugging:

{
    // Use IntelliSense to learn about related attributes.
    // Hover to view descriptions of existing properties.
    / / for more information, please visit: < https://go.microsoft.com/fwlink/?linkid=830387 >
    "version": "0.2.0"."configurations": [{"type": "pwa-node"."request": "launch"."name": "Launch Program"."skipFiles": [
                "<node_internals>/**"]."program": "${workspaceFolder}/bin/npm-cli.js"."args": ["install"."dayjs"]."stopOnEntry": true}}]Copy the code

Once the configuration is complete, we try hitting the Run and Debug button:

The debugger started successfully and stayednpm-cli.jsOn the file. Now you can debug further.

In addition to the normal debugging configuration above, we can take some shortcuts, because we are debugging an NPM package, so we can use the NPM command to debug.

For details about VSCode NPM debugging commands, see the official documentation

Add a script for debug (whatever you want to call it) to package.json:

 "scripts": {
    "debugger": "node ./bin/npm-cli.js install dayjs",},Copy the code

This is equivalent to the following configuration:

{
  "name": "Launch via npm"."type": "node"."request": "launch"."cwd": "${workspaceFolder}"."runtimeExecutable": "npm"."runtimeArgs": ["run-script"."debugger"]}Copy the code

After that, go to our package.json in VScode and hit the debug button:

Select our configured Debugger command from the command options:

Ok, I have also entered the debug process and hit the breakpoint I made in the cli.js file.

Install Process Parsing

In the previous steps, we found the entry to the NPM command, which is./bin/npm-cli.js. The file contains only one line of code:

#! /usr/bin/env node
require('.. /lib/cli.js')(process)
Copy the code

The obvious next step is to look at the./lib/cli.js file. The document is not much:

// Separated out for easier unit testing
module.exports = async process => {
  // set it here so that regardless of what happens later, we don't
  // leak any private CLI configs to other programs
  process.title = 'npm'

  // We used to differentiate between known broken and unsupported
  // versions of node and attempt to only log unsupported but still run.
  // After we dropped node 10 support, we can use new features
  // (like static, private, etc) which will only give vague syntax errors,
  // so now both broken and unsupported use console, but only broken
  // will process.exit. It is important to now perform *both* of these
  // checks as early as possible so the user gets the error message.
  const { checkForBrokenNode, checkForUnsupportedNode } = require('./utils/unsupported.js')
  checkForBrokenNode()
  checkForUnsupportedNode()

  const exitHandler = require('./utils/exit-handler.js')
  process.on('uncaughtException', exitHandler)
  process.on('unhandledRejection', exitHandler)

  const Npm = require('./npm.js')
  const npm = new Npm()
  exitHandler.setNpm(npm)

  // if npm is called as "npmg" or "npm_g", then
  // run in global mode.
  if (process.argv[1][process.argv[1].length - 1= = ='g') {
    process.argv.splice(1.1.'npm'.'-g')}const log = require('./utils/log-shim.js')
  const replaceInfo = require('./utils/replace-info.js')
  log.verbose('cli', replaceInfo(process.argv))

  log.info('using'.'npm@%s', npm.version)
  log.info('using'.'node@%s', process.version)

  const updateNotifier = require('./utils/update-notifier.js')

  let cmd
  // now actually fire up npm and run the command.
  // this is how to use npm programmatically:
  try {
    await npm.load()
    if (npm.config.get('version'.'cli')) {
      npm.output(npm.version)
      return exitHandler()
    }

    // npm --versions=cli
    if (npm.config.get('versions'.'cli')) {
      npm.argv = ['version']
      npm.config.set('usage'.false.'cli')
    }

    updateNotifier(npm)

    cmd = npm.argv.shift()
    if(! cmd) { npm.output(await npm.usage)
      process.exitCode = 1
      return exitHandler()
    }

    await npm.exec(cmd, npm.argv)
    return exitHandler()
  } catch (err) {
    if (err.code === 'EUNKNOWNCOMMAND') {
      const didYouMean = require('./utils/did-you-mean.js')
      const suggestions = await didYouMean(npm, npm.localPrefix, cmd)
      npm.output(`Unknown command: "${cmd}"${suggestions}\n`)
      npm.output('To see a list of supported npm commands, run:\n npm help')
      process.exitCode = 1
      return exitHandler()
    }
    return exitHandler(err)
  }
}
Copy the code

As we read down line by line, all the way up to the try catch is checking and initializing; The actual loading of NPM is at the part of the try catch block.

When you read the source code, you get a sense of what well-written code is. Even without line-by-line debugging, the naming of methods, variables, and clear comments provide a pretty good idea of how code works, which shows how important good naming and good comments are…

Try catch code, we will focus on the following:

cmd = npm.argv.shift()
if(! cmd) { npm.output(await npm.usage)
  process.exitCode = 1
  return exitHandler()
}

await npm.exec(cmd, npm.argv)
return exitHandler()
Copy the code

Here the command in the NPM parameter is first fetched, with CMD to install. If there is no command, it will output an error message and exit the process. NPM. Exec (CMD, NPM. Argv) returns exitHandler() to exit process.

Exec method in./lib/npm.js:

Here’s a look at the exec method to simplify the source code to the core:

// Call an npm command
async exec (cmd, args) {
  // Initialize the command....
  // Check for invalid characters.... in the command
  // Check the execution workspace...

  if (filterByWorkspaces) {
    // Execute commands in workspace...
  } else {
    return command.exec(args).finally(() = > {
      process.emit('timeEnd'.`command:${cmd}`)}}}Copy the code

Since we are executing NPM install under the project directory, we hit the last else branch here, executing the command-.exec () method. Continue to step through to find the method in. / lib/commands/install in js:

Wow, it feels like we’re almost at the end of this!

Let’s see what this method says, to simplify it:

async exec (args) {
  // the /path/to/node_modules/..
  // Initialize some variables...

  // be very strict about engines when trying to update npm itself
  // Upgrade NPM requires special handling...
	// Special handling of global installation

  constopts = { ... this.npm.flatOptions,auditLevel: null.path: where,
    add: args,
    workspaces: this.workspaceNames,
  }
  const arb = new Arborist(opts)
  await arb.reify(opts)

  // Handling of special installation commands, such as preinstall...
  await reifyFinish(this.npm, arb)
}
Copy the code

The focus here is the Reify method. In this method. / workspaces/arborist/lib/arborist/reify js.

Here, at last, was the dawn…

await this[_validatePath]()
await this[_loadTrees](options)
await this[_diffTrees]()
await this[_reifyPackages]()
await this[_saveIdealTree](options)
await this[_copyIdealToActual]()
await this[_awaitQuickAudit]()
Copy the code

Yes, this row of await methods is at the heart of NPM install!

Congratulations, now that you’ve read this, you’re finally about to see the real core of NPM Install, but do you think I’m going to read any more?

No, I tried to step through each method, but there was just too much content to figure out all the constructs for node_modules at once… So we’ll dig a hole in this part for now, and we’ll break it down one at a time.

One of the rules to keep in mind when debugging source code is to focus on the problem and not try to see the whole logic at once

conclusion

Although the source code debugging did not go completely process, but also harvest a lot:

  • Good code really reads like natural language;
  • Clear naming, comments, and code splitting are all very important (think about writing code in general, and try to learn from ducks);
  • Source code debugging, to take a specific problem to look at the analysis;

If you don’t finish reading it, just set a flag. Next time, be sure to.jpg