Skip to content

Fix npm alias packages (e.g. "npm:eslint@^9.x") failing every operation#5039

Open
Scott (7ranceaddic7) wants to merge 2 commits into
Devolutions:mainfrom
7ranceaddic7:fix/npm-alias-package-operations
Open

Fix npm alias packages (e.g. "npm:eslint@^9.x") failing every operation#5039
Scott (7ranceaddic7) wants to merge 2 commits into
Devolutions:mainfrom
7ranceaddic7:fix/npm-alias-package-operations

Conversation

@7ranceaddic7

@7ranceaddic7 Scott (7ranceaddic7) commented Jul 3, 2026

Copy link
Copy Markdown
  • I have read the contributing guidelines, and I agree with the Code of Conduct.
  • Have you checked that there aren't other open pull requests for the same changes?
  • Have you tested that the committed code can be executed without errors?
  • Have you confirmed that this issue is caused by UniGetUI itself, and not by the package manager or the package involved?

npm supports aliasing a dependency to install a different underlying package under a local name, e.g.:

"eslint-v9": "npm:eslint@^9.39.4"

npm outdated --json / npm list --json report these with a package id shaped like eslint-v9:eslint@^9.39.4 -- the local alias name, a literal colon, then the raw alias target specifier with the npm: prefix stripped. NpmPkgOperationHelper was using that id verbatim as the npm package specifier, producing commands like:

npm install 'eslint-v9:eslint@^9.39.4@10.6.0'

npm rejects this outright:

npm error code EINVALIDPACKAGENAME
npm error Invalid package name "eslint-v9:eslint" of package "eslint-v9:eslint@9.39.4@10.6.0": name can only contain URL-friendly characters.

So any npm-aliased package could never be installed, updated, or uninstalled through UniGetUI -- every operation fails with this error.

Fix: real npm package names can never contain a colon, so its presence in package.Id unambiguously identifies an alias (this is confirmed directly against real npm outdated --json output, not inferred). Added alias-aware resolution in NpmPkgOperationHelper: reconstructs the proper localName@npm:targetName@version syntax for install/update, and uses just the local name for uninstall (npm doesn't need or accept the target spec to remove a package). Handles scoped alias targets too (npm:@babel/core@^7.x), since the target name can contain its own @.

Real-world verification: ran this against a real project with this exact scenario. The generated update command changed from the invalid eslint-v9:eslint@^9.39.4@10.6.0 to the valid eslint-v9@npm:eslint@10.6.0, which npm accepted and executed successfully (exit code 0). Done via a temporary, uncommitted test calling the production GetParameters API directly to get the real command, then running that exact command for real -- not part of this diff.

One caveat worth flagging for reviewers, discovered during that real-world run: this fix makes the operation succeed, but "succeeds" means npm does exactly what an update always does -- rewrites the alias's version specifier in package.json to the new resolved version. If someone is using an alias specifically to pin a dependency below its target's latest major (as in the motivating case: keeping a second eslint-v9 on the 9.x line to satisfy a plugin's stale peer range while a top-level eslint stays on 10.x), clicking "Update" on that alias will happily bump it past the pin and defeat its purpose -- because from npm's perspective it genuinely is just an update. That's expected/correct npm behavior, not a bug this PR introduces, and not something this fix should try to guess around. Users in that situation should use UniGetUI's existing "ignore future updates" option on that specific package.

Verified with dotnet build on UniGetUI.PackageEngine.Managers.Npm.csproj (0 errors) and dotnet test on UniGetUI.PackageEngine.Tests (256/256 passing, 4 new regression tests added, 0 regressions) against SDK 10.0.301.

N/A. No existing issue tracks this; not creating one per the PR template guidance.

npm outdated --json / npm list --json report npm-aliased dependencies
(package.json entries like "eslint-v9": "npm:eslint@^9.39.4") with a
package id shaped like "eslint-v9:eslint@^9.39.4" -- the local alias
name, a literal colon, then the raw alias target specifier with the
"npm:" prefix stripped. NpmPkgOperationHelper was using that id
verbatim as the npm package specifier, producing commands like
"npm install 'eslint-v9:eslint@^9.39.4@10.6.0'", which npm rejects
outright with EINVALIDPACKAGENAME (colons aren't valid in npm package
names) -- so any aliased package could never be installed, updated,
or uninstalled through UniGetUI.

Real npm package names can never contain a colon, so its presence in
package.Id unambiguously identifies an alias. Added alias-aware
resolution in NpmPkgOperationHelper: reconstructs the proper
"localName@npm:targetName@version" syntax for install/update, and
uses just the local name for uninstall (npm doesn't need or accept
the target spec to remove a package).

Verified against a real project with this exact scenario: the
generated update command changed from the invalid
"eslint-v9:eslint@^9.39.4@10.6.0" to the valid
"eslint-v9@npm:eslint@10.6.0", which npm accepted and executed
successfully (confirmed via a temporary, uncommitted test calling the
production GetParameters API directly, then running the exact
returned command for real -- not committed to this diff).

Added 4 regression tests to NpmManagerTests.cs covering: alias
resolution on update, a scoped alias target (target names can contain
their own '@', e.g. "npm:@babel/core@^7.x"), local-name-only
uninstall, and that ordinary non-aliased package ids are left
untouched.
@7ranceaddic7

Copy link
Copy Markdown
Author

Raw npm debug log from the original crash, for reference (npm's own log, not reconstructed):

7 verbose title npm install eslint-v9:eslint@9.39.4@10.6.0
8 verbose argv "install" "eslint-v9:eslint@9.39.4@10.6.0"
...
14 verbose stack Error: Invalid package name "eslint-v9:eslint" of package "eslint-v9:eslint@9.39.4@10.6.0": name can only contain URL-friendly characters.
14 verbose stack     at invalidPackageName (npm-package-arg/lib/npa.js:134:15)
14 verbose stack     at Result.setName (npm-package-arg/lib/npa.js:182:13)
14 verbose stack     at new Result (npm-package-arg/lib/npa.js:170:12)
14 verbose stack     at resolve (npm-package-arg/lib/npa.js:80:15)
14 verbose stack     at npa (npm-package-arg/lib/npa.js:56:10)
14 verbose stack     at @npmcli/arborist/lib/arborist/build-ideal-tree.js:517:18
15 error code EINVALIDPACKAGENAME
16 error Invalid package name "eslint-v9:eslint" of package "eslint-v9:eslint@9.39.4@10.6.0": name can only contain URL-friendly characters.

npm 11.13.0 / Node v24.16.0 / Windows 10.0.28120. Confirms the rejection happens at npm-package-arg's name-validation stage before any resolution logic runs -- exactly consistent with this PR's fix, which stops UniGetUI from ever constructing that invalid eslint-v9:eslint@9.39.4@10.6.0 specifier in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant