fix(pm): abort on npm's legacy-bundling, not just install-strategy=nested - #680
fix(pm): abort on npm's legacy-bundling, not just install-strategy=nested#680colinhacks wants to merge 2 commits into
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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
.npmrclegacy-bundlingwhen set truthy, reported under the user-authored key. - Update scan documentation to note
install-strategy=nested’s aliasing vialegacy-bundling. - Add a regression test covering
legacy-bundling=true, barelegacy-bundling, andlegacy-bundling=false.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// 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. |
There was a problem hiding this comment.
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.npmrclegacy-bundlingnow aborts withERR_NUB_UNSUPPORTED_CONFIG, named under the key the user wrote rather than theinstall-strategy=nestedit expands to. Confirmed againstworkspaces/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::Npmbranch and reads throughnpmrc_project_bool_set, so it inherits the project-scoped-only discipline that keeps a personal~/.npmrcfrom aborting an unrelated project. With both keys set the earlierinstall-strategyarm wins the message; the install aborts either way, so the order is not load-bearing. - New test —
scan_fatal_on_legacy_bundling_the_nested_aliascovers=true, the bare key, and a=falsenegative. It can actually fail: theScanResult::Warnarm panics, so removing the fatal arm turns it red rather than leaving it green. - Doc-comment updates — the
CONFIG_TEXT_CACHEkey list and the FATAL-set rationale onscan_unsupported_configboth 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.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") { |
There was a problem hiding this comment.
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.

The npm config definition flattens
legacy-bundling=truetoinstall-strategy=nestedbefore anything reads it, so the two spellings request the same nested tree (workspaces/config/lib/definitions/definitions.js):The unsupported-config scan matched only the modern key, so an npm-incumbent project with
legacy-bundling=truein.npmrcpassed 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=trueabort whilelegacy-bundling=false(npm's default) still installs.Verification
Against an npm-incumbent fixture, running the install with
--lockfile-only --offline:.npmrclegacy-bundling=trueERR_NUB_UNSUPPORTED_CONFIGnaminglegacy-bundlinglegacy-bundling(bare key)legacy-bundling=falseinstall-strategy=nestedThe new test was confirmed to fail without the fix: with only the fatal arm commented out,
scan_fatal_on_legacy_bundling_the_nested_aliasfails through its own assertion (21 passed, 1 failed); with it restored, 22 pass.Gates run: clippy
--all-targets --all-featuresandcargo fmt --checkare clean, and bothunsupported_configandlayout_axis_scopingpass.