avatarJennifer Fu

Summary

npm 10 introduces five significant changes, including updated workspace script handling, network request agent usage, removal of strict mode in npm-package-arg, and the elimination of deprecated or unused configuration settings, as well as a change in handling 409 errors during package publication.

Abstract

The release of npm 10 marks a significant update to the package manager for Node.js, with a focus on standardizing defaults and cleaning up legacy configurations. Among the key updates is the change in how workspace mode handles missing scripts, now exiting with a non-zero exit code when a script is missing, aligning with the behavior of individual package management. Network requests in npm are now managed by @npmcli/agent, which provides a set of HTTP agent classes for improved network handling. The update also removes strict mode from npm-package-arg, which was previously used for parsing package installation arguments, and eliminates several deprecated or unused configuration settings such as tmp, ci-name, hashAlgorithm, and metrics-registry. Additionally, npm 10 no longer retries package publication upon encountering a 409 error, a decision made to enhance security and reliability.

Opinions

  • The change in workspace script handling is presented as a removal of special treatment for missing scripts, which may be seen as a move towards more consistent and predictable behavior across workspaces.
  • The adoption of @npmcli/agent for network requests is likely viewed as a modernization effort, providing a more robust and flexible approach to handling HTTP and HTTPS connections.
  • Removing strict mode from npm-package-arg suggests a shift towards better compatibility and ease of use, potentially reducing friction for developers when specifying package versions and sources.
  • The removal of deprecated or unused configuration settings reflects a commitment to maintaining a clean and relevant set of configuration options, which can simplify the user experience and reduce potential points of confusion or error.
  • The decision to stop retrying 409 errors during package publication is framed as a security measure, aiming to prevent potential abuse and ensure the integrity of the package publication process.

5 Features in npm 10

Details on 5 major changes

Photo by Nick Jio on Unsplash

Introduction

npm is the package manager for the node.js JavaScript platform. It puts modules in place (node_modules) so that the node can find them. It also manages dependency conflicts intelligently.

npm is configurable to support a variety of use cases to publish, discover, install, and develop node programs. It has a list of powerful commands.

npm 10 was released on August 31, 2023. The goal for this major release was to standardize appropriate defaults and clean up legacy configurations where possible.

Here is the command to install npm 10:

% npm install -g npm@10

After the installation, npm is at version 10.2.0.

% npm --version
10.2.0

This is the help manual:

% npm --help
npm <command>

Usage:

npm install        install all the dependencies in your project
npm install <foo>  add the <foo> dependency to your project
npm test           run this project's tests
npm run <foo>      run the script named <foo>
npm <command> -h   quick help on <command>
npm -l             display usage info for all commands
npm help <term>    search for help on <term>
npm help npm       more involved overview

All commands:

    access, adduser, audit, bugs, cache, ci, completion,
    config, dedupe, deprecate, diff, dist-tag, docs, doctor,
    edit, exec, explain, explore, find-dupes, fund, get, help,
    help-search, hook, init, install, install-ci-test,
    install-test, link, ll, login, logout, ls, org, outdated,
    owner, pack, ping, pkg, prefix, profile, prune, publish,
    query, rebuild, repo, restart, root, run-script, sbom,
    search, set, shrinkwrap, star, stars, start, stop, team,
    test, token, uninstall, unpublish, unstar, update, version,
    view, whoami

Specify configs in the ini-formatted file:
    /Users/jenniferfu/.npmrc
or on the command line via: npm <command> --key=value

More configuration info: npm help config
Configuration fields: npm help 7 config

[email protected] /Users/jenniferfu/.nvm/versions/node/v21.1.0/lib/node_modules/npm

npm 10 upgrade can be performed for any supported node version, ^18.17.0 || >=20.5.0.

nvm is a simple way to manage versions for node and npm. We explore npm 10 features in the node.js 21 working environment.

% node --version
v21.1.0
% npm --version
10.2.0

These are the new features in npm 10:

  • No longer treats missing scripts as a special case in workspace mode
  • @npmcli/agent is now used as the agent for network requests
  • Removed strict mode from npm-package-arg
  • Removed the deprecated or unused configs
  • Don’t retry 409 errors during npm publish

No longer treats missing scripts as a special case in workspace mode

Workspace is a feature in the npm cli that manages multiple packages within a singular root package. This type of repository is also known as a monorepo.

Here is a repository structure of npm-workspace, which will be used throughout this article:

npm-workspace
├── package.json
└── packages
    ├── package-a
    │   └── package.json
    └── package-b
        └── package.json

npm-workspace/package.json is defined as follows, where workspaces are placed under the packages folder:

{
  "name": "npm-workspace",
  "version": "1.0.0",
  "description": "Show npm 10 changes",
  "workspaces": ["./packages/*"]
}

npm-workspace/packages/package-a.json is defined as follows, with a test script:

{
  "name": "workspace-a",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Run test in package-b\" && exit 0"
  }
}

npm-workspace/packages/package-a.json is defined as follows, without a test script:

{
  "name": "workspace-b",
  "version": "1.0.0"
}

Run the following command to have an exit code in the zsh shell:

% PROMPT_COMMAND='printf "Exit code: %s\n" $?'
% precmd() { eval "$PROMPT_COMMAND" }

Execute the test script with npm 9 for all workspaces:

% npm --version
9.0.0
Exit code: 0                               
%  npm --workspaces run test

> [email protected] test
> echo "Run test in package-b" && exit 0

Run test in package-b
npm ERR! Lifecycle script `test` failed with error: 
npm ERR! Error: Missing script: "test"

To see a list of scripts, run:
  npm run 
npm ERR!   in workspace: [email protected] 
npm ERR!   at location: /Users/jenniferfu/npm-workspace/packages/package-b 
Exit code: 0                             

npm 9 ignores the missing scripts error, and exit with code 0. The special treatment is coded in runWorkspaces. The bold lines check whether it is scriptMissing, and only set process.exitCode = 1 when it is not scriptMissing.

async runWorkspaces (args, filters) {
  const res = []
  await this.setWorkspaces()

  for (const workspacePath of this.workspacePaths) {
    const { content: pkg } = await pkgJson.normalize(workspacePath)
    const runResult = await this.run(args, {
      path: workspacePath,
      pkg,
    }).catch(err => {
      log.error(`Lifecycle script \`${args[0]}\` failed with error:`)
      log.error(err)
      log.error(`  in workspace: ${pkg._id || pkg.name}`)
      log.error(`  at location: ${workspacePath}`)

      const scriptMissing = err.message.startsWith('Missing script')

      // avoids exiting with error code in case there's scripts missing
      // in some workspaces since other scripts might have succeeded
      if (!scriptMissing) {
        process.exitCode = 1
      }
      return scriptMissing
    })

    res.push(runResult)
  }

  // in case **all** tests are missing, then it should exit with error code
  if (res.every(Boolean)) {
   throw new Error(`Missing script: ${args[0]}`)
  }
}

Starting from npm 10, the bold lines have been removed in lib/commands/run-script.js.

Execute the test script with npm 10 for all workspaces:

% npm --version             
10.2.0
Exit code: 0                                                                                                
%  npm --workspaces run test

> [email protected] test
> echo "Run test in package-b" && exit 0

Run test in package-b
npm ERR! Lifecycle script `test` failed with error: 
npm ERR! Error: Missing script: "test"

To see a list of scripts, run:
  npm run 
npm ERR!   in workspace: [email protected] 
npm ERR!   at location: /Users/jenniferfu/npm-workspace/packages/package-b 
Exit code: 1               

As expected, npm 10 has no special treatment for missing scripts in workspace, and exit with code 1.

If we want to ignore missing scripts in workspace, --if-present can be used to ignore missing scripts and exit with code 0. It works for both npm 9 and 10.

%  npm --workspaces --if-present run test

> workspace-a@1.0.0 test
> echo "Run test in package-b" && exit 0

Run test in package-b
Exit code: 0              

@npmcli/agent is now used as the agent for network requests

@npmcli/agent is a set of Node.js HTTP Agent classes used by the npm cli. Go to the repository npm-workspack, and cd packages/package-a.

Install the package @npmcli/agent:

% yarn add @npmcli/agent

After the installation, @npmcli/agent becomes a member of dependencies in npm-workspace/packages/package-a/package.json:

{
  "name": "workspace-a",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Run test in package-b\" && exit 0"
  },
  "dependencies": {
    "@npmcli/agent": "^2.2.0"
  }
}

There are a number of agents in @npmcli/agent:

 module.exports = {
  getAgent,
  Agent,
  // these are exported for backwards compatability
  HttpAgent: Agent,
  HttpsAgent: Agent,
  cache: {
    proxy: proxyCache,
    agent: agentCache,
    dns: dns.cache,
    clear: () => {
      proxyCache.clear()
      agentCache.clear()
      dns.cache.clear()
    },
  },
}

We pick up HttpAgent, where agentOptions can be defined with options of allowHalfOpen, highWaterMark, pauseOnConnect, noDelay, keepAlive, and keepAliveInitialDelay.

Create agent.js under npm-workspace/packages/package-a/:

// agent.js

import { HttpAgent } from '@npmcli/agent';
import fetch from 'minipass-fetch';

const getNpmVersion = async () => {
  // define agent options as needed
  const agentOptions = {};
  const agent = new HttpAgent(agentOptions);
  const data = await fetch('https://registry.npmjs.org/npm', { agent })
    .then((response) => response.json())
    .then((data) => {
      return data;
    })
    .catch((error) => {
      console.error(error);
    });
  console.log(data.versions['10.2.1'].version); // data is too large to be printed
};

getNpmVersion();

Execute the code, and it fetches npm information from https://registry.npmjs.org/npm.

% node --experimental-default-type=module agent.js
10.2.1
Exit code: 0 

Starting from npm 10, @npmcli/agent is used as the agent for network requests.

DEPENDENCIES.md includes the following addtion:

npmcli-agent-->http-proxy-agent;
npmcli-agent-->https-proxy-agent;
npmcli-agent-->lru-cache;
npmcli-agent-->socks-proxy-agent;

Removed strict mode from npm-package-arg

npm-package-arg is a package to parse a value that can be an argument to npm install.

Go to the repository npm-workspack, and cd packages/package-a.

Install the package @npmcli/agent:

% yarn add npm-package-arg

After the installation, npm-package-arg becomes a member of dependencies in npm-workspace/packages/package-a/package.json:

{
  "name": "workspace-a",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Run test in package-b\" && exit 0"
  },
  "dependencies": {
    "@npmcli/agent": "^2.2.0",
    "npm-package-arg": "^11.0.1"
  }
}

Create packageArg.js under npm-workspace/packages/package-a/:

// packageArg.js

import packageArg from 'npm-package-arg';
try {
  console.log(packageArg('[email protected]'));
  console.log(packageArg('git+https://github.com/user/john'));
} catch (error) {
  console.error(error);
}

Execute the code, and it parses the package names of [email protected] and git+https://github.com/user/john:

% node --experimental-default-type=module packageArg.js
Result {
  type: 'version',
  registry: true,
  where: undefined,
  raw: '[email protected]',
  name: 'npm',
  escapedName: 'npm',
  scope: undefined,
  rawSpec: '10.2.0',
  saveSpec: null,
  fetchSpec: '10.2.0',
  gitRange: undefined,
  gitCommittish: undefined,
  gitSubdir: undefined,
  hosted: undefined
}
Result {
  type: 'git',
  registry: undefined,
  where: undefined,
  raw: 'git+https://github.com/user/john',
  name: undefined,
  escapedName: undefined,
  scope: undefined,
  rawSpec: 'git+https://github.com/user/john',
  saveSpec: 'git+https://github.com/user/john.git',
  fetchSpec: 'https://github.com/user/john.git',
  gitRange: undefined,
  gitCommittish: null,
  gitSubdir: undefined,
  hosted: GitHost {
    sshtemplate: [Function: sshtemplate],
    sshurltemplate: [Function: sshurltemplate],
    edittemplate: [Function: edittemplate],
    browsetemplate: [Function: browsetemplate],
    browsetreetemplate: [Function: browsetreetemplate],
    browseblobtemplate: [Function: browseblobtemplate],
    docstemplate: [Function: docstemplate],
    httpstemplate: [Function: httpstemplate],
    filetemplate: [Function: filetemplate],
    shortcuttemplate: [Function: shortcuttemplate],
    pathtemplate: [Function: pathtemplate],
    bugstemplate: [Function: bugstemplate],
    hashformat: [Function: formatHashFragment],
    protocols: [ 'git:', 'http:', 'git+ssh:', 'git+https:', 'ssh:', 'https:' ],
    domain: 'github.com',
    treepath: 'tree',
    blobpath: 'blob',
    editpath: 'edit',
    gittemplate: [Function: gittemplate],
    tarballtemplate: [Function: tarballtemplate],
    extract: [Function: extract],
    type: 'github',
    user: 'user',
    auth: null,
    project: 'john',
    committish: '',
    default: 'https',
    opts: { noGitPlus: true, noCommittish: true }
  }
}
Exit code: 0

Before npm 10, npm-package-arg can be set to run the strict RFC 8909 mode, when the environ NPM_PACKAGE_ARG_8909_STRICT=1 was set. The strict RFC 8909 mode has been removed.

The if condition in lib/npa.js has been removed.

if (process.env.NPM_PACKAGE_ARG_8909_STRICT !== '1') {
  // XXX backwards compatibility lack of compliance with 8909
  // Remove when we want a breaking change to come into RFC compliance.
  if (resolvedUrl.host && resolvedUrl.host !== 'localhost') {
    const rawSpec = res.rawSpec.replace(/^file:\/\//, 'file:///')
    resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`)
    specUrl = new url.URL(rawSpec)
    rawNoPrefix = rawSpec.replace(/^file:/, '')
  }
  // turn file:/../foo into file:../foo
  // for 1, 2 or 3 leading slashes since we attempted
  // in the previous step to make it a file protocol url with a leading slash
  if (/^\/{1,3}\.\.?(\/|$)/.test(rawNoPrefix)) {
    const rawSpec = res.rawSpec.replace(/^file:\/{1,3}/, 'file:')
    resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`)
    specUrl = new url.URL(rawSpec)
    rawNoPrefix = rawSpec.replace(/^file:/, '')
  }
  // XXX end 8909 violation backwards compatibility section
}

Removed the deprecated or unused configs

npm 10 removed the deprecated or unused configs: tmp, ci-name, hashAlgorithm, and metrics-registry.

  • tmp: The tmp location stored temporary files. This setting is no longer used, as npm stores temporary files in a special location in the cache to be managed by cacache.

The following code is removed in workspaces/config/lib/definitions/definitions.js.

define('tmp', {
  default: tmpdir(),
  defaultDescription: `
    The value returned by the Node.js \`os.tmpdir()\` method
    <https://nodejs.org/api/os.html#os_os_tmpdir>
  `,
  type: path,
  deprecated: `
    This setting is no longer used.  npm stores temporary files in a special
    location in the cache, and they are managed by
    [\`cacache\`](http://npm.im/cacache).
  `,
  description: `
    Historically, the location where temporary files were stored.  No longer
    relevant.
  `,
})
  • ci-name: It was the name of a continuous integration (CI) system. The CI portion of the default user-agent will now only be derived from the environment and cannot be manually overridden.

The following code is removed in workspaces/config/lib/definitions/definitions.js.

define('ci-name', {
  default: ciInfo.name ? ciInfo.name.toLowerCase().split(' ').join('-') : null,
  defaultDescription: `
    The name of the current CI system, or \`null\` when not on a known CI
    platform.
  `,
  type: [null, String],
  deprecated: `
    This config is deprecated and will not be changeable in future version of npm.
  `,
  description: `
    The name of a continuous integration system.  If not set explicitly, npm
    will detect the current CI environment using the
    [\`ci-info\`](http://npm.im/ci-info) module.
  `,
  flatten,
})
  • hashAlgorithm: The hard-coded hashAlgorithm value is no longer being passed through flatOptions.

The following code is removed in workspaces/config/lib/definitions/index.js:

flat.hashAlgorithm = 'sha1'
  • metrics-registry: The hard-coded metrics-registry config has not be used by any of the npm modules that consume config. Also, npm does not send data to any metrics server. Hence, it is removed.

The following code is removed in workspaces/config/lib/index.js.

// the metrics-registry defaults to the current resolved value of
// the registry, unless overridden somewhere else.
settableGetter(data, 'metrics-registry', () => this.#get('registry'))

Don’t retry 409 errors during npm publish

Before npm 10, if the npm CLI gets a 409 from the registry during publish, it attempts a single retry by re-fetching the manifest and attempting to apply a new patch on what is being published. This is not the safest approach, especially as the code used to patch is not the same code that is used to build the original manifest. It is much safer to simply have the user re-run the publish command.

The following code shows the removed try-catch block (in bold) in workspaces/libnpmpublish/lib/publish.js:

try {
  const res = await npmFetch(spec.escapedName, {
    ...opts,
    method: 'PUT',
    body: metadata,
    ignoreBody: true,
  })
  if (transparencyLogUrl) {
    res.transparencyLogUrl = transparencyLogUrl
  }
  return res
} catch (err) {
  if (err.code !== 'E409') {
    throw err
  }
  // if E409, we attempt exactly ONE retry, to protect us
  // against malicious activity like trying to publish
  // a bunch of new versions of a package at the same time
  // and/or spamming the registry
  const current = await npmFetch.json(spec.escapedName, {
    ...opts,
    query: { write: true },
  })
  const newMetadata = patchMetadata(current, metadata)
  const res = await npmFetch(spec.escapedName, {
    ...opts,
    method: 'PUT',
    body: newMetadata,
    ignoreBody: true,
  })
  /* istanbul ignore next */
  if (transparencyLogUrl) {
    res.transparencyLogUrl = transparencyLogUrl
  }
  return res
}

Conclusion

npm 10 has been released. It supports node versions ^18.17.0 || >=20.5.0.

npm 9 made changes to handle missing scripts in workspace mode, how agent is used for network requests, and whether to retry 409 errors during npm publish. It also removed strict mode from npm-package-arg, as well as the deprecated or unused configs.

If you want to check out features of previous releases, take a look at the following articles:

Here is a list of Node.js:

Thanks for reading.

Want to Connect?

If you are interested, check out my directory of web development articles.

In Plain English

Thank you for being a part of our community! Before you go:

Programming
NPM
Nodejs
Web Development
Software Development
Recommended from ReadMedium