Skip to content

feat: add support for github prefix and named registries#11324

Merged
zkochan merged 17 commits into
pnpm:mainfrom
kibertoad:feat/github-alias
Apr 29, 2026
Merged

feat: add support for github prefix and named registries#11324
zkochan merged 17 commits into
pnpm:mainfrom
kibertoad:feat/github-alias

Conversation

@kibertoad

@kibertoad kibertoad commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

This is consistent with #9358, but implements support for the GitHub Packages npm registry and, more broadly, for vlt-style https://docs.vlt.sh/cli/registries for any registry.

This PR adds a built-in gh: specifier that resolves against the GitHub Packages npm registry, plus a namedRegistries config key so a project can map its own aliases to arbitrary registries. A project can mix public npm packages and private GitHub Packages (or self-hosted) ones without applying a scope-wide registry override to every @scope/* package.

  • pnpm add gh:@acme/private writes "@acme/private": "gh:^1.0.0" and resolves from https://npm.pkg.github.com/.
  • pnpm add gh:@acme/private@^1.0.0 (with or without an alias) is also supported. Aliased form writes "my-alias": "gh:@acme/private@^1.0.0".
  • Auth comes from the existing per-URL .npmrc mechanism, e.g. //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}. No new auth surface.
  • @github is intentionally not defaulted to https://npm.pkg.github.com/ - hardcoding that would hijack installs of the public @github/* packages on npmjs.org (e.g. @github/relative-time-element) for users without a scope-wide override. Use gh: to install from GitHub Packages, or configure @github:registry=... yourself if that's really what you want.
  • Additional named registries (a self-hosted proxy, GitHub Enterprise Server, etc.) can be configured in pnpm-workspace.yaml:
namedRegistries:
  gh: https://npm.pkg.github.example.com/   # optional: overrides the built-in `gh` alias for GHES
  work: https://npm.work.example.com/
  • Then work:@corp/lib@^2.0.0 resolves against https://npm.work.example.com/, and the built-in gh alias can be redirected to a GHES host.
  • Env-var substitution (${VAR}) is supported in namedRegistries values, mirroring the .npmrc convention.
  • Reserved alias names (npm, jsr, github, workspace, catalog, file, git, http, https, link, patch, and related git host shorthands) cannot be redefined as user-named registries - the resolver throws ERR_PNPM_RESERVED_NAMED_REGISTRY_ALIAS at startup rather than silently shadowing another protocol. Malformed URLs throw ERR_PNPM_INVALID_NAMED_REGISTRY_URL at startup too, instead of failing as a confusing 404 during resolution.
  • On publish, createExportableManifest strips any named-registry prefix (both the built-in gh: and any user-configured alias) so npm and yarn consumers can still resolve the dependency via their own scope-registry configuration - mirroring the user-facing requirement when installing such a dep without the prefix.

The prefix is gh: rather than github: because github: is reserved by npm-package-arg / hosted-git-info as a git host shorthand (e.g. github:owner/repo) - reusing it would be a deviation from the specs used by the npm CLI. gh: is shorter, matches vlt's convention, and cannot collide with any existing npm scheme.

Unlike jsr:, gh: (and any other named-registry alias) does not rewrite the package name - gh:@acme/foo resolves @acme/foo from the GitHub Packages registry as-is. This also means npm/yarn consumers see the original name after the prefix is stripped on publish.

@kibertoad kibertoad requested a review from zkochan as a code owner April 21, 2026 09:29
@welcome

welcome Bot commented Apr 21, 2026

Copy link
Copy Markdown

💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.

@zkochan zkochan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the github: prefix is already preserved for github-hosted repositories. So, this would a be a breaking change and a deviation from the specs used by npm CLI.

@kibertoad kibertoad requested a review from zkochan April 21, 2026 12:55
@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan I've changed to ghpkg, please rereview

@kibertoad kibertoad marked this pull request as draft April 21, 2026 12:59
@zkochan

zkochan commented Apr 21, 2026

Copy link
Copy Markdown
Member

Use gh: instead. We can also support named registries, like what vlt does: https://docs.vlt.sh/cli/registries

@kibertoad

Copy link
Copy Markdown
Contributor Author

there is another problem - name collision for github namespace packages. fixing it now

@kibertoad kibertoad changed the title feat: add support for github prefix feat: add support for github prefix and named registries Apr 21, 2026
@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan named registries somewhat expanded the scope of PR, but here it is now. Please rereview :)

@kibertoad kibertoad marked this pull request as ready for review April 21, 2026 14:35
@zkochan zkochan requested a review from Copilot April 21, 2026 15:08

Copilot AI 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.

Pull request overview

Adds support for installing dependencies from non-default npm registries using vlt-style <alias>: specifiers, including a built-in gh: alias for GitHub Packages, plus publish-time rewriting so these pnpm-specific prefixes don’t leak into published manifests.

Changes:

  • Introduces namedRegistries config and a new gh:/named-registry specifier parser, and wires named-registry resolution into the default resolver pipeline.
  • Adds named-registry-aware publish manifest rewriting (strip named-registry prefixes, emit npm: aliases when needed) and updates pack/publish plumbing to pass config through.
  • Extends deps-resolver fast paths and metadata classification to recognize named-registry resolutions; adds test coverage and fixtures.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
store/connection-manager/src/createNewStoreController.ts Passes namedRegistries through to the client/resolver.
resolving/npm-resolver/tsconfig.json Adds project reference to new gh/named-registry parser package.
resolving/npm-resolver/src/index.ts Merges/validates named registries, adds resolveFromNamedRegistry() and result type.
resolving/npm-resolver/src/parseBareSpecifier.ts Adds helper to parse named-registry specifiers into registry package specs.
resolving/npm-resolver/test/resolveNamedRegistry.test.ts New resolver tests covering gh alias, user aliases, auth lookup, and validation errors.
resolving/npm-resolver/test/fixtures/gh-acme-private.json Fixture metadata for GitHub Packages-style scoped package.
resolving/npm-resolver/package.json Adds dependency on @pnpm/resolving.gh-specifier-parser.
resolving/gh-specifier-parser/src/index.ts New parser for gh: and generic named-registry specifiers + reserved-alias logic.
resolving/gh-specifier-parser/test/parse.test.ts Unit tests for gh/named-registry parsing behavior and error cases.
resolving/gh-specifier-parser/{README.md,package.json,tsconfig*.json} New package scaffolding/docs/build config.
resolving/default-resolver/src/index.ts Plumbs resolveFromNamedRegistry into the resolution chain.
core/types/src/package.ts Adds namedRegistries?: Record<string, string> to PnpmSettings.
config/reader/src/Config.ts Exposes namedRegistries on the config type.
config/reader/src/getOptionsFromRootManifest.ts Adds env-var replacement support for namedRegistries values.
releasing/exportable-manifest/src/index.ts Strips named-registry prefixes on publish, similar to existing jsr: handling.
releasing/exportable-manifest/test/index.test.ts Tests for stripping gh: and user-defined named-registry prefixes.
releasing/exportable-manifest/{package.json,tsconfig.json} Adds dependency/reference on gh-specifier parser package.
releasing/commands/src/publish/{publish.ts,recursivePublish.ts,pack.ts} Threads namedRegistries through publish/pack codepaths.
installing/deps-resolver/src/replaceVersionInBareSpecifier.ts Supports gh: in the “paste locked version” fast path.
installing/deps-resolver/test/replaceVersionInPref.test.ts Tests for gh: replacement and non-registry prefixes.
installing/deps-resolver/src/resolveDependencies.ts Treats named-registry as non-exotic for blockExoticSubdeps.
cspell.json Adds “ghes” to dictionary.
.changeset/gh-packages-prefix.md Changeset describing new feature and config.
pnpm-lock.yaml Lockfile updates including new workspace package + various catalog version shifts.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread releasing/exportable-manifest/src/index.ts Outdated
Comment thread resolving/gh-specifier-parser/package.json Outdated
Comment thread pnpm-lock.yaml
Comment thread installing/deps-resolver/src/replaceVersionInBareSpecifier.ts Outdated
Comment thread .changeset/gh-packages-prefix.md Outdated
Comment thread resolving/named-registry-specifier-parser/src/index.ts Outdated
@kibertoad kibertoad marked this pull request as draft April 21, 2026 15:36
@kibertoad kibertoad marked this pull request as draft April 21, 2026 15:36
@kibertoad kibertoad marked this pull request as ready for review April 21, 2026 15:49
@kibertoad kibertoad requested a review from zkochan April 21, 2026 15:50
@zkochan

zkochan commented Apr 21, 2026

Copy link
Copy Markdown
Member

compile failed

@zkochan zkochan added this to the v11.1 milestone Apr 21, 2026
@kibertoad

kibertoad commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan Can you run it now? I hope the latest push should help, it builds locally

edit: wait, it is still broken, fixing

@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan ok this time should be good, can you please rerun?

@zkochan

zkochan commented Apr 21, 2026

Copy link
Copy Markdown
Member

No, it failed. Why do you push with --no-verify?

@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan let me try fix local setup, it was failing with cryptic error locally while trying to execute webhooks

@kibertoad kibertoad force-pushed the feat/github-alias branch 2 times, most recently from 563fac3 to a64ad44 Compare April 21, 2026 18:46
zkochan added 3 commits April 29, 2026 00:47
…resolvers' schemes

Move resolveFromNamedRegistry to the end of the resolver chain so built-in
schemes (npm:, jsr:, git:/github:/gitlab:/…, file:, link:, tarball URLs,
runtime resolvers) are always claimed by their dedicated resolver before a
user-configured alias gets a chance to shadow them. With the ordering
guarantee in place, npm-resolver no longer needs to know about other
resolvers' prefixes — drop RESERVED_REGISTRY_ALIASES, isReservedRegistryAlias,
and the reserved-alias check from mergeNamedRegistries. URL validation
stays since it is npm-resolver's local concern.
…esolver gh: hardcoding

The @pnpm/resolving.named-registry-specifier-parser package was only
imported by npm-resolver, so its types/parser/built-in alias map are now
collocated in npm-resolver/src/parseBareSpecifier.ts and the standalone
package is deleted.

replaceVersionInBareSpecifier no longer hardcodes 'gh:' alongside 'npm:'
- deps-resolver does not know which prefixes are configured as named
registries, so a single hardcoded entry is the wrong design. Named-
registry specifiers (built-in or user-defined) skip this fast path on
purpose; the cost is one extra metadata fetch on re-resolution rather
than a correctness bug.
replaceVersionInBareSpecifier now accepts a runtime-built list of named-
registry prefixes instead of hardcoding 'gh:'. The deps-resolver merges
the built-in defaults (BUILTIN_NAMED_REGISTRIES) with user-configured
namedRegistries from pnpm-workspace.yaml and threads the resulting prefix
list through ResolutionContext to the call site. Re-installs of locked
gh:/work:/etc. specifiers skip the metadata fetch as they did with the
hardcoded list, and deps-resolver is no longer pinned to a specific
named-registry alias.

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread resolving/npm-resolver/src/index.ts
Comment thread resolving/npm-resolver/src/parseBareSpecifier.ts Outdated
Comment thread installing/deps-resolver/src/replaceVersionInBareSpecifier.ts Outdated
Comment thread config/reader/src/getOptionsFromRootManifest.ts
zkochan added 6 commits April 29, 2026 01:07
…egistries

namedRegistries is read from pnpm-workspace.yaml. Reading settings from the
root package.json's pnpm field is deprecated in v11.
The previous version restructured the function around a `body` slice and
rewrote both the lastIndexOf condition and the no-range branch. The same
behavior can be achieved with two minimal changes to the original:
swap the single 'npm:' check for a prefix-list check and add one branch
for the new `<alias>:<version_selector>` form. The original
versionDelimiter logic stays untouched.
Repeating the same description on each PnpmSettings/Config/ResolverFactoryOptions/
ResolutionContext field doesn't match the repo's convention. Also collapse
DEFAULT_GH_REGISTRY/BUILTIN_GH_ALIAS into the BUILTIN_NAMED_REGISTRIES literal.
…ge name

Unifies the npm: parser with the named-registry shape (gh:^1.0.0): when
the body of an npm: specifier is a bare semver range or version, treat
the outer dependency alias as the package name. Previously npm:^1.0.0
was parsed as "package literally named ^1.0.0" and 404'd. Restricted to
semver.validRange to preserve npm's package-aliasing meaning for tag-like
bodies (npm:is-positive, npm:latest).
* Relax named-registry parser to accept unscoped names (work:lodash@^4) and
  fall back to any (scoped or unscoped) outer alias for <prefix>:<range>.
  Drops the validateScopedPackageName helper; scoped-name validation stays
  inline for the @-prefixed branch.
* Optimize replaceVersionInBareSpecifier hot path: check 'npm:' first then
  for-loop over namedRegistryPrefixes, avoiding a per-call array allocation.
* Guard replaceEnvInStringValues against arrays so a YAML list under
  registries:/namedRegistries: surfaces as a config error instead of being
  silently coerced to {0: ..., 1: ...}.
@zkochan

zkochan commented Apr 29, 2026

Copy link
Copy Markdown
Member

Re: Copilot's mergeNamedRegistries validation comment — pushing back. The fast path consuming namedRegistryPrefixes (replaceVersionInBareSpecifier) is gated on currentPkg.pkgId.endsWith('@<version>'), which only matches resolutions that produced an npm-style id. A git:foo#main resolved via the git resolver has a different pkgId shape (URL + commit) so it never enters the fast path even if git were configured as a named-registry alias. The resolver chain itself runs resolveFromNamedRegistry last, so dedicated resolvers (git/jsr/tarball/local) claim their own schemes first regardless of namedRegistries. Adding a reserved-alias check here would re-introduce the cross-resolver knowledge we removed earlier in this PR — happy to revisit if a concrete bypass surfaces.

Other Copilot feedback addressed in 0cba981:

  • relaxed parser to accept unscoped named-registry names (work:lodash@^4) and fall back to any outer alias for <prefix>:<range>
  • dropped the per-call array allocation in replaceVersionInBareSpecifier
  • guarded replaceEnvInStringValues against arrays so a YAML list under registries:/namedRegistries: surfaces as a config error

zkochan added 2 commits April 29, 2026 02:17
…gistry

Both resolvers had near-identical bodies: parse spec → look up registry URL
→ getAuthHeaderValueByURI → pickPackage (with NoMatchingVersionError) → build
id/resolution. Extract a pickFromSimpleRegistry helper that returns the
common result fragment, and unify calcJsrSpecifier/calcNamedRegistrySpecifier
into a single calcPrefixedSpecifier. Net -89/+54 lines. Side effect: jsr
now also forwards publishedByExclude and optional through to pickPackage,
matching the named-registry resolver and the type signature it already had.
Comment thread releasing/exportable-manifest/src/index.ts

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread resolving/npm-resolver/src/index.ts
Comment thread installing/deps-resolver/src/resolveDependencyTree.ts
Comment thread config/reader/src/getOptionsFromRootManifest.ts
@zkochan

zkochan commented Apr 29, 2026

Copy link
Copy Markdown
Member

Re Copilot's two new comments on mergeNamedRegistries and namedRegistryPrefixes: same gate as the previous round. The fast path's call site is

if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(\`@\${currentPkg.version}\`) && !calcSpecifier) {
  wantedDependency.bareSpecifier = replaceVersionInBareSpecifier(...)
}

A file:./pkg resolves via local-resolver and produces pkgId = "file:./pkg" (it's \${protocol}\${normalizedPath}, see resolving/local-resolver/src/parseBareSpecifier.ts), which doesn't end with @<version>. Same for link:, git:, tarball URLs. The fast path can't be tricked into rewriting those even if a user picked one of those names as a namedRegistries alias, because the pkgId shape eliminates them at the gate before the prefix list is consulted.

If we ever want to harden further, the right fix is at the gate (e.g. require pkgId to start with the package name), not by re-introducing cross-resolver knowledge in npm-resolver/deps-resolver.

Test gap (env-var substitution inside registries / namedRegistries) addressed in 0b21b80.

@zkochan zkochan enabled auto-merge (squash) April 29, 2026 00:35
@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan seems like there is an unrelated flakiness that blocked the merge?..

@zkochan zkochan disabled auto-merge April 29, 2026 10:38
@zkochan zkochan merged commit b61e268 into pnpm:main Apr 29, 2026
7 of 8 checks passed
@welcome

welcome Bot commented Apr 29, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉🎉🎉

@kibertoad

Copy link
Copy Markdown
Contributor Author

@zkochan I think an old issue abour JSR support got linked in the release notes for 11.1.0 instead of this one, I assume by accident?..
see https://github.com/pnpm/pnpm/releases/tag/v11.1.0

@zkochan

zkochan commented May 12, 2026

Copy link
Copy Markdown
Member

fixed.

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.

3 participants