fix(install): pin pre-release npm versions added via dist-tag (#35586)
Installing an npm dist-tag that resolves to a pre-release version
installed a different (and often much older) build than the one reported and
written to the manifest.
Reproduction from #35577: `deno install -D npm:prettier-plugin-tailwindcss@insiders`
resolves the `insiders` tag to `0.0.0-insiders.fc3a063` (the latest
commit), prints and writes that version to `package.json`, but then actually
installs `0.0.0-insiders.fe49d70` (an old 2023 commit) and records it in
`deno.lock`.
The cause is in `cli/tools/pm/mod.rs`. When a package is added via a
tag, the resolved version was written with a caret range, e.g.
`^0.0.0-insiders.fc3a063`. A caret range over a pre-release matches
every pre-release sharing the same `major.minor.patch` and resolves to the
lexicographically greatest one. The pre-release identifiers here are git
hashes, which sort lexically rather than chronologically (`fe49d70` >
`fc3a063` because `c` < `e`), so resolution lands on an older commit. That
re-resolved version is what ends up in the lockfile and node_modules, disagreeing
with what was printed and written to the manifest within the same command.
The fix pins pre-release versions exactly instead of applying a range
operator. This matches pnpm, which saves a pre-release version exactly rather than
with a range, and it avoids floating across pre-releases whose ordering
(especially hash-based) is unreliable. Note this differs from npm: npm still writes
a caret range to `package.json` for a tag install and relies on the lockfile to
pin the exact build, so its `package.json` entry can still float across
pre-releases on a fresh resolve.
A spec test reproduces the scenario with a new `@denotest/insiders-tag`
registry fixture: the `insiders` dist-tag points at the
lexically-smaller hash while a lexically-greater untagged pre-release also exists. The test
asserts the install pins the tagged version in both `package.json` and
`deno.lock`.
Closes #35577