diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 8ad6d5654f5fb..bda3f954d3014 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -180,6 +180,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"]; @@ -567,6 +568,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 c077129719657..0fac0e01f3fa7 100644 --- a/docs/lib/content/configuring-npm/package-json.md +++ b/docs/lib/content/configuring-npm/package-json.md @@ -1018,6 +1018,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 bb8f525dd2479..72cd08a33d845 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') @@ -80,6 +81,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 5dacd3919882e..1ec55a995935d 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) @@ -333,6 +348,11 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => { ? ' ' + chalk.dim('overridden') : '' ) + + ( + formatPackageExtensions(node.packageExtensionsApplied) + ? ' ' + chalk.dim(`packageExtensions: ${formatPackageExtensions(node.packageExtensionsApplied)}`) + : '' + ) + (isGitNode(node) ? ` (${node.resolved})` : '') + (node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') + (long ? `\n${node.package.description || ''}` : '') @@ -357,6 +377,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 29161ec55bb79..a7464b888afae 100644 --- a/lib/utils/validate-lockfile.js +++ b/lib/utils/validate-lockfile.js @@ -26,4 +26,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 2e42b5d5f7818..a2ab1f54d858b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14725,6 +14725,7 @@ "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", + "validate-npm-package-name": "^7.0.2", "walk-up-path": "^4.0.0" }, "bin": { 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 15559dc688b09..bc0a2e6c93c31 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 ab98773bc68e5..a6d9182bab43a 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 2478cd3fb1ff0..5337810c64f79 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -218,6 +218,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 25939c5f89cda..90bf4762c16d3 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 7fecd6759c041..4e54938967adc 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -24,6 +24,7 @@ const PlaceDep = require('../place-dep.js') const debug = require('../debug.js') const fromPath = require('../from-path.js') const calcDepFlags = require('../calc-dep-flags.js') +const PackageExtensions = require('../package-extensions.js') const Shrinkwrap = require('../shrinkwrap.js') const { defaultLockfileVersion } = Shrinkwrap const Node = require('../node.js') @@ -96,6 +97,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. @@ -173,12 +175,14 @@ module.exports = cls => class IdealTreeBuilder extends cls { try { await this.#initTree() + this.#loadPackageExtensions() await this.#inflateAncientLockfile() await this.#applyUserRequests(options) await this.#buildDeps() await this.#fixDepFlags() await this.#pruneFailedOptional() await this.#checkEngineAndPlatform() + this.#warnWorkspacePackageExtensions() } finally { timeEnd() this.finishTracker('idealTree') @@ -226,6 +230,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 } @@ -1365,7 +1430,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 36e57a011da5f..89d7bbd6553f4 100644 --- a/workspaces/arborist/lib/arborist/load-virtual.js +++ b/workspaces/arborist/lib/arborist/load-virtual.js @@ -242,6 +242,7 @@ To fix: path, realpath: path, integrity: sw.integrity, + 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 78b7f31e2c870..3baee5dab286c 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, path, peer = true, @@ -169,6 +170,9 @@ class Node { } } this.integrity = integrity || this.package._integrity || 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 ce2c58457098d..a8b8f8b232da1 100644 --- a/workspaces/arborist/lib/shrinkwrap.js +++ b/workspaces/arborist/lib/shrinkwrap.js @@ -10,6 +10,10 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const defaultLockfileVersion = 3 +// packageExtensions provenance forces lockfileVersion 4 so older clients abort rather than silently dropping the repaired graph. +// This shares version 4 with native dependency patching; both features are root-owned graph repairs an old npm must not drop. +const packageExtensionsLockfileVersion = 4 +const maxLockfileVersion = 4 // for comparing nodes to yarn.lock entries const mismatch = (a, b) => a && b && a !== b @@ -107,6 +111,7 @@ const nodeMetaKeys = [ 'integrity', 'inBundle', 'hasInstallScript', + 'packageExtensionsApplied', ] const metaFieldFromPkg = (pkg, key) => { @@ -347,6 +352,7 @@ class Shrinkwrap { reset () { this.tree = null this.#awaitingUpdate = new Map() + this.packageExtensionsHash = null const lockfileVersion = this.lockfileVersion || defaultLockfileVersion this.originalLockfileVersion = lockfileVersion @@ -458,6 +464,13 @@ class Shrinkwrap { this.ancientLockfile = false data = {} } + // refuse lockfiles newer than we understand so we never drop a 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.`), + { code: 'ELOCKFILEVERSION' } + ) + } // auto convert v1 lockfiles to v3 // leave v2 in place unless configured // v3 by default @@ -478,6 +491,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. @@ -895,6 +911,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 @@ -934,6 +954,13 @@ class Shrinkwrap { if (!this.lockfileVersion) { this.lockfileVersion = defaultLockfileVersion } + // packageExtensions state forces lockfileVersion 4 so older clients abort instead of dropping the repaired graph + const hasExtensionState = 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 12496f22b26ed..13178756b106c 100644 --- a/workspaces/arborist/package.json +++ b/workspaces/arborist/package.json @@ -36,6 +36,7 @@ "semver": "^7.3.7", "ssri": "^13.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 4147de62640d7..7e9c0de9c384c 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, "path": "/home/user/some/other/path", "peer": true, "queryContext": Object {}, @@ -72,6 +73,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "", "name": "path", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/some/kind/of/path", "peer": true, "queryContext": Object {}, @@ -88,6 +90,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "../../../../../some/other/path", "name": "path", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/some/other/path", "peer": true, "queryContext": Object {}, @@ -116,6 +119,7 @@ exports[`test/link.js TAP > instantiate without providing target 1`] = ` "location": "", "name": "path", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/some/kind/of/path", "peer": true, "queryContext": Object {}, diff --git a/workspaces/arborist/tap-snapshots/test/node.js.test.cjs b/workspaces/arborist/tap-snapshots/test/node.js.test.cjs index 03af283d2fbc5..275149d89403e 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, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -217,6 +218,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, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, "queryContext": Object {}, @@ -244,6 +246,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, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, "queryContext": Object {}, @@ -308,6 +311,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, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, "queryContext": Object {}, @@ -319,6 +323,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, "queryContext": Object {}, @@ -364,6 +369,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, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, "queryContext": Object {}, @@ -375,6 +381,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, "queryContext": Object {}, @@ -413,6 +420,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, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, "queryContext": Object {}, @@ -461,6 +469,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, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, "queryContext": Object {}, @@ -472,6 +481,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, "queryContext": Object {}, @@ -500,6 +510,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, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, "queryContext": Object {}, @@ -544,6 +555,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, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, "queryContext": Object {}, @@ -555,6 +567,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, "queryContext": Object {}, @@ -570,6 +583,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "", "name": "workspaces_root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root", "peer": true, "queryContext": Object {}, @@ -618,6 +632,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, "path": "/home/user/projects/workspaces_root/node_modules/foo", "peer": true, "queryContext": Object {}, @@ -629,6 +644,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "foo", "name": "foo", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/foo", "peer": true, "queryContext": Object {}, @@ -674,6 +690,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, "path": "/home/user/projects/workspaces_root/node_modules/unknown", "peer": true, "queryContext": Object {}, @@ -685,6 +702,7 @@ exports[`test/node.js TAP set workspaces > should setup edges out for each works "location": "unknown", "name": "unknown", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/workspaces_root/unknown", "peer": true, "queryContext": Object {}, @@ -724,6 +742,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -761,6 +780,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -813,6 +833,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -832,6 +853,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -871,6 +893,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -903,6 +926,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -935,6 +959,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -967,6 +992,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -995,6 +1021,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -1048,6 +1075,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -1059,6 +1087,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -1127,6 +1156,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -1164,6 +1194,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -1216,6 +1247,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -1235,6 +1267,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -1267,6 +1300,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -1306,6 +1340,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -1338,6 +1373,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -1370,6 +1406,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -1402,6 +1439,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -1430,6 +1468,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -1483,6 +1522,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -1494,6 +1534,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -1524,6 +1565,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -1561,6 +1603,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -1589,6 +1632,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -1603,6 +1647,7 @@ exports[`test/node.js TAP testing with dep tree with meta > add new meta under p "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -1634,6 +1679,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -1682,6 +1728,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -1734,6 +1781,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -1753,6 +1801,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -1792,6 +1841,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -1824,6 +1874,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -1856,6 +1907,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -1888,6 +1940,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -1916,6 +1969,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -1993,6 +2047,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -2045,6 +2100,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2064,6 +2120,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -2096,6 +2153,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2135,6 +2193,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -2174,6 +2233,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -2206,6 +2266,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -2238,6 +2299,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -2270,6 +2332,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -2298,6 +2361,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -2313,6 +2377,7 @@ exports[`test/node.js TAP testing with dep tree with meta > initial load with so "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -2344,6 +2409,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2404,6 +2470,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2423,6 +2490,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -2462,6 +2530,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -2494,6 +2563,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -2526,6 +2596,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -2558,6 +2629,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -2586,6 +2658,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -2628,6 +2701,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -2717,6 +2791,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2736,6 +2811,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -2768,6 +2844,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -2807,6 +2884,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -2839,6 +2917,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -2871,6 +2950,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -2903,6 +2983,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -2931,6 +3012,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -2973,6 +3055,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -2988,6 +3071,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move meta to top lev "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -3019,6 +3103,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3079,6 +3164,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3098,6 +3184,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -3134,6 +3221,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -3166,6 +3254,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -3198,6 +3287,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -3230,6 +3320,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -3258,6 +3349,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -3288,6 +3380,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -3345,6 +3438,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -3356,6 +3450,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -3445,6 +3540,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3464,6 +3560,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -3496,6 +3593,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3532,6 +3630,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -3564,6 +3663,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -3596,6 +3696,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -3628,6 +3729,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -3656,6 +3758,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -3684,6 +3787,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -3713,6 +3817,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -3770,6 +3875,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -3781,6 +3887,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -3796,6 +3903,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -3827,6 +3935,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3887,6 +3996,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -3906,6 +4016,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -3942,6 +4053,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -3974,6 +4086,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -4006,6 +4119,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -4038,6 +4152,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -4066,6 +4181,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -4096,6 +4212,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4153,6 +4270,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4164,6 +4282,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -4253,6 +4372,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -4272,6 +4392,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -4304,6 +4425,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -4340,6 +4462,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -4372,6 +4495,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -4404,6 +4528,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -4436,6 +4561,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -4464,6 +4590,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -4492,6 +4619,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4521,6 +4649,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4578,6 +4707,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4589,6 +4719,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -4604,6 +4735,7 @@ exports[`test/node.js TAP testing with dep tree with meta > move new meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -4635,6 +4767,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -4674,6 +4807,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -4711,6 +4845,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -4763,6 +4898,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -4782,6 +4918,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -4821,6 +4958,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -4853,6 +4991,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -4885,6 +5024,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -4917,6 +5057,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -4945,6 +5086,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -4998,6 +5140,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -5009,6 +5152,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5077,6 +5221,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -5114,6 +5259,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5166,6 +5312,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -5185,6 +5332,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -5217,6 +5365,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -5256,6 +5405,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -5288,6 +5438,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -5320,6 +5471,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -5352,6 +5504,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -5380,6 +5533,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -5433,6 +5587,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -5444,6 +5599,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5474,6 +5630,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -5511,6 +5668,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5539,6 +5697,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -5553,6 +5712,7 @@ exports[`test/node.js TAP testing with dep tree without meta > add new meta unde "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -5584,6 +5744,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -5632,6 +5793,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5684,6 +5846,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -5703,6 +5866,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -5742,6 +5906,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -5774,6 +5939,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -5806,6 +5972,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -5838,6 +6005,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -5866,6 +6034,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -5943,6 +6112,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -5995,6 +6165,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6014,6 +6185,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -6046,6 +6218,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6085,6 +6258,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, "path": "/home/user/projects/root/node_modules/prod/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -6124,6 +6298,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -6156,6 +6331,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -6188,6 +6364,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -6220,6 +6397,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -6248,6 +6426,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -6263,6 +6442,7 @@ exports[`test/node.js TAP testing with dep tree without meta > initial load with "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -6294,6 +6474,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6354,6 +6535,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6373,6 +6555,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -6412,6 +6595,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -6444,6 +6628,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -6476,6 +6661,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -6508,6 +6694,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -6536,6 +6723,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -6578,6 +6766,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -6667,6 +6856,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6686,6 +6876,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -6718,6 +6909,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -6757,6 +6949,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -6789,6 +6982,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -6821,6 +7015,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -6853,6 +7048,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -6881,6 +7077,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -6923,6 +7120,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -6938,6 +7136,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move meta to top "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -6969,6 +7168,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7029,6 +7229,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7048,6 +7249,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -7084,6 +7286,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -7116,6 +7319,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -7148,6 +7352,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -7180,6 +7385,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -7208,6 +7414,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -7238,6 +7445,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -7295,6 +7503,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -7306,6 +7515,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -7395,6 +7605,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7414,6 +7625,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -7446,6 +7658,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7482,6 +7695,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -7514,6 +7728,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -7546,6 +7761,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -7578,6 +7794,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -7606,6 +7823,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -7634,6 +7852,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -7663,6 +7882,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -7720,6 +7940,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -7731,6 +7952,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -7746,6 +7968,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -7777,6 +8000,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7837,6 +8061,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -7856,6 +8081,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -7892,6 +8118,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -7924,6 +8151,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -7956,6 +8184,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -7988,6 +8217,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -8016,6 +8246,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -8046,6 +8277,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -8103,6 +8335,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -8114,6 +8347,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -8203,6 +8437,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -8222,6 +8457,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, "path": "/home/user/projects/root/node_modules/prod", "peer": true, "queryContext": Object {}, @@ -8254,6 +8490,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, @@ -8290,6 +8527,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, "path": "/home/user/projects/root/node_modules/bundled", "peer": true, "queryContext": Object {}, @@ -8322,6 +8560,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, "path": "/home/user/projects/root/node_modules/dev", "peer": true, "queryContext": Object {}, @@ -8354,6 +8593,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, "path": "/home/user/projects/root/node_modules/optional", "peer": true, "queryContext": Object {}, @@ -8386,6 +8626,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, "path": "/home/user/projects/root/node_modules/peer", "peer": true, "queryContext": Object {}, @@ -8414,6 +8655,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, "path": "/home/user/projects/root/node_modules/extraneous", "peer": true, "queryContext": Object {}, @@ -8442,6 +8684,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -8471,6 +8714,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -8528,6 +8772,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, "path": "/home/user/projects/root/node_modules/meta/node_modules/metameta", "peer": true, "queryContext": Object {}, @@ -8539,6 +8784,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, "path": "/home/user/projects/root/node_modules/meta", "peer": true, "queryContext": Object {}, @@ -8554,6 +8800,7 @@ exports[`test/node.js TAP testing with dep tree without meta > move new meta to "location": "", "name": "root", "optional": true, + "packageExtensionsApplied": null, "path": "/home/user/projects/root", "peer": true, "queryContext": Object {}, @@ -8585,6 +8832,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, "path": "/home/user/projects/root/node_modules/prod/foo", "peer": true, "queryContext": Object {}, 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 414e07b78bf72..9ef019db1a8f3 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 389c2a8fe98b3..21c6575a3c86a 100644 --- a/workspaces/libnpmpublish/test/publish.js +++ b/workspaces/libnpmpublish/test/publish.js @@ -75,6 +75,23 @@ t.test('basic publish - no npmVersion', async t => { t.ok(ret, 'publish succeeded') }) +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({