diff --git a/src/install/lockfile.rs b/src/install/lockfile.rs index 5e5b8ec418f6..f4c78a03cf18 100644 --- a/src/install/lockfile.rs +++ b/src/install/lockfile.rs @@ -1020,6 +1020,7 @@ impl Lockfile { lockfile: &mut *new, mapping: &mut package_id_mapping, clone_queue: clone_queue_, + optional_peers: PendingResolutions::new(), log, old_preinstall_state, manager: &mut *manager, @@ -1292,6 +1293,8 @@ impl Lockfile { pub struct Cloner<'a> { pub(crate) clone_queue: PendingResolutions, + /// Bound in `flush`, once `clone_queue` has decided which targets survive. + pub(crate) optional_peers: PendingResolutions, pub lockfile: &'a mut Lockfile, pub(crate) old: &'a mut Lockfile, pub(crate) mapping: &'a mut [PackageID], @@ -1318,6 +1321,14 @@ impl<'a> Cloner<'a> { self.lockfile.buffers.resolutions[to_clone.resolve_id as usize] = new_id; } + // bun.lock.rs binds these before hoisting; the hoist below has to see the same bindings. + for pending in self.optional_peers.drain(..) { + let mapping = self.mapping[pending.old_resolution as usize]; + if (mapping as usize) < max_package_id { + self.lockfile.buffers.resolutions[pending.resolve_id as usize] = mapping; + } + } + // cloning finished, items in lockfile buffer might have a different order, meaning // package ids and dependency ids have changed self.manager @@ -1345,8 +1356,10 @@ impl<'a> Cloner<'a> { // ──────────────────────────────────────────────────────────────────────────── impl Lockfile { + /// Re-hoists while a pass bound an optional peer late; a reload has that binding up front. pub(crate) fn resolve(&mut self, log: &mut bun_ast::Log) -> Result<(), tree::SubtreeError> { - self.hoist::<{ tree::BuilderMethod::Resolvable }>(log, None, true, &[], None) + while self.hoist::<{ tree::BuilderMethod::Resolvable }>(log, None, true, &[], None)? {} + Ok(()) } pub(crate) fn filter( @@ -1363,10 +1376,11 @@ impl Lockfile { install_root_dependencies, workspace_filters, packages_to_install, - ) + )?; + Ok(()) } - /// Sets `buffers.trees` and `buffers.hoisted_dependencies` + /// Sets `buffers.trees`/`hoisted_dependencies`; returns `Builder::late_bound_optional_peer`. pub(crate) fn hoist( &mut self, log: &mut bun_ast::Log, @@ -1376,7 +1390,7 @@ impl Lockfile { install_root_dependencies: bool, workspace_filters: &[WorkspaceFilter], packages_to_install: Option<&[PackageID]>, - ) -> Result<(), tree::SubtreeError> { + ) -> Result { let slice = self.packages.slice(); // `tree::Builder` stores `lockfile: ParentRef` so @@ -1398,6 +1412,7 @@ impl Lockfile { workspace_filters, packages_to_install, pending_optional_peers: Default::default(), + late_bound_optional_peer: false, list: Default::default(), sort_buf: Default::default(), }; @@ -1416,9 +1431,10 @@ impl Lockfile { } let cleaned = builder.clean()?; + let late_bound_optional_peer = builder.late_bound_optional_peer; self.buffers.trees = cleaned.trees; self.buffers.hoisted_dependencies = cleaned.dep_ids; - Ok(()) + Ok(late_bound_optional_peer) } } diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index da74ede1d3b9..a183ccf2ac6e 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -606,14 +606,19 @@ impl Package { .zip(resolutions.iter_mut()) .enumerate() { - // Optional-peer slots are re-derived by `hoist` (Cloner::flush), not carried over. - if old_dependencies[i].behavior.is_optional_peer() { + if *old_resolution >= max_package_id { *resolution = invalid_package_id; continue; } - if *old_resolution >= max_package_id { - *resolution = invalid_package_id; + let pending = PendingResolution { + old_resolution: *old_resolution, + resolve_id: new_package.resolutions.off + PackageID::try_from(i).expect("int cast"), + }; + + // Peer slots must not keep their target alive; bound in `Cloner::flush`. + if old_dependencies[i].behavior.is_optional_peer() { + cloner.optional_peers.push(pending); continue; } @@ -621,11 +626,7 @@ impl Package { if mapped < max_package_id { *resolution = mapped; } else { - cloner.clone_queue.push(PendingResolution { - old_resolution: *old_resolution, - resolve_id: new_package.resolutions.off - + PackageID::try_from(i).expect("int cast"), - }); + cloner.clone_queue.push(pending); } } diff --git a/src/install/lockfile/Tree.rs b/src/install/lockfile/Tree.rs index 65c717839caa..66d7c3c067ce 100644 --- a/src/install/lockfile/Tree.rs +++ b/src/install/lockfile/Tree.rs @@ -135,6 +135,8 @@ enum HoistDependencyResult { Resolve(PackageID), ResolveReplace(ResolveReplace), ResolveLater, + /// `Hoisted`, plus the optional peer's slot now points at the version it deduplicated onto. + Rebind(PackageID), Placement(Placement), } @@ -442,6 +444,8 @@ pub struct Builder<'a, const METHOD: BuilderMethod> { // could be visited multiple times before it's resolved. pub(crate) pending_optional_peers: ArrayHashMap>, + /// An optional peer got bound after its dependent was placed; see `Lockfile::resolve`. + pub(crate) late_bound_optional_peer: bool, pub(crate) manager: Option<&'a PackageManager>, pub(crate) sort_buf: Vec, pub(crate) workspace_filters: &'a [WorkspaceFilter], @@ -874,6 +878,7 @@ impl Tree { } HoistDependencyResult::ResolveReplace(replace) => { debug_assert!(pkg_id != invalid_package_id); + builder.late_bound_optional_peer = true; builder.resolutions[replace.dep_id as usize] = pkg_id; if let Some(entry) = builder .pending_optional_peers @@ -909,6 +914,10 @@ impl Tree { })?; } } + HoistDependencyResult::Rebind(res_id) => { + debug_assert!(dependency.behavior.is_optional_peer()); + builder.resolutions[dep_id as usize] = res_id; + } HoistDependencyResult::ResolveLater => { // `dep_id` is an unresolved optional peer. while hoisting it deduplicated // with another unresolved optional peer. save it so we remember resolve it @@ -1046,6 +1055,16 @@ impl Tree { // or hoist if peer version allows it if dependency.behavior.is_peer() { + // An optional peer's binding follows the dedupe, but only in the tree being saved. + let dedupe = || { + if METHOD == BuilderMethod::Resolvable && dependency.behavior.is_optional_peer() + { + HoistDependencyResult::Rebind(res_id) + } else { + HoistDependencyResult::Hoisted + } + }; + if dependency.version.tag == crate::dependency::VersionTag::Npm { let resolution: Resolution = builder.lockfile().packages.items_resolution()[res_id as usize]; @@ -1053,7 +1072,7 @@ impl Tree { if resolution.tag == crate::resolution::Tag::Npm && version.satisfies(resolution.npm().version, builder.buf(), builder.buf()) { - return Ok(HoistDependencyResult::Hoisted); // 1 + return Ok(dedupe()); // 1 } } @@ -1061,7 +1080,7 @@ impl Tree { // to hoist other peers even if they don't satisfy the version if builder.lockfile().is_workspace_root_dependency(dep_id) { // TODO: warning about peer dependency version mismatch - return Ok(HoistDependencyResult::Hoisted); // 1 + return Ok(dedupe()); // 1 } } diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 8180f837292a..11ba7ce3a7f6 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -1,5 +1,6 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, expect, it } from "bun:test"; +import { readlinkSync } from "fs"; import { access, copyFile, cp, exists, open, rm, writeFile } from "fs/promises"; import { bunExe, @@ -1004,11 +1005,209 @@ it("optional peer with a non-wildcard range is idempotent with two versions of t expect(first).toContain('"no-deps": ["no-deps@'); // A second install over the same lockfile must be a byte-for-byte no-op: the - // cleared optional-peer slot re-derives to the same value hoist produced on - // fresh install. + // optional peer stays bound to the no-deps the fresh install bound it to. await run(["install"]); expect(await file(join(packageDir, "bun.lock")).text()).toBe(first); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await run(["install", "--frozen-lockfile"]); }); + +// The optional-peer-hoist-* fixtures are described in +// registry/packages/create-optional-peer-hoist-packages.ts. In short: consumer +// has an optional peer on target, and deep -> deep-child reaches target@1.0.0 +// (which depends on leaf@2.0.0) as well as leaf@1.0.0. Hoisting is +// breadth-first, so leaf@2.0.0 only wins the root slot if consumer's peer is +// already bound to target when the tree is built. A loaded bun.lock always has +// the peer bound, so that is the tree every install has to build, otherwise +// --frozen-lockfile compares two different trees. +const optionalPeerHoistDeps = { + "optional-peer-hoist-consumer": "1.0.0", + "optional-peer-hoist-deep": "1.0.0", +}; + +it("a fresh install hoists around an optional peer the same way a reinstall does", async () => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: true } }); + const run = makeInstallRunner(packageDir); + + await write(packageJson, JSON.stringify({ name: "foo", dependencies: optionalPeerHoistDeps })); + await run(["install"]); + const fresh = await file(join(packageDir, "bun.lock")).text(); + expect(fresh).toContain('"optional-peer-hoist-leaf": ["optional-peer-hoist-leaf@2.0.0"'); + expect(fresh).toContain( + '"optional-peer-hoist-deep-child/optional-peer-hoist-leaf": ["optional-peer-hoist-leaf@1.0.0"', + ); + + await run(["install", "--frozen-lockfile"]); + + // --lockfile-only always writes, so this checks the tree a reload builds + // prints back to the same text. + await run(["install", "--lockfile-only"]); + expect(await file(join(packageDir, "bun.lock")).text()).toBe(fresh); +}); + +it("a fresh install settles hoisting around a peer that only becomes bindable once another peer is bound", async () => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: true } }); + const run = makeInstallRunner(packageDir); + + // Shape 2 in the fixture generator: binding consumer's peer hoists leaf@3.0.0 + // out from under target, which is what lets target2@1.0.0 reach consumer2's + // peer, and only with that one bound too does target2's tail@2.0.0 beat + // deep-child's tail@1.0.0 to the root, the way it does on every reload. + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { + "optional-peer-hoist-consumer": "1.0.0", + "optional-peer-hoist-consumer2": "1.0.0", + "optional-peer-hoist-deep": "2.0.0", + }, + }), + ); + await run(["install"]); + const fresh = await file(join(packageDir, "bun.lock")).text(); + expect(fresh).toContain('"optional-peer-hoist-tail": ["optional-peer-hoist-tail@2.0.0"'); + expect(fresh).toContain( + '"optional-peer-hoist-deep-child/optional-peer-hoist-tail": ["optional-peer-hoist-tail@1.0.0"', + ); + + await run(["install", "--frozen-lockfile"]); + await run(["install", "--lockfile-only"]); + expect(await file(join(packageDir, "bun.lock")).text()).toBe(fresh); +}); + +it.each([ + [ + "leaf@2.0.0 hoisted (target placed from consumer)", + { + "optional-peer-hoist-leaf": "2.0.0", + "optional-peer-hoist-deep-child/optional-peer-hoist-leaf": "1.0.0", + }, + ], + [ + // What a fresh install wrote before the peer binding was carried over. + "leaf@1.0.0 hoisted (target placed from deep-child)", + { + "optional-peer-hoist-leaf": "1.0.0", + "optional-peer-hoist-target/optional-peer-hoist-leaf": "2.0.0", + }, + ], +])("--frozen-lockfile accepts an existing bun.lock with %s", async (_, leafPlacement) => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: true } }); + const run = makeInstallRunner(packageDir); + + const pkg = (name: string, version: string, info: object = {}) => [ + `${name}@${version}`, + `${registry.registryUrl()}${name}/-/${name}-${version}.tgz`, + info, + "", + ]; + const packages: Record = { + "optional-peer-hoist-consumer": pkg("optional-peer-hoist-consumer", "1.0.0", { + peerDependencies: { "optional-peer-hoist-target": "*" }, + optionalPeers: ["optional-peer-hoist-target"], + }), + "optional-peer-hoist-deep": pkg("optional-peer-hoist-deep", "1.0.0", { + dependencies: { "optional-peer-hoist-deep-child": "1.0.0" }, + }), + "optional-peer-hoist-deep-child": pkg("optional-peer-hoist-deep-child", "1.0.0", { + dependencies: { "optional-peer-hoist-leaf": "1.0.0", "optional-peer-hoist-target": "1.0.0" }, + }), + "optional-peer-hoist-target": pkg("optional-peer-hoist-target", "1.0.0", { + dependencies: { "optional-peer-hoist-leaf": "2.0.0" }, + }), + }; + for (const [path, version] of Object.entries(leafPlacement)) { + packages[path] = pkg("optional-peer-hoist-leaf", version); + } + + await write(packageJson, JSON.stringify({ name: "foo", dependencies: optionalPeerHoistDeps })); + await write( + join(packageDir, "bun.lock"), + JSON.stringify({ + lockfileVersion: 2, + configVersion: 1, + workspaces: { "": { name: "foo", dependencies: optionalPeerHoistDeps } }, + packages, + }), + ); + + await run(["install", "--frozen-lockfile"]); +}); + +it("adding a dependency keeps an optional peer on the package bun.lock bound it to while that package stays next to it", async () => { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: true } }); + const run = makeInstallRunner(packageDir); + + await write(packageJson, JSON.stringify({ name: "foo", dependencies: optionalPeerHoistDeps })); + await run(["install"]); + expect(await file(join(packageDir, "bun.lock")).text()).toContain( + '"optional-peer-hoist-target": ["optional-peer-hoist-target@1.0.0"', + ); + + // provider brings in target@2.0.0, which consumer's peer range would accept + // too. bun.lock binds consumer to target@1.0.0, and consumer sorts before + // provider, so target@1.0.0 is placed from consumer first, keeps the root + // slot and the binding, and target@2.0.0 nests under provider. + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { ...optionalPeerHoistDeps, "optional-peer-hoist-provider": "1.0.0" }, + }), + ); + await run(["install"]); + const lockfile = await file(join(packageDir, "bun.lock")).text(); + expect(lockfile).toContain('"optional-peer-hoist-target": ["optional-peer-hoist-target@1.0.0"'); + expect(lockfile).toContain( + '"optional-peer-hoist-provider/optional-peer-hoist-target": ["optional-peer-hoist-target@2.0.0"', + ); + expect(lockfile).toContain('"optional-peer-hoist-leaf": ["optional-peer-hoist-leaf@2.0.0"'); + + await run(["install", "--frozen-lockfile"]); + await run(["install", "--lockfile-only"]); + expect(await file(join(packageDir, "bun.lock")).text()).toBe(lockfile); +}); + +it("an optional peer is rebound when another version of its package takes the slot next to it", async () => { + // The isolated linker is the one consumer of the binding itself: consumer's + // store entry is keyed by the target it was linked against. + const { packageDir, packageJson } = await registry.createTestDir({ + bunfigOpts: { saveTextLockfile: true, linker: "isolated" }, + }); + const run = makeInstallRunner(packageDir); + const consumerLink = () => readlinkSync(join(packageDir, "node_modules", "optional-peer-hoist-consumer")); + + await write(packageJson, JSON.stringify({ name: "foo", dependencies: optionalPeerHoistDeps })); + await run(["install"]); + const boundToTarget1 = consumerLink(); + + // Same as the previous test, but aliased so the provider sorts before + // consumer: target@2.0.0 takes the root slot before consumer's bound + // target@1.0.0 can be placed, and since the peer range accepts it, consumer + // dedupes onto it. That is what a reload of this bun.lock binds consumer to, + // so it is also what this install has to link consumer against. + await write( + packageJson, + JSON.stringify({ + name: "foo", + dependencies: { "a-provider": "npm:optional-peer-hoist-provider@1.0.0", ...optionalPeerHoistDeps }, + }), + ); + await run(["install"]); + const lockfile = await file(join(packageDir, "bun.lock")).text(); + expect(lockfile).toContain('"optional-peer-hoist-target": ["optional-peer-hoist-target@2.0.0"'); + expect(lockfile).toContain( + '"optional-peer-hoist-deep-child/optional-peer-hoist-target": ["optional-peer-hoist-target@1.0.0"', + ); + const linkedByThisInstall = consumerLink(); + expect(linkedByThisInstall).not.toBe(boundToTarget1); + + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await run(["install", "--frozen-lockfile"]); + expect(consumerLink()).toBe(linkedByThisInstall); + + await run(["install", "--lockfile-only"]); + expect(await file(join(packageDir, "bun.lock")).text()).toBe(lockfile); +}); diff --git a/test/cli/install/registry/packages/create-optional-peer-hoist-packages.ts b/test/cli/install/registry/packages/create-optional-peer-hoist-packages.ts new file mode 100644 index 000000000000..15a9d0c9ca1f --- /dev/null +++ b/test/cli/install/registry/packages/create-optional-peer-hoist-packages.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env bun +/** + * Generates the `optional-peer-hoist-*` fixtures used by bun-lock.test.ts. + * + * Hoisting is breadth-first, so where a package lands can depend on whether an + * optional peer is already bound to its target when the tree is built: bound, + * the target's subtree is queued right after the dependent; unbound, only once + * a regular dependency edge reaches the target. These packages make that + * difference visible in bun.lock. + * + * Shape 1 (consumer + deep@1.0.0, optionally provider): + * + * - optional-peer-hoist-consumer@1.0.0 optional peer on optional-peer-hoist-target (any version) + * - optional-peer-hoist-deep@1.0.0 -> deep-child@1.0.0 + * - optional-peer-hoist-deep-child@1.0.0 -> leaf@1.0.0, target@1.0.0 + * - optional-peer-hoist-target@1.0.0 -> leaf@2.0.0 + * - optional-peer-hoist-target@2.0.0 no dependencies + * - optional-peer-hoist-leaf@1.0.0/2.0.0 no dependencies + * - optional-peer-hoist-provider@1.0.0 -> target@2.0.0 + * + * `consumer` sorts before `deep`, so with the peer bound, target@1.0.0 is placed + * from consumer and its leaf@2.0.0 reaches the root before deep-child's + * leaf@1.0.0; unbound, target is only placed once deep-child is reached and + * leaf@1.0.0 wins the root. + * + * Shape 2 (consumer + consumer2 + deep@2.0.0) takes more than one extra hoist + * pass to settle: + * + * - optional-peer-hoist-consumer2@1.0.0 optional peer on optional-peer-hoist-target2 (any version) + * - optional-peer-hoist-deep@2.0.0 -> deep-child@2.0.0 + * - optional-peer-hoist-deep-child@2.0.0 -> leaf@1.0.0, target@3.0.0, tail@1.0.0 + * - optional-peer-hoist-target@3.0.0 -> leaf@3.0.0, and bundles target2@0.0.1 + * - optional-peer-hoist-leaf@3.0.0 -> target2@1.0.0 + * - optional-peer-hoist-target2@0.0.1 no dependencies (the bundled copy) + * - optional-peer-hoist-target2@1.0.0 -> tail@2.0.0 + * - optional-peer-hoist-tail@1.0.0/2.0.0 no dependencies + * + * While consumer's peer is unbound, leaf@3.0.0 nests under target (deep-child's + * leaf@1.0.0 holds the root), so its target2@1.0.0 runs into the bundled + * target2@0.0.1 in target's node_modules and nests there too, out of reach of + * consumer2's peer. Once consumer's peer is bound, leaf@3.0.0 is hoisted to the + * root and target2@1.0.0 does reach consumer2's peer, but only after deep-child + * has put tail@1.0.0 at the root. Once consumer2's peer is bound as well, + * target2's subtree is queued from consumer2 and tail@2.0.0 takes the root. + */ + +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; + +const packagesDir = import.meta.dir; + +const prefix = "optional-peer-hoist-"; + +type Manifest = { + version: string; + dependencies?: Record; + peerDependencies?: Record; + peerDependenciesMeta?: Record; + bundleDependencies?: string[]; +}; + +const optionalPeerOn = (suffix: string): Manifest => ({ + version: "1.0.0", + peerDependencies: { [prefix + suffix]: "*" }, + peerDependenciesMeta: { [prefix + suffix]: { optional: true } }, +}); + +const packages: Record = { + consumer: [optionalPeerOn("target")], + consumer2: [optionalPeerOn("target2")], + deep: [ + { version: "1.0.0", dependencies: { [`${prefix}deep-child`]: "1.0.0" } }, + { version: "2.0.0", dependencies: { [`${prefix}deep-child`]: "2.0.0" } }, + ], + "deep-child": [ + { version: "1.0.0", dependencies: { [`${prefix}leaf`]: "1.0.0", [`${prefix}target`]: "1.0.0" } }, + { + version: "2.0.0", + dependencies: { [`${prefix}leaf`]: "1.0.0", [`${prefix}target`]: "3.0.0", [`${prefix}tail`]: "1.0.0" }, + }, + ], + target: [ + { version: "1.0.0", dependencies: { [`${prefix}leaf`]: "2.0.0" } }, + { version: "2.0.0" }, + { + version: "3.0.0", + dependencies: { [`${prefix}leaf`]: "3.0.0", [`${prefix}target2`]: "0.0.1" }, + bundleDependencies: [`${prefix}target2`], + }, + ], + target2: [{ version: "0.0.1" }, { version: "1.0.0", dependencies: { [`${prefix}tail`]: "2.0.0" } }], + leaf: [ + { version: "1.0.0" }, + { version: "2.0.0" }, + { version: "3.0.0", dependencies: { [`${prefix}target2`]: "1.0.0" } }, + ], + tail: [{ version: "1.0.0" }, { version: "2.0.0" }], + provider: [{ version: "1.0.0", dependencies: { [`${prefix}target`]: "2.0.0" } }], +}; + +for (const [suffix, manifests] of Object.entries(packages)) { + const name = prefix + suffix; + const dir = join(packagesDir, name); + await mkdir(dir, { recursive: true }); + + const versions: Record = {}; + let latest = ""; + for (const manifest of manifests) { + const pkgJson = { name, ...manifest }; + const files: Record = { "package/package.json": JSON.stringify(pkgJson, null, 2) }; + for (const bundled of manifest.bundleDependencies ?? []) { + files[`package/node_modules/${bundled}/package.json`] = JSON.stringify( + { name: bundled, version: manifest.dependencies![bundled] }, + null, + 2, + ); + } + const tarball = join(dir, `${name}-${manifest.version}.tgz`); + await Bun.Archive.write(tarball, files, { compress: "gzip" }); + + const bytes = await Bun.file(tarball).bytes(); + versions[manifest.version] = { + ...pkgJson, + _id: `${name}@${manifest.version}`, + dist: { + integrity: `sha512-${Buffer.from(new Bun.CryptoHasher("sha512").update(bytes).digest()).toString("base64")}`, + shasum: new Bun.CryptoHasher("sha1").update(bytes).digest("hex"), + tarball: `http://localhost:4873/${name}/-/${name}-${manifest.version}.tgz`, + }, + }; + latest = manifest.version; + } + + await writeFile( + join(dir, "package.json"), + JSON.stringify({ _id: name, name, "dist-tags": { latest }, versions }, null, 2), + ); +} + +console.log("Created optional-peer-hoist test packages"); diff --git a/test/cli/install/registry/packages/optional-peer-hoist-consumer/optional-peer-hoist-consumer-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-consumer/optional-peer-hoist-consumer-1.0.0.tgz new file mode 100644 index 000000000000..12b8c53939cd Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-consumer/optional-peer-hoist-consumer-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-consumer/package.json b/test/cli/install/registry/packages/optional-peer-hoist-consumer/package.json new file mode 100644 index 000000000000..5e10ffcbf135 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-consumer/package.json @@ -0,0 +1,27 @@ +{ + "_id": "optional-peer-hoist-consumer", + "name": "optional-peer-hoist-consumer", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-consumer", + "version": "1.0.0", + "peerDependencies": { + "optional-peer-hoist-target": "*" + }, + "peerDependenciesMeta": { + "optional-peer-hoist-target": { + "optional": true + } + }, + "_id": "optional-peer-hoist-consumer@1.0.0", + "dist": { + "integrity": "sha512-sVIugo50KczyFT1h3oEAZTIvptOi5hfwwveKVbLw3MjOsQC4XBkB8FYFpgQH2ltjpU88VhHpN6g3LSF+OuQOYQ==", + "shasum": "b96f9a0bb75bbcf182c94187b007131b52534742", + "tarball": "http://localhost:4873/optional-peer-hoist-consumer/-/optional-peer-hoist-consumer-1.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-consumer2/optional-peer-hoist-consumer2-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-consumer2/optional-peer-hoist-consumer2-1.0.0.tgz new file mode 100644 index 000000000000..53654651773b Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-consumer2/optional-peer-hoist-consumer2-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-consumer2/package.json b/test/cli/install/registry/packages/optional-peer-hoist-consumer2/package.json new file mode 100644 index 000000000000..a37b4cd92c38 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-consumer2/package.json @@ -0,0 +1,27 @@ +{ + "_id": "optional-peer-hoist-consumer2", + "name": "optional-peer-hoist-consumer2", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-consumer2", + "version": "1.0.0", + "peerDependencies": { + "optional-peer-hoist-target2": "*" + }, + "peerDependenciesMeta": { + "optional-peer-hoist-target2": { + "optional": true + } + }, + "_id": "optional-peer-hoist-consumer2@1.0.0", + "dist": { + "integrity": "sha512-9cKPjtskNfVsoLMh7v3AKDZxX3zI0ayq4lD5bCtQzpcscobikU1WNK9PWrPir/xjvGQ8P1YsleCO2OacONQ1iA==", + "shasum": "1d3aefe2e6828fa609bfe8b40c35ea541d9846b9", + "tarball": "http://localhost:4873/optional-peer-hoist-consumer2/-/optional-peer-hoist-consumer2-1.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-1.0.0.tgz new file mode 100644 index 000000000000..be42b51658ab Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-2.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-2.0.0.tgz new file mode 100644 index 000000000000..3e0df4238cad Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/optional-peer-hoist-deep-child-2.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep-child/package.json b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/package.json new file mode 100644 index 000000000000..8048eb3f5102 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-deep-child/package.json @@ -0,0 +1,38 @@ +{ + "_id": "optional-peer-hoist-deep-child", + "name": "optional-peer-hoist-deep-child", + "dist-tags": { + "latest": "2.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-deep-child", + "version": "1.0.0", + "dependencies": { + "optional-peer-hoist-leaf": "1.0.0", + "optional-peer-hoist-target": "1.0.0" + }, + "_id": "optional-peer-hoist-deep-child@1.0.0", + "dist": { + "integrity": "sha512-s+joWU9Q0rD813sf/B6wMc3b9wNT+gaKC0Z7+nFwFXx81J1mA+XIJ1ppLZg8JJup4f+7lBuz0viJDHudI/U08Q==", + "shasum": "ea89ffe7f38b2f18c9ca3d02f72030741a8347f4", + "tarball": "http://localhost:4873/optional-peer-hoist-deep-child/-/optional-peer-hoist-deep-child-1.0.0.tgz" + } + }, + "2.0.0": { + "name": "optional-peer-hoist-deep-child", + "version": "2.0.0", + "dependencies": { + "optional-peer-hoist-leaf": "1.0.0", + "optional-peer-hoist-target": "3.0.0", + "optional-peer-hoist-tail": "1.0.0" + }, + "_id": "optional-peer-hoist-deep-child@2.0.0", + "dist": { + "integrity": "sha512-YocWmQN6LECrF6fsAnQlTn63MxR1tmQiPmk2TfFe9UlTOHg4cJaLcxoRAx3ENFqiDA23z2O2YXXEQfH7CwgnDQ==", + "shasum": "590338e91f3aeb5e59a3f501d40e5244d4f3650c", + "tarball": "http://localhost:4873/optional-peer-hoist-deep-child/-/optional-peer-hoist-deep-child-2.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-1.0.0.tgz new file mode 100644 index 000000000000..e57632f6315a Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-2.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-2.0.0.tgz new file mode 100644 index 000000000000..d865d3b2a733 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-deep/optional-peer-hoist-deep-2.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-deep/package.json b/test/cli/install/registry/packages/optional-peer-hoist-deep/package.json new file mode 100644 index 000000000000..9fc336d3eada --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-deep/package.json @@ -0,0 +1,35 @@ +{ + "_id": "optional-peer-hoist-deep", + "name": "optional-peer-hoist-deep", + "dist-tags": { + "latest": "2.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-deep", + "version": "1.0.0", + "dependencies": { + "optional-peer-hoist-deep-child": "1.0.0" + }, + "_id": "optional-peer-hoist-deep@1.0.0", + "dist": { + "integrity": "sha512-Nl/l5CFE2ostDfGRN+FKlbWP0HgYdjZ+rUZHT5kKwmVGMyKUPsParH9qCHGVaWMcxwQfqzPqUJyAMaHXW+ci1w==", + "shasum": "2fc39ef50643af6eac1abaf7ad78f1ed48806e06", + "tarball": "http://localhost:4873/optional-peer-hoist-deep/-/optional-peer-hoist-deep-1.0.0.tgz" + } + }, + "2.0.0": { + "name": "optional-peer-hoist-deep", + "version": "2.0.0", + "dependencies": { + "optional-peer-hoist-deep-child": "2.0.0" + }, + "_id": "optional-peer-hoist-deep@2.0.0", + "dist": { + "integrity": "sha512-UJk6zpTjUOU812aFbNk2J3UyY072P6rDAVfbDKSNheRzStX2U+fdA05CQyFlGd3SHk5HWJojFvyF1ssLjW9QhA==", + "shasum": "282a38f52b9ef982c7c5bed3be0312b96c8bd83c", + "tarball": "http://localhost:4873/optional-peer-hoist-deep/-/optional-peer-hoist-deep-2.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-1.0.0.tgz new file mode 100644 index 000000000000..1eee72aa3c60 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-2.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-2.0.0.tgz new file mode 100644 index 000000000000..9bf7b5e8d10c Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-2.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-3.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-3.0.0.tgz new file mode 100644 index 000000000000..b64f89391d63 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-leaf/optional-peer-hoist-leaf-3.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-leaf/package.json b/test/cli/install/registry/packages/optional-peer-hoist-leaf/package.json new file mode 100644 index 000000000000..d1cb1eccd9f3 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-leaf/package.json @@ -0,0 +1,42 @@ +{ + "_id": "optional-peer-hoist-leaf", + "name": "optional-peer-hoist-leaf", + "dist-tags": { + "latest": "3.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-leaf", + "version": "1.0.0", + "_id": "optional-peer-hoist-leaf@1.0.0", + "dist": { + "integrity": "sha512-57x3rWhz3tBcdNnWCudX5V6XvzIHn7taFxSdYzNgAUCAkYIrc+945lIT5CziLvmeXDh4EbRxamaYFHbIDA1BeA==", + "shasum": "37a38558bbefe6dbfdc785c4c61841b0e704caac", + "tarball": "http://localhost:4873/optional-peer-hoist-leaf/-/optional-peer-hoist-leaf-1.0.0.tgz" + } + }, + "2.0.0": { + "name": "optional-peer-hoist-leaf", + "version": "2.0.0", + "_id": "optional-peer-hoist-leaf@2.0.0", + "dist": { + "integrity": "sha512-tfK30752Xwe6oDLE4Iv0cnDxJILQVmb+HiGrOHDHqnn/Oi3g+RIGH3NuSqX2WmluXbH0C/bkJOrjqgTlGk6wNg==", + "shasum": "510479d612daa8950f98e5a63238e6916a2f7ee9", + "tarball": "http://localhost:4873/optional-peer-hoist-leaf/-/optional-peer-hoist-leaf-2.0.0.tgz" + } + }, + "3.0.0": { + "name": "optional-peer-hoist-leaf", + "version": "3.0.0", + "dependencies": { + "optional-peer-hoist-target2": "1.0.0" + }, + "_id": "optional-peer-hoist-leaf@3.0.0", + "dist": { + "integrity": "sha512-xSPMrxzzfuv9sFRWtGGfB9+DOZj8UOB8i7BUt+0xul0M+GgZV6QsM/PtWtybEMOfr45ASiN3HRuVUHF2Tix4Gw==", + "shasum": "dc5163d16ef66c63acbc8c1eb151a43c7abe076d", + "tarball": "http://localhost:4873/optional-peer-hoist-leaf/-/optional-peer-hoist-leaf-3.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-provider/optional-peer-hoist-provider-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-provider/optional-peer-hoist-provider-1.0.0.tgz new file mode 100644 index 000000000000..cc228d7ce52c Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-provider/optional-peer-hoist-provider-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-provider/package.json b/test/cli/install/registry/packages/optional-peer-hoist-provider/package.json new file mode 100644 index 000000000000..fe42ae163bb9 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-provider/package.json @@ -0,0 +1,22 @@ +{ + "_id": "optional-peer-hoist-provider", + "name": "optional-peer-hoist-provider", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-provider", + "version": "1.0.0", + "dependencies": { + "optional-peer-hoist-target": "2.0.0" + }, + "_id": "optional-peer-hoist-provider@1.0.0", + "dist": { + "integrity": "sha512-h9jQ95AgpiWboNU8u/+OtU9GFMySNeOg3sRQUipTUWJzOom4NFJjYnpSYC7CGG1zVVTfuFaMNtJSxocFfGsG0g==", + "shasum": "f195ae5906efce71a33a94ee982f50ea263aad69", + "tarball": "http://localhost:4873/optional-peer-hoist-provider/-/optional-peer-hoist-provider-1.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-1.0.0.tgz new file mode 100644 index 000000000000..1a0927fd37be Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-2.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-2.0.0.tgz new file mode 100644 index 000000000000..b0a48ffc6ec3 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-tail/optional-peer-hoist-tail-2.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-tail/package.json b/test/cli/install/registry/packages/optional-peer-hoist-tail/package.json new file mode 100644 index 000000000000..530b57c6907b --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-tail/package.json @@ -0,0 +1,29 @@ +{ + "_id": "optional-peer-hoist-tail", + "name": "optional-peer-hoist-tail", + "dist-tags": { + "latest": "2.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-tail", + "version": "1.0.0", + "_id": "optional-peer-hoist-tail@1.0.0", + "dist": { + "integrity": "sha512-i21/HNGwoNAh9tByxH1LSqDKZWZ/4NGrCkr3agBtULedm7OSBSBSjMGZXl1IrKlkKPxfnjRZleppc4GJioFlMA==", + "shasum": "2690a02f19b910b38e36d350e1b896b4413948ea", + "tarball": "http://localhost:4873/optional-peer-hoist-tail/-/optional-peer-hoist-tail-1.0.0.tgz" + } + }, + "2.0.0": { + "name": "optional-peer-hoist-tail", + "version": "2.0.0", + "_id": "optional-peer-hoist-tail@2.0.0", + "dist": { + "integrity": "sha512-kpgMHe5pfRAlvKcEQxStwpcwccS84yEPlOOL11rPETiBCpiXb7raFnCCpVlfHY4vUBjIj7qdszdsOhFoHcMvfQ==", + "shasum": "8d81a1d0f565a303d3b972886e345eee836981ca", + "tarball": "http://localhost:4873/optional-peer-hoist-tail/-/optional-peer-hoist-tail-2.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-1.0.0.tgz new file mode 100644 index 000000000000..bdeb2887f34a Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-2.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-2.0.0.tgz new file mode 100644 index 000000000000..e0e9d1b3f362 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-2.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-3.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-3.0.0.tgz new file mode 100644 index 000000000000..aafe670eea31 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-target/optional-peer-hoist-target-3.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target/package.json b/test/cli/install/registry/packages/optional-peer-hoist-target/package.json new file mode 100644 index 000000000000..1f0170dbb5e8 --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-target/package.json @@ -0,0 +1,49 @@ +{ + "_id": "optional-peer-hoist-target", + "name": "optional-peer-hoist-target", + "dist-tags": { + "latest": "3.0.0" + }, + "versions": { + "1.0.0": { + "name": "optional-peer-hoist-target", + "version": "1.0.0", + "dependencies": { + "optional-peer-hoist-leaf": "2.0.0" + }, + "_id": "optional-peer-hoist-target@1.0.0", + "dist": { + "integrity": "sha512-c/9pX5Py1cPx8MFGcGLWJE4BQhhJcTWNF82CEJCvj4HZjNFAx0f6IfrbY5Q8oP1deuM3zhcXffkZubzS2aMsxw==", + "shasum": "3f6d09eed565676afab8e4d4fac966f88a16107a", + "tarball": "http://localhost:4873/optional-peer-hoist-target/-/optional-peer-hoist-target-1.0.0.tgz" + } + }, + "2.0.0": { + "name": "optional-peer-hoist-target", + "version": "2.0.0", + "_id": "optional-peer-hoist-target@2.0.0", + "dist": { + "integrity": "sha512-RPfRh293tMWQ287l/q2mNJOIPUHjnnnMuWPo8pJADoLSZM6GHiAdN5u2oRQIY3doznM5YgOUeSI/pTXFPeB7GQ==", + "shasum": "47baefe5a9da2810ed447abbbaec917051f7f943", + "tarball": "http://localhost:4873/optional-peer-hoist-target/-/optional-peer-hoist-target-2.0.0.tgz" + } + }, + "3.0.0": { + "name": "optional-peer-hoist-target", + "version": "3.0.0", + "dependencies": { + "optional-peer-hoist-leaf": "3.0.0", + "optional-peer-hoist-target2": "0.0.1" + }, + "bundleDependencies": [ + "optional-peer-hoist-target2" + ], + "_id": "optional-peer-hoist-target@3.0.0", + "dist": { + "integrity": "sha512-SwwctCjcixDaNMqKkBG6x+egE6myCOkZYzYjEuNIFVzE9nwwRdOVm79B1BnX0T42sp2rR8n0Q3Lg7urJZmIfTQ==", + "shasum": "4e3f603cb221247c96867b537abcff8557da3454", + "tarball": "http://localhost:4873/optional-peer-hoist-target/-/optional-peer-hoist-target-3.0.0.tgz" + } + } + } +} \ No newline at end of file diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-0.0.1.tgz b/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-0.0.1.tgz new file mode 100644 index 000000000000..922edfb53737 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-0.0.1.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-1.0.0.tgz b/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-1.0.0.tgz new file mode 100644 index 000000000000..783235da95f4 Binary files /dev/null and b/test/cli/install/registry/packages/optional-peer-hoist-target2/optional-peer-hoist-target2-1.0.0.tgz differ diff --git a/test/cli/install/registry/packages/optional-peer-hoist-target2/package.json b/test/cli/install/registry/packages/optional-peer-hoist-target2/package.json new file mode 100644 index 000000000000..ce98e15ba5ae --- /dev/null +++ b/test/cli/install/registry/packages/optional-peer-hoist-target2/package.json @@ -0,0 +1,32 @@ +{ + "_id": "optional-peer-hoist-target2", + "name": "optional-peer-hoist-target2", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "0.0.1": { + "name": "optional-peer-hoist-target2", + "version": "0.0.1", + "_id": "optional-peer-hoist-target2@0.0.1", + "dist": { + "integrity": "sha512-xY6X5B7Kf871dRTuI3LmN7kjI+MylkvmStfISbJUodW45kVfJBZqbvA0rAIy2ZRs6hfjwRkc5pDMZSxrRwp8rg==", + "shasum": "bd2acff2d6b98ce81b856a2c1eb8b5b220c2e3e9", + "tarball": "http://localhost:4873/optional-peer-hoist-target2/-/optional-peer-hoist-target2-0.0.1.tgz" + } + }, + "1.0.0": { + "name": "optional-peer-hoist-target2", + "version": "1.0.0", + "dependencies": { + "optional-peer-hoist-tail": "2.0.0" + }, + "_id": "optional-peer-hoist-target2@1.0.0", + "dist": { + "integrity": "sha512-k2m7NpKAOXV0lYfAicW/eCTRFYlAJcoSkGp0z/qL7GlaQEtRqtLitQJSsb5p5cXTYezuuLe61PsbGggVWgS65A==", + "shasum": "3512fb5ad917d139ce5af219d14673ddc4d4b9e9", + "tarball": "http://localhost:4873/optional-peer-hoist-target2/-/optional-peer-hoist-target2-1.0.0.tgz" + } + } + } +} \ No newline at end of file