Skip to content

Ensure forward-compatibility with npm v12 #1868

Description

@caugner

Summary

npm v12 is out (12.0.2, already latest on the registry) and changes several npm install defaults that break our repos. npm has asked for it to be backported into the active LTS lines, Node 24 and Node 26 (nodejs/Release#1161), so pinning .nvmrc to Node 24 only protects us until that lands. Node 24.20.0 and 26.8.1 still bundle npm 11.19.0 today.

This issue tracks making the MDN repos forward-compatible with npm 12 while we stay on npm 11. Most of the work can land now: npm 11.16+ writes and understands allowScripts, but treats it as advisory.

Refs: GitHub changelog · npm/cli v12.0.0

All claims below were verified by running npm@12.0.2 against the real manifests and lockfiles.

1. Dependency install scripts are blocked by default

npm 12 won't run preinstall/install/postinstall from dependencies unless the root package.json has an allowScripts allowlist. Root-package scripts still run.

npm ci still succeeds — the binaries just never get fetched, so this surfaces later as confusing errors.

Packages that ship platform optionalDependencies (esbuild, unrs-resolver, @parcel/watcher, @swc/core) are fine. lefthook is degraded (binary resolves, git hooks not registered). The hard breaks have no fallback:

Repo Blocked Hard breaks
fred 11 @mdn/rari (x2), gifsicle, mozjpeg, pngquant-bin, edgedriver, geckodriver
mdn-http-observatory 6 root postinstall, libpq, node_extra_ca_certs_mozilla_bundle
content 5 @mdn/rari, gifsicle, mozjpeg, pngquant-bin
mcp 5 @mdn/rari
dex 3 @mdn/rari
tree-sitter-mdn 2 root install: node-gyp-build, tree-sitter-cli
express-locallibrary-tutorial 2 mongodb-memory-server
browser-compat-data, bcd-utils, curriculum, triagebot, translated-content{,-de,-it}, todo-vue, client-toolchain-example 1–2

@mdn/rari is the most consequential: its postinstall is the only thing that downloads bin/rari, so every consumer breaks.

Fix: on npm >= 11.16, run npm approve-scripts --all --no-allow-scripts-pin and commit the resulting package.json. We want name-only entries, not version pins — pinned entries silently stop matching after a Dependabot bump, which would mean a re-approval commit on every dependency update. Note npm approve-scripts is workspace-unaware.

npx/npm exec have no project package.json to read an allowlist from, so CI calls like npx @mdn/rari build … and npx --package=@mdn/fred fred-ssr need --allow-scripts.

Note

Longer term, the packages we publish should stop relying on install scripts entirely (shipping per-platform optionalDependencies instead), which would remove the need for any consumer to allowlist us. That's a significant piece of work and should be tracked separately.

2. engines.npm: "^11" + engine-strict=true is a hard failure

browser-compat-data is the only repo with an engines.npm constraint. Under npm 12:

npm error code EBADENGINE
npm error notsup Required: {"node":">=24","npm":"^11"}
npm error notsup Actual:   {"node":"v24.20.0","npm":"12.0.2"}

Downstream consumers are unaffected, though: the published package is built from a generated build/package.json that carries only name and version, so engines never reaches the registry.

3. Unpinned npm/Node in CI

  • mdn-http-observatorynpm-publish.yml pins npm install -g npm@^11.8.0; needs a conscious bump.
  • mdn-http-observatorytest.yml has a node-version: [24, 26] matrix; the 26 leg flips to npm 12 once the backport lands.
  • webextensions-exampleslint.yml uses node-version: "latest" (twice).

Everything else uses node-version-file: .nvmrc, which keeps our exposure small.

4. npm adduser was removed — and we document it

Referenced in content (files/en-us/webassembly/guides/rust_to_wasm/index.md, line 397, incl. a link to the npm-adduser docs page) and the ja/ko/ru/zh-cn/de translations. Should become npm login.

5. packageManager pins

15 manifests pin packageManager: "npm@11.x". Nothing enforces them today (no CI enables Corepack), but they need to move with the npm bump.

Verified as not a problem

Listed so nobody re-investigates: allow-git/allow-remote defaulting to none (no lockfile in the org has a git or remote-tarball resolution; tree-sitter-mdn's npm install file:$TARBALL is unaffected since allow-file keeps its default) · unknown configs/abbreviated flags now throwing (every config in our committed .npmrc files and every CI flag we use is still valid) · npm pkg no longer forcing JSON (fred's publish simulation passes --json explicitly) · npm view --json returning an array (our one npm view call doesn't pass --json) · npm shrinkwrap removed (no npm-shrinkwrap.json anywhere) · npm star/stars/unstar removed (unused) · root preinstall now running first (no repo has one) · npm init license default (no impact).

Tasks

  • browser-compat-data: widen or drop engines.npm: "^11"
  • Adopt allowScripts (--no-allow-scripts-pin): fred, content, mcp, dex, mdn-http-observatory, browser-compat-data, bcd-utils, curriculum, triagebot, translated-content{,-de,-it}, tree-sitter-mdn, express-locallibrary-tutorial, client-toolchain-example, todo-vue
  • Add --allow-scripts to CI npx/npm exec calls
  • webextensions-examples: replace node-version: "latest" with node-version-file
  • mdn-http-observatory: bump the pinned global npm, and confirm the Node 26 matrix leg passes on npm 12
  • content + translated-content*: replace npm adduser with npm login
  • Add a single scheduled matrix job in mdn/workflows that runs npm ci under npm@latest against the affected repos, so regressions surface early; remove it once we're on npm 12
  • Bump .nvmrc, engines.node, and packageManager everywhere when Node 26 goes LTS (October 2026)

Pull requests

All opened as drafts. Verified against npm@12.0.2 with npm ci --strict-allow-scripts.

Two notes on the task list:

  • No --allow-scripts flags are needed for our CI npx/npm exec calls. Every one of them resolves the package from a local node_modules (the @mdn/rari and @mdn/fred calls all run with working-directory: mdn/content, where both are dependencies), so the root allowScripts applies. Confirmed under npm 12 for both a direct and a transitive local dependency; it only fails when there is no local tree.
  • dex needs three allowlists, not one. prepare runs npm run install:all, which installs deployer-js and cloud-function as separate projects that do not read the root package.json.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

p1We will address this soon and will provide capacity from our team for it in the next few releases.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions