feat(add): mirror npm save-prefix for bare-version add under npm incumbent#97
Conversation
…mbent When the project's incumbent package manager is npm, `nub add <pkg>@<exact>` (or a bare `nub add <pkg>` that resolves to a concrete version) now writes the saved specifier with npm's default save-prefix applied (`"^1.2.3"`), matching real `npm install`. pnpm/bun/nub-identity projects keep literal-preserve (`"1.2.3"`), matching real pnpm/bun. An explicit range (`^1`, `~1.2`, `>=1`) is preserved verbatim on every PM. The behavior is keyed off the project's incumbent via a new per-invocation EngineContext posture (npm_save_prefix_on_bare_exact, default off = upstream-neutral), set true only under an npm compat surface. The resolved save-prefix is honored, so `.npmrc save-prefix=~` yields `"~1.2.3"`. Also wires a saveExact setting into the aube settings table so an existing `.npmrc save-exact=true` (or env) pins the exact version on add, matching both npm and pnpm, which read this knob. Previously only the CLI `--save-exact` flag took effect. Un-gates the m1-add-noconflict and m3-add-dedup npm mutation-harness legs, which now produce package.json specifiers byte-equivalent to real npm. Refs: npm-add-save-prefix-convention thread.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — under an npm incumbent, nub add pkg@1.2.3 (and bare adds resolving to a concrete version) now write the saved specifier with npm's ^ save-prefix, matching real npm install; pnpm/bun/nub-identity keep literal-preserve.
- New per-invocation posture
npm_save_prefix_on_bare_exact— added toEngineContext(defaultfalse, so standalone aube is byte-for-byte unaffected); default-construction test updated. - Host gates the posture to npm only —
pm_engine/mod.rssets ittrueexclusively forConfigSurface::NonPnpmCompat { role: "npm", .. }, consistent with the sibling incumbent-keyed read postures. - Add manifest writer pins a bare-exact version — guard
npm_save_prefix_on_bare_exact && spec.has_explicit_range && Version::parse(&spec.range).is_ok()folds intopin_to_resolved; explicit ranges never parse as a strictVersionand fall through to verbatim preservation. save-exactconfig knob now honored onadd—.npmrc/envsave-exact=truepins the exact version (previously only the--save-exactflag did); new[saveExact]bool setting insettings.toml.- Mutation harness un-gated —
m1-add-noconflict npmandm3-add-dedup npmremoved fromexpected-failures.txt(harness 9/9; XPASS-STALE self-check guards against regression).
ℹ️ A few range-shaped npm inputs still preserve verbatim rather than rewriting to ^resolved
The guard keys on node_semver::Version::parse succeeding, which (verified empirically) accepts 1.2.3, v1.2.3, and pre-release/build variants, but rejects =2.1.3, 1.x, and partials like 1.2. Real npm 11.13.0 applies its save-prefix to some of those rejected forms too — npm install ms@=2.1.3 writes ^2.1.3 and ms@1.x writes ^1.0.0, whereas nub preserves =2.1.3 / 1.x verbatim.
This is consistent with the PR's stated contract (scope is bare-exact; explicit ranges are preserved verbatim on every PM), and the inputs are narrow edge cases. Flagging only for awareness — preserving a user-typed range is arguably the friendlier behavior, so no change is recommended.
Technical details
# Range-shaped npm inputs preserve verbatim vs npm's `^resolved` rewrite
## Affected sites
- `vendor/aube/crates/aube/src/commands/add/manifest.rs:438-440` — `bare_exact_npm_save_prefix` gates on `node_semver::Version::parse(&spec.range).is_ok()`.
## Observed behavior (empirical, this review)
`node_semver` v2 `Version::parse`:
- accepts: `1.2.3`, `v1.2.3` (→ `1.2.3`), `1.2.3-beta.1`, `1.2.3+build`
- rejects: `1.2`, `1`, `=2.1.3`, `1.x`, ` 1.2.3 `
Real npm 11.13.0 `npm install <spec> --save`:
- `ms@2.1.3` / `ms@v2.1.3` / `ms@=2.1.3` → `"^2.1.3"`
- `ms@1.x` → `"^1.0.0"`
So `nub add pkg@=2.1.3` → `"=2.1.3"` (npm: `"^2.1.3"`), and `nub add pkg@1.x` → `"1.x"` (npm: `"^1.0.0"`).
## Required outcome
None — informational. The divergence is documented (ranges preserved verbatim) and the inputs are rare. If exact npm parity on `=X.Y.Z` is later wanted, that single form is the only true bare-exact synonym npm normalizes; `1.x`/range rewrites are a separate, debatable product call.Claude Opus | 𝕏
|
Shipped in v0.1.14: https://github.com/nubjs/nub/releases/tag/v0.1.14 |

What
When the project's incumbent package manager is npm,
nub add <pkg>@<exact>(or a barenub add <pkg>that resolves to a concrete version) now writes the saved specifier with npm's default save-prefix applied ("^1.2.3"), matching realnpm install. pnpm/bun/nub-identity projects keep literal-preserve ("1.2.3"), matching real pnpm/bun. An explicit range (^1,~1.2,>=1) is preserved verbatim on every PM.This is the npm-incumbent behavior nub users expect (Option A, decided by the maintainer).
How
vendor/aubeEngineContext: new per-invocation posturenpm_save_prefix_on_bare_exact(defaultfalse= upstream-neutral; standalone aube byte-for-byte unaffected). Settrueonly under an npm compat surface.vendor/aubeadd manifest writer: a bare exact version (detected viaVersion::parseon the user's explicit range) is written with the resolved save-prefix when the posture is on. Explicit ranges always fall through to verbatim preservation.vendor/aubesettings: newsaveExactknob so an existing.npmrc save-exact=true(or env) pins the exact version on add — npm and pnpm both read this knob; previously only the CLI--save-exactflag took effect.pm_engine: sets the posturetrueonly forConfigSurface::NonPnpmCompat { role: "npm" }.The resolved save-prefix is honored, so
.npmrc save-prefix=~yields"~1.2.3";save-exact=trueyields the exact pin"1.2.3".Mutation harness
Un-gates
m1-add-noconflict npm+m3-add-dedup npm(removed fromtests/mutation/expected-failures.txt). The full differential harness is now 9/9 PASS (npm legs produce package.json specifiers byte-equivalent to real npm; pnpm/bun literal-preserve legs unchanged).Verification
cargo clippy --all-targets --all-features -- -D warningsandcargo fmt --check: clean.aube-settings61 passed,aubeadd 71 passed.ms@2.1.3→^2.1.3,~2.1.0→~2.1.0(preserved), barems→^2.1.3,save-exact=true→2.1.3,save-prefix=~→~2.1.3,--save-exact→2.1.3; pnpm/bun/nub-identity all keep2.1.3.Refs: npm-add-save-prefix-convention thread.