Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
/packages/interactivity @luisherranz @darerodz

# Tooling
/.npm-extension.mjs @manzoorwanijk
/.npmrc @manzoorwanijk
# Emptied out by #83033; the entry stays so anything reappearing there gets reviewed.
/bin @manzoorwanijk
Expand Down
194 changes: 194 additions & 0 deletions .npm-extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* Root-owned repairs to third-party manifests, applied by npm before it
* resolves the tree. Drop a repair once the linked upstream fix ships.
*
* Every repair is conditional: it asserts the manifest still needs it and
* calls `obsolete()` when it does not, so an upstream fix fails the install
* naming the entry to delete instead of being overwritten unnoticed.
* Keep that contract when adding one, and never repair a field blindly.
*
* @see https://docs.npmjs.com/cli/configuring-npm/npm-extension
*/

/**
* Peer ranges that upstream has not widened yet, keyed by package name.
* `major` limits each repair to the release line that needs it.
*/
const PEER_RANGES = {
// https://github.com/import-js/eslint-plugin-import/issues/3227
'eslint-plugin-import': {
major: 2,
peerDependencies: {
eslint: '^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 || ^10',
},
},
// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/1075
'eslint-plugin-jsx-a11y': {
major: 6,
peerDependencies: {
eslint: '^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 || ^10',
},
},
// https://github.com/jsx-eslint/eslint-plugin-react/issues/3977
'eslint-plugin-react': {
major: 7,
peerDependencies: {
eslint: '^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 || ^10',
},
},
};

/**
* Dependency ranges a dependent pins below a release it needs, keyed by the
* dependent. `major` limits each repair to the release line that needs it.
*/
const DEPENDENCY_RANGES = {
/*
* Yjs 14 ships as @y/y, and @y/protocols encodes with it. Resolving the
* declared `yjs` range instead gives the server a second implementation,
* whose documents lack store.getClock.
*/
'@y/websocket-server': {
major: 0,
dependencies: {
yjs: 'npm:@y/y@^14.0.0-rc.7',
},
},
// Exact pins below two advisories: GHSA-2883-xcg3-v3hh and GHSA-w4pp-8pjf-rmxw. https://github.com/lerna/lerna/issues/4411
lerna: {
major: 10,
dependencies: {
'js-yaml': '^4.3.2',
pacote: '^21.5.1',
},
},
// 0.3.24 is the last release, so GHSA-w5hq-g745-h8pq stays unless we raise the range.
sockjs: {
major: 0,
dependencies: {
uuid: '^11.1.1',
},
},
};

/**
* Packages whose declarations import React types they never declare. Under
* `install-strategy=linked` those types resolve to `any` for every consumer.
*/
const UNDECLARED_TYPES = {
'@ariakit/react-components': [ '@types/react' ],
'@ariakit/react-utils': [ '@types/react' ],
'@dnd-kit/core': [ '@types/react' ],
'@dnd-kit/sortable': [ '@types/react' ],
'@dnd-kit/utilities': [ '@types/react' ],
'@emotion/use-insertion-effect-with-fallbacks': [ '@types/react' ],
'@floating-ui/react-dom': [ '@types/react' ],
'@react-spring/core': [ '@types/react' ],
'@react-spring/shared': [ '@types/react' ],
'@react-spring/types': [ '@types/react' ],
'@react-spring/web': [ '@types/react' ],
'@tanstack/react-router': [ '@types/react' ],
cmdk: [ '@types/react' ],
'framer-motion': [ '@types/react' ],
're-resizable': [ '@types/react' ],
'react-colorful': [ '@types/react' ],
'react-easy-crop': [ '@types/react' ],
};

/**
* @param {string} version Version to read the release line from.
* @return {number} The major version.
*/
function getMajor( version ) {
return Number( version.split( '.' )[ 0 ] );
}

/**
* Fails the install when a manifest no longer needs its repair, naming the
* entry to delete. A silent skip would instead hide the upstream fix.
*
* @param {Object} pkg The manifest npm is about to read.
* @param {string} reason What upstream changed, as a clause.
*/
function obsolete( pkg, reason ) {
throw new Error(
`${ pkg.name }@${ pkg.version } ${ reason }, so its .npm-extension.mjs repair is obsolete: remove the entry.`
);
}

export function transformManifest( pkg, context ) {
const peerRanges = PEER_RANGES[ pkg.name ];
if ( peerRanges && getMajor( pkg.version ) === peerRanges.major ) {
for ( const [ name, range ] of Object.entries(
peerRanges.peerDependencies
) ) {
if ( ! pkg.peerDependencies?.[ name ] ) {
obsolete( pkg, `no longer declares ${ name } as a peer` );
}
if ( pkg.peerDependencies[ name ] === range ) {
obsolete( pkg, `already declares ${ name } as ${ range }` );
}
}
pkg.peerDependencies = {
...pkg.peerDependencies,
...peerRanges.peerDependencies,
};
context.log( `widened peer ranges of ${ pkg.name }@${ pkg.version }` );
}

const dependencyRanges = DEPENDENCY_RANGES[ pkg.name ];
if (
dependencyRanges &&
getMajor( pkg.version ) === dependencyRanges.major
) {
for ( const [ name, range ] of Object.entries(
dependencyRanges.dependencies
) ) {
if ( ! pkg.dependencies?.[ name ] ) {
obsolete( pkg, `no longer depends on ${ name }` );
}
if ( pkg.dependencies[ name ] === range ) {
obsolete( pkg, `already asks for ${ name }@${ range }` );
}
pkg.dependencies[ name ] = range;
context.log(
`raised ${ name } to ${ range } for ${ pkg.name }@${ pkg.version }`
);
}
}

/*
* The plugin only needs `@terrazzo/parser`; the CLI is just one consumer.
* No upstream issue yet.
*/
if (
pkg.name === '@terrazzo/plugin-css' &&
getMajor( pkg.version ) === 2
) {
if ( ! pkg.peerDependencies?.[ '@terrazzo/cli' ] ) {
obsolete( pkg, 'no longer declares @terrazzo/cli as a peer' );
}
if ( pkg.peerDependenciesMeta?.[ '@terrazzo/cli' ]?.optional ) {
obsolete( pkg, 'already makes @terrazzo/cli optional' );
}
pkg.peerDependenciesMeta = {
...pkg.peerDependenciesMeta,
'@terrazzo/cli': { optional: true },
};
context.log( `made @terrazzo/cli optional for ${ pkg.name }` );
}

for ( const name of UNDECLARED_TYPES[ pkg.name ] ?? [] ) {
if ( pkg.dependencies?.[ name ] || pkg.peerDependencies?.[ name ] ) {
obsolete( pkg, `declares ${ name } itself now` );
}
pkg.peerDependencies = { ...pkg.peerDependencies, [ name ]: '*' };
pkg.peerDependenciesMeta = {
...pkg.peerDependenciesMeta,
[ name ]: { optional: true },
};
context.log( `added optional peer ${ name } to ${ pkg.name }` );
}

return pkg;
}
6 changes: 1 addition & 5 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ install-strategy = "linked"
engine-strict = true
strict-peer-deps = true
prefer-dedupe = true
lockfile-version = 3
min-release-age = 1
# fail the install on any dependency script not covered by allowScripts in package.json
strict-allow-scripts = true
# opt in to the npm v12 defaults that refuse git and remote URL dependencies
allow-git = none
allow-remote = none
# extend that to local tarballs
# npm v12 already refuses git and remote URL dependencies; extend that to local tarballs
allow-file = none
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```bash
# Setup
nvm use # Use the required node version
npm install --global npm@12 # Install npm explicitly, not the version Node bundles
npm install && composer install
npm run wp-env-test status # Always check status first.
npm run wp-env-test start # Only start if not already running.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ The following guide is for setting up your local environment to contribute to th
## Prerequisites

- Node.js
Gutenberg is a JavaScript project that requires [Node.js](https://nodejs.org/). The project is currently built using Node.js v24 and npm v11. Though best efforts are made to always use the Active LTS version of Node.js, this will not always be the case. For more details, please refer to the [Node.js release schedule](https://github.com/nodejs/Release#release-schedule).
Gutenberg is a JavaScript project that requires [Node.js](https://nodejs.org/). The project is currently built using Node.js v24 and npm v12. Though best efforts are made to always use the Active LTS version of Node.js, this will not always be the case. For more details, please refer to the [Node.js release schedule](https://github.com/nodejs/Release#release-schedule).

We recommend using the [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm) since it is the easiest way to install and manage node for macOS, Linux, and Windows 10 using WSL2. See [our Development Tools guide](/docs/getting-started/devenv/README.md#node-js-development-tools) or the Nodejs site for additional installation instructions.

- npm
Install npm explicitly rather than relying on the version Node.js bundles:

```bash
npm install --global npm@^12
```

- Git
Gutenberg is using git for source control. Make sure you have an updated version of git installed on your computer, as well as a GitHub account. You can read the [Git Workflow](/docs/contributors/code/git-workflow.md) to learn more about using git and GitHub with Gutenberg

Expand Down
6 changes: 5 additions & 1 deletion docs/contributors/code/managing-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The repository also contains internal workspaces under `tools/` and `test/` for

## Supply chain policy

`.npmrc` opts in to the npm v12 defaults that refuse git references (`EALLOWGIT`) and tarball URLs (`EALLOWREMOTE`), and extends that to local tarball files (`EALLOWFILE`). Local directories stay at the npm default, which gates nothing here: the `file:` links between workspaces resolve as workspaces rather than directory dependencies.
npm v12 refuses git references (`EALLOWGIT`) and tarball URLs (`EALLOWREMOTE`) by default, and `.npmrc` extends that to local tarball files (`EALLOWFILE`). Local directories stay at the npm default, which gates nothing here: the `file:` links between workspaces resolve as workspaces rather than directory dependencies.

Install scripts are opt-in: every dependency that ships one is recorded in `allowScripts` in the root `package.json`, and `strict-allow-scripts` fails the install with `ESTRICTALLOWSCRIPTS` on anything missing from that list. Every entry is `false`, so nothing compiles on install. `test/ai-development` installs with `--prefix`, so it carries its own copy of the policy.

Expand All @@ -28,3 +28,7 @@ When an install fails that way, read the script, then edit `allowScripts` by han
```

Do not use the `npm install-scripts` subcommands until an npm release ships [npm/cli#9941](https://github.com/npm/cli/pull/9941). All of them read the hoisted layout, so under `install-strategy=linked` they misbehave: `ls` reports covered packages as uncovered, `prune` deletes every entry as unused, and `approve` and `deny` write unusable `node_modules/.store` paths instead of the package name.

## Peer dependencies

`.npmrc` sets `strict-peer-deps`, so a peer range conflict fails the install with `ERESOLVE`. When a third-party package declares a wrong or outdated peer range, repair its manifest in the root [`.npm-extension.mjs`](https://docs.npmjs.com/cli/configuring-npm/npm-extension) with a link to the upstream issue, and remove the repair once the fix is released.
Loading
Loading