Skip to content

feat(pkg-manager): add new hoisting limits config#6468

Closed
jakebailey wants to merge 3 commits into
pnpm:mainfrom
jakebailey:new-hoisting
Closed

feat(pkg-manager): add new hoisting limits config#6468
jakebailey wants to merge 3 commits into
pnpm:mainfrom
jakebailey:new-hoisting

Conversation

@jakebailey

@jakebailey jakebailey commented Apr 25, 2023

Copy link
Copy Markdown
Member

Fixes #6457

This adds a new option which behaves the same as yarn's nmHoistingLimits config when pnpm is using node-linker=hoisted.

I have reused the same config names, however, they may require tweaking to reflect pnpm's terminology. I'd love some feedback on that.

It also includes a bugfix needed to get things working; I can split that into another PR.

This PR needs tests, I'm pretty sure. Will work on that.

@jakebailey jakebailey changed the title feat(pkg-manager): add new hoist-limit-mode config feat(pkg-manager): add new hoisting-limit-mode config Apr 25, 2023
@jakebailey jakebailey marked this pull request as ready for review April 25, 2023 23:59
@jakebailey jakebailey requested a review from zkochan as a code owner April 25, 2023 23:59
@jakebailey

Copy link
Copy Markdown
Member Author

I have absolutely no clue what failed in CI...

@jakebailey jakebailey marked this pull request as draft April 26, 2023 02:14
@zkochan

zkochan commented Apr 26, 2023

Copy link
Copy Markdown
Member

It was an issue with verdaccio. I have downgraded it. Now you can see the failed test.

@jakebailey

jakebailey commented Apr 26, 2023

Copy link
Copy Markdown
Member Author

Seems like my bugfix introduces a breaking change to the lockfile. Before, a dependency like "minimist": "" would be encoded like:

dependencies:
  minimist:
    specifier: ''
    version: 1.2.8

But, now, it's encoded like:

dependencies:
  minimist:
    version: 1.2.8

The problem I was papering over was that some random deps got '' as their specifier, which was tripping up headlessInstall and making it throw OUTDATED_LOCKFILE as it believed that the manifest didn't satisfy the wanted lockfile.

I guess I can not do this bugfix, and try and come up with some other fix for that issue, which, of course also deserves a test, if I can figure out how to write one anyway 😅

@jakebailey

Copy link
Copy Markdown
Member Author

I think in my bugfix I was mirroring how addLinkToLockfile ignores '', but maybe the fix is just to still only do this for linked deps. But, I'll try and write a proper test instead.

manifest: Pick<ProjectManifest, DependenciesOrPeersField>,
depName: string
) {
): string | undefined {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically now breaking but not sure I can do better here. I'm failing to figure out what the deal is, effectively what's happening is that a package depends on ts-node, which peer deps on @types/node, which exists in the repo and should be linked, but that peer dep appears as a dep of the user of ts-node instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it? looks like CI is passing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking API change, I mean, in that this used to return string but now returns string | undefined.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, I searched github and found nobody but pnpm calling this function so I don't expect it to acutally matter...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think that this change is good, but, I also think I'm masking another bug regarding peer deps (but I am at least getting DT to work after this).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if I'm making the other major bumps, I may as well declare this one as major too?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no, I already got that in the PR.

Comment thread pkg-manager/core/src/link/index.ts Outdated
Comment thread pkg-manager/real-hoist/src/index.ts Outdated
*/
export type HoistingLimitMode = 'none' | 'workspaces' | 'dependencies'

export function getHoistingLimits (lockfile: Lockfile, mode: HoistingLimitMode | undefined): HoistingLimits | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no need to make this function public. Just call it from the hoist function instead of passing hoistingLimits to the hoist function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I can shove it down the stack; the main thing I was trying to reconcile is the fact that this API takes in hoisting limits and so I need to respect them. But, I guess I can just do this somewhere else.

@zkochan zkochan May 11, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to take in hoisting limits. It is not used anywhere.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following; are you saying that #4300 can in effect be reverted? That is the API I'm referring to, and it's exposed all the way up to headlessInstall.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I needed it for Bit but then I came up with a different solution. So, it is not needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to revert that API and make this logic private, if you're confident that it's safe to do so! I'm not totally sure what API guarantees you make, so I went with the conservative option.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is OK. It will be a major version bump.

@jakebailey

Copy link
Copy Markdown
Member Author

One thing I'm struggling with is producing a test for the bugfix; it's going to require adding a new package to the fake registry in pkg-manager/core, but I'm not sure how to do that.

In effect, I need to have these packages:

// packages/a/package.json
{
    "name": "a"
}
// packages/b/package.json
{
    "name": "b",
    "devDependencies": {
        "external": "*"
    }
}
// On npm, not declared locally
{
    "name": "external"
    "peerDependencies": {
        "a": "*"
    }
}

I can try and find real npm packages out there that could do this, but most have a lot of other dependencies that would make pulling in the full tree awkward.

@zkochan

zkochan commented May 11, 2023

Copy link
Copy Markdown
Member

The fake registry is here: https://github.com/pnpm/registry-mock

You can link it locally to the pnpm repo or I can publish a new version when you add the packages.

@zkochan

zkochan commented May 11, 2023

Copy link
Copy Markdown
Member

Isn't what you posted a workspace?

// packages/a/package.json
{
    "name": "a"
}
// packages/b/package.json
{
    "name": "b",
    "devDependencies": {
        "external": "*"
    }
}
// On npm, not declared locally
{
    "name": "external"
    "peerDependencies": {
        "a": "*"
    }
}

If it is a workspace these packages shouldn't be in the registry mock.

@jakebailey

Copy link
Copy Markdown
Member Author

Two of them are, but if external is declared locally, then it's a linked local dep and the bug doesn't appear.

I didn't realize the mock was pulling from anything except https://github.com/pnpm/pnpm/tree/main/pkg-manager/core/test/registry-mirror; I'll go check that mock repo to see if it has something I can use!

@jakebailey jakebailey changed the title feat(pkg-manager): add new hoisting-limit-mode config feat(pkg-manager): add new hoisting limits config May 11, 2023
Comment on lines +2 to +7
"@pnpm/resolve-dependencies": major
"@pnpm/manifest-utils": major
"@pnpm/real-hoist": major
"@pnpm/headless": major
"@pnpm/core": major
"@pnpm/config": major

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was not sure here as the API changed, so... just said they're all major. I'm not sure if that's accurate or not.

Comment thread pkg-manager/headless/src/index.ts Outdated
@jakebailey

Copy link
Copy Markdown
Member Author

Was able to get a test for the bugfix in the first commit. Will work on tests for the hoisting limits.

I can also split the bugfix apart if you want that sooner than I can write tests for the second half.

@zkochan

zkochan commented May 12, 2023

Copy link
Copy Markdown
Member

I am ok with whichever approach

@zkochan

zkochan commented May 11, 2025

Copy link
Copy Markdown
Member

is this still needed?

@jakebailey

Copy link
Copy Markdown
Member Author

I unfortunately haven't had time to work on this; doing this would enable DT to stop needing to disable the shared lock file mode and therefore take way less time to install. It's just been "fine" since we switched to pnpm such that other stuff has been more pressing...

@AsamK

AsamK commented May 13, 2025

Copy link
Copy Markdown

I'm also interested in yarn like hoisting limits and would have time to work on it.
What's still missing to get this finished? Just the tests for the new hoisting limits?

For the terminology, it might make sense to rename "workspaces" to "packages" to match the pnpm workspace terminology.
-> 'hoisting-limits': ['none', 'packages', 'dependencies'],

@jakebailey

Copy link
Copy Markdown
Member Author

I'm honetly not 100% certain what all is left at this point; the primary place to test this is on DefiniteltyTyped by enabling this option and seeing if the install is both faster and still passes tests (within reason).

If you have more time than me to work on it, by all means feel free to.

@zkochan

zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

This will be shipped via #12041

@zkochan zkochan closed this May 28, 2026
zkochan added a commit that referenced this pull request May 28, 2026
Expose `hoistingLimits` as a user-facing setting for `nodeLinker: hoisted`
installs, mirroring yarn's `nmHoistingLimits`. Accepts `none` (default —
hoist as far as possible), `workspaces` (hoist only as far as each
workspace package), or `dependencies` (hoist only up to each workspace
package's direct dependencies).

The setting is the user-friendly enum; `@pnpm/installing.linking.real-hoist`
translates it into the `@yarnpkg/nm` hoister's per-locator border map via
`getHoistingLimits`. Wires the key through config types, the config-file
key registry, and the install/add/recursive command option lists.

Revives the approach from #6468 (closes #6457).
zkochan added a commit that referenced this pull request May 28, 2026
Expose `hoistingLimits` as a user-facing setting for `nodeLinker: hoisted`
installs, mirroring yarn's `nmHoistingLimits`. Accepts `none` (default —
hoist as far as possible), `workspaces` (hoist only as far as each
workspace package), or `dependencies` (hoist only up to each workspace
package's direct dependencies).

The setting is the user-friendly enum; `@pnpm/installing.linking.real-hoist`
translates it into the `@yarnpkg/nm` hoister's per-locator border map via
`getHoistingLimits`. Wires the key through config types, the config-file
key registry, and the install/add/recursive command option lists.

Revives the approach from #6468 (closes #6457).
zkochan added a commit that referenced this pull request May 28, 2026
Expose `hoistingLimits` as a user-facing setting for `nodeLinker: hoisted`
installs, mirroring yarn's `nmHoistingLimits`. Accepts `none` (default —
hoist as far as possible), `workspaces` (hoist only as far as each
workspace package), or `dependencies` (hoist only up to each workspace
package's direct dependencies).

The setting is the user-friendly enum; `@pnpm/installing.linking.real-hoist`
translates it into the `@yarnpkg/nm` hoister's per-locator border map via
`getHoistingLimits`. Wires the key through config types, the config-file
key registry, and the install/add/recursive command option lists.

Revives the approach from #6468 (closes #6457).
zkochan added a commit that referenced this pull request May 28, 2026
…its setting (#12041)

## 1. Support `nodeLinker: hoisted` on the fresh-lockfile install path (pacquet)

Closes #11871. Until now pacquet's `Install::run` hard-refused `nodeLinker: hoisted` without a checked-in lockfile (`ERR_PNPM_…UNSUPPORTED_FRESH_INSTALL_NODE_LINKER`).

- Extracted a shared `run_hoisted_linker` helper from the frozen path's hoisted branch (walker → `link_hoisted_modules` → `SymlinkDirectDependencies { link_only: true }` → `pkg_root_by_key` → walker-skip folding), so both install paths run identical logic.
- Fresh path now threads `node_linker` + `supported_architectures`, hands `CreateVirtualStore` the real linker (populating `cas_paths_by_pkg_id`), branches on `is_hoisted`, and returns `hoisted_locations` so `.modules.yaml` round-trips.
- Removed the guard and the dead `UnsupportedFreshInstallNodeLinker` error variant.

Ported upstream's `hoistedNodeLinker/install.ts` into `crates/cli/tests/hoisted_node_linker.rs` (real tests for the core layout, no-lockfile, `externalDependencies`, `autoInstallPeers`, and `hoistingLimits`; the rest stubbed as `known_failures` against `pnpm add`/update (#433) and build-phase (#11870) gaps), and ticked the boxes in `plans/TEST_PORTING.md`.

## 2. Add the `hoistingLimits` setting (pnpm CLI **and** pacquet)

Revives the stale #6468 (closes #6457) and brings both stacks to parity. `hoistingLimits` mirrors yarn's `nmHoistingLimits`: `none` (default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), `dependencies` (hoist only up to each workspace package's direct deps). It was previously a programmatic-only option in pnpm (no config surface) and a pacquet-only raw-map yaml field.

**pnpm CLI:** `config/reader` (`types.ts` enum + `Config.ts` + `configFileKey.ts`), `installing/linking/real-hoist`'s new `getHoistingLimits` (mode → the `@yarnpkg/nm` hoister's per-locator border map), and the install/add/recursive command option lists. Tests: `hoistedNodeLinker/install.ts` (`dependencies` mode) + `real-hoist` `getHoistingLimits` unit tests. Changeset included (minor).

**pacquet:** replaced the raw-map config with the same enum; added `get_hoisting_limits` (port of `getHoistingLimits`); and **fixed `real-hoist`'s border semantics** — a name in the limits marks a *border* whose descendants stay nested beneath it, not a leaf to block. (The earlier leaf-blocking behavior was the divergence flagged while porting; its unit tests were rewritten to the corrected semantics.)
@zkochan

zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

Revived and merged in #12041.

Your getHoistingLimits design — the none / workspaces / dependencies enum translating into the @yarnpkg/nm hoister's per-locator border map — landed essentially as proposed here, refreshed for the current repo layout (config/reader, installing/linking/real-hoist, installing/commands), with tests and a matching port in the pacquet Rust implementation. Thanks @jakebailey for the original work — closing this as superseded.


Written by an agent (Claude Code, claude-opus-4-8).

zkochan added a commit that referenced this pull request May 29, 2026
…its setting (#12041)

## 1. Support `nodeLinker: hoisted` on the fresh-lockfile install path (pacquet)

Closes #11871. Until now pacquet's `Install::run` hard-refused `nodeLinker: hoisted` without a checked-in lockfile (`ERR_PNPM_…UNSUPPORTED_FRESH_INSTALL_NODE_LINKER`).

- Extracted a shared `run_hoisted_linker` helper from the frozen path's hoisted branch (walker → `link_hoisted_modules` → `SymlinkDirectDependencies { link_only: true }` → `pkg_root_by_key` → walker-skip folding), so both install paths run identical logic.
- Fresh path now threads `node_linker` + `supported_architectures`, hands `CreateVirtualStore` the real linker (populating `cas_paths_by_pkg_id`), branches on `is_hoisted`, and returns `hoisted_locations` so `.modules.yaml` round-trips.
- Removed the guard and the dead `UnsupportedFreshInstallNodeLinker` error variant.

Ported upstream's `hoistedNodeLinker/install.ts` into `crates/cli/tests/hoisted_node_linker.rs` (real tests for the core layout, no-lockfile, `externalDependencies`, `autoInstallPeers`, and `hoistingLimits`; the rest stubbed as `known_failures` against `pnpm add`/update (#433) and build-phase (#11870) gaps), and ticked the boxes in `plans/TEST_PORTING.md`.

## 2. Add the `hoistingLimits` setting (pnpm CLI **and** pacquet)

Revives the stale #6468 (closes #6457) and brings both stacks to parity. `hoistingLimits` mirrors yarn's `nmHoistingLimits`: `none` (default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), `dependencies` (hoist only up to each workspace package's direct deps). It was previously a programmatic-only option in pnpm (no config surface) and a pacquet-only raw-map yaml field.

**pnpm CLI:** `config/reader` (`types.ts` enum + `Config.ts` + `configFileKey.ts`), `installing/linking/real-hoist`'s new `getHoistingLimits` (mode → the `@yarnpkg/nm` hoister's per-locator border map), and the install/add/recursive command option lists. Tests: `hoistedNodeLinker/install.ts` (`dependencies` mode) + `real-hoist` `getHoistingLimits` unit tests. Changeset included (minor).

**pacquet:** replaced the raw-map config with the same enum; added `get_hoisting_limits` (port of `getHoistingLimits`); and **fixed `real-hoist`'s border semantics** — a name in the limits marks a *border* whose descendants stay nested beneath it, not a leaf to block. (The earlier leaf-blocking behavior was the divergence flagged while porting; its unit tests were rewritten to the corrected semantics.)
@zkochan

zkochan commented May 29, 2026

Copy link
Copy Markdown
Member

🚢 11.5

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shared lockfile results in nondeterministic top-level packages, differing behavior from npm install

3 participants