Skip to content

fix(pm): abort on npm's legacy-bundling, not just install-strategy=nested - #680

Open
colinhacks wants to merge 2 commits into
mainfrom
legacy-bundling-abort
Open

fix(pm): abort on npm's legacy-bundling, not just install-strategy=nested#680
colinhacks wants to merge 2 commits into
mainfrom
legacy-bundling-abort

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

The npm config definition flattens legacy-bundling=true to install-strategy=nested before anything reads it, so the two spellings request the same nested tree (workspaces/config/lib/definitions/definitions.js):

flatten (key, obj, flatOptions) {
  if (obj[key]) {
    obj['install-strategy'] = 'nested'
    flatOptions.installStrategy = 'nested'
  }
}

The unsupported-config scan matched only the modern key, so an npm-incumbent project with legacy-bundling=true in .npmrc passed the scan and installed a divergent tree with no diagnostic — the outcome the scan exists to prevent.

This adds a fatal arm for the alias, reported under the key the user wrote rather than the one it expands to. The truthy gate matches npm's own if (obj[key]), so the bare key and =true abort while legacy-bundling=false (npm's default) still installs.

Verification

Against an npm-incumbent fixture, running the install with --lockfile-only --offline:

.npmrc Exit Result
legacy-bundling=true 1 Aborts, ERR_NUB_UNSUPPORTED_CONFIG naming legacy-bundling
legacy-bundling (bare key) 1 Aborts, same error
legacy-bundling=false 0 Installs
install-strategy=nested 1 Aborts, unchanged
Error: nub: `legacy-bundling` (npm) is not supported — npm expands it to `install-strategy=nested`; nub installs a hoisted/isolated tree, and npm's nested layout can change which version a require() resolves to. remove `legacy-bundling` from .npmrc [ERR_NUB_UNSUPPORTED_CONFIG]

The new test was confirmed to fail without the fix: with only the fatal arm commented out, scan_fatal_on_legacy_bundling_the_nested_alias fails through its own assertion (21 passed, 1 failed); with it restored, 22 pass.

Gates run: clippy --all-targets --all-features and cargo fmt --check are clean, and both unsupported_config and layout_axis_scoping pass.

…sted

npm's config definition flattens `legacy-bundling=true` to
`install-strategy=nested` before anything reads it, so the two spellings
request the same nested tree. The unsupported-config scan matched only the
modern key, so `legacy-bundling` passed the scan and installed a divergent
tree with no diagnostic.

Add a fatal arm for the alias, reported under the key the user wrote. The
truthy gate matches npm's own `if (obj[key])`, so `legacy-bundling=false`
(npm's default) still installs.
Copilot AI lite review requested due to automatic review settings August 5, 2026 18:21
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview Aug 5, 2026 6:47pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens Nub’s npm-incumbent unsupported-config scan to also abort when npm’s deprecated legacy-bundling setting is used, since npm internally flattens it to install-strategy=nested and it can silently produce a divergent dependency tree.

Changes:

  • Add a new FATAL scan arm for project-scoped .npmrc legacy-bundling when set truthy, reported under the user-authored key.
  • Update scan documentation to note install-strategy=nested’s aliasing via legacy-bundling.
  • Add a regression test covering legacy-bundling=true, bare legacy-bundling, and legacy-bundling=false.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +884 to +886
/// requests the same tree and must hit the same abort. The negative case
/// pins that it is the truthy VALUE that aborts, not the key's presence —
/// `legacy-bundling=false` is npm's default and asks for nothing.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The behavior change is correct and well-grounded, but the one docs surface that describes this setting now states the opposite of what nub does.

Reviewed changes — the single-file addition of a legacy-bundling arm to the npm unsupported-config FATAL scan, verified against npm's own config source.

  • New FATAL arm for legacy-bundling — a truthy project-scoped .npmrc legacy-bundling now aborts with ERR_NUB_UNSUPPORTED_CONFIG, named under the key the user wrote rather than the install-strategy=nested it expands to. Confirmed against workspaces/config/lib/definitions/definitions.js: the definition is still actively flattened (if (obj[key]) { obj['install-strategy'] = 'nested' }), not deprecated-and-ignored, so the two spellings genuinely request the same tree.
  • Placement and role gating — the arm sits inside the existing Role::Npm branch and reads through npmrc_project_bool_set, so it inherits the project-scoped-only discipline that keeps a personal ~/.npmrc from aborting an unrelated project. With both keys set the earlier install-strategy arm wins the message; the install aborts either way, so the order is not load-bearing.
  • New testscan_fatal_on_legacy_bundling_the_nested_alias covers =true, the bare key, and a =false negative. It can actually fail: the ScanResult::Warn arm panics, so removing the fatal arm turns it red rather than leaving it green.
  • Doc-comment updates — the CONFIG_TEXT_CACHE key list and the FATAL-set rationale on scan_unsupported_config both pick up the new key.

⚠️ The layout-settings docs table still lists legacy-bundling as having no effect

site/content/docs/install/index.mdx:180 reads No effect — except install-strategy=nested, which aborts …, grouping legacy-bundling with the settings nub silently ignores. After this PR that row is wrong for legacy-bundling, so a user follows the docs, leaves the setting in place, and gets a hard abort the docs told them could not happen. It is the only live docs surface naming the key, so the fix is a one-row edit.

Technical details
# Docs row contradicts the new abort

## Affected sites
- `site/content/docs/install/index.mdx:180` — the npm row of the "Resolution, not layout" table claims every npm layout setting except `install-strategy=nested` has no effect. `legacy-bundling` now aborts too.

## Required outcome
- The npm row states that `legacy-bundling` aborts alongside `install-strategy=nested`, while keeping `install-strategy`'s other values and `global-style` on the no-effect side (those remain deliberately ignored — nub's isolated linker already puts only direct deps at the top level, which is what `shallow` asks for).

## Out of scope
- `site/content/blog/nub-0-1-1.mdx:16` also enumerates the fatal set, but it is a historical release note for a shipped version. Leave it alone.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

// would let the deprecated one through to a silently divergent
// install — the one outcome this scan exists to prevent. Reported
// under the key the user actually wrote, not the alias it expands to.
if npmrc_project_bool_set(root, "legacy-bundling") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

npmrc_project_bool_set accepts only a bare key or a case-insensitive true, but npm's .npmrc path never coerces booleans: parse-field.js returns anything other than the exact-case true/false/null as a raw string, and if (obj[key]) is then true for every non-empty value. So legacy-bundling=0, =1, =yes, and even =False all nest under npm while this scan stays silent — the same divergence the arm exists to close. Raising it because the PR body states the gate matches npm's if (obj[key]), and because the root cause is the shared helper rather than this line.

Technical details
# Truthy gate is narrower than npm's file-config truthiness

## Affected sites
- `crates/nub-cli/src/pm_engine/unsupported_config.rs:332` — the new `legacy-bundling` arm.
- `crates/nub-cli/src/pm_engine/unsupported_config.rs:484``npmrc_bool_set_in`, the shared helper that defines the gate (`v.is_empty() || v.eq_ignore_ascii_case("true")`).
- `crates/nub-cli/src/pm_engine/unsupported_config.rs:303` — the pre-existing `legacy-peer-deps` arm has the identical gap through the same helper.

## Evidence
- `workspaces/config/lib/parse-field.js` only special-cases the exact-case strings `true` / `false` / `null` / `undefined` (plus empty-string to `true` for a bare boolean key); every other value is returned as a raw trimmed string.
- `lib/npm.js`'s `#load()` contains no `config.validate()` call, and `flatOptions` reads `config.flat` directly — so nopt's `validateBoolean` (which would map `"0"` to `false`) never runs on the file-sourced value in a normal `npm install`.
- Net effect: for a Boolean-typed key read from `.npmrc`, npm's `if (obj[key])` is true for every non-empty value except lowercase `false` and `null`.

## Required outcome
- Either widen the shared gate so nub's notion of a truthy npm boolean matches npm's, or record the narrower gate as a deliberate limitation so the next reader does not assume parity.

## Open questions for the human
- Widening `npmrc_bool_set_in` also changes when `legacy-peer-deps` aborts, which is a real behavior change on a second field. Is that acceptable in this PR, or should it be split out?
- Matching npm here means matching a quirk: `legacy-bundling=0` would become fatal even though the author plainly meant "off". Aborting with a clear remedy is still better than silently installing a different tree, but it is a product call.

The comment said the abort keys on a truthy VALUE rather than the key's
presence, but a bare `legacy-bundling` carries no value and still aborts —
`npmrc_bool_set_in` counts an empty value as true, matching npm. Both truthy
spellings are in the test, so the comment described one of its own cases wrong.
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.

2 participants