diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 42166c249a3c5..7e64150920914 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -184,6 +184,7 @@ graph LR; npmcli-arborist-->proggy; npmcli-arborist-->semver; npmcli-arborist-->ssri; + npmcli-arborist-->validate-npm-package-name; npmcli-config-->ini; npmcli-config-->nopt; npmcli-config-->npmcli-eslint-config["@npmcli/eslint-config"]; @@ -579,6 +580,7 @@ graph LR; npmcli-arborist-->tar-stream; npmcli-arborist-->tcompare; npmcli-arborist-->treeverse; + npmcli-arborist-->validate-npm-package-name; npmcli-arborist-->walk-up-path; npmcli-config-->ci-info; npmcli-config-->ini; diff --git a/docs/lib/content/configuring-npm/package-json.md b/docs/lib/content/configuring-npm/package-json.md index 8bdf3c3e74f2a..56724afe699ab 100644 --- a/docs/lib/content/configuring-npm/package-json.md +++ b/docs/lib/content/configuring-npm/package-json.md @@ -1033,6 +1033,66 @@ For example, to replace a transitive dependency with a fork: } ``` +### packageExtensions + +`packageExtensions` lets a project apply small, declarative repairs to the manifests of third-party dependencies before npm resolves the dependency tree. +Use it to add a missing `dependencies`, `optionalDependencies`, or `peerDependencies` entry, or to correct `peerDependencies` and `peerDependenciesMeta`, while you wait for the upstream package to publish a fix. + +This is especially useful with [`install-strategy=linked`](/using-npm/config#install-strategy), where dependencies are fully isolated and a package only sees what it actually declared. +A package that worked under a hoisted layout because a dependency happened to be hoisted above it can fail under `linked`; `packageExtensions` records the missing edge as explicit, reviewable, root-owned policy. + +`packageExtensions` complements [`overrides`](#overrides): `overrides` changes what an existing dependency edge resolves to, while `packageExtensions` adds or corrects the dependency metadata that creates the edge in the first place. +For changing the resolved version of a dependency that is already declared, use `overrides`. + +Like `overrides`, `packageExtensions` is only honored in the root `package.json` of a project (the workspace root in a workspace). +The field in installed dependencies and in non-root workspace packages is ignored. +Because it is root-only project policy, npm refuses to publish a non-private package that contains `packageExtensions`; it remains available to private packages and unpublished local projects. + +Each key is a package selector: a package name with an optional semver range. + +```json +{ + "packageExtensions": { + "broken-package@1": { + "dependencies": { + "missing-runtime-dep": "^2.0.0" + } + }, + "typescript-plugin@4.3.0": { + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "@scope/uses-types@2": { + "dependencies": { + "@types/node": "^22.0.0" + } + } + } +} +``` + +- `"foo"` matches all versions of `foo`. +- `"foo@1"` matches versions satisfying `1`. +- `"@scope/foo@^2.3.0"` matches versions satisfying `^2.3.0`. + +Selectors match a candidate package's own `name` and `version`. They do not accept dist-tags, git, file, directory, URL, or `npm:` alias specs. For aliases, the selector matches the underlying package name. At most one selector may match a given package; overlapping selectors that both match the same package fail the install. + +Only `dependencies`, `optionalDependencies`, `peerDependencies`, and `peerDependenciesMeta` may be extended. The merge rules are: + +- `dependencies` and `optionalDependencies` entries add a missing dependency only. Adding a name that the package already declares in either field is an error; use `overrides` to change a version. +- `peerDependencies` entries are merged by name, replacing an existing range. +- `peerDependenciesMeta` entries are merged by name and then by key, so you can add `optional: true` without dropping other metadata. Every `peerDependenciesMeta` entry must correspond to a `peerDependencies` entry. + +Deletion is not supported; a `null`, `false`, or `"-"` value is an error. + +`packageExtensions` does not rewrite the installed package's `package.json` on disk and does not modify `bundleDependencies`. Affected packages are recorded in `package-lock.json` and surfaced by [`npm explain`](/commands/npm-explain) and [`npm ls`](/commands/npm-ls), so each repair is easy to audit and to remove once upstream is fixed. + ### engines You can specify the version of node that your stuff works on: diff --git a/lib/commands/ci.js b/lib/commands/ci.js index ef5ce206aff6f..941270e0bad22 100644 --- a/lib/commands/ci.js +++ b/lib/commands/ci.js @@ -6,6 +6,7 @@ const fs = require('node:fs/promises') const path = require('node:path') const { log, time } = require('proc-log') const validateLockfile = require('../utils/validate-lockfile.js') +const { validatePackageExtensions } = require('../utils/validate-lockfile.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') const getWorkspaces = require('../utils/get-workspaces.js') @@ -89,6 +90,8 @@ class CI extends ArboristWorkspaceCmd { // Verifies that the packages from the ideal tree will match the same versions that are present in the virtual tree (lock file). const errors = validateLockfile(virtualInventory, arb.idealTree.inventory) + // Verifies that the root packageExtensions state matches the lockfile and is still consistent with the locked tree. + errors.push(...validatePackageExtensions(virtualArb.virtualTree, arb.idealTree)) if (errors.length) { throw this.usageError( '`npm ci` can only install packages when your package.json and package-lock.json are in sync. ' + diff --git a/lib/commands/ls.js b/lib/commands/ls.js index 1e46443d32c58..738052c5a9c05 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -278,6 +278,21 @@ const augmentItemWithIncludeMetadata = (node, item) => { return item } +// Render a node's packageExtensions provenance as a short "field.name" list, empty when none. +const formatPackageExtensions = (applied) => { + if (!applied) { + return '' + } + const fields = ['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta'] + const parts = [] + for (const field of fields) { + for (const name of applied[field] || []) { + parts.push(`${field}.${name}`) + } + } + return parts.join(', ') +} + const getHumanOutputItem = (node, { args, chalk, global, long }) => { const { pkgid, path } = node const workspacePkgId = chalk.blueBright(pkgid) @@ -338,6 +353,11 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => { ? ' ' + chalk.cyan(`[patched: ${node.patched.path}]`) : '' ) + + ( + formatPackageExtensions(node.packageExtensionsApplied) + ? ' ' + chalk.dim(`packageExtensions: ${formatPackageExtensions(node.packageExtensionsApplied)}`) + : '' + ) + (isGitNode(node) ? ` (${node.resolved})` : '') + (node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') + (long ? `\n${node.package.description || ''}` : '') @@ -362,6 +382,10 @@ const getJsonOutputItem = (node, { global, long }) => { item.overridden = node.overridden } + if (node.packageExtensionsApplied) { + item.packageExtensionsApplied = node.packageExtensionsApplied + } + item[_name] = node.name // special formatting for top-level package name diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 015e4312f6113..f13bc026f3295 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -96,6 +96,9 @@ class Publish extends BaseCommand { const spec = npa(args[0]) let manifest = await this.#getManifest(spec, opts) + // packageExtensions is root-only project policy and must never be published; fail fast so dry-run reports it too + this.#assertNoPackageExtensions(manifest) + // only run scripts for directory type publishes if (spec.type === 'directory' && !ignoreScripts) { await runScript({ @@ -122,6 +125,8 @@ class Publish extends BaseCommand { // The purpose of re-reading the manifest is in case it changed, so that we send the latest and greatest thing to the registry note that publishConfig might have changed as well! manifest = await this.#getManifest(spec, opts, true) + // re-check the authoritative manifest in case a lifecycle script introduced packageExtensions + this.#assertNoPackageExtensions(manifest) const force = this.npm.config.get('force') const isDefaultTag = this.npm.config.isDefault('tag') && !manifest.publishConfig?.tag @@ -273,6 +278,16 @@ class Publish extends BaseCommand { } } + // packageExtensions is root-only project policy and must never reach the registry; private packages may keep it for local use + #assertNoPackageExtensions (manifest) { + if (!manifest.private && manifest.packageExtensions !== undefined) { + throw Object.assign( + new Error('packageExtensions is only honored at the project root and must not be published.'), + { code: 'EPACKAGEEXTENSIONS' } + ) + } + } + // if it's a directory, read it from the file system // otherwise, get the full metadata from whatever it is // XXX can't pacote read the manifest from a directory? diff --git a/lib/utils/explain-dep.js b/lib/utils/explain-dep.js index 6c84aa4ebbc39..75af3fbcbc5e9 100644 --- a/lib/utils/explain-dep.js +++ b/lib/utils/explain-dep.js @@ -76,7 +76,7 @@ const explainDependents = ({ dependents }, depth, chalk, seen) => { } const explainEdge = ( - { name, type, bundled, from, spec, rawSpec, overridden }, + { name, type, bundled, from, spec, rawSpec, overridden, packageExtensions }, depth, chalk, seen = new Set() ) => { let dep = type === 'workspace' @@ -88,9 +88,14 @@ const explainEdge = ( const fromMsg = ` from ${explainFrom(from, depth, chalk, seen)}` + // note an edge created by a root packageExtensions repair + const extMsg = packageExtensions + ? chalk.dim(` (added by packageExtensions["${packageExtensions.selector}"].${packageExtensions.field}.${name})`) + : '' + return (type === 'prod' ? '' : `${colorType(type, chalk)} `) + (bundled ? `${colorType('bundled', chalk)} ` : '') + - `${dep}${fromMsg}` + `${dep}${fromMsg}${extMsg}` } const explainFrom = (from, depth, chalk, seen) => { diff --git a/lib/utils/validate-lockfile.js b/lib/utils/validate-lockfile.js index cdab0ed0ea046..2aa9e5f4741a0 100644 --- a/lib/utils/validate-lockfile.js +++ b/lib/utils/validate-lockfile.js @@ -33,4 +33,59 @@ function validateLockfile (virtualTree, idealTree) { return errors } +// validates that the root packageExtensions state matches what the lockfile recorded, and that the locked tree is still consistent with the rule set. +// Returns an array of human-readable error strings, empty when valid. +function validatePackageExtensions (virtualTree, idealTree) { + const errors = [] + const lockHash = virtualTree.meta?.packageExtensionsHash || null + const idealHash = idealTree.meta?.packageExtensionsHash || null + + if (idealHash !== lockHash) { + if (idealHash && !lockHash) { + errors.push('Missing: packageExtensions state from lock file') + } else if (!idealHash && lockHash) { + errors.push('Invalid: lock file records packageExtensions state but package.json has none') + } else { + errors.push('Invalid: packageExtensions in package.json do not match the lock file') + } + // once the canonical hashes diverge, the deeper per-node checks are moot + return errors + } + + // the hashes match, so validate the locked tree's own consistency against the rules + const { PackageExtensions } = require('@npmcli/arborist') + const root = idealTree.target || idealTree + let pe + try { + pe = new PackageExtensions(root.package?.packageExtensions) + } catch (err) { + return [`Invalid: ${err.message}`] + } + + for (const node of virtualTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace) { + continue + } + // selectors match the underlying package identity, which is the alias target for aliased installs + const name = node.packageName || node.name + // a locked package identity must not match more than one selector + try { + pe.match(name, node.version) + } catch (err) { + errors.push(`Invalid: ${err.message}`) + } + // recorded provenance must still correspond to a selector that matches the node + const applied = node.packageExtensionsApplied + if (applied) { + const sel = pe.selectors.find(s => s.key === applied.selector) + if (!sel || !pe.wouldMatch(name, node.version)) { + errors.push( + `Invalid: stale packageExtensions provenance for ${node.name}@${node.version} (selector "${applied.selector}")`) + } + } + } + return errors +} + module.exports = validateLockfile +module.exports.validatePackageExtensions = validatePackageExtensions diff --git a/package-lock.json b/package-lock.json index 439c5044cda74..7f2f76e4da60e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16915,6 +16915,7 @@ "semver": "^7.3.7", "ssri": "^14.0.0", "treeverse": "^3.0.0", + "validate-npm-package-name": "^7.0.2", "walk-up-path": "^4.0.0" }, "bin": { @@ -16935,6 +16936,15 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "workspaces/arborist/node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "workspaces/config": { "name": "@npmcli/config", "version": "11.0.0-pre.0", diff --git a/tap-snapshots/test/lib/commands/ls.js.test.cjs b/tap-snapshots/test/lib/commands/ls.js.test.cjs index fc7fbdf8a906f..6a4918a32d539 100644 --- a/tap-snapshots/test/lib/commands/ls.js.test.cjs +++ b/tap-snapshots/test/lib/commands/ls.js.test.cjs @@ -556,6 +556,12 @@ exports[`test/lib/commands/ls.js TAP ls overridden dep w/ color > should contain  ` +exports[`test/lib/commands/ls.js TAP ls packageExtensions dep > human output annotates the extended node 1`] = ` +test-package-extensions@1.0.0 {CWD}/prefix +\`-- foo@1.0.0 packageExtensions: dependencies.bar + \`-- bar@1.0.0 +` + exports[`test/lib/commands/ls.js TAP ls print deduped symlinks > should output tree containing linked deps 1`] = ` print-deduped-symlinks@1.0.0 {CWD}/prefix +-- a@1.0.0 diff --git a/tap-snapshots/test/lib/utils/explain-dep.js.test.cjs b/tap-snapshots/test/lib/utils/explain-dep.js.test.cjs index 60fca466bb43f..d432d6fc9776e 100644 --- a/tap-snapshots/test/lib/utils/explain-dep.js.test.cjs +++ b/tap-snapshots/test/lib/utils/explain-dep.js.test.cjs @@ -205,6 +205,30 @@ overridden-root@1.0.0 overridden node_modules/overridden-root ` +exports[`test/lib/utils/explain-dep.js TAP basic packageExtensions > explain color deep 1`] = ` +bar@1.2.3 +node_modules/bar + bar@"^1.0.0" from foo@1.0.0 + node_modules/foo (added by packageExtensions["foo@1"].dependencies.bar) +` + +exports[`test/lib/utils/explain-dep.js TAP basic packageExtensions > explain nocolor shallow 1`] = ` +bar@1.2.3 +node_modules/bar + bar@"^1.0.0" from foo@1.0.0 + node_modules/foo (added by packageExtensions["foo@1"].dependencies.bar) +` + +exports[`test/lib/utils/explain-dep.js TAP basic packageExtensions > print color 1`] = ` +bar@1.2.3 +node_modules/bar +` + +exports[`test/lib/utils/explain-dep.js TAP basic packageExtensions > print nocolor 1`] = ` +bar@1.2.3 +node_modules/bar +` + exports[`test/lib/utils/explain-dep.js TAP basic peer > explain color deep 1`] = ` peer@1.0.0 peer node_modules/peer diff --git a/test/lib/commands/ci.js b/test/lib/commands/ci.js index e8b2a69264674..618560d064a46 100644 --- a/test/lib/commands/ci.js +++ b/test/lib/commands/ci.js @@ -122,6 +122,23 @@ t.test('reifies, audits, removes node_modules on repeat run', async t => { t.equal(fs.existsSync(nmAbbrev), true, 'installs abbrev') }) +t.test('fails when packageExtensions are out of sync with the lock file', async t => { + const { npm } = await loadMockNpm(t, { + config: { audit: false }, + prefixDir: { + abbrev, + // packageExtensions present in package.json but the lock file records no hash + 'package.json': JSON.stringify({ ...packageJson, packageExtensions: {} }), + 'package-lock.json': JSON.stringify(packageLock), + }, + }) + await t.rejects( + npm.exec('ci', []), + /packageExtensions state from lock file/, + 'ci refuses to install with stale packageExtensions state' + ) +}) + t.test('--no-audit and --ignore-scripts', async t => { const { npm, joinedOutput, registry } = await loadMockNpm(t, { config: { diff --git a/test/lib/commands/ls.js b/test/lib/commands/ls.js index 878ffb3f38c53..f63787eec10d2 100644 --- a/test/lib/commands/ls.js +++ b/test/lib/commands/ls.js @@ -308,6 +308,49 @@ t.test('ls', async t => { t.matchSnapshot(cleanCwd(result()), 'should contain overridden output') }) + const packageExtensionsPrefix = { + 'package.json': JSON.stringify({ + name: 'test-package-extensions', + version: '1.0.0', + dependencies: { foo: '^1.0.0' }, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + }), + node_modules: { + '.package-lock.json': JSON.stringify({ + packages: { + 'node_modules/foo': { + version: '1.0.0', + dependencies: { bar: '^1.0.0' }, + packageExtensionsApplied: { selector: 'foo@1', dependencies: ['bar'] }, + }, + 'node_modules/bar': { version: '1.0.0' }, + }, + }), + foo: { + 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0', dependencies: { bar: '^1.0.0' } }), + }, + bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }) }, + }, + } + + t.test('packageExtensions dep', async t => { + const { npm, result, ls } = await mockLs(t, { config: {}, prefixDir: packageExtensionsPrefix }) + touchHiddenPackageLock(npm.prefix) + await ls.exec([]) + t.matchSnapshot(cleanCwd(result()), 'human output annotates the extended node') + }) + + t.test('packageExtensions dep --json', async t => { + const { npm, result, ls } = await mockLs(t, { + config: { json: true }, + prefixDir: packageExtensionsPrefix, + }) + touchHiddenPackageLock(npm.prefix) + await ls.exec([]) + const applied = JSON.parse(result()).dependencies.foo.packageExtensionsApplied + t.match(applied, { selector: 'foo@1', dependencies: ['bar'] }, 'json output includes provenance') + }) + t.test('with filter arg', async t => { const config = { color: 'always', diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 5b2780dbf4c7e..fe286ff46b748 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -214,6 +214,67 @@ t.test('shows usage with wrong set of arguments', async t => { await t.rejects(publish.exec(['a', 'b', 'c']), publish.usage) }) +t.test('fails for a non-private package containing packageExtensions', async t => { + const { npm } = await loadNpmWithRegistry(t, { + config: { ...auth }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + }, null, 2), + }, + authorization: token, + }) + await t.rejects( + npm.exec('publish', []), + { code: 'EPACKAGEEXTENSIONS', message: /must not be published/ }, + 'refuses to publish' + ) +}) + +t.test('fails on --dry-run for a package containing packageExtensions', async t => { + const { npm } = await loadNpmWithRegistry(t, { + config: { 'dry-run': true, ...auth }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + }, null, 2), + }, + authorization: token, + }) + await t.rejects( + npm.exec('publish', []), + { code: 'EPACKAGEEXTENSIONS' }, + 'dry-run also reports the failure' + ) +}) + +t.test('fails when a lifecycle script injects packageExtensions before the re-read', async t => { + const { npm } = await loadNpmWithRegistry(t, { + config: { ...auth }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + scripts: { prepublishOnly: 'node inject.js' }, + }, null, 2), + // the first manifest read is clean; this hook adds packageExtensions before the authoritative re-read + 'inject.js': [ + "const fs = require('fs')", + "const p = JSON.parse(fs.readFileSync('package.json'))", + "p.packageExtensions = { 'foo@1': { dependencies: { bar: '^1.0.0' } } }", + "fs.writeFileSync('package.json', JSON.stringify(p))", + ].join('\n'), + }, + authorization: token, + }) + await t.rejects( + npm.exec('publish', []), + { code: 'EPACKAGEEXTENSIONS' }, + 'the post-script manifest re-read catches the injected field' + ) +}) + t.test('throws when invalid tag is semver', async t => { const { npm } = await loadNpmWithRegistry(t, { config: { diff --git a/test/lib/utils/explain-dep.js b/test/lib/utils/explain-dep.js index 2a9a93f2b529e..d847c4106e1a7 100644 --- a/test/lib/utils/explain-dep.js +++ b/test/lib/utils/explain-dep.js @@ -140,6 +140,23 @@ const getCases = (testdir) => { }, }], }, + + packageExtensions: { + name: 'bar', + version: '1.2.3', + location: 'node_modules/bar', + dependents: [{ + type: 'prod', + name: 'bar', + spec: '^1.0.0', + packageExtensions: { selector: 'foo@1', field: 'dependencies' }, + from: { + name: 'foo', + version: '1.0.0', + location: 'node_modules/foo', + }, + }], + }, } cases.manyDeps = { diff --git a/test/lib/utils/validate-lockfile.js b/test/lib/utils/validate-lockfile.js index a3942a6903658..eae43b70008fa 100644 --- a/test/lib/utils/validate-lockfile.js +++ b/test/lib/utils/validate-lockfile.js @@ -1,5 +1,102 @@ const t = require('tap') const validateLockfile = require('../../../lib/utils/validate-lockfile.js') +const { validatePackageExtensions } = require('../../../lib/utils/validate-lockfile.js') + +// build mock virtual/ideal trees for validatePackageExtensions +const tree = ({ hash = null, packageExtensions, nodes = [] }) => ({ + meta: { packageExtensionsHash: hash }, + target: { package: packageExtensions === undefined ? {} : { packageExtensions } }, + inventory: { values: () => nodes }, +}) + +t.test('packageExtensions: matching hashes and clean tree', async t => { + const errors = validatePackageExtensions( + tree({ hash: 'sha512-abc' }), + tree({ hash: 'sha512-abc' }) + ) + t.strictSame(errors, [], 'no errors when hashes match and no provenance') +}) + +t.test('packageExtensions: both absent', async t => { + t.strictSame(validatePackageExtensions(tree({}), tree({})), [], 'no errors when neither has state') +}) + +t.test('packageExtensions: missing from lock file', async t => { + const errors = validatePackageExtensions(tree({ hash: null }), tree({ hash: 'sha512-abc' })) + t.match(errors[0], /Missing: packageExtensions state from lock file/, 'reports missing lock state') +}) + +t.test('packageExtensions: present in lock but not package.json', async t => { + const errors = validatePackageExtensions(tree({ hash: 'sha512-abc' }), tree({ hash: null })) + t.match(errors[0], /lock file records packageExtensions state but package.json has none/, 'reports stray lock state') +}) + +t.test('packageExtensions: hash mismatch', async t => { + const errors = validatePackageExtensions(tree({ hash: 'sha512-aaa' }), tree({ hash: 'sha512-bbb' })) + t.match(errors[0], /do not match the lock file/, 'reports a mismatch') +}) + +t.test('packageExtensions: stale provenance with matching hash', async t => { + // both hashes equal, but a locked node references a selector that no longer exists + const node = { name: 'foo', version: '1.0.0', packageExtensionsApplied: { selector: 'foo@1', dependencies: ['bar'] } } + const errors = validatePackageExtensions( + tree({ hash: 'h', nodes: [node] }), + tree({ hash: 'h', packageExtensions: {} }) + ) + t.match(errors[0], /stale packageExtensions provenance for foo@1.0.0/, 'reports stale provenance') +}) + +t.test('packageExtensions: valid provenance with matching hash', async t => { + const node = { name: 'foo', version: '1.0.0', packageExtensionsApplied: { selector: 'foo@1', dependencies: ['bar'] } } + // root and workspace nodes are skipped by the validation + const root = { name: 'root', version: '1.0.0', isProjectRoot: true } + const errors = validatePackageExtensions( + tree({ hash: 'h', nodes: [root, node] }), + tree({ hash: 'h', packageExtensions: { 'foo@1': { dependencies: { bar: '^1' } } } }) + ) + t.strictSame(errors, [], 'no errors when provenance still matches a selector') +}) + +t.test('packageExtensions: ideal tree without a target uses the tree itself', async t => { + const idealTree = { + meta: { packageExtensionsHash: 'h' }, + package: { packageExtensions: { 'foo@1': { dependencies: { bar: '^1' } } } }, + inventory: { values: () => [] }, + } + t.strictSame(validatePackageExtensions(tree({ hash: 'h' }), idealTree), [], 'reads package off the tree directly') +}) + +t.test('packageExtensions: invalid rule set surfaces the engine error', async t => { + const errors = validatePackageExtensions( + tree({ hash: 'h' }), + tree({ hash: 'h', packageExtensions: { foo: { devDependencies: { a: '1' } } } }) + ) + t.match(errors[0], /Invalid: .*unsupported field/, 'reports the engine validation error') +}) + +t.test('packageExtensions: alias node matches the underlying package name', async t => { + // an aliased install: node.name is the alias, node.packageName is the real package + const node = { + name: 'my-alias', + packageName: 'real-pkg', + version: '1.0.0', + packageExtensionsApplied: { selector: 'real-pkg@1', dependencies: ['bar'] }, + } + const errors = validatePackageExtensions( + tree({ hash: 'h', nodes: [node] }), + tree({ hash: 'h', packageExtensions: { 'real-pkg@1': { dependencies: { bar: '^1' } } } }) + ) + t.strictSame(errors, [], 'provenance validated against the underlying package name, not the alias') +}) + +t.test('packageExtensions: locked identity matching two selectors', async t => { + const node = { name: 'foo', version: '1.0.0' } + const errors = validatePackageExtensions( + tree({ hash: 'h', nodes: [node] }), + tree({ hash: 'h', packageExtensions: { foo: { dependencies: { a: '^1' } }, 'foo@1': { dependencies: { b: '^1' } } } }) + ) + t.match(errors[0], /Multiple packageExtensions selectors match foo@1.0.0/, 'reports a selector conflict') +}) t.test('identical inventory for both idealTree and virtualTree', async t => { t.matchSnapshot( diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 5c2b4add9afc5..d3dd99e764a27 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -26,6 +26,7 @@ const fromPath = require('../from-path.js') const calcDepFlags = require('../calc-dep-flags.js') const { isReleaseAgeExcluded, trustedSpecName } = require('../release-age-exclude.js') const { resolvePatchedDependencies } = require('../patched-dependencies.js') +const PackageExtensions = require('../package-extensions.js') const Shrinkwrap = require('../shrinkwrap.js') const { defaultLockfileVersion } = Shrinkwrap const Node = require('../node.js') @@ -98,6 +99,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { #loadFailures = new Set() #manifests = new Map() #mutateTree = false + #packageExtensions = null // a map of each module in a peer set to the thing that depended on // that set of peers in the first place. Use a WeakMap so that we // don't hold onto references for nodes that are garbage collected. @@ -175,6 +177,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { try { await this.#initTree() + this.#loadPackageExtensions() await this.#inflateAncientLockfile() await this.#applyUserRequests(options) await this.#buildDeps() @@ -185,6 +188,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { path: this.path, allowUnusedPatches: this.options.allowUnusedPatches, }) + this.#warnWorkspacePackageExtensions() } finally { timeEnd() this.finishTracker('idealTree') @@ -232,6 +236,67 @@ module.exports = cls => class IdealTreeBuilder extends cls { } } + // Load the root project's packageExtensions rule set. + // Only the workspace root is authoritative, matching the root-only model of overrides. + // The canonical hash is stashed on the lockfile meta so commit() can persist it. + #loadPackageExtensions () { + const rootPkg = this.idealTree.target.package + const lockedHash = this.idealTree.meta.packageExtensionsHash + this.#packageExtensions = new PackageExtensions(rootPkg.packageExtensions) + this.idealTree.meta.packageExtensionsHash = this.#packageExtensions.hash + + // When the rule set has changed since the lockfile was written, the locked manifests for affected packages are stale. + // The locked manifest is the effective, already-extended manifest, so detach those nodes and rebuild them from fresh manifests under the current rules. + if (this.idealTree.meta.loadedFromDisk && lockedHash !== this.#packageExtensions.hash) { + for (const node of [...this.idealTree.inventory.values()]) { + if (node.isProjectRoot || node.isWorkspace || node.isTop) { + continue + } + // a node is affected if it carries provenance from the old rules or matches a current selector + const affected = node.packageExtensionsApplied || + this.#packageExtensions.wouldMatch(node.packageName, node.version) + if (affected) { + for (const edge of node.edgesIn) { + this.#depsQueue.push(edge.from) + } + node.parent = null + } + } + } + } + + // Apply a matching root packageExtension to a copy of a candidate manifest. + // Returns the possibly-extended manifest and the provenance to attach to the node. + // Workspace candidates are never extended; that warning is emitted separately. + #applyPackageExtension (pkg) { + if (!this.#packageExtensions?.present) { + return { pkg, applied: null } + } + const res = this.#packageExtensions.apply(pkg) + return res ? { pkg: res.pkg, applied: res.applied } : { pkg, applied: null } + } + + // Warn when packageExtensions appears in a non-root workspace, or when a root selector matches a workspace member. + // Workspace package manifests are edited directly and are never extension targets. + #warnWorkspacePackageExtensions () { + if (!this.#packageExtensions?.present) { + return + } + for (const node of this.idealTree.inventory.values()) { + if (!node.isWorkspace) { + continue + } + if (node.package.packageExtensions !== undefined) { + log.warn('packageExtensions', + `"packageExtensions" in workspace ${node.name} is ignored; it is only honored at the workspace root`) + } + if (this.#packageExtensions.wouldMatch(node.name, node.version)) { + log.warn('packageExtensions', + `selector matches workspace package ${node.name}@${node.version}; edit its package.json directly instead of using packageExtensions`) + } + } + } + #parseSettings (options) { const update = options.update === true ? { all: true } : Array.isArray(options.update) ? { names: options.update } @@ -1401,7 +1466,13 @@ This is a one-time fix-up, please be patient... ) return this.#failureNode(name, parent, error, edge) } - return new Node({ name, pkg, parent, installLinks, legacyPeerDeps }) + // Apply a matching root packageExtension to a manifest copy before the Node reads its dependency and peer edges. + const { pkg: extended, applied } = this.#applyPackageExtension(pkg) + const node = new Node({ name, pkg: extended, parent, installLinks, legacyPeerDeps }) + if (applied) { + node.packageExtensionsApplied = applied + } + return node }, error => this.#failureNode(name, parent, error, edge) ) diff --git a/workspaces/arborist/lib/arborist/load-virtual.js b/workspaces/arborist/lib/arborist/load-virtual.js index d10b198681d44..acf8b6c4220ac 100644 --- a/workspaces/arborist/lib/arborist/load-virtual.js +++ b/workspaces/arborist/lib/arborist/load-virtual.js @@ -243,6 +243,7 @@ To fix: realpath: path, integrity: sw.integrity, patched: sw.patched, + packageExtensionsApplied: sw.packageExtensionsApplied, resolved: consistentResolve(sw.resolved, this.path, path), pkg: sw, loadOverrides, diff --git a/workspaces/arborist/lib/edge.js b/workspaces/arborist/lib/edge.js index c56799ea01133..04b7f14953ef3 100644 --- a/workspaces/arborist/lib/edge.js +++ b/workspaces/arborist/lib/edge.js @@ -160,6 +160,16 @@ class Edge { } if (this.#from) { explanation.from = this.#from.explain(null, seen) + // note when this edge was created by a root packageExtensions repair on the from node + const applied = this.#from.packageExtensionsApplied + if (applied) { + for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta']) { + if (applied[field]?.includes(this.#name)) { + explanation.packageExtensions = { selector: applied.selector, field } + break + } + } + } } this.#explanation = explanation } diff --git a/workspaces/arborist/lib/index.js b/workspaces/arborist/lib/index.js index 5baaee6ee7c93..2f0c4aec7938b 100644 --- a/workspaces/arborist/lib/index.js +++ b/workspaces/arborist/lib/index.js @@ -4,3 +4,4 @@ module.exports.Node = require('./node.js') module.exports.Link = require('./link.js') module.exports.Edge = require('./edge.js') module.exports.Shrinkwrap = require('./shrinkwrap.js') +module.exports.PackageExtensions = require('./package-extensions.js') diff --git a/workspaces/arborist/lib/node.js b/workspaces/arborist/lib/node.js index 1e1d1bae298e7..94da3c48c6e98 100644 --- a/workspaces/arborist/lib/node.js +++ b/workspaces/arborist/lib/node.js @@ -93,6 +93,7 @@ class Node { name, // allow setting name explicitly when we haven't set a path yet optional = true, overrides, + packageExtensionsApplied = null, parent, patched = null, path, @@ -172,6 +173,9 @@ class Node { this.integrity = integrity || this.package._integrity || null // Patch record { path, integrity } or null, set from patchedDependencies or the lockfile. this.patched = patched || null + // Provenance for a root packageExtensions repair applied to this node's manifest, or null. + // Shape: { selector, dependencies?, optionalDependencies?, peerDependencies?, peerDependenciesMeta? }. + this.packageExtensionsApplied = packageExtensionsApplied this.installLinks = installLinks this.legacyPeerDeps = legacyPeerDeps diff --git a/workspaces/arborist/lib/package-extensions.js b/workspaces/arborist/lib/package-extensions.js new file mode 100644 index 0000000000000..870ac4cbeaf2d --- /dev/null +++ b/workspaces/arborist/lib/package-extensions.js @@ -0,0 +1,236 @@ +// Root-owned `packageExtensions`: declarative repairs to third-party manifests applied before Arborist reads a candidate's dependency edges. +// See RFC: https://github.com/npm/rfcs/pull/889 +// This module is pure: it parses and validates the root rule set, matches a candidate manifest by name and version, and returns an extended manifest copy plus minimal provenance. +// It never mutates the input manifest or any shared cache object. +const semver = require('semver') +const ssri = require('ssri') +const validateName = require('validate-npm-package-name') + +// The only manifest fields a package extension may add or correct, because they are the fields that affect dependency and peer resolution. +const EXTENSION_FIELDS = [ + 'dependencies', + 'optionalDependencies', + 'peerDependencies', + 'peerDependenciesMeta', +] + +// The two normal dependency fields; a name may exist in only one of them. +const NORMAL_DEP_FIELDS = ['dependencies', 'optionalDependencies'] + +const err = (message, code, extra = {}) => + Object.assign(new Error(message), { code, ...extra }) + +// Parse a selector key into { name, range }, where range is null for a name-only key. +// Selectors are a package name with an optional semver range; dist-tags, git, file, directory, url, and alias specs are rejected. +const parseSelector = key => { + if (typeof key !== 'string' || !key) { + throw err(`Invalid packageExtensions selector: ${JSON.stringify(key)}`, 'EEXTENSIONSELECTOR') + } + // The separator @ is the first @ after a leading scope @. + const at = key.indexOf('@', key.startsWith('@') ? 1 : 0) + const name = at === -1 ? key : key.slice(0, at) + const range = at === -1 ? null : key.slice(at + 1) + + const { validForOldPackages, validForNewPackages } = validateName(name) + if (!validForOldPackages && !validForNewPackages) { + throw err(`Invalid package name in packageExtensions selector: "${key}"`, 'EEXTENSIONSELECTOR', { selector: key }) + } + // A blank range such as "foo@" is malformed; the name-only form "foo" is how you match every version. + if (range !== null && range.trim() === '') { + throw err( + `Invalid packageExtensions selector: "${key}". Use the name only to match every version.`, + 'EEXTENSIONSELECTOR', { selector: key }) + } + // A versioned selector must be a valid semver range, which rejects dist-tags, git, file, url, and alias specs. + if (range !== null && semver.validRange(range, { loose: true }) === null) { + throw err( + `Invalid version range in packageExtensions selector: "${key}". Selectors accept a package name with an optional semver range only.`, + 'EEXTENSIONSELECTOR', { selector: key }) + } + return { name, range } +} + +// A selector matches a candidate manifest by its own name and version. +// Name-only selectors match every version, including non-semver versions. +// Versioned selectors only match versions that parse as semver and satisfy the range. +const rangeMatches = (range, version) => { + if (range === null) { + return true + } + return semver.valid(version, { loose: true }) !== null && + semver.satisfies(version, range, { loose: true }) +} + +// Validate a single selector's extension object before it is ever applied. +const validateExtensionObject = (key, ext) => { + if (ext === null || typeof ext !== 'object' || Array.isArray(ext)) { + throw err(`packageExtensions["${key}"] must be an object`, 'EEXTENSIONVALUE', { selector: key }) + } + for (const field of Object.keys(ext)) { + if (!EXTENSION_FIELDS.includes(field)) { + throw err( + `packageExtensions["${key}"] has unsupported field "${field}". Supported fields: ${EXTENSION_FIELDS.join(', ')}.`, + 'EEXTENSIONFIELD', { selector: key, field }) + } + const val = ext[field] + if (val === null || typeof val !== 'object' || Array.isArray(val)) { + throw err(`packageExtensions["${key}"].${field} must be an object`, 'EEXTENSIONVALUE', { selector: key, field }) + } + } + // Deletion is not supported in v1, so a null, false, or "-" value is an error. + for (const field of [...NORMAL_DEP_FIELDS, 'peerDependencies']) { + for (const [name, spec] of Object.entries(ext[field] || {})) { + if (spec === null || spec === false || spec === '-') { + throw err( + `packageExtensions["${key}"].${field}.${name} attempts deletion, which is not supported.`, + 'EEXTENSIONDELETE', { selector: key, field, name }) + } + } + } + // Each peerDependenciesMeta entry must be a non-null metadata object, never a deletion sentinel or primitive. + for (const [name, meta] of Object.entries(ext.peerDependenciesMeta || {})) { + if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) { + throw err( + `packageExtensions["${key}"].peerDependenciesMeta.${name} must be an object`, + 'EEXTENSIONVALUE', { selector: key, field: 'peerDependenciesMeta', name }) + } + } +} + +// Apply a matched extension to a manifest, returning { pkg, applied } where pkg is a copy with extended fields and applied is minimal provenance. +// The input manifest is never mutated. +const applyExtension = (pkg, { key, ext }) => { + const applied = { selector: key } + + // Clone only the fields we may touch; the rest of the manifest is shared by reference since it is never mutated. + const next = { ...pkg } + for (const field of EXTENSION_FIELDS) { + if (pkg[field] && typeof pkg[field] === 'object') { + next[field] = field === 'peerDependenciesMeta' + ? Object.fromEntries(Object.entries(pkg[field]).map(([n, m]) => [n, { ...m }])) + : { ...pkg[field] } + } + } + + // dependencies and optionalDependencies add missing names only. + // A name already declared in either normal dependency field is an error, which also prevents moving a name between the fields. + for (const field of NORMAL_DEP_FIELDS) { + const adds = ext[field] + if (!adds) { + continue + } + for (const [name, spec] of Object.entries(adds)) { + for (const existingField of NORMAL_DEP_FIELDS) { + if (next[existingField] && name in next[existingField]) { + throw err( + `packageExtensions["${key}"].${field}.${name} conflicts with the package's existing ${existingField}.${name}. Use overrides to change a dependency version; packageExtensions only adds missing dependencies.`, + 'EEXTENSIONDUPDEP', { selector: key, field, name, existingField }) + } + } + next[field] = next[field] || {} + next[field][name] = spec + ;(applied[field] = applied[field] || []).push(name) + } + } + + // peerDependencies shallow-merges by peer name, and the extension value replaces an existing range. + if (ext.peerDependencies) { + next.peerDependencies = next.peerDependencies || {} + for (const [name, spec] of Object.entries(ext.peerDependencies)) { + next.peerDependencies[name] = spec + ;(applied.peerDependencies = applied.peerDependencies || []).push(name) + } + } + + // peerDependenciesMeta merges by peer name, then shallow-merges each meta object so an extension can add optional without dropping other meta keys. + if (ext.peerDependenciesMeta) { + next.peerDependenciesMeta = next.peerDependenciesMeta || {} + for (const [name, meta] of Object.entries(ext.peerDependenciesMeta)) { + next.peerDependenciesMeta[name] = { ...next.peerDependenciesMeta[name], ...meta } + ;(applied.peerDependenciesMeta = applied.peerDependenciesMeta || []).push(name) + // Every peerDependenciesMeta entry an extension adds must correspond to a peerDependencies entry present after extension application. + if (!next.peerDependencies || !(name in next.peerDependencies)) { + throw err( + `packageExtensions["${key}"].peerDependenciesMeta.${name} has no corresponding peerDependencies.${name} after extension application.`, + 'EEXTENSIONORPHANMETA', { selector: key, name }) + } + } + } + + return { pkg: next, applied } +} + +// Deterministic JSON for hashing: keys sorted lexicographically at every level, string and number values preserved exactly, no insignificant whitespace. +const canonicalStringify = val => { + if (Array.isArray(val)) { + return `[${val.map(canonicalStringify).join(',')}]` + } + if (val && typeof val === 'object') { + return `{${Object.keys(val).sort() + .map(k => `${JSON.stringify(k)}:${canonicalStringify(val[k])}`) + .join(',')}}` + } + return JSON.stringify(val) +} + +// Hash the canonical form of the root packageExtensions object using npm's existing lockfile digest encoding. +const canonicalHash = packageExtensions => + ssri.fromData(canonicalStringify(packageExtensions), { algorithms: ['sha512'] }).toString() + +class PackageExtensions { + constructor (raw) { + this.raw = raw + this.present = raw !== undefined + this.selectors = [] + this.hash = null + + if (!this.present) { + return + } + if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) { + throw err('packageExtensions must be an object', 'EEXTENSIONROOT') + } + for (const [key, ext] of Object.entries(raw)) { + const { name, range } = parseSelector(key) + validateExtensionObject(key, ext) + this.selectors.push({ key, name, range, ext }) + } + this.hash = canonicalHash(raw) + } + + // Non-throwing check used for warnings: whether any selector matches the candidate. + wouldMatch (name, version) { + return this.selectors.some(s => s.name === name && rangeMatches(s.range, version)) + } + + // Return the single selector matching a candidate manifest, or null. + // Throws EEXTENSIONCONFLICT when more than one selector matches the same candidate. + match (name, version) { + const matches = this.selectors.filter(s => s.name === name && rangeMatches(s.range, version)) + if (matches.length > 1) { + const keys = matches.map(s => `"${s.key}"`).join(', ') + throw err( + `Multiple packageExtensions selectors match ${name}@${version}: ${keys}. Narrow or remove one of the overlapping rules.`, + 'EEXTENSIONCONFLICT', { name, version, selectors: matches.map(s => s.key) }) + } + return matches[0] || null + } + + // Apply the matching extension to a manifest copy, returning { pkg, applied } or null when no selector matches. + // Throws on selector conflict or invalid merge. + apply (pkg) { + if (!this.present || !this.selectors.length || !pkg || !pkg.name) { + return null + } + const sel = this.match(pkg.name, pkg.version) + return sel ? applyExtension(pkg, sel) : null + } +} + +module.exports = PackageExtensions +module.exports.PackageExtensions = PackageExtensions +module.exports.parseSelector = parseSelector +module.exports.rangeMatches = rangeMatches +module.exports.canonicalHash = canonicalHash +module.exports.canonicalStringify = canonicalStringify +module.exports.EXTENSION_FIELDS = EXTENSION_FIELDS diff --git a/workspaces/arborist/lib/place-dep.js b/workspaces/arborist/lib/place-dep.js index c7b3e10d408d0..6fd272e50600d 100644 --- a/workspaces/arborist/lib/place-dep.js +++ b/workspaces/arborist/lib/place-dep.js @@ -247,6 +247,9 @@ class PlaceDep { installLinks: this.installLinks, legacyPeerDeps: this.legacyPeerDeps, error: this.dep.errors[0], + ...(this.dep.packageExtensionsApplied + ? { packageExtensionsApplied: this.dep.packageExtensionsApplied } + : {}), ...(this.dep.overrides ? { overrides: this.dep.overrides } : {}), ...(this.dep.isLink ? { target: this.dep.target, realpath: this.dep.realpath } : {}), }) diff --git a/workspaces/arborist/lib/shrinkwrap.js b/workspaces/arborist/lib/shrinkwrap.js index 01e0b11abc33c..132afd59676c2 100644 --- a/workspaces/arborist/lib/shrinkwrap.js +++ b/workspaces/arborist/lib/shrinkwrap.js @@ -12,6 +12,9 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const defaultLockfileVersion = 3 // Bumped to 4 only when a node carries a patch record, so older clients abort. const patchedLockfileVersion = 4 +// packageExtensions provenance also forces lockfileVersion 4 so older clients abort rather than silently dropping the repaired graph. +// Both features share version 4: they are root-owned graph repairs an old npm must not drop. +const packageExtensionsLockfileVersion = 4 const maxLockfileVersion = 4 // for comparing nodes to yarn.lock entries @@ -111,6 +114,7 @@ const nodeMetaKeys = [ 'inBundle', 'hasInstallScript', 'patched', + 'packageExtensionsApplied', ] const metaFieldFromPkg = (pkg, key) => { @@ -351,6 +355,7 @@ class Shrinkwrap { reset () { this.tree = null this.#awaitingUpdate = new Map() + this.packageExtensionsHash = null const lockfileVersion = this.lockfileVersion || defaultLockfileVersion this.originalLockfileVersion = lockfileVersion @@ -462,11 +467,10 @@ class Shrinkwrap { this.ancientLockfile = false data = {} } - // refuse lockfiles newer than we understand so we never install unpatched + // refuse lockfiles newer than we understand so we never drop a patched or repaired graph we cannot read if (data.lockfileVersion > maxLockfileVersion) { throw Object.assign( - new Error(`Unsupported lockfileVersion ${data.lockfileVersion}. ` + - `This npm only supports up to ${maxLockfileVersion}. Please upgrade npm.`), + new Error(`Unsupported lockfileVersion ${data.lockfileVersion}. This npm only supports up to ${maxLockfileVersion}. Please upgrade npm.`), { code: 'ELOCKFILEVERSION' } ) } @@ -490,6 +494,9 @@ class Shrinkwrap { this.originalLockfileVersion = data.lockfileVersion + // the canonical packageExtensions hash, if the lockfile recorded one on its root entry + this.packageExtensionsHash = data.packages?.['']?.packageExtensionsHash || null + // use default if it wasn't explicitly set, and the current file is // less than our default. otherwise, keep whatever is in the file, // unless we had an explicit setting already. @@ -907,6 +914,10 @@ class Shrinkwrap { this.tree.target, this.path, this.resolveOptions) + // record the canonical packageExtensions hash on the root entry so npm ci can detect stale extension state + if (this.packageExtensionsHash) { + root.packageExtensionsHash = this.packageExtensionsHash + } this.data.packages = {} if (Object.keys(root).length) { this.data.packages[''] = root @@ -960,6 +971,14 @@ class Shrinkwrap { log.warn('shrinkwrap', `patchedDependencies requires lockfileVersion ${patchedLockfileVersion}; upgrading the lockfile from version ${this.lockfileVersion}.`) this.lockfileVersion = patchedLockfileVersion } + // packageExtensions state likewise forces lockfileVersion 4 so older clients abort instead of dropping the repaired graph + const hasExtensionState = !this.hiddenLockfile && + (this.packageExtensionsHash || + Object.values(this.data.packages).some(p => p.packageExtensionsApplied)) + if (hasExtensionState && this.lockfileVersion < packageExtensionsLockfileVersion) { + log.warn('shrinkwrap', `packageExtensions requires lockfileVersion ${packageExtensionsLockfileVersion}; upgrading the lockfile from version ${this.lockfileVersion}.`) + this.lockfileVersion = packageExtensionsLockfileVersion + } this.data.lockfileVersion = this.lockfileVersion // hidden lockfiles don't include legacy metadata or a root entry diff --git a/workspaces/arborist/package.json b/workspaces/arborist/package.json index e7eb5c9736b5f..2e5f9a66827b5 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -37,6 +37,7 @@ "semver": "^7.3.7", "ssri": "^14.0.0", "treeverse": "^3.0.0", + "validate-npm-package-name": "^7.0.2", "walk-up-path": "^4.0.0" }, "devDependencies": { diff --git a/workspaces/arborist/tap-snapshots/test/link.js.test.cjs b/workspaces/arborist/tap-snapshots/test/link.js.test.cjs index aa2afbd6ccdcf..af8593a9fce22 100644 --- a/workspaces/arborist/tap-snapshots/test/link.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/link.js.test.cjs @@ -26,6 +26,7 @@ Link { "location": "../../../../../some/other/path", "name": "path", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/some/other/path", "peer": true, @@ -73,6 +74,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "", "name": "path", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/some/kind/of/path", "peer": true, @@ -90,6 +92,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "../../../../../some/other/path", "name": "path", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/some/other/path", "peer": true, @@ -119,6 +122,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "", "name": "path", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/some/kind/of/path", "peer": true, diff --git a/workspaces/arborist/tap-snapshots/test/node.js.test.cjs b/workspaces/arborist/tap-snapshots/test/node.js.test.cjs index e1075bd0cbdd3..5664976285c5e 100644 --- a/workspaces/arborist/tap-snapshots/test/node.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/node.js.test.cjs @@ -41,6 +41,7 @@ exports[`test/node.js TAP basic instantiation > just a lone root node 1`] = ` }, "parent": undefined, }, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -218,6 +219,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, @@ -246,6 +248,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, @@ -311,6 +314,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, @@ -323,6 +327,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, @@ -369,6 +374,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, @@ -381,6 +387,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, @@ -420,6 +427,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, @@ -469,6 +477,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, @@ -481,6 +490,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, @@ -510,6 +520,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, @@ -555,6 +566,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, @@ -567,6 +579,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, @@ -583,6 +596,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "", "name": "workspaces_root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root", "peer": true, @@ -632,6 +646,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, @@ -644,6 +659,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, @@ -690,6 +706,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "node_modules/unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, @@ -702,6 +719,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, @@ -742,6 +760,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -780,6 +799,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -833,6 +853,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -853,6 +874,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -893,6 +915,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -926,6 +949,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -959,6 +983,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -992,6 +1017,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -1021,6 +1047,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -1075,6 +1102,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -1087,6 +1115,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -1156,6 +1185,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -1194,6 +1224,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -1247,6 +1278,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -1267,6 +1299,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -1300,6 +1333,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -1340,6 +1374,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -1373,6 +1408,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -1406,6 +1442,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -1439,6 +1476,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -1468,6 +1506,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -1522,6 +1561,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -1534,6 +1574,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -1565,6 +1606,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -1603,6 +1645,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -1632,6 +1675,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -1647,6 +1691,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -1679,6 +1724,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -1728,6 +1774,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -1781,6 +1828,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -1801,6 +1849,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -1841,6 +1890,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -1874,6 +1924,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -1907,6 +1958,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -1940,6 +1992,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -1969,6 +2022,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -2047,6 +2101,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -2100,6 +2155,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2120,6 +2176,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -2153,6 +2210,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2193,6 +2251,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -2233,6 +2292,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -2266,6 +2326,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -2299,6 +2360,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -2332,6 +2394,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -2361,6 +2424,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -2377,6 +2441,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -2409,6 +2474,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2470,6 +2536,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2490,6 +2557,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -2530,6 +2598,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -2563,6 +2632,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -2596,6 +2666,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -2629,6 +2700,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -2658,6 +2730,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -2701,6 +2774,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -2791,6 +2865,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2811,6 +2886,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -2844,6 +2920,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -2884,6 +2961,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -2917,6 +2995,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -2950,6 +3029,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -2983,6 +3063,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -3012,6 +3093,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -3055,6 +3137,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -3071,6 +3154,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -3103,6 +3187,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -3164,6 +3249,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -3184,6 +3270,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -3221,6 +3308,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -3254,6 +3342,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -3287,6 +3376,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -3320,6 +3410,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -3349,6 +3440,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -3380,6 +3472,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -3438,6 +3531,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -3450,6 +3544,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -3540,6 +3635,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -3560,6 +3656,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -3593,6 +3690,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -3630,6 +3728,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -3663,6 +3762,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -3696,6 +3796,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -3729,6 +3830,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -3758,6 +3860,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -3787,6 +3890,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -3817,6 +3921,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -3875,6 +3980,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -3887,6 +3993,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -3903,6 +4010,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -3935,6 +4043,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -3996,6 +4105,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -4016,6 +4126,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -4053,6 +4164,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -4086,6 +4198,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -4119,6 +4232,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -4152,6 +4266,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -4181,6 +4296,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -4212,6 +4328,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -4270,6 +4387,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -4282,6 +4400,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -4372,6 +4491,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -4392,6 +4512,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -4425,6 +4546,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -4462,6 +4584,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -4495,6 +4618,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -4528,6 +4652,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -4561,6 +4686,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -4590,6 +4716,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -4619,6 +4746,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -4649,6 +4777,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -4707,6 +4836,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -4719,6 +4849,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -4735,6 +4866,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -4767,6 +4899,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -4807,6 +4940,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -4845,6 +4979,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -4898,6 +5033,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -4918,6 +5054,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -4958,6 +5095,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -4991,6 +5129,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -5024,6 +5163,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -5057,6 +5197,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -5086,6 +5227,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -5140,6 +5282,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -5152,6 +5295,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -5221,6 +5365,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -5259,6 +5404,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -5312,6 +5458,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -5332,6 +5479,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -5365,6 +5513,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -5405,6 +5554,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -5438,6 +5588,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -5471,6 +5622,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -5504,6 +5656,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -5533,6 +5686,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -5587,6 +5741,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -5599,6 +5754,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -5630,6 +5786,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -5668,6 +5825,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -5697,6 +5855,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, @@ -5712,6 +5871,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -5744,6 +5904,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -5793,6 +5954,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -5846,6 +6008,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -5866,6 +6029,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -5906,6 +6070,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -5939,6 +6104,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -5972,6 +6138,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -6005,6 +6172,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -6034,6 +6202,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -6112,6 +6281,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -6165,6 +6335,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6185,6 +6356,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -6218,6 +6390,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6258,6 +6431,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, @@ -6298,6 +6472,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -6331,6 +6506,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -6364,6 +6540,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -6397,6 +6574,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -6426,6 +6604,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -6442,6 +6621,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -6474,6 +6654,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6535,6 +6716,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6555,6 +6737,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -6595,6 +6778,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -6628,6 +6812,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -6661,6 +6846,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -6694,6 +6880,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -6723,6 +6910,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -6766,6 +6954,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -6856,6 +7045,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6876,6 +7066,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -6909,6 +7100,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -6949,6 +7141,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -6982,6 +7175,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -7015,6 +7209,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -7048,6 +7243,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -7077,6 +7273,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -7120,6 +7317,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -7136,6 +7334,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -7168,6 +7367,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -7229,6 +7429,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -7249,6 +7450,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -7286,6 +7488,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -7319,6 +7522,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -7352,6 +7556,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -7385,6 +7590,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -7414,6 +7620,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -7445,6 +7652,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -7503,6 +7711,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -7515,6 +7724,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -7605,6 +7815,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -7625,6 +7836,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -7658,6 +7870,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -7695,6 +7908,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -7728,6 +7942,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -7761,6 +7976,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -7794,6 +8010,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -7823,6 +8040,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -7852,6 +8070,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -7882,6 +8101,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -7940,6 +8160,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -7952,6 +8173,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -7968,6 +8190,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -8000,6 +8223,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -8061,6 +8285,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -8081,6 +8306,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -8118,6 +8344,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -8151,6 +8378,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -8184,6 +8412,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -8217,6 +8446,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -8246,6 +8476,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -8277,6 +8508,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -8335,6 +8567,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -8347,6 +8580,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -8437,6 +8671,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -8457,6 +8692,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod", "name": "prod", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod", "peer": true, @@ -8490,6 +8726,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, @@ -8527,6 +8764,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/bundled", "name": "bundled", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, @@ -8560,6 +8798,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/dev", "name": "dev", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/dev", "peer": true, @@ -8593,6 +8832,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/optional", "name": "optional", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/optional", "peer": true, @@ -8626,6 +8866,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/peer", "name": "peer", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/peer", "peer": true, @@ -8655,6 +8896,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/extraneous", "name": "extraneous", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, @@ -8684,6 +8926,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -8714,6 +8957,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -8772,6 +9016,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta/node_modules/metameta", "name": "metameta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, @@ -8784,6 +9029,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/meta", "name": "meta", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/meta", "peer": true, @@ -8800,6 +9046,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root", "peer": true, @@ -8832,6 +9079,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "node_modules/prod/foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "patched": null, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, diff --git a/workspaces/arborist/test/arborist/package-extensions.js b/workspaces/arborist/test/arborist/package-extensions.js new file mode 100644 index 0000000000000..61d81dd3a704f --- /dev/null +++ b/workspaces/arborist/test/arborist/package-extensions.js @@ -0,0 +1,214 @@ +const { resolve } = require('node:path') +const t = require('tap') +const Arborist = require('../..') +const fixtures = resolve(__dirname, '../fixtures') +require(fixtures) +const MockRegistry = require('@npmcli/mock-registry') + +const createRegistry = (t) => new MockRegistry({ + strict: false, + tap: t, + registry: 'https://registry.npmjs.org', +}) + +const warningTracker = (t) => { + const warnings = [] + const onlog = (...msg) => msg[0] === 'warn' && warnings.push(msg) + process.on('log', onlog) + t.teardown(() => process.removeListener('log', onlog)) + return warnings +} + +const cache = t.testdir() +const newArb = (path, opt = {}) => new Arborist({ timeout: 30 * 60 * 1000, path, cache, ...opt }) +const buildIdeal = (path, opt) => newArb(path, opt).buildIdealTree(opt) + +// foo@1.0.0 imports bar but does not declare it; bar is published separately. +// withBar is false for tests that reject before bar is ever fetched. +const mockFooBar = async (t, { fooDeps, withBar = true } = {}) => { + const registry = createRegistry(t) + const fooManifest = registry.manifest({ + name: 'foo', + packuments: registry.packuments([{ version: '1.0.0', dependencies: fooDeps }], 'foo'), + }) + await registry.package({ manifest: fooManifest }) + if (withBar) { + const barManifest = registry.manifest({ + name: 'bar', + packuments: registry.packuments(['1.0.0', '1.2.3', '2.0.0'], 'bar'), + }) + await registry.package({ manifest: barManifest }) + } +} + +t.test('adds a missing dependency edge', async t => { + await mockFooBar(t) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + }), + }) + const tree = await buildIdeal(path) + const foo = tree.edgesOut.get('foo').to + const barEdge = foo.edgesOut.get('bar') + t.ok(barEdge, 'foo has a bar edge created by the extension') + t.equal(barEdge.valid, true, 'bar edge is valid') + t.equal(barEdge.to.version, '1.2.3', 'resolved to the highest 1.x') + t.strictSame(foo.packageExtensionsApplied, { selector: 'foo@1', dependencies: ['bar'] }, + 'provenance attached to the extended node') + t.strictSame(barEdge.explain().packageExtensions, { selector: 'foo@1', field: 'dependencies' }, + 'edge explanation records the extension provenance') +}) + +t.test('edge explanation omits provenance for non-extension edges', async t => { + // foo declares baz itself; the extension only adds bar + const registry = createRegistry(t) + const fooManifest = registry.manifest({ + name: 'foo', + packuments: registry.packuments([{ version: '1.0.0', dependencies: { baz: '1.0.0' } }], 'foo'), + }) + const barManifest = registry.manifest({ name: 'bar', packuments: registry.packuments(['1.2.3'], 'bar') }) + const bazManifest = registry.manifest({ name: 'baz', packuments: registry.packuments(['1.0.0'], 'baz') }) + await registry.package({ manifest: fooManifest }) + await registry.package({ manifest: barManifest }) + await registry.package({ manifest: bazManifest }) + + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + }), + }) + const tree = await buildIdeal(path) + const foo = tree.edgesOut.get('foo').to + t.ok(foo.edgesOut.get('bar').explain().packageExtensions, 'extension-created edge has provenance') + t.equal(foo.edgesOut.get('baz').explain().packageExtensions, undefined, + 'a self-declared edge from the same node has no provenance') +}) + +t.test('composes with overrides', async t => { + await mockFooBar(t) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + overrides: { bar: '1.0.0' }, + }), + }) + const tree = await buildIdeal(path) + const foo = tree.edgesOut.get('foo').to + t.equal(foo.edgesOut.get('bar').to.version, '1.0.0', 'override forces the extension-created edge') +}) + +t.test('name-only selector matches every version', async t => { + await mockFooBar(t) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { foo: { dependencies: { bar: '^1.0.0' } } }, + }), + }) + const tree = await buildIdeal(path) + t.ok(tree.edgesOut.get('foo').to.edgesOut.get('bar'), 'name-only selector applied') +}) + +t.test('conflicting selectors fail the install', async t => { + await mockFooBar(t, { withBar: false }) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { + foo: { dependencies: { bar: '^1.0.0' } }, + 'foo@1': { dependencies: { bar: '^2.0.0' } }, + }, + }), + }) + await t.rejects(buildIdeal(path), { code: 'EEXTENSIONCONFLICT' }, 'two matching selectors reject') +}) + +t.test('invalid selector is rejected at load', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + packageExtensions: { 'foo@latest': { dependencies: { bar: '^1.0.0' } } }, + }), + }) + await t.rejects(buildIdeal(path), { code: 'EEXTENSIONSELECTOR' }, 'dist-tag selector rejected') +}) + +t.test('rejects replacing an existing dependency', async t => { + await mockFooBar(t, { fooDeps: { bar: '1.0.0' }, withBar: false }) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { foo: '1.0.0' }, + packageExtensions: { 'foo@1': { dependencies: { bar: '^2.0.0' } } }, + }), + }) + await t.rejects(buildIdeal(path), { code: 'EEXTENSIONDUPDEP' }, 'cannot replace existing dependency') +}) + +t.test('does not extend workspace members but warns', async t => { + const warnings = warningTracker(t) + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + workspaces: ['packages/*'], + packageExtensions: { ws: { dependencies: { bar: '^1.0.0' } } }, + }), + packages: { + ws: { + 'package.json': JSON.stringify({ + name: 'ws', + version: '1.0.0', + // a non-root workspace declaring packageExtensions is ignored with a warning + packageExtensions: { other: { dependencies: { x: '^1' } } }, + }), + }, + // a second workspace that neither declares packageExtensions nor matches a selector + 'other-ws': { 'package.json': JSON.stringify({ name: 'other-ws', version: '1.0.0' }) }, + }, + }) + createRegistry(t) + const tree = await buildIdeal(path) + const ws = [...tree.inventory.values()].find(n => n.name === 'ws') + t.notOk(ws.edgesOut.get('bar'), 'workspace member is not extended') + t.ok(warnings.some(w => /workspace package ws/.test(w[2])), 'warns about the workspace selector match') + t.ok(warnings.some(w => /in workspace ws is ignored/.test(w[2])), 'warns about non-root workspace packageExtensions') +}) + +t.test('ignores packageExtensions from an installed dependency', async t => { + const registry = createRegistry(t) + const fooManifest = registry.manifest({ + name: 'foo', + packuments: registry.packuments([{ + version: '1.0.0', + // a published package trying to extend itself must have no effect + packageExtensions: { foo: { dependencies: { bar: '^1.0.0' } } }, + }], 'foo'), + }) + await registry.package({ manifest: fooManifest }) + const path = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' } }), + }) + const tree = await buildIdeal(path) + t.notOk(tree.edgesOut.get('foo').to.edgesOut.get('bar'), + 'dependency-level packageExtensions is ignored') +}) + +t.test('records the canonical hash on the lockfile meta', async t => { + await mockFooBar(t) + const { canonicalHash } = require('../../lib/package-extensions.js') + const packageExtensions = { 'foo@1': { dependencies: { bar: '^1.0.0' } } } + const path = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' }, packageExtensions }), + }) + const tree = await buildIdeal(path) + t.equal(tree.meta.packageExtensionsHash, canonicalHash(packageExtensions), 'hash stashed on meta') +}) diff --git a/workspaces/arborist/test/arborist/reify-package-extensions.js b/workspaces/arborist/test/arborist/reify-package-extensions.js new file mode 100644 index 0000000000000..3090d164d5991 --- /dev/null +++ b/workspaces/arborist/test/arborist/reify-package-extensions.js @@ -0,0 +1,177 @@ +const { join, resolve } = require('node:path') +const fs = require('node:fs') +const t = require('tap') +const Arborist = require('../..') +const fixtures = resolve(__dirname, '../fixtures') +require(fixtures) +const MockRegistry = require('@npmcli/mock-registry') +const { canonicalHash } = require('../../lib/package-extensions.js') + +const createRegistry = (t) => new MockRegistry({ + strict: false, + tap: t, + registry: 'https://registry.npmjs.org', +}) + +// Serve foo@1.0.0 and bar@1.2.3 as installable tarballs; bar is optional so a reify that does not need it leaves no unconsumed mock. +const register = async (t, dir, { withBar = true } = {}) => { + const registry = createRegistry(t) + const fooManifest = registry.manifest({ name: 'foo', packuments: [{ version: '1.0.0' }] }) + await registry.package({ manifest: fooManifest, tarballs: { '1.0.0': join(dir, 'src/foo') } }) + if (withBar) { + const barManifest = registry.manifest({ name: 'bar', packuments: [{ version: '1.2.3' }] }) + await registry.package({ manifest: barManifest, tarballs: { '1.2.3': join(dir, 'src/bar') } }) + } +} + +// foo@1.0.0 does not declare bar; both are served as installable tarballs from source dirs. +const setup = async (t, { packageExtensions, dependencies = { foo: '1.0.0' }, overrides }) => { + const dir = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', dependencies, packageExtensions, overrides }), + src: { + foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) }, + bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.2.3' }) }, + }, + }) + await register(t, dir) + return dir +} + +const newArb = (dir, opt = {}) => new Arborist({ + path: dir, + cache: join(dir, 'cache'), + registry: 'https://registry.npmjs.org', + audit: false, + timeout: 30 * 60 * 1000, + ...opt, +}) + +const readLock = dir => JSON.parse(fs.readFileSync(join(dir, 'package-lock.json'), 'utf8')) + +const ext = { 'foo@1': { dependencies: { bar: '^1.0.0' } } } + +for (const installStrategy of ['hoisted', 'nested', 'shallow', 'linked']) { + t.test(`installs the extension-created edge under install-strategy=${installStrategy}`, async t => { + const dir = await setup(t, { packageExtensions: ext }) + const tree = await newArb(dir, { installStrategy }).reify() + const foo = [...tree.inventory.values()].find(n => n.name === 'foo') + const barEdge = foo.edgesOut.get('bar') + t.ok(barEdge && barEdge.valid && barEdge.to, `bar edge resolved under ${installStrategy}`) + t.equal(barEdge.to.version, '1.2.3', 'bar resolved to a real installed node') + }) +} + +t.test('lockfile records hash, provenance, effective deps, and version 4', async t => { + const dir = await setup(t, { packageExtensions: ext }) + await newArb(dir).reify() + const lock = readLock(dir) + t.equal(lock.lockfileVersion, 4, 'bumped to lockfileVersion 4') + t.equal(lock.packages[''].packageExtensionsHash, canonicalHash(ext), 'root entry carries the canonical hash') + const fooEntry = lock.packages['node_modules/foo'] + t.strictSame(fooEntry.packageExtensionsApplied, { selector: 'foo@1', dependencies: ['bar'] }, + 'foo entry carries minimal provenance') + t.strictSame(fooEntry.dependencies, { bar: '^1.0.0' }, 'foo entry carries the effective dependency metadata') +}) + +t.test('does not rewrite the installed dependency package.json', async t => { + const dir = await setup(t, { packageExtensions: ext }) + await newArb(dir).reify() + const installed = JSON.parse(fs.readFileSync(join(dir, 'node_modules/foo/package.json'), 'utf8')) + t.notOk(installed.dependencies, 'the on-disk foo/package.json is not given a bar dependency') +}) + +t.test('composes with overrides during reify', async t => { + const dir = await setup(t, { packageExtensions: ext, overrides: { bar: '1.2.3' } }) + const tree = await newArb(dir).reify() + const bar = [...tree.inventory.values()].find(n => n.name === 'bar') + t.equal(bar.version, '1.2.3', 'override applied to the extension-created edge') +}) + +t.test('provenance round-trips through the lockfile (npm ci style)', async t => { + const dir = await setup(t, { packageExtensions: ext }) + await newArb(dir).reify() + // a fresh build loaded from the lockfile retains the provenance and hash + const virtual = await newArb(dir).loadVirtual() + const foo = [...virtual.inventory.values()].find(n => n.name === 'foo') + t.strictSame(foo.packageExtensionsApplied, { selector: 'foo@1', dependencies: ['bar'] }, + 'provenance restored from the lockfile') + t.equal(virtual.meta.packageExtensionsHash, canonicalHash(ext), 'hash restored from the lockfile') +}) + +t.test('refuses a lockfile newer than the supported version', async t => { + const dir = await setup(t, { packageExtensions: ext }) + await newArb(dir).reify() + const lock = readLock(dir) + lock.lockfileVersion = 5 + fs.writeFileSync(join(dir, 'package-lock.json'), JSON.stringify(lock)) + await t.rejects(newArb(dir).loadVirtual(), { code: 'ELOCKFILEVERSION' }, 'too-new lockfile is rejected') +}) + +t.test('removing an extension on reinstall reverts the locked graph', async t => { + const dir = await setup(t, { packageExtensions: ext }) + await newArb(dir).reify() + t.ok(readLock(dir).packages['node_modules/bar'], 'bar installed by the extension') + + // remove the extension and reinstall; the stale extended manifest must not persist + fs.writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' } })) + await register(t, dir, { withBar: false }) + await newArb(dir).reify() + const lock = readLock(dir) + t.notOk(lock.packages['node_modules/bar'], 'bar removed once the extension is gone') + t.notOk(lock.packages[''].packageExtensionsHash, 'root hash cleared') + t.notOk(lock.packages['node_modules/foo'].packageExtensionsApplied, 'foo provenance cleared') +}) + +t.test('adding an extension to an existing lockfile applies it on reinstall', async t => { + // first install with no extension, so the lockfile has foo but no bar + const dir = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' } }), + src: { + foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) }, + bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.2.3' }) }, + }, + }) + await register(t, dir, { withBar: false }) + await newArb(dir).reify() + t.notOk(readLock(dir).packages['node_modules/bar'], 'no bar before the extension is added') + + // add the extension and reinstall; the stale foo node must be rebuilt and gain the bar edge + fs.writeFileSync(join(dir, 'package.json'), + JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' }, packageExtensions: ext })) + await register(t, dir) + await newArb(dir).reify() + const lock = readLock(dir) + t.ok(lock.packages['node_modules/bar'], 'bar added after the extension is introduced') + t.strictSame(lock.packages['node_modules/foo'].packageExtensionsApplied, + { selector: 'foo@1', dependencies: ['bar'] }, 'provenance recorded for the newly extended node') +}) + +t.test('changing an extension range on reinstall re-resolves the edge', async t => { + const dir = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' }, packageExtensions: ext }), + src: { + foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }) }, + bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.2.3' }) }, + bar2: { 'package.json': JSON.stringify({ name: 'bar', version: '2.0.0' }) }, + }, + }) + const registerBoth = async () => { + const registry = createRegistry(t) + const fooManifest = registry.manifest({ name: 'foo', packuments: [{ version: '1.0.0' }] }) + const barManifest = registry.manifest({ name: 'bar', packuments: [{ version: '1.2.3' }, { version: '2.0.0' }] }) + await registry.package({ manifest: fooManifest, tarballs: { '1.0.0': join(dir, 'src/foo') } }) + await registry.package({ + manifest: barManifest, + tarballs: { '1.2.3': join(dir, 'src/bar'), '2.0.0': join(dir, 'src/bar2') }, + }) + } + await registerBoth() + await newArb(dir).reify() + t.equal(readLock(dir).packages['node_modules/bar'].version, '1.2.3', 'bar resolved to 1.x') + + fs.writeFileSync(join(dir, 'package.json'), + JSON.stringify({ name: 'root', dependencies: { foo: '1.0.0' }, packageExtensions: { 'foo@1': { dependencies: { bar: '^2.0.0' } } } })) + await registerBoth() + await newArb(dir).reify() + t.equal(readLock(dir).packages['node_modules/bar'].version, '2.0.0', 'bar re-resolved to 2.x after the range change') +}) diff --git a/workspaces/arborist/test/package-extensions.js b/workspaces/arborist/test/package-extensions.js new file mode 100644 index 0000000000000..72046b3ad1d31 --- /dev/null +++ b/workspaces/arborist/test/package-extensions.js @@ -0,0 +1,214 @@ +const t = require('tap') +const PackageExtensions = require('../lib/package-extensions.js') +const { + parseSelector, + rangeMatches, + canonicalHash, + canonicalStringify, +} = require('../lib/package-extensions.js') + +t.test('parseSelector', async t => { + t.strictSame(parseSelector('foo'), { name: 'foo', range: null }, 'name only') + t.strictSame(parseSelector('foo@1'), { name: 'foo', range: '1' }, 'name with range') + t.strictSame(parseSelector('@scope/foo'), { name: '@scope/foo', range: null }, 'scoped name only') + t.strictSame(parseSelector('@scope/foo@^2.3.0'), { name: '@scope/foo', range: '^2.3.0' }, 'scoped with range') + + for (const bad of ['', null, undefined, 5]) { + t.throws(() => parseSelector(bad), { code: 'EEXTENSIONSELECTOR' }, `rejects ${JSON.stringify(bad)}`) + } + // dist-tags, git, file, url, alias specs are not valid selectors + for (const bad of ['foo@latest', 'foo@next', 'foo@git+https://x.com/a.git', 'foo@file:../x', 'foo@npm:bar@1', 'foo@https://x.com/a.tgz']) { + t.throws(() => parseSelector(bad), { code: 'EEXTENSIONSELECTOR' }, `rejects spec selector ${bad}`) + } + // a blank range is malformed; use the name-only form to match every version + for (const bad of ['foo@', 'foo@ ', '@scope/foo@']) { + t.throws(() => parseSelector(bad), { code: 'EEXTENSIONSELECTOR' }, `rejects blank range ${JSON.stringify(bad)}`) + } + // invalid package names + for (const bad of [' @1', ' space ', '.hidden']) { + t.throws(() => parseSelector(bad), { code: 'EEXTENSIONSELECTOR' }, `rejects invalid name ${bad}`) + } +}) + +t.test('rangeMatches', async t => { + t.ok(rangeMatches(null, '1.2.3'), 'name-only matches semver version') + t.ok(rangeMatches(null, 'not-semver'), 'name-only matches non-semver version') + t.ok(rangeMatches('1', '1.2.3'), 'range matches satisfying version') + t.notOk(rangeMatches('1', '2.0.0'), 'range rejects non-satisfying version') + t.notOk(rangeMatches('1', 'not-semver'), 'versioned selector rejects non-semver version') +}) + +t.test('constructor validation', async t => { + t.equal(new PackageExtensions(undefined).present, false, 'absent field is allowed and not present') + t.equal(new PackageExtensions(undefined).hash, null, 'absent field has no hash') + + const empty = new PackageExtensions({}) + t.equal(empty.present, true, 'empty object is present') + t.ok(empty.hash, 'empty object still hashes') + + for (const bad of [null, [], 'x', 5]) { + t.throws(() => new PackageExtensions(bad), { code: 'EEXTENSIONROOT' }, `rejects root ${JSON.stringify(bad)}`) + } + + t.throws(() => new PackageExtensions({ foo: { devDependencies: { a: '1' } } }), + { code: 'EEXTENSIONFIELD' }, 'rejects unsupported field') + t.throws(() => new PackageExtensions({ foo: { dependencies: [] } }), + { code: 'EEXTENSIONVALUE' }, 'rejects non-object field value') + t.throws(() => new PackageExtensions({ foo: 'bar' }), + { code: 'EEXTENSIONVALUE' }, 'rejects non-object extension') + + for (const del of [null, false, '-']) { + t.throws(() => new PackageExtensions({ foo: { dependencies: { bar: del } } }), + { code: 'EEXTENSIONDELETE' }, `rejects deletion value ${JSON.stringify(del)}`) + } + + for (const bad of [null, false, '-', 'x', 5]) { + t.throws(() => new PackageExtensions({ foo: { peerDependenciesMeta: { bar: bad } } }), + { code: 'EEXTENSIONVALUE' }, `rejects non-object peerDependenciesMeta value ${JSON.stringify(bad)}`) + } +}) + +t.test('match', async t => { + const pe = new PackageExtensions({ + foo: { dependencies: { a: '1' } }, + 'bar@^2': { dependencies: { b: '1' } }, + }) + t.equal(pe.match('foo', '9.9.9').key, 'foo', 'name-only matches any version') + t.equal(pe.match('bar', '2.5.0').key, 'bar@^2', 'range matches satisfying version') + t.equal(pe.match('bar', '1.0.0'), null, 'range misses non-satisfying version') + t.equal(pe.match('nope', '1.0.0'), null, 'unknown name misses') + + const conflict = new PackageExtensions({ + foo: { dependencies: { a: '1' } }, + 'foo@1': { dependencies: { b: '1' } }, + }) + t.throws(() => conflict.match('foo', '1.2.3'), { code: 'EEXTENSIONCONFLICT' }, 'two matching selectors conflict') + t.equal(conflict.match('foo', '2.0.0').key, 'foo', 'only one matches at 2.0.0, no conflict') +}) + +t.test('apply: add missing dependencies and optionalDependencies', async t => { + const pe = new PackageExtensions({ + foo: { + dependencies: { 'missing-dep': '^2.0.0' }, + optionalDependencies: { 'opt-dep': '^1.0.0' }, + }, + }) + const { pkg, applied } = pe.apply({ name: 'foo', version: '1.0.0', dependencies: { existing: '^1' } }) + t.strictSame(pkg.dependencies, { existing: '^1', 'missing-dep': '^2.0.0' }, 'added to dependencies, kept existing') + t.strictSame(pkg.optionalDependencies, { 'opt-dep': '^1.0.0' }, 'created optionalDependencies') + t.strictSame(applied, { + selector: 'foo', + dependencies: ['missing-dep'], + optionalDependencies: ['opt-dep'], + }, 'provenance records selector and changed names') +}) + +t.test('apply: does not mutate the input manifest', async t => { + const pe = new PackageExtensions({ foo: { dependencies: { bar: '^1' } } }) + const input = { name: 'foo', version: '1.0.0', dependencies: { a: '1' } } + const inputDeps = input.dependencies + const { pkg } = pe.apply(input) + t.strictSame(input.dependencies, { a: '1' }, 'input dependencies unchanged') + t.equal(input.dependencies, inputDeps, 'input dependencies object identity unchanged') + t.not(pkg.dependencies, input.dependencies, 'output has a fresh dependencies object') +}) + +t.test('apply: rejects replacing an existing normal dependency', async t => { + const pe = new PackageExtensions({ foo: { dependencies: { bar: '^2' } } }) + t.throws(() => pe.apply({ name: 'foo', version: '1.0.0', dependencies: { bar: '^1' } }), + { code: 'EEXTENSIONDUPDEP' }, 'cannot replace existing dependency') + + const peOpt = new PackageExtensions({ foo: { dependencies: { bar: '^2' } } }) + t.throws(() => peOpt.apply({ name: 'foo', version: '1.0.0', optionalDependencies: { bar: '^1' } }), + { code: 'EEXTENSIONDUPDEP' }, 'cannot add a dependency already in optionalDependencies (no field move)') + + const peMove = new PackageExtensions({ foo: { optionalDependencies: { bar: '^2' } } }) + t.throws(() => peMove.apply({ name: 'foo', version: '1.0.0', dependencies: { bar: '^1' } }), + { code: 'EEXTENSIONDUPDEP' }, 'cannot add an optionalDependency already in dependencies (no field move)') +}) + +t.test('apply: peerDependencies merge and replace', async t => { + const pe = new PackageExtensions({ + foo: { + peerDependencies: { typescript: '>=5', react: '^18' }, + }, + }) + const { pkg, applied } = pe.apply({ + name: 'foo', + version: '1.0.0', + peerDependencies: { typescript: '>=4', vue: '^3' }, + }) + t.strictSame(pkg.peerDependencies, { typescript: '>=5', vue: '^3', react: '^18' }, + 'replaced existing range, added new, kept unrelated') + t.strictSame(applied.peerDependencies.sort(), ['react', 'typescript'], 'provenance lists changed peers') +}) + +t.test('apply: peerDependenciesMeta merge by key', async t => { + const pe = new PackageExtensions({ + foo: { + peerDependenciesMeta: { typescript: { optional: true } }, + }, + }) + const { pkg } = pe.apply({ + name: 'foo', + version: '1.0.0', + peerDependencies: { typescript: '>=5' }, + peerDependenciesMeta: { typescript: { somethingElse: true } }, + }) + t.strictSame(pkg.peerDependenciesMeta.typescript, { somethingElse: true, optional: true }, + 'shallow-merged meta object without dropping existing keys') +}) + +t.test('apply: peerDependenciesMeta with same extension adding the peer', async t => { + const pe = new PackageExtensions({ + foo: { + peerDependencies: { typescript: '>=5' }, + peerDependenciesMeta: { typescript: { optional: true } }, + }, + }) + const { pkg } = pe.apply({ name: 'foo', version: '1.0.0' }) + t.strictSame(pkg.peerDependencies, { typescript: '>=5' }, 'peer added') + t.strictSame(pkg.peerDependenciesMeta, { typescript: { optional: true } }, 'meta added') +}) + +t.test('apply: orphan peerDependenciesMeta is an error', async t => { + const pe = new PackageExtensions({ + foo: { peerDependenciesMeta: { typescript: { optional: true } } }, + }) + t.throws(() => pe.apply({ name: 'foo', version: '1.0.0' }), + { code: 'EEXTENSIONORPHANMETA' }, 'meta without corresponding peer fails') +}) + +t.test('apply: peer may overlap with dependencies', async t => { + const pe = new PackageExtensions({ + foo: { peerDependencies: { bar: '^1' } }, + }) + const { pkg } = pe.apply({ name: 'foo', version: '1.0.0', dependencies: { bar: '^1' } }) + t.strictSame(pkg.dependencies, { bar: '^1' }, 'dependency kept') + t.strictSame(pkg.peerDependencies, { bar: '^1' }, 'peer added alongside dependency') +}) + +t.test('apply: returns null when nothing matches', async t => { + const pe = new PackageExtensions({ foo: { dependencies: { a: '1' } } }) + t.equal(pe.apply({ name: 'other', version: '1.0.0' }), null, 'no match returns null') + t.equal(new PackageExtensions(undefined).apply({ name: 'foo', version: '1' }), null, 'absent returns null') + t.equal(pe.apply(null), null, 'no manifest returns null') +}) + +t.test('canonical hash is order-independent and value-sensitive', async t => { + const a = canonicalHash({ foo: { dependencies: { a: '1', b: '2' } }, bar: { dependencies: { c: '3' } } }) + const b = canonicalHash({ bar: { dependencies: { c: '3' } }, foo: { dependencies: { b: '2', a: '1' } } }) + t.equal(a, b, 'key order does not change the hash') + + const c = canonicalHash({ foo: { dependencies: { a: '1.0.0' } } }) + t.not(a, c, 'value changes change the hash') + t.match(a, /^sha512-/, 'uses sha512 digest encoding') + + t.equal(canonicalStringify({ b: 1, a: 2 }), '{"a":2,"b":1}', 'sorts keys') + t.equal(canonicalStringify({ a: [3, 1] }), '{"a":[3,1]}', 'preserves array order') +}) + +t.test('constructor stores hash of present field', async t => { + const raw = { foo: { dependencies: { bar: '^1' } } } + t.equal(new PackageExtensions(raw).hash, canonicalHash(raw), 'instance hash matches canonicalHash') +}) diff --git a/workspaces/libnpmpublish/lib/publish.js b/workspaces/libnpmpublish/lib/publish.js index eae4de7ae9c2d..796cef6aa392a 100644 --- a/workspaces/libnpmpublish/lib/publish.js +++ b/workspaces/libnpmpublish/lib/publish.js @@ -20,6 +20,14 @@ Remove the 'private' field from the package.json to publish it.`), ) } + // packageExtensions is root-only project policy and must never reach the registry manifest or the published tarball + if (manifest.packageExtensions !== undefined) { + throw Object.assign( + new Error('packageExtensions is only honored at the project root and must not be published.'), + { code: 'EPACKAGEEXTENSIONS' } + ) + } + // spec is used to pick the appropriate registry/auth combo const spec = npa.resolve(manifest.name, manifest.version) opts = { diff --git a/workspaces/libnpmpublish/test/publish.js b/workspaces/libnpmpublish/test/publish.js index fa2b688f427db..3b41656bef17a 100644 --- a/workspaces/libnpmpublish/test/publish.js +++ b/workspaces/libnpmpublish/test/publish.js @@ -129,6 +129,23 @@ t.test('publish strips patchedDependencies from the registry manifest', async t t.ok(ret, 'publish succeeded with patchedDependencies stripped') }) +t.test('fails when publishing a package with packageExtensions', async t => { + const { publish } = t.mock('..') + // no registry interceptor: the publish must fail before any request is made + const manifest = { + name: 'libnpmpublish-test', + version: '1.0.0', + description: 'test libnpmpublish package', + packageExtensions: { 'foo@1': { dependencies: { bar: '^1.0.0' } } }, + } + + await t.rejects( + publish(manifest, tarData, { ...opts, npmVersion: null }), + { code: 'EPACKAGEEXTENSIONS', message: /must not be published/ }, + 'refuses to publish a package containing packageExtensions' + ) +}) + t.test('scoped publish', async t => { const { publish } = t.mock('..') const registry = new MockRegistry({