Skip to content

feat(add): mirror npm save-prefix for bare-version add under npm incumbent#97

Merged
colinhacks merged 1 commit into
mainfrom
npm-add-save-prefix
Jun 23, 2026
Merged

feat(add): mirror npm save-prefix for bare-version add under npm incumbent#97
colinhacks merged 1 commit into
mainfrom
npm-add-save-prefix

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

What

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.

This is the npm-incumbent behavior nub users expect (Option A, decided by the maintainer).

How

  • vendor/aube EngineContext: new per-invocation posture npm_save_prefix_on_bare_exact (default false = upstream-neutral; standalone aube byte-for-byte unaffected). Set true only under an npm compat surface.
  • vendor/aube add manifest writer: a bare exact version (detected via Version::parse on 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/aube settings: new saveExact knob 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-exact flag took effect.
  • nub pm_engine: sets the posture true only for ConfigSurface::NonPnpmCompat { role: "npm" }.

The resolved save-prefix is honored, so .npmrc save-prefix=~ yields "~1.2.3"; save-exact=true yields the exact pin "1.2.3".

Mutation harness

Un-gates m1-add-noconflict npm + m3-add-dedup npm (removed from tests/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 warnings and cargo fmt --check: clean.
  • aube unit tests: aube-settings 61 passed, aube add 71 passed.
  • Mutation harness (real npm 11.13.0 / pnpm 10.15.1 / bun 1.3.14): 9/9 PASS.
  • e2e parity vs real npm: ms@2.1.3^2.1.3, ~2.1.0~2.1.0 (preserved), bare ms^2.1.3, save-exact=true2.1.3, save-prefix=~~2.1.3, --save-exact2.1.3; pnpm/bun/nub-identity all keep 2.1.3.

Refs: npm-add-save-prefix-convention thread.

…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.
Copilot AI review requested due to automatic review settings June 23, 2026 22:15

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Jun 23, 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, Comment Jun 23, 2026 10:17pm

Request Review

@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.

✅ 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 to EngineContext (default false, so standalone aube is byte-for-byte unaffected); default-construction test updated.
  • Host gates the posture to npm onlypm_engine/mod.rs sets it true exclusively for ConfigSurface::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 into pin_to_resolved; explicit ranges never parse as a strict Version and fall through to verbatim preservation.
  • save-exact config knob now honored on add.npmrc/env save-exact=true pins the exact version (previously only the --save-exact flag did); new [saveExact] bool setting in settings.toml.
  • Mutation harness un-gatedm1-add-noconflict npm and m3-add-dedup npm removed from expected-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.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@colinhacks colinhacks merged commit b0021c6 into main Jun 23, 2026
32 checks passed
@colinhacks

Copy link
Copy Markdown
Contributor Author

Shipped in v0.1.14: https://github.com/nubjs/nub/releases/tag/v0.1.14

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