Files
pnpm/config/reader/src/Config.ts
Igor Savin b61e268d57 feat: add support for github prefix and named registries (#11324)
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:

```yml
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.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-29 12:38:56 +02:00

301 lines
8.6 KiB
TypeScript

import type { Catalogs } from '@pnpm/catalogs.types'
import type { Hooks } from '@pnpm/hooks.pnpmfile'
import type {
EngineDependency,
Finder,
Project,
ProjectManifest,
ProjectsGraph,
Registries,
RegistryConfig,
TrustPolicy,
} from '@pnpm/types'
import type { OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'authConfig'>
export type VerifyDepsBeforeRun = 'install' | 'warn' | 'error' | 'prompt' | false
/**
* Runtime state, workspace context, and CLI metadata.
* These fields are NOT user-facing settings — they are computed at startup
* or populated later by the CLI harness (e.g. workspace filtering, hook loading).
*/
export interface ConfigContext {
// -- Runtime state --
hooks?: Hooks
finders?: Record<string, Finder>
// -- Workspace context --
allProjects?: Project[]
selectedProjectsGraph?: ProjectsGraph
allProjectsGraph?: ProjectsGraph
rootProjectManifest?: ProjectManifest
rootProjectManifestDir: string
// -- CLI metadata --
cliOptions: Record<string, any> // eslint-disable-line
/** Keys explicitly set from workspace yaml, CLI, or env vars (not defaults). */
explicitlySetKeys: Set<string>
packageManager: {
name: string
version: string
}
wantedPackageManager?: WantedPackageManager
}
/**
* The package manager requested by the root project's manifest.
* Extends {@link EngineDependency} with the source of the declaration so that
* callers can treat the legacy `packageManager` field and
* `devEngines.packageManager` differently (e.g. only the latter persists
* resolved pnpm integrity info to `pnpm-lock.yaml`).
*/
export interface WantedPackageManager extends EngineDependency {
fromDevEngines?: boolean
}
/**
* User-facing settings + auth/network config.
* Does NOT include runtime state — see {@link ConfigContext} for that.
*/
export interface Config extends OptionsFromRootManifest {
allowNew: boolean
autoConfirmAllPrompts?: boolean
autoInstallPeers?: boolean
bail: boolean
color: 'always' | 'auto' | 'never'
useBetaCli: boolean
excludeLinksFromLockfile: boolean
extraBinPaths: string[]
extraEnv: Record<string, string>
failIfNoMatch: boolean
filter: string[]
filterProd: string[]
authConfig: Record<string, any>, // eslint-disable-line
dryRun?: boolean // This option might be not supported ever
global?: boolean
dir: string
bin: string
verifyDepsBeforeRun?: VerifyDepsBeforeRun
ignoreScripts?: boolean
ignoreCompatibilityDb?: boolean
includeWorkspaceRoot?: boolean
optimisticRepeatInstall?: boolean
save?: boolean
saveProd?: boolean
saveDev?: boolean
saveOptional?: boolean
savePeer?: boolean
saveCatalogName?: string
saveWorkspaceProtocol?: boolean | 'rolling'
lockfileIncludeTarballUrl?: boolean
scriptShell?: string
stream?: boolean
pnpmExecPath: string
pnpmHomeDir: string
production?: boolean
fetchRetries?: number
fetchRetryFactor?: number
fetchRetryMintimeout?: number
fetchRetryMaxtimeout?: number
fetchTimeout?: number
saveExact?: boolean
savePrefix?: string
shellEmulator?: boolean
scriptsPrependNodePath?: boolean | 'warn-only'
force?: boolean
depth?: number
engineStrict?: boolean
nodeVersion?: string
nodeDownloadMirrors?: Record<string, string>
offline?: boolean
registry?: string
optional?: boolean
unsafePerm?: boolean
loglevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug'
frozenLockfile?: boolean
preferFrozenLockfile?: boolean
only?: 'prod' | 'production' | 'dev' | 'development'
preferOffline?: boolean
sideEffectsCache?: boolean // for backward compatibility
sideEffectsCacheReadonly?: boolean // for backward compatibility
sideEffectsCacheRead?: boolean
sideEffectsCacheWrite?: boolean
shamefullyHoist?: boolean
dev?: boolean
ignoreCurrentSpecifiers?: boolean
recursive?: boolean
enablePrePostScripts?: boolean
useStderr?: boolean
nodeLinker?: 'hoisted' | 'isolated' | 'pnp'
preferSymlinkedExecutables?: boolean
resolutionMode?: 'highest' | 'time-based' | 'lowest-direct'
registrySupportsTimeField?: boolean
resolvePeersFromWorkspaceRoot?: boolean
deployAllFiles?: boolean
forceLegacyDeploy?: boolean
reporterHidePrefix?: boolean
// proxy
httpProxy?: string
httpsProxy?: string
localAddress?: string
noProxy?: string | boolean
// ssl
cert?: string | string[]
key?: string
ca?: string | string[]
strictSsl?: boolean
userAgent?: string
tag?: string
updateNotifier?: boolean
// pnpm specific configs
cacheDir: string
configDir: string
stateDir: string
storeDir?: string
virtualStoreDir?: string
virtualStoreOnly?: boolean
enableGlobalVirtualStore?: boolean
verifyStoreIntegrity?: boolean
maxSockets?: number
networkConcurrency?: number
fetchingConcurrency?: number
lockfileOnly?: boolean // like npm's --package-lock-only
childConcurrency?: number
ignorePnpmfile?: boolean
pnpmfile: string[] | string
tryLoadDefaultPnpmfile?: boolean
packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'clone' | 'clone-or-copy'
hoistPattern?: string[]
publicHoistPattern?: string[] | string
hoistWorkspacePackages?: boolean
useStoreServer?: boolean
useRunningStoreServer?: boolean
workspaceConcurrency: number
workspaceDir?: string
workspacePackagePatterns?: string[]
catalogs?: Catalogs
catalogMode?: 'strict' | 'prefer' | 'manual'
cleanupUnusedCatalogs?: boolean
reporter?: string
aggregateOutput: boolean
linkWorkspacePackages: boolean | 'deep'
injectWorkspacePackages?: boolean
preferWorkspacePackages: boolean
reverse: boolean
sort: boolean
strictPeerDependencies: boolean
lockfileDir?: string
modulesDir?: string
sharedWorkspaceLockfile?: boolean
useLockfile: boolean
useGitBranchLockfile: boolean
mergeGitBranchLockfiles?: boolean
mergeGitBranchLockfilesBranchPattern?: string[]
globalPnpmfile?: string
npmPath?: string
gitChecks?: boolean
publishBranch?: string
recursiveInstall?: boolean
symlink: boolean
enablePnp?: boolean
enableModulesDir: boolean
modulesCacheMaxAge: number
dlxCacheMaxAge: number
embedReadme?: boolean
gitShallowHosts?: string[]
legacyDirFiltering?: boolean
allowBuilds?: Record<string, boolean | string>
dedupePeerDependents?: boolean
dedupePeers?: boolean
patchesDir?: string
ignoreWorkspaceCycles?: boolean
disallowWorkspaceCycles?: boolean
packGzipLevel?: number
blockExoticSubdeps?: boolean
agent?: string
registries: Registries
namedRegistries?: Record<string, string>
configByUri: Record<string, RegistryConfig>
ignoreWorkspaceRootCheck: boolean
workspaceRoot: boolean
testPattern?: string[]
changedFilesIgnorePattern?: string[]
userConfig: Record<string, string>
hoist: boolean
packageLock: boolean
pending: boolean
userconfig: string
npmrcAuthFile?: string
workspacePrefix?: string
dedupeDirectDeps?: boolean
extendNodePath?: boolean
gitBranchLockfile?: boolean
globalBinDir?: string
globalDir?: string
globalPkgDir: string
lockfile?: boolean
dedupeInjectedDeps?: boolean
nodeOptions?: string
pmOnFail?: 'download' | 'error' | 'warn' | 'ignore'
runtimeOnFail?: 'download' | 'error' | 'warn' | 'ignore'
virtualStoreDirMaxLength: number
peersSuffixMaxLength?: number
strictStorePkgContentCheck: boolean
strictDepBuilds: boolean
syncInjectedDepsAfterScripts?: string[]
initPackageManager: boolean
initType: 'commonjs' | 'module'
dangerouslyAllowAllBuilds: boolean
ci: boolean
preserveAbsolutePaths?: boolean
minimumReleaseAge?: number
minimumReleaseAgeExclude?: string[]
minimumReleaseAgeIgnoreMissingTime?: boolean
minimumReleaseAgeStrict?: boolean
fetchWarnTimeoutMs?: number
fetchMinSpeedKiBps?: number
trustPolicy?: TrustPolicy
trustPolicyExclude?: string[]
trustPolicyIgnoreAfter?: number
auditLevel?: 'info' | 'low' | 'moderate' | 'high' | 'critical'
packageConfigs?: ProjectConfigSet
}
export interface ConfigWithDeprecatedSettings extends Config {
globalPrefix?: string
proxy?: string
}
export const PROJECT_CONFIG_FIELDS = [
'hoist',
'modulesDir',
'overrides',
'saveExact',
'savePrefix',
] as const satisfies Array<keyof Config>
export type ProjectConfig = Partial<Pick<Config, typeof PROJECT_CONFIG_FIELDS[number] | 'hoistPattern'>>
/** Simple map from project names to {@link ProjectConfig} */
export type ProjectConfigRecord = Record<string, ProjectConfig>
/** Map multiple project names to a shared {@link ProjectConfig} */
export type ProjectConfigMultiMatch = { match: string[] } & ProjectConfig
export type ProjectConfigSet =
| ProjectConfigRecord
| ProjectConfigMultiMatch[]