refactor: resolvers should calculate the specifiers that are saved into package.json (#9426)

* refactor: resolvers should return specifier templates

* refactor: updating workspace protocol specs in package.json

* refactor: move workspace selector calculation logic to npm-resolver

* refactor: move workspace selector calculation logic to npm-resolver

* refactor: calculating range in npm-resolver

* refactor: rename normalizedPref and specifierTemplate to specifier

* refactor: specifiers creation

* refactor: npm-resolver

* refactor: remove which-version-is-pinned package

* refactor: which version is pinned

* docs: add changesets

* refactor: implement suggestions

* refactor: revert regex usage
This commit is contained in:
Zoltan Kochan
2025-04-18 17:48:03 +02:00
committed by GitHub
parent f337e7182f
commit 5b73df1eb1
48 changed files with 334 additions and 521 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/manifest-utils": major
---
Moved out `createVersionSpec` to `@pnpm/npm-resolver`.

View File

@@ -0,0 +1,14 @@
---
"@pnpm/resolve-dependencies": major
"@pnpm/store-connection-manager": major
"@pnpm/package-requester": major
"@pnpm/store-controller-types": major
"@pnpm/tarball-resolver": major
"@pnpm/local-resolver": major
"@pnpm/resolver-base": major
"@pnpm/git-resolver": major
"@pnpm/npm-resolver": major
"@pnpm/core": patch
---
Renamed `normalizedPref` to `specifiers`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/types": minor
---
Added `PinnedVersion`.

View File

@@ -36,3 +36,9 @@ export type PkgIdWithPatchHash = string & { __brand: 'PkgIdWithPatchHash' }
export type DepPath = string & { __brand: 'DepPath' }
export type ProjectId = string & { __brand: 'ProjectId' }
export type PinnedVersion =
| 'none'
| 'patch'
| 'minor'
| 'major'

View File

@@ -1,47 +0,0 @@
# @pnpm/which-version-is-pinned
## 6.0.0
### Major Changes
- 43cdd87: Node.js v16 support dropped. Use at least Node.js v18.12.
## 5.0.1
### Patch Changes
- 4fc497882: When updating dependencies, preserve the range prefix in aliased dependencies. So `npm:foo@1.0.0` becomes `npm:foo@1.1.0`.
## 5.0.0
### Major Changes
- eceaa8b8b: Node.js 14 support dropped.
## 4.0.0
### Major Changes
- f884689e0: Require `@pnpm/logger` v5.
## 3.0.0
### Major Changes
- f5621a42c: A new value `rolling` for option `save-workspace-protocol`. When selected, pnpm will save workspace versions using a rolling alias (e.g. `"foo": "workspace:^"`) instead of pinning the current version number (e.g. `"foo": "workspace:^1.0.0"`). Usage example:
```
pnpm --save-workspace-protocol=rolling add foo
```
## 2.0.0
### Major Changes
- 542014839: Node.js 12 is not supported.
## 1.0.0
### Major Changes
- ae32d313e: Initial release.

View File

@@ -1,22 +0,0 @@
# @pnpm/which-version-is-pinned
> Takes a version spec and returns which version is pinned by it
## Installation
```
pnpm add @pnpm/which-version-is-pinned
```
## Usage
```ts
import { whichVersionIsPinned } from '@pnpm/which-version-is-pinned'
whichVersionIsPinned('^1.0.0')
// major
```
## License
[MIT](LICENSE)

View File

@@ -1,44 +0,0 @@
{
"name": "@pnpm/which-version-is-pinned",
"version": "1000.0.0",
"description": "Takes a version spec and returns which version is pinned by it",
"keywords": [
"pnpm",
"pnpm10"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/packages/which-version-is-pinned",
"homepage": "https://github.com/pnpm/pnpm/blob/main/packages/which-version-is-pinned#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"semver-utils": "catalog:"
},
"devDependencies": {
"@pnpm/which-version-is-pinned": "workspace:*"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -1,17 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../test.lib",
"rootDir": "."
},
"include": [
"**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -1,12 +0,0 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": []
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -102,7 +102,6 @@
"@pnpm/store-controller-types": "workspace:*",
"@pnpm/symlink-dependency": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/which-version-is-pinned": "workspace:*",
"@zkochan/rimraf": "catalog:",
"ci-info": "catalog:",
"enquirer": "catalog:",

View File

@@ -476,7 +476,7 @@ export async function mutateModules (
includeDirect: opts.includeDirect,
nodeExecPath: opts.nodeExecPath,
})
.map((wantedDependency) => ({ ...wantedDependency, updateSpec: true, preserveNonSemverVersionSpec: true }))
.map((wantedDependency) => ({ ...wantedDependency, updateSpec: true }))
if (ctx.wantedLockfile?.importers) {
forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, _isWantedDepPrefSame)
@@ -898,7 +898,7 @@ export type ImporterToUpdate = {
pruneDirectDependencies: boolean
removePackages?: string[]
updatePackageManifest: boolean
wantedDependencies: Array<WantedDependency & { isNew?: boolean, updateSpec?: boolean, preserveNonSemverVersionSpec?: boolean }>
wantedDependencies: Array<WantedDependency & { isNew?: boolean, updateSpec?: boolean }>
} & DependenciesMutation
export interface UpdatedProject {

View File

@@ -1,7 +1,6 @@
import { parseWantedDependency } from '@pnpm/parse-wanted-dependency'
import { type Dependencies } from '@pnpm/types'
import { whichVersionIsPinned } from '@pnpm/which-version-is-pinned'
import { type PinnedVersion, type WantedDependency } from '@pnpm/resolve-dependencies/lib/getWantedDependencies'
import { type WantedDependency } from '@pnpm/resolve-dependencies'
import { type Catalog } from '@pnpm/catalogs.types'
export function parseWantedDependencies (
@@ -25,7 +24,6 @@ export function parseWantedDependencies (
const parsed = parseWantedDependency(rawWantedDependency)
const alias = parsed['alias']
let pref = parsed['pref']
let pinnedVersion!: PinnedVersion | undefined
if (!opts.allowNew && (!alias || !opts.currentPrefs[alias])) {
return null
@@ -39,13 +37,12 @@ export function parseWantedDependencies (
}
if (alias && opts.currentPrefs[alias]) {
pref ??= opts.currentPrefs[alias]
pinnedVersion = whichVersionIsPinned(opts.currentPrefs[alias])
}
const result = {
alias,
dev: Boolean(opts.dev || alias && !!opts.devDependencies[alias]),
optional: Boolean(opts.optional || alias && !!opts.optionalDependencies[alias]),
pinnedVersion,
prevSpecifier: alias && opts.currentPrefs[alias],
}
if (pref) {
return {

View File

@@ -361,6 +361,7 @@ test('overwriting (magic-hook@2.0.0 and @0.1.0)', async () => {
test('overwriting (is-positive@3.0.0 with is-positive@latest)', async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-positive@3.0.0'], testDefaults({ save: true }))
expect(manifest.dependencies?.['is-positive']).toBe('3.0.0')
project.storeHas('is-positive', '3.0.0')

View File

@@ -978,7 +978,6 @@ test('adding a new dependency with the workspace: protocol', async () => {
rootDir: path.resolve('project-1') as ProjectRootDir,
},
], testDefaults({
saveWorkspaceProtocol: true,
allProjects: [
{
manifest: {
@@ -995,9 +994,11 @@ test('adding a new dependency with the workspace: protocol', async () => {
rootDir: path.resolve('project-1') as ProjectRootDir,
},
],
}, {
saveWorkspaceProtocol: 'rolling',
}))
expect(updatedProjects[0].manifest.dependencies).toStrictEqual({ foo: 'workspace:^1.0.0' })
expect(updatedProjects[0].manifest.dependencies).toStrictEqual({ foo: 'workspace:^' })
})
test('adding a new dependency with the workspace: protocol and save-workspace-protocol is "rolling"', async () => {
@@ -1011,7 +1012,6 @@ test('adding a new dependency with the workspace: protocol and save-workspace-pr
rootDir: path.resolve('project-1') as ProjectRootDir,
},
], testDefaults({
saveWorkspaceProtocol: 'rolling',
allProjects: [
{
manifest: {
@@ -1028,6 +1028,8 @@ test('adding a new dependency with the workspace: protocol and save-workspace-pr
rootDir: path.resolve('project-1') as ProjectRootDir,
},
],
}, {
saveWorkspaceProtocol: 'rolling',
}))
expect(updatedProjects[0].manifest.dependencies).toStrictEqual({ foo: 'workspace:^' })
@@ -1152,15 +1154,16 @@ test('update workspace range', async () => {
rootDir: path.resolve('dep8') as ProjectRootDir,
},
],
saveWorkspaceProtocol: true,
}, {
saveWorkspaceProtocol: 'rolling',
}))
const expected = {
dep1: 'workspace:2.0.0',
dep2: 'workspace:~2.0.0',
dep3: 'workspace:^2.0.0',
dep4: 'workspace:^2.0.0',
dep5: 'workspace:~2.0.0',
dep1: 'workspace:*',
dep2: 'workspace:~',
dep3: 'workspace:^',
dep4: 'workspace:^',
dep5: 'workspace:~',
dep6: 'workspace:*',
dep7: 'workspace:^',
dep8: 'workspace:~',
@@ -1268,6 +1271,7 @@ test('update workspace range when save-workspace-protocol is "rolling"', async (
rootDir: path.resolve('dep6') as ProjectRootDir,
},
],
}, {
saveWorkspaceProtocol: 'rolling',
}))

View File

@@ -27,14 +27,14 @@ test("don't override existing spec in package.json on named installation", async
sec: 'sindresorhus/sec#main',
},
}, ['is-positive'], testDefaults())
manifest = (await addDependenciesToPackage(manifest, ['is-negative'], testDefaults())).updatedManifest
manifest = (await addDependenciesToPackage(manifest, ['is-negative@latest'], testDefaults())).updatedManifest
manifest = (await addDependenciesToPackage(manifest, ['sec'], testDefaults())).updatedManifest
expect(project.requireModule('is-positive/package.json').version).toBe('2.0.0')
expect(project.requireModule('is-negative/package.json').version).toBe('1.0.1')
expect(project.requireModule('is-negative/package.json').version).toBe('2.1.0')
expect(manifest.dependencies).toStrictEqual({
'is-negative': '^1.0.1',
'is-negative': '^2.1.0',
'is-positive': '^2.0.0',
sec: 'sindresorhus/sec#main',
})

View File

@@ -123,9 +123,6 @@
{
"path": "../../packages/types"
},
{
"path": "../../packages/which-version-is-pinned"
},
{
"path": "../../patching/config"
},

View File

@@ -161,7 +161,7 @@ async function resolveAndFetch (
): Promise<PackageResponse> {
let latest: string | undefined
let manifest: DependencyManifest | undefined
let normalizedPref: string | undefined
let specifier: string | undefined
let resolution = options.currentPkg?.resolution as Resolution
let pkgId = options.currentPkg?.id
const skipResolution = resolution && !options.update
@@ -188,6 +188,8 @@ async function resolveAndFetch (
workspacePackages: options.workspacePackages,
update: options.update,
injectWorkspacePackages: options.injectWorkspacePackages,
calcSpecifier: options.calcSpecifier,
pinnedVersion: options.pinnedVersion,
}), { priority: options.downloadPriority })
manifest = resolveResult.manifest
@@ -206,7 +208,7 @@ async function resolveAndFetch (
updated = pkgId !== resolveResult.id || !resolution || forceFetch
resolution = resolveResult.resolution
pkgId = resolveResult.id
normalizedPref = resolveResult.normalizedPref
specifier = resolveResult.specifier
}
const id = pkgId!
@@ -220,10 +222,10 @@ async function resolveAndFetch (
id,
isLocal: true,
manifest,
normalizedPref,
resolution: resolution as DirectoryResolution,
resolvedVia,
updated,
specifier,
},
}
}
@@ -252,7 +254,7 @@ async function resolveAndFetch (
isInstallable: isInstallable ?? undefined,
latest,
manifest,
normalizedPref,
specifier,
resolution,
resolvedVia,
updated,
@@ -288,7 +290,7 @@ async function resolveAndFetch (
isInstallable: isInstallable ?? undefined,
latest,
manifest,
normalizedPref,
specifier,
resolution,
resolvedVia,
updated,

View File

@@ -62,7 +62,7 @@ test('request package', async () => {
expect(pkgResponse.body.isLocal).toBe(false)
expect(typeof pkgResponse.body.latest).toBe('string')
expect(pkgResponse.body.manifest?.name).toBe('is-positive')
expect(!pkgResponse.body.normalizedPref).toBeTruthy()
expect(!pkgResponse.body.specifier).toBeTruthy()
expect(pkgResponse.body.resolution).toStrictEqual({
integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==',
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-positive/-/is-positive-1.0.0.tgz`,
@@ -103,7 +103,7 @@ test('request package but skip fetching', async () => {
expect(pkgResponse.body.isLocal).toBe(false)
expect(typeof pkgResponse.body.latest).toBe('string')
expect(pkgResponse.body.manifest?.name).toBe('is-positive')
expect(!pkgResponse.body.normalizedPref).toBeTruthy()
expect(!pkgResponse.body.specifier).toBeTruthy()
expect(pkgResponse.body.resolution).toStrictEqual({
integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==',
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-positive/-/is-positive-1.0.0.tgz`,
@@ -155,7 +155,7 @@ test('request package but skip fetching, when resolution is already available',
expect(pkgResponse.body.isLocal).toBe(false)
expect(typeof pkgResponse.body.latest).toBe('string')
expect(pkgResponse.body.manifest.name).toBe('is-positive')
expect(!pkgResponse.body.normalizedPref).toBeTruthy()
expect(!pkgResponse.body.specifier).toBeTruthy()
expect(pkgResponse.body.resolution).toStrictEqual({
integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==',
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-positive/-/is-positive-1.0.0.tgz`,

View File

@@ -23,7 +23,7 @@ export function createWorkspaceSpecs (specs: string[], workspacePackages: Worksp
const parsed = parseWantedDependency(spec)
if (!parsed.alias) throw new PnpmError('NO_PKG_NAME_IN_SPEC', `Cannot update/install from workspace through "${spec}"`)
if (!workspacePackages.has(parsed.alias)) throw new PnpmError('WORKSPACE_PACKAGE_NOT_FOUND', `"${parsed.alias}" not found in the workspace`)
if (!parsed.pref) return `${parsed.alias}@workspace:>=0.0.0`
if (!parsed.pref) return `${parsed.alias}@workspace:*`
if (parsed.pref.startsWith('workspace:')) return spec
return `${parsed.alias}@workspace:${parsed.pref}`
})

View File

@@ -91,7 +91,7 @@ test('installing with "workspace:" should work even if link-workspace-packages i
const pkg = await import(path.resolve('project-1/package.json'))
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:^' })
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:*' })
projects['project-1'].has('project-2')
})
@@ -208,7 +208,7 @@ test('installing with "workspace=true" with linkWorkspacePackages on and saveWor
const pkg = await import(path.resolve('project-1/package.json'))
expect(pkg?.dependencies).toStrictEqual({ 'project-2': '^2.0.0' })
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:^2.0.0' })
projects['project-1'].has('project-2')
})

View File

@@ -439,6 +439,7 @@ test('recursive update with aliased workspace dependency (#7975)', async () => {
dir: process.cwd(),
recursive: true,
workspaceDir: process.cwd(),
saveWorkspaceProtocol: 'rolling',
})
projects['project-1'].has('pkg')

View File

@@ -73,7 +73,7 @@ test('updateToWorkspacePackagesFromManifest()', () => {
})
test('createWorkspaceSpecs', () => {
expect(createWorkspaceSpecs(['bar', 'foo@2', 'qar@workspace:3'], WORKSPACE_PACKAGES)).toStrictEqual(['bar@workspace:>=0.0.0', 'foo@workspace:2', 'qar@workspace:3'])
expect(createWorkspaceSpecs(['bar', 'foo@2', 'qar@workspace:3'], WORKSPACE_PACKAGES)).toStrictEqual(['bar@workspace:*', 'foo@workspace:2', 'qar@workspace:3'])
let err!: PnpmError
try {
createWorkspaceSpecs(['express'], WORKSPACE_PACKAGES)

View File

@@ -51,7 +51,6 @@
"@pnpm/semver.peer-range": "workspace:*",
"@pnpm/store-controller-types": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/which-version-is-pinned": "workspace:*",
"@pnpm/workspace.spec-parser": "workspace:*",
"@yarnpkg/core": "catalog:",
"filenamify": "catalog:",

View File

@@ -5,18 +5,15 @@ import {
type IncludedDependencies,
type ProjectManifest,
} from '@pnpm/types'
import { whichVersionIsPinned } from '@pnpm/which-version-is-pinned'
export type PinnedVersion = 'major' | 'minor' | 'patch' | 'none'
export interface WantedDependency {
alias: string
pref: string // package reference
dev: boolean
optional: boolean
pinnedVersion?: PinnedVersion
nodeExecPath?: string
updateSpec?: boolean
prevSpecifier?: string
}
export function getWantedDependencies (
@@ -72,8 +69,8 @@ function getWantedDependenciesFromGivenSet (
injected: opts.dependenciesMeta[alias]?.injected,
optional: depType === 'optional',
nodeExecPath: opts.nodeExecPath ?? opts.dependenciesMeta[alias]?.node,
pinnedVersion: whichVersionIsPinned(pref),
pref,
prevSpecifier: pref,
}
})
}

View File

@@ -9,7 +9,6 @@ import {
import {
getAllDependenciesFromManifest,
getSpecFromPackageManifest,
type PinnedVersion,
} from '@pnpm/manifest-utils'
import { verifyPatches } from '@pnpm/patching.config'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
@@ -18,6 +17,7 @@ import {
DEPENDENCIES_FIELDS,
type DependencyManifest,
type PeerDependencyIssuesByProjects,
type PinnedVersion,
type ProjectManifest,
type ProjectId,
type ProjectRootDir,

View File

@@ -38,6 +38,7 @@ import {
type ReadPackageHook,
type Registries,
type PkgIdWithPatchHash,
type PinnedVersion,
} from '@pnpm/types'
import * as dp from '@pnpm/dependency-path'
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
@@ -110,9 +111,9 @@ export interface LinkedDependency {
pkgId: PkgResolutionId
version: string
name: string
normalizedPref?: string
alias: string
catalogLookup?: CatalogLookupMetadata
specifier?: string
}
export interface PendingNode {
@@ -192,7 +193,6 @@ export type PkgAddress = {
resolvedVia?: string
nodeId: NodeId
pkgId: PkgResolutionId
normalizedPref?: string // is returned only for root dependencies
installable: boolean
pkg: PackageManifest
version?: string
@@ -203,6 +203,7 @@ export type PkgAddress = {
publishedAt?: string
catalogLookup?: CatalogLookupMetadata
optional: boolean
specifier?: string
} & ({
isLinkedDependency: true
version: string
@@ -274,6 +275,7 @@ interface ResolvedDependenciesOptions {
prefix: string
supportedArchitectures?: SupportedArchitectures
updateToLatest?: boolean
pinnedVersion?: PinnedVersion
}
interface PostponedResolutionOpts {
@@ -406,6 +408,7 @@ export interface ImporterToResolve {
parentPkgAliases: ParentPkgAliases
wantedDependencies: Array<WantedDependency & { updateDepth?: number }>
options: ImporterToResolveOptions
pinnedVersion?: PinnedVersion
}
interface ResolveDependenciesOfImportersResult {
@@ -570,6 +573,7 @@ async function resolveDependenciesOfImporterDependency (
updateToLatest: catalogLookup != null
? false
: importer.options.updateToLatest,
pinnedVersion: importer.pinnedVersion,
},
extendedWantedDep
)
@@ -821,6 +825,7 @@ async function resolveDependenciesOfDependency (
updateMatching: options.updateMatching,
supportedArchitectures: options.supportedArchitectures,
parentIds: options.parentIds,
pinnedVersion: options.pinnedVersion,
}
// The catalog protocol is normally replaced when resolving the dependencies
@@ -1208,6 +1213,7 @@ interface ResolveDependencyOptions {
updateDepth: number
updateMatching?: UpdateMatchingFunction
supportedArchitectures?: SupportedArchitectures
pinnedVersion?: PinnedVersion
}
type ResolveDependencyResult = PkgAddress | LinkedDependency | null
@@ -1252,7 +1258,8 @@ async function resolveDependency (
}
}
try {
if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`)) {
const calcSpecifier = options.currentDepth === 0
if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`) && !calcSpecifier) {
wantedDependency.pref = replaceVersionInPref(wantedDependency.pref, currentPkg.version)
}
pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
@@ -1288,6 +1295,8 @@ async function resolveDependency (
return err
},
injectWorkspacePackages: ctx.injectWorkspacePackages,
calcSpecifier,
pinnedVersion: options.pinnedVersion,
})
} catch (err: any) { // eslint-disable-line
const wantedDependencyDetails = {
@@ -1346,11 +1355,11 @@ async function resolveDependency (
dev: wantedDependency.dev,
isLinkedDependency: true,
name: pkgResponse.body.manifest.name,
normalizedPref: pkgResponse.body.normalizedPref,
optional: wantedDependency.optional,
pkgId: pkgResponse.body.id,
resolution: pkgResponse.body.resolution,
version: pkgResponse.body.manifest.version,
specifier: pkgResponse.body.specifier,
}
}
@@ -1562,7 +1571,7 @@ async function resolveDependency (
resolvedVia: pkgResponse.body.resolvedVia,
isNew,
nodeId,
normalizedPref: options.currentDepth === 0 ? pkgResponse.body.normalizedPref : undefined,
specifier: pkgResponse.body.specifier,
missingPeersOfChildren,
pkgId: pkgResponse.body.id,
rootDir,

View File

@@ -7,6 +7,7 @@ import { type StoreController } from '@pnpm/store-controller-types'
import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type PinnedVersion,
type PkgResolutionId,
type ProjectManifest,
type ProjectId,
@@ -52,8 +53,8 @@ export interface ResolvedDirectDependency {
pkgId: PkgResolutionId
version: string
name: string
normalizedPref?: string
catalogLookup?: CatalogLookupMetadata
specifier?: string
}
/**
@@ -92,6 +93,7 @@ export interface ImporterToResolveGeneric<WantedDepExtraProps> extends Importer<
hasRemovedDependencies?: boolean
preferredVersions?: PreferredVersions
wantedDependencies: Array<WantedDepExtraProps & WantedDependency & { updateDepth: number }>
pinnedVersion?: PinnedVersion
}
export interface ResolveDependenciesOptions {
@@ -226,6 +228,7 @@ export async function resolveDependencyTree<T> (
preferredVersions: importer.preferredVersions ?? {},
wantedDependencies: importer.wantedDependencies,
options: resolveOpts,
pinnedVersion: importer.pinnedVersion,
}
})
const { pkgAddressesByImporters, time } = await resolveRootDependencies(ctx, resolveArgs)
@@ -259,11 +262,11 @@ export async function resolveDependencyTree<T> (
catalogLookup: dep.catalogLookup,
dev: resolvedPackage.dev,
name: resolvedPackage.name,
normalizedPref: dep.normalizedPref,
optional: resolvedPackage.optional,
pkgId: resolvedPackage.id,
resolution: resolvedPackage.resolution,
version: resolvedPackage.version,
specifier: dep.specifier,
}
}),
directNodeIdsByAlias: new Map(directNonLinkedDeps.map(({ alias, nodeId }) => [alias, nodeId])),
@@ -338,12 +341,12 @@ function buildTree (
function dedupeSameAliasDirectDeps (directDeps: Array<PkgAddress | LinkedDependency>, wantedDependencies: Array<WantedDependency & { isNew?: boolean }>): Array<PkgAddress | LinkedDependency> {
const deps = new Map<string, PkgAddress | LinkedDependency>()
for (const directDep of directDeps) {
const { alias, normalizedPref } = directDep
const { alias, specifier } = directDep
if (!deps.has(alias)) {
deps.set(alias, directDep)
} else {
const wantedDep = wantedDependencies.find(dep =>
dep.alias ? dep.alias === alias : dep.pref === normalizedPref
dep.alias ? dep.alias === alias : dep.pref === specifier
)
if (wantedDep?.isNew) {
deps.set(alias, directDep)

View File

@@ -1,13 +1,7 @@
import {
createVersionSpec,
type PackageSpecObject,
type PinnedVersion,
updateProjectManifestObject,
} from '@pnpm/manifest-utils'
import versionSelectorType from 'version-selector-type'
import semver from 'semver'
import { isGitHostedPkgUrl } from '@pnpm/pick-fetcher'
import { type TarballResolution } from '@pnpm/resolver-base'
import { type ProjectManifest } from '@pnpm/types'
import { type ResolvedDirectDependency } from './resolveDependencyTree'
import { type ImporterToResolve } from '.'
@@ -23,25 +17,17 @@ export async function updateProjectManifest (
if (!importer.manifest) {
throw new Error('Cannot save because no package.json found')
}
const specsToUpsert = opts.directDependencies
const specsToUpsert: PackageSpecObject[] = opts.directDependencies
.filter((rdd, index) => importer.wantedDependencies[index]?.updateSpec)
.map((rdd, index) => {
const wantedDep = importer.wantedDependencies[index]!
return resolvedDirectDepToSpecObject({
...rdd,
isNew:
wantedDep.isNew,
currentPref: wantedDep.pref,
preserveNonSemverVersionSpec: wantedDep.preserveNonSemverVersionSpec,
// For git-protocol dependencies that are already installed locally, there is no normalizedPref unless do force resolve,
// so we use pref in wantedDependency here.
normalizedPref: rdd.normalizedPref ?? (isGitHostedPkgUrl((rdd.resolution as TarballResolution).tarball ?? '') ? wantedDep.pref : undefined),
}, importer, {
return {
alias: rdd.alias,
nodeExecPath: wantedDep.nodeExecPath,
pinnedVersion: wantedDep.pinnedVersion ?? importer.pinnedVersion ?? 'major',
preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
})
peer: importer.peer,
pref: rdd.catalogLookup?.userSpecifiedPref ?? rdd.specifier ?? wantedDep.pref,
saveType: importer.targetDependenciesField,
}
})
for (const pkgToInstall of importer.wantedDependencies) {
if (pkgToInstall.updateSpec && pkgToInstall.alias && !specsToUpsert.some(({ alias }) => alias === pkgToInstall.alias)) {
@@ -67,133 +53,3 @@ export async function updateProjectManifest (
: undefined
return [hookedManifest, originalManifest]
}
function resolvedDirectDepToSpecObject (
{
alias,
catalogLookup,
isNew,
name,
normalizedPref,
resolution,
currentPref,
version,
preserveNonSemverVersionSpec,
}: ResolvedDirectDependency & { isNew?: boolean, currentPref: string, preserveNonSemverVersionSpec?: boolean },
importer: ImporterToResolve,
opts: {
nodeExecPath?: string
pinnedVersion: PinnedVersion
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean | 'rolling'
}
): PackageSpecObject {
let pref!: string
if (catalogLookup) {
pref = catalogLookup.userSpecifiedPref
} else if (normalizedPref) {
pref = normalizedPref
} else {
const shouldUseWorkspaceProtocol = resolution.type === 'directory' &&
(
Boolean(opts.saveWorkspaceProtocol) ||
(opts.preserveWorkspaceProtocol && currentPref.startsWith('workspace:'))
) &&
opts.pinnedVersion !== 'none'
if (isNew === true) {
pref = getPrefPreferSpecifiedSpec({
alias,
name,
pinnedVersion: opts.pinnedVersion,
currentPref,
version,
rolling: shouldUseWorkspaceProtocol && opts.saveWorkspaceProtocol === 'rolling',
})
} else {
pref = getPrefPreferSpecifiedExoticSpec({
alias,
name,
pinnedVersion: opts.pinnedVersion,
currentPref,
version,
rolling: shouldUseWorkspaceProtocol && opts.saveWorkspaceProtocol === 'rolling',
preserveNonSemverVersionSpec,
})
}
if (
shouldUseWorkspaceProtocol &&
!pref.startsWith('workspace:')
) {
pref = pref.replace(/^npm:/, '')
pref = `workspace:${pref}`
}
}
return {
alias,
nodeExecPath: opts.nodeExecPath,
peer: importer['peer'],
pref,
saveType: importer['targetDependenciesField'],
}
}
function getPrefPreferSpecifiedSpec (
opts: {
alias: string
name: string
version: string
currentPref: string
pinnedVersion?: PinnedVersion
rolling: boolean
}
): string {
const prefix = opts.currentPref.startsWith('npm:') ? `npm:${opts.name}@` : ''
const range = opts.currentPref.slice(prefix.length)
if (range) {
const selector = versionSelectorType(range)
if ((selector != null) && (selector.type === 'version' || selector.type === 'range')) {
return opts.currentPref
}
}
// A prerelease version is always added as an exact version
if (semver.parse(opts.version)?.prerelease.length) {
return `${prefix}${opts.version}`
}
return `${prefix}${createVersionSpec(opts.version, { pinnedVersion: opts.pinnedVersion, rolling: opts.rolling })}`
}
function getPrefPreferSpecifiedExoticSpec (
opts: {
alias: string
name: string
version: string
currentPref: string
pinnedVersion: PinnedVersion
rolling: boolean
preserveNonSemverVersionSpec?: boolean
}
): string {
let prefix = opts.currentPref.startsWith('npm:') ? `npm:${opts.name}@` : ''
let specWithoutName = opts.currentPref.slice(prefix.length)
if (specWithoutName.startsWith('workspace:')) {
prefix = 'workspace:'
specWithoutName = specWithoutName.slice(10)
if (specWithoutName === '*' || specWithoutName === '^' || specWithoutName === '~') {
return specWithoutName
}
}
const selector = versionSelectorType(specWithoutName)
if (
((selector == null) || (selector.type !== 'version' && selector.type !== 'range')) &&
opts.preserveNonSemverVersionSpec
) {
return opts.currentPref
}
// A prerelease version is always added as an exact version
if (semver.parse(opts.version)?.prerelease.length) {
return `${prefix}${opts.version}`
}
return `${prefix}${createVersionSpec(opts.version, { pinnedVersion: opts.pinnedVersion, rolling: opts.rolling })}`
}

View File

@@ -48,9 +48,6 @@
{
"path": "../../packages/types"
},
{
"path": "../../packages/which-version-is-pinned"
},
{
"path": "../../patching/config"
},

View File

@@ -1,34 +0,0 @@
import { PnpmError } from '@pnpm/error'
export type PinnedVersion = 'major' | 'minor' | 'patch' | 'none'
export const getPrefix = (alias: string, name: string): string => alias !== name ? `npm:${name}@` : ''
export function getPref (
alias: string,
name: string,
version: string | undefined,
opts: {
pinnedVersion?: PinnedVersion
}
): string {
const prefix = getPrefix(alias, name)
return `${prefix}${createVersionSpec(version, { pinnedVersion: opts.pinnedVersion })}`
}
export function createVersionSpec (version: string | undefined, opts: { pinnedVersion?: PinnedVersion, rolling?: boolean }): string {
switch (opts.pinnedVersion ?? 'major') {
case 'none':
case 'major':
if (opts.rolling) return '^'
return !version ? '*' : `^${version}`
case 'minor':
if (opts.rolling) return '~'
return !version ? '*' : `~${version}`
case 'patch':
if (opts.rolling) return '*'
return !version ? '*' : `${version}`
default:
throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${opts.pinnedVersion ?? 'undefined'}'`)
}
}

View File

@@ -6,7 +6,6 @@ import {
import { getAllUniqueSpecs } from './getAllUniqueSpecs'
import { getSpecFromPackageManifest } from './getSpecFromPackageManifest'
export * from './getPref'
export * from './updateProjectManifestObject'
export * from './getDependencyTypeFromManifest'

View File

@@ -1,35 +0,0 @@
import { getPref } from '@pnpm/manifest-utils'
test('getPref()', () => {
expect(getPref('foo', 'foo', '1.0.0', {})).toEqual('^1.0.0')
expect(
getPref('foo', 'foo', '1.0.0', {
pinnedVersion: 'major',
})
).toEqual('^1.0.0')
expect(
getPref('foo', 'foo', '2.0.0', {
pinnedVersion: 'minor',
})
).toEqual('~2.0.0')
expect(
getPref('foo', 'foo', '3.0.0', {
pinnedVersion: 'patch',
})
).toEqual('3.0.0')
expect(
getPref('foo', 'foo', '4.0.0', {
pinnedVersion: 'none',
})
).toEqual('^4.0.0')
expect(
getPref('foo', 'foo', undefined, {
pinnedVersion: 'major',
})
).toEqual('*')
})

19
pnpm-lock.yaml generated
View File

@@ -4250,16 +4250,6 @@ importers:
specifier: workspace:*
version: 'link:'
packages/which-version-is-pinned:
dependencies:
semver-utils:
specifier: 'catalog:'
version: 1.1.4
devDependencies:
'@pnpm/which-version-is-pinned':
specifier: workspace:*
version: 'link:'
patching/apply-patch:
dependencies:
'@pnpm/error':
@@ -4638,9 +4628,6 @@ importers:
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@pnpm/which-version-is-pinned':
specifier: workspace:*
version: link:../../packages/which-version-is-pinned
'@pnpm/worker':
specifier: workspace:^
version: link:../../worker
@@ -5786,9 +5773,6 @@ importers:
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@pnpm/which-version-is-pinned':
specifier: workspace:*
version: link:../../packages/which-version-is-pinned
'@pnpm/workspace.spec-parser':
specifier: workspace:*
version: link:../../workspace/spec-parser
@@ -6824,6 +6808,9 @@ importers:
semver:
specifier: 'catalog:'
version: 7.7.1
semver-utils:
specifier: 'catalog:'
version: 1.1.4
ssri:
specifier: 'catalog:'
version: 10.0.5

View File

@@ -63,7 +63,7 @@ export function createGitResolver (
return {
id,
normalizedPref: parsedSpec.normalizedPref,
specifier: parsedSpec.normalizedPref,
resolution,
resolvedVia: 'git-repository',
}

View File

@@ -19,7 +19,7 @@ test('resolveFromGit() with commit', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
normalizedPref: 'github:zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99',
specifier: 'github:zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
},
@@ -28,7 +28,7 @@ test('resolveFromGit() with commit', async () => {
})
test('resolveFromGit() with no commit', async () => {
// This is repeated twice because there was a bug which caused the normalizedPref
// This is repeated twice because there was a bug which caused the specifier
// to contain the commit hash on second call.
// The issue occurred because .hosted field (which is class from the 'hosted-git-info' package)
// was mutated. A 'committish' field was added to it.
@@ -36,7 +36,7 @@ test('resolveFromGit() with no commit', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative' }) // eslint-disable-line no-await-in-loop
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
normalizedPref: 'github:zkochan/is-negative',
specifier: 'github:zkochan/is-negative',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
},
@@ -49,7 +49,7 @@ test('resolveFromGit() with no commit, when main branch is not master', async ()
const resolveResult = await resolveFromGit({ pref: 'zoli-forks/cmd-shim' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
normalizedPref: 'github:zoli-forks/cmd-shim',
specifier: 'github:zoli-forks/cmd-shim',
resolution: {
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
},
@@ -61,7 +61,7 @@ test('resolveFromGit() with partial commit', async () => {
const resolveResult = await resolveFromGit({ pref: 'zoli-forks/cmd-shim#a00a83a' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a',
normalizedPref: 'github:zoli-forks/cmd-shim#a00a83a',
specifier: 'github:zoli-forks/cmd-shim#a00a83a',
resolution: {
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a',
},
@@ -73,7 +73,7 @@ test('resolveFromGit() with branch', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#canary' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
normalizedPref: 'github:zkochan/is-negative#canary',
specifier: 'github:zkochan/is-negative#canary',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
},
@@ -85,7 +85,7 @@ test('resolveFromGit() with branch relative to refs', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#heads/canary' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
normalizedPref: 'github:zkochan/is-negative#heads/canary',
specifier: 'github:zkochan/is-negative#heads/canary',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
},
@@ -97,7 +97,7 @@ test('resolveFromGit() with tag', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:zkochan/is-negative#2.0.1',
specifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
},
@@ -109,7 +109,7 @@ test.skip('resolveFromGit() with tag (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ pref: 'andreineculau/npm-publish-git#v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedPref: 'github:andreineculau/npm-publish-git#v0.0.7',
specifier: 'github:andreineculau/npm-publish-git#v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
},
@@ -121,7 +121,7 @@ test('resolveFromGit() with strict semver', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#semver:1.0.0' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
normalizedPref: 'github:zkochan/is-negative#semver:1.0.0',
specifier: 'github:zkochan/is-negative#semver:1.0.0',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
},
@@ -133,7 +133,7 @@ test.skip('resolveFromGit() with strict semver (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ pref: 'andreineculau/npm-publish-git#semver:v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedPref: 'github:andreineculau/npm-publish-git#semver:v0.0.7',
specifier: 'github:andreineculau/npm-publish-git#semver:v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
},
@@ -145,7 +145,7 @@ test('resolveFromGit() with range semver', async () => {
const resolveResult = await resolveFromGit({ pref: 'zkochan/is-negative#semver:^1.0.0' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72',
normalizedPref: 'github:zkochan/is-negative#semver:^1.0.0',
specifier: 'github:zkochan/is-negative#semver:^1.0.0',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/9a89df745b2ec20ae7445d3d9853ceaeef5b0b72',
},
@@ -157,7 +157,7 @@ test.skip('resolveFromGit() with range semver (v-prefixed tag)', async () => {
const resolveResult = await resolveFromGit({ pref: 'andreineculau/npm-publish-git#semver:<=v0.0.7' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
normalizedPref: 'github:andreineculau/npm-publish-git#semver:<=v0.0.7',
specifier: 'github:andreineculau/npm-publish-git#semver:<=v0.0.7',
resolution: {
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
},
@@ -169,7 +169,7 @@ test('resolveFromGit() with sub folder', async () => {
const resolveResult = await resolveFromGit({ pref: 'github:RexSkz/test-git-subfolder-fetch.git#path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22#path:/packages/simple-react-app',
normalizedPref: 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
specifier: 'github:RexSkz/test-git-subfolder-fetch#path:/packages/simple-react-app',
resolution: {
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
path: '/packages/simple-react-app',
@@ -182,7 +182,7 @@ test('resolveFromGit() with both sub folder and branch', async () => {
const resolveResult = await resolveFromGit({ pref: 'github:RexSkz/test-git-subfolder-fetch.git#beta&path:/packages/simple-react-app' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/777e8a3e78cc89bbf41fb3fd9f6cf922d5463313#path:/packages/simple-react-app',
normalizedPref: 'github:RexSkz/test-git-subfolder-fetch#beta&path:/packages/simple-react-app',
specifier: 'github:RexSkz/test-git-subfolder-fetch#beta&path:/packages/simple-react-app',
resolution: {
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/777e8a3e78cc89bbf41fb3fd9f6cf922d5463313',
path: '/packages/simple-react-app',
@@ -212,7 +212,7 @@ test('resolveFromGit() with commit from non-github repo', async () => {
const resolveResult = await resolveFromGit({ pref: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc` })
expect(resolveResult).toStrictEqual({
id: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc`,
normalizedPref: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc`,
specifier: `git+file://${localPath}#988c61e11dc8d9ca0b5580cb15291951812549dc`,
resolution: {
commit: '988c61e11dc8d9ca0b5580cb15291951812549dc',
repo: `file://${localPath}`,
@@ -230,7 +230,7 @@ test.skip('resolveFromGit() with commit from non-github repo with no commit', as
const resolveResult = await resolveFromGit({ pref: `git+file://${localPath}` })
expect(resolveResult).toStrictEqual({
id: `git+file://${localPath}#${hash}`,
normalizedPref: `git+file://${localPath}`,
specifier: `git+file://${localPath}`,
resolution: {
commit: hash,
repo: `file://${localPath}`,
@@ -249,7 +249,7 @@ test.skip('resolveFromGit() bitbucket with commit', async () => {
const resolveResult = await resolveFromGit({ pref: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'https://bitbucket.org/pnpmjs/git-resolver/get/988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
normalizedPref: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
specifier: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
},
@@ -264,7 +264,7 @@ test.skip('resolveFromGit() bitbucket with no commit', async () => {
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
normalizedPref: 'bitbucket:pnpmjs/git-resolver',
specifier: 'bitbucket:pnpmjs/git-resolver',
resolution: {
tarball: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
},
@@ -279,7 +279,7 @@ test.skip('resolveFromGit() bitbucket with branch', async () => {
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
normalizedPref: 'bitbucket:pnpmjs/git-resolver#master',
specifier: 'bitbucket:pnpmjs/git-resolver#master',
resolution: {
tarball: `https://bitbucket.org/pnpmjs/git-resolver/get/${hash}.tar.gz`,
},
@@ -292,7 +292,7 @@ test.skip('resolveFromGit() bitbucket with tag', async () => {
const resolveResult = await resolveFromGit({ pref: 'bitbucket:pnpmjs/git-resolver#0.3.4' })
expect(resolveResult).toStrictEqual({
id: 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz',
normalizedPref: 'bitbucket:pnpmjs/git-resolver#0.3.4',
specifier: 'bitbucket:pnpmjs/git-resolver#0.3.4',
resolution: {
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz',
},
@@ -304,7 +304,7 @@ test('resolveFromGit() gitlab with colon in the URL', async () => {
const resolveResult = await resolveFromGit({ pref: 'ssh://git@gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@gitlab/pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
normalizedPref: 'ssh://git@gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
specifier: 'ssh://git@gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
commit: '988c61e11dc8d9ca0b5580cb15291951812549dc',
repo: 'ssh://git@gitlab/pnpm/git-resolver',
@@ -319,7 +319,7 @@ test.skip('resolveFromGit() gitlab with commit', async () => {
const resolveResult = await resolveFromGit({ pref: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc' })
expect(resolveResult).toStrictEqual({
id: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=988c61e11dc8d9ca0b5580cb15291951812549dc',
normalizedPref: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
specifier: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
resolution: {
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=988c61e11dc8d9ca0b5580cb15291951812549dc',
},
@@ -334,7 +334,7 @@ test.skip('resolveFromGit() gitlab with no commit', async () => {
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
normalizedPref: 'gitlab:pnpm/git-resolver',
specifier: 'gitlab:pnpm/git-resolver',
resolution: {
tarball: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
},
@@ -349,7 +349,7 @@ test.skip('resolveFromGit() gitlab with branch', async () => {
const hash: string = result.stdout.trim().split('\t')[0]
expect(resolveResult).toStrictEqual({
id: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
normalizedPref: 'gitlab:pnpm/git-resolver#master',
specifier: 'gitlab:pnpm/git-resolver#master',
resolution: {
tarball: `https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=${hash}`,
},
@@ -362,7 +362,7 @@ test.skip('resolveFromGit() gitlab with tag', async () => {
const resolveResult = await resolveFromGit({ pref: 'gitlab:pnpm/git-resolver#0.3.4' })
expect(resolveResult).toStrictEqual({
id: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=87cf6a67064d2ce56e8cd20624769a5512b83ff9',
normalizedPref: 'gitlab:pnpm/git-resolver#0.3.4',
specifier: 'gitlab:pnpm/git-resolver#0.3.4',
resolution: {
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=87cf6a67064d2ce56e8cd20624769a5512b83ff9',
},
@@ -374,7 +374,7 @@ test('resolveFromGit() normalizes full url', async () => {
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@github.com:zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:zkochan/is-negative#2.0.1',
specifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
},
@@ -386,7 +386,7 @@ test('resolveFromGit() normalizes full url with port', async () => {
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@github.com:22:zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:zkochan/is-negative#2.0.1',
specifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
},
@@ -398,7 +398,7 @@ test('resolveFromGit() normalizes full url (alternative form)', async () => {
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@github.com/zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:zkochan/is-negative#2.0.1',
specifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
},
@@ -410,7 +410,7 @@ test('resolveFromGit() normalizes full url (alternative form 2)', async () => {
const resolveResult = await resolveFromGit({ pref: 'https://github.com/zkochan/is-negative.git#2.0.1' })
expect(resolveResult).toStrictEqual({
id: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:zkochan/is-negative#2.0.1',
specifier: 'github:zkochan/is-negative#2.0.1',
resolution: {
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
},
@@ -425,7 +425,7 @@ test('resolveFromGit() private repo with commit hash', async () => {
const resolveResult = await resolveFromGit({ pref: 'fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@github.com/fake/private-repo.git#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
normalizedPref: 'github:fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
specifier: 'github:fake/private-repo#2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
resolution: {
commit: '2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5',
repo: 'git+ssh://git@github.com/fake/private-repo.git',
@@ -451,7 +451,7 @@ test('resolve a private repository using the HTTPS protocol without auth token',
const resolveResult = await resolveFromGit({ pref: 'git+https://github.com/foo/bar.git' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@github.com/foo/bar.git#0000000000000000000000000000000000000000',
normalizedPref: 'github:foo/bar',
specifier: 'github:foo/bar',
resolution: {
commit: '0000000000000000000000000000000000000000',
repo: 'git+ssh://git@github.com/foo/bar.git',
@@ -473,7 +473,7 @@ test('resolve a private repository using the HTTPS protocol with a commit hash',
const resolveResult = await resolveFromGit({ pref: 'git+https://github.com/foo/bar.git#aabbccddeeff' })
expect(resolveResult).toStrictEqual({
id: 'git+https://github.com/foo/bar.git#aabbccddeeff',
normalizedPref: 'git+https://github.com/foo/bar.git',
specifier: 'git+https://github.com/foo/bar.git',
resolution: {
// cspell:ignore aabbccddeeff
commit: 'aabbccddeeff',
@@ -500,7 +500,7 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/heads/master\
const resolveResult = await resolveFromGit({ pref: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git' })
expect(resolveResult).toStrictEqual({
id: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git#0000000000000000000000000000000000000000',
normalizedPref: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git',
specifier: 'git+https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git',
resolution: {
commit: '0000000000000000000000000000000000000000',
repo: 'https://0000000000000000000000000000000000000000:x-oauth-basic@github.com/foo/bar.git',
@@ -530,7 +530,7 @@ cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@example.com/org/repo.git#cba04669e621b85fbdb33371604de1a2898e68e9',
normalizedPref: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38',
specifier: 'git+ssh://git@example.com/org/repo.git#semver:~0.0.38',
resolution: {
commit: 'cba04669e621b85fbdb33371604de1a2898e68e9',
repo: 'ssh://git@example.com/org/repo.git',
@@ -560,7 +560,7 @@ cba04669e621b85fbdb33371604de1a2898e68e9\trefs/tags/v0.0.39',
const resolveResult = await resolveFromGit({ pref: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38' })
expect(resolveResult).toStrictEqual({
id: 'git+ssh://git@example.com/org/repo.git#cba04669e621b85fbdb33371604de1a2898e68e9',
normalizedPref: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38',
specifier: 'git+ssh://git@example.com:org/repo.git#semver:~0.0.38',
resolution: {
commit: 'cba04669e621b85fbdb33371604de1a2898e68e9',
repo: 'ssh://git@example.com/org/repo.git',

View File

@@ -15,7 +15,7 @@ import { parsePref, type WantedLocalDependency } from './parsePref'
export type { WantedLocalDependency }
export interface ResolveFromLocalResult extends ResolveResult {
normalizedPref: string
specifier: string
resolution: TarballResolution | DirectoryResolution
manifest?: DependencyManifest
}
@@ -35,7 +35,7 @@ export async function resolveFromLocal (
if (spec.type === 'file') {
return {
id: spec.id,
normalizedPref: spec.normalizedPref,
specifier: spec.normalizedPref,
resolution: {
integrity: await getTarballIntegrity(spec.fetchSpec),
tarball: spec.id,
@@ -84,7 +84,7 @@ export async function resolveFromLocal (
return {
id: spec.id,
manifest: localDependencyManifest,
normalizedPref: spec.normalizedPref,
specifier: spec.normalizedPref,
resolution: {
directory: spec.dependencyPath,
type: 'directory',

View File

@@ -10,7 +10,7 @@ const TEST_DIR = path.dirname(require.resolve('@pnpm/tgz-fixtures/tgz/pnpm-local
test('resolve directory', async () => {
const resolveResult = await resolveFromLocal({ pref: '..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!.specifier).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalize(path.join(__dirname, '..')))
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -21,7 +21,7 @@ test('resolve directory specified using absolute path', async () => {
const normalizedLinkedDir = normalize(linkedDir)
const resolveResult = await resolveFromLocal({ pref: `link:${linkedDir}` }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual(`link:${normalizedLinkedDir}`)
expect(resolveResult!.specifier).toEqual(`link:${normalizedLinkedDir}`)
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalizedLinkedDir)
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -30,7 +30,7 @@ test('resolve directory specified using absolute path', async () => {
test('resolve injected directory', async () => {
const resolveResult = await resolveFromLocal({ injected: true, pref: '..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('file:..')
expect(resolveResult!.normalizedPref).toEqual('file:..')
expect(resolveResult!.specifier).toEqual('file:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual('..')
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -39,7 +39,7 @@ test('resolve injected directory', async () => {
test('resolve workspace directory', async () => {
const resolveResult = await resolveFromLocal({ pref: 'workspace:..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!.specifier).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalize(path.join(__dirname, '..')))
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -48,7 +48,7 @@ test('resolve workspace directory', async () => {
test('resolve directory specified using the file: protocol', async () => {
const resolveResult = await resolveFromLocal({ pref: 'file:..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('file:..')
expect(resolveResult!.normalizedPref).toEqual('file:..')
expect(resolveResult!.specifier).toEqual('file:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual('..')
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -57,7 +57,7 @@ test('resolve directory specified using the file: protocol', async () => {
test('resolve directory specified using the link: protocol', async () => {
const resolveResult = await resolveFromLocal({ pref: 'link:..' }, { projectDir: __dirname })
expect(resolveResult!.id).toEqual('link:..')
expect(resolveResult!.normalizedPref).toEqual('link:..')
expect(resolveResult!.specifier).toEqual('link:..')
expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver')
expect((resolveResult!.resolution as DirectoryResolution).directory).toEqual(normalize(path.join(__dirname, '..')))
expect((resolveResult!.resolution as DirectoryResolution).type).toEqual('directory')
@@ -69,7 +69,7 @@ test('resolve file', async () => {
expect(resolveResult).toEqual({
id: 'file:pnpm-local-resolver-0.1.1.tgz',
normalizedPref: 'file:pnpm-local-resolver-0.1.1.tgz',
specifier: 'file:pnpm-local-resolver-0.1.1.tgz',
resolution: {
integrity: 'sha512-UHd2zKRT/w70KKzFlj4qcT81A1Q0H7NM9uKxLzIZ/VZqJXzt5Hnnp2PYPb5Ezq/hAamoYKIn5g7fuv69kP258w==',
tarball: 'file:pnpm-local-resolver-0.1.1.tgz',
@@ -87,7 +87,7 @@ test("resolve file when lockfile directory differs from the package's dir", asyn
expect(resolveResult).toEqual({
id: 'file:tgz/pnpm-local-resolver-0.1.1.tgz',
normalizedPref: 'file:pnpm-local-resolver-0.1.1.tgz',
specifier: 'file:pnpm-local-resolver-0.1.1.tgz',
resolution: {
integrity: 'sha512-UHd2zKRT/w70KKzFlj4qcT81A1Q0H7NM9uKxLzIZ/VZqJXzt5Hnnp2PYPb5Ezq/hAamoYKIn5g7fuv69kP258w==',
tarball: 'file:tgz/pnpm-local-resolver-0.1.1.tgz',
@@ -102,7 +102,7 @@ test('resolve tarball specified with file: protocol', async () => {
expect(resolveResult).toEqual({
id: 'file:pnpm-local-resolver-0.1.1.tgz',
normalizedPref: 'file:pnpm-local-resolver-0.1.1.tgz',
specifier: 'file:pnpm-local-resolver-0.1.1.tgz',
resolution: {
integrity: 'sha512-UHd2zKRT/w70KKzFlj4qcT81A1Q0H7NM9uKxLzIZ/VZqJXzt5Hnnp2PYPb5Ezq/hAamoYKIn5g7fuv69kP258w==',
tarball: 'file:pnpm-local-resolver-0.1.1.tgz',

View File

@@ -55,6 +55,7 @@
"ramda": "catalog:",
"rename-overwrite": "catalog:",
"semver": "catalog:",
"semver-utils": "catalog:",
"ssri": "catalog:",
"version-selector-type": "catalog:"
},

View File

@@ -18,13 +18,14 @@ import {
type WorkspacePackagesByVersion,
type WorkspaceResolveResult,
} from '@pnpm/resolver-base'
import { type Registries } from '@pnpm/types'
import { type Registries, type PinnedVersion } from '@pnpm/types'
import { LRUCache } from 'lru-cache'
import normalize from 'normalize-path'
import pMemoize from 'p-memoize'
import clone from 'ramda/src/clone'
import semver from 'semver'
import ssri from 'ssri'
import versionSelectorType from 'version-selector-type'
import {
type PackageInRegistry,
type PackageMeta,
@@ -38,6 +39,7 @@ import {
} from './parsePref'
import { fromRegistry, RegistryResponseError } from './fetch'
import { workspacePrefToNpm } from './workspacePrefToNpm'
import { whichVersionIsPinned } from './whichVersionIsPinned'
export class NoMatchingVersionError extends PnpmError {
public readonly packageMeta: PackageMeta
@@ -68,6 +70,7 @@ export interface ResolverFactoryOptions {
retry?: RetryTimeoutOptions
timeout?: number
registries: Registries
saveWorkspaceProtocol?: boolean | 'rolling'
}
export type NpmResolver = (wantedDependency: WantedDependency, opts: ResolveFromNpmOptions) => Promise<ResolveResult | null>
@@ -105,6 +108,7 @@ export function createNpmResolver (
cacheDir: opts.cacheDir,
}),
registries: opts.registries,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
}),
clearCache: () => {
metaCache.clear()
@@ -123,6 +127,8 @@ export type ResolveFromNpmOptions = {
preferWorkspacePackages?: boolean
update?: false | 'compatible' | 'latest'
injectWorkspacePackages?: boolean
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
} & ({
projectDir?: string
workspacePackages?: undefined
@@ -136,6 +142,7 @@ async function resolveNpm (
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>
getAuthHeaderValueByURI: (registry: string) => string | undefined
registries: Registries
saveWorkspaceProtocol?: boolean | 'rolling'
},
wantedDependency: WantedDependency,
opts: ResolveFromNpmOptions
@@ -154,6 +161,9 @@ async function resolveNpm (
workspacePackages: opts.workspacePackages,
injectWorkspacePackages: opts.injectWorkspacePackages,
update: Boolean(opts.update),
saveWorkspaceProtocol: ctx.saveWorkspaceProtocol !== false ? ctx.saveWorkspaceProtocol : true,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
})
if (resolvedFromWorkspace != null) {
return resolvedFromWorkspace
@@ -186,6 +196,9 @@ async function resolveNpm (
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
update: Boolean(opts.update),
saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
})
} catch {
// ignore
@@ -204,6 +217,9 @@ async function resolveNpm (
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
update: Boolean(opts.update),
saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
})
} catch {
// ignore
@@ -217,10 +233,14 @@ async function resolveNpm (
const matchedPkg = workspacePkgsMatchingName.get(pickedPackage.version)
if (matchedPkg) {
return {
...resolveFromLocalPackage(matchedPkg, spec.normalizedPref, {
...resolveFromLocalPackage(matchedPkg, spec, {
wantedDependency,
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
}),
latest: meta['dist-tags'].latest,
}
@@ -228,10 +248,14 @@ async function resolveNpm (
const localVersion = pickMatchingLocalVersionOrNull(workspacePkgsMatchingName, spec)
if (localVersion && (semver.gt(localVersion, pickedPackage.version) || opts.preferWorkspacePackages)) {
return {
...resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec.normalizedPref, {
...resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec, {
wantedDependency,
projectDir: opts.projectDir,
lockfileDir: opts.lockfileDir,
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
saveWorkspaceProtocol: ctx.saveWorkspaceProtocol,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
}),
latest: meta['dist-tags'].latest,
}
@@ -243,17 +267,55 @@ async function resolveNpm (
integrity: getIntegrity(pickedPackage.dist),
tarball: pickedPackage.dist.tarball,
}
let specifier: string | undefined
if (opts.calcSpecifier) {
specifier = spec.normalizedPref ?? calcSpecifier({
wantedDependency,
spec,
version: pickedPackage.version,
defaultPinnedVersion: opts.pinnedVersion,
})
}
return {
id,
latest: meta['dist-tags'].latest,
manifest: pickedPackage,
normalizedPref: spec.normalizedPref,
resolution,
resolvedVia: 'npm-registry',
publishedAt: meta.time?.[pickedPackage.version],
specifier,
}
}
function calcSpecifier ({
wantedDependency,
spec,
version,
defaultPinnedVersion,
}: {
wantedDependency: WantedDependency
spec: RegistryPackageSpec
version: string
defaultPinnedVersion?: PinnedVersion
}): string {
if (wantedDependency.prevSpecifier === wantedDependency.pref && wantedDependency.prevSpecifier && versionSelectorType(wantedDependency.prevSpecifier)?.type === 'tag') {
return wantedDependency.prevSpecifier
}
const range = calcRange(version, wantedDependency, defaultPinnedVersion)
if (!wantedDependency.alias || spec.name === wantedDependency.alias) return range
return `npm:${spec.name}@${range}`
}
function calcRange (version: string, wantedDependency: WantedDependency, defaultPinnedVersion?: PinnedVersion): string {
if (semver.parse(version)?.prerelease.length) {
return version
}
const pinnedVersion = (wantedDependency.prevSpecifier ? whichVersionIsPinned(wantedDependency.prevSpecifier) : undefined) ??
(wantedDependency.pref ? whichVersionIsPinned(wantedDependency.pref) : undefined) ??
defaultPinnedVersion
return createVersionSpec(version, pinnedVersion)
}
function tryResolveFromWorkspace (
wantedDependency: WantedDependency,
opts: {
@@ -264,6 +326,9 @@ function tryResolveFromWorkspace (
workspacePackages?: WorkspacePackages
injectWorkspacePackages?: boolean
update?: boolean
saveWorkspaceProtocol?: boolean | 'rolling'
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
}
): WorkspaceResolveResult | null {
if (!wantedDependency.pref?.startsWith('workspace:')) {
@@ -285,6 +350,9 @@ function tryResolveFromWorkspace (
hardLinkLocalPackages: opts.injectWorkspacePackages === true || wantedDependency.injected,
lockfileDir: opts.lockfileDir,
update: opts.update,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
calcSpecifier: opts.calcSpecifier,
pinnedVersion: opts.pinnedVersion,
})
}
@@ -297,6 +365,9 @@ function tryResolveFromWorkspacePackages (
projectDir: string
lockfileDir?: string
update?: boolean
saveWorkspaceProtocol?: boolean | 'rolling'
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
}
): WorkspaceResolveResult {
const workspacePkgsMatchingName = workspacePackages.get(spec.name)
@@ -319,7 +390,7 @@ function tryResolveFromWorkspacePackages (
`In ${path.relative(process.cwd(), opts.projectDir)}: No matching version found for ${opts.wantedDependency.alias ?? ''}@${opts.wantedDependency.pref ?? ''} inside the workspace`
)
}
return resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec.normalizedPref, opts)
return resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec, opts)
}
function pickMatchingLocalVersionOrNull (
@@ -342,11 +413,15 @@ function pickMatchingLocalVersionOrNull (
function resolveFromLocalPackage (
localPackage: WorkspacePackage,
normalizedPref: string | undefined,
spec: RegistryPackageSpec,
opts: {
wantedDependency: WantedDependency
hardLinkLocalPackages?: boolean
projectDir: string
lockfileDir?: string
saveWorkspaceProtocol?: boolean | 'rolling'
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
}
): WorkspaceResolveResult {
let id!: PkgResolutionId
@@ -359,18 +434,67 @@ function resolveFromLocalPackage (
directory = localPackageDir
id = `link:${normalize(path.relative(opts.projectDir, localPackageDir))}` as PkgResolutionId
}
let specifier: string | undefined
if (opts.calcSpecifier) {
specifier = spec.normalizedPref ?? calcSpecifierForWorkspaceDep({
wantedDependency: opts.wantedDependency,
spec,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
version: localPackage.manifest.version,
defaultPinnedVersion: opts.pinnedVersion,
})
}
return {
id,
manifest: clone(localPackage.manifest),
normalizedPref,
resolution: {
directory,
type: 'directory',
},
resolvedVia: 'workspace',
specifier,
}
}
function calcSpecifierForWorkspaceDep ({
wantedDependency,
spec,
saveWorkspaceProtocol,
version,
defaultPinnedVersion,
}: {
wantedDependency: WantedDependency
spec: RegistryPackageSpec
saveWorkspaceProtocol: boolean | 'rolling' | undefined
version: string
defaultPinnedVersion?: PinnedVersion
}): string {
if (!saveWorkspaceProtocol && !wantedDependency.pref?.startsWith('workspace:')) {
return calcSpecifier({ wantedDependency, spec, version, defaultPinnedVersion })
}
const prefix = (!wantedDependency.alias || spec.name === wantedDependency.alias) ? 'workspace:' : `workspace:${spec.name}@`
if (saveWorkspaceProtocol === 'rolling') {
const specifier = wantedDependency.prevSpecifier ?? wantedDependency.pref
if (specifier) {
if ([`${prefix}*`, `${prefix}^`, `${prefix}~`].includes(specifier)) return specifier
const pinnedVersion = whichVersionIsPinned(specifier)
switch (pinnedVersion) {
case 'major': return `${prefix}^`
case 'minor': return `${prefix}~`
case 'patch':
case 'none': return `${prefix}*`
}
}
return `${prefix}^`
}
if (semver.parse(version)?.prerelease.length) {
return `${prefix}${version}`
}
const pinnedVersion = (wantedDependency.prevSpecifier ? whichVersionIsPinned(wantedDependency.prevSpecifier) : undefined) ?? defaultPinnedVersion
const range = createVersionSpec(version, pinnedVersion)
return `${prefix}${range}`
}
function resolveLocalPackageDir (localPackage: WorkspacePackage): string {
if (
localPackage.manifest.publishConfig?.directory == null ||
@@ -404,3 +528,17 @@ function getIntegrity (dist: {
}
return integrity.toString()
}
function createVersionSpec (version: string, pinnedVersion?: PinnedVersion): string {
switch (pinnedVersion ?? 'major') {
case 'none':
case 'major':
return `^${version}`
case 'minor':
return `~${version}`
case 'patch':
return version
default:
throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${pinnedVersion ?? 'undefined'}'`)
}
}

View File

@@ -1,19 +1,16 @@
import { type PinnedVersion } from '@pnpm/types'
import { parseRange } from 'semver-utils'
export type PinnedVersion =
| 'none'
| 'patch'
| 'minor'
| 'major'
export function whichVersionIsPinned (spec: string): PinnedVersion | undefined {
const isWorkspaceProtocol = spec.startsWith('workspace:')
if (isWorkspaceProtocol) spec = spec.slice('workspace:'.length)
if (spec === '*') return isWorkspaceProtocol ? 'patch' : 'none'
if (spec.startsWith('npm:')) {
const index = spec.lastIndexOf('@')
const colonIndex = spec.indexOf(':')
if (colonIndex !== -1) {
spec = spec.substring(colonIndex + 1)
}
const index = spec.lastIndexOf('@')
if (index !== -1) {
spec = spec.slice(index + 1)
}
if (spec === '*') return 'none'
const parsedRange = parseRange(spec)
if (parsedRange.length !== 1) return undefined
const versionObject = parsedRange[0]
@@ -23,6 +20,7 @@ export function whichVersionIsPinned (spec: string): PinnedVersion | undefined {
case undefined:
if (versionObject.patch) return 'patch'
if (versionObject.minor) return 'minor'
if (versionObject.major) return 'major'
}
return undefined
}

View File

@@ -74,10 +74,11 @@ test('resolveFromNpm()', async () => {
cacheDir,
registries,
})
const resolveResult = await resolveFromNpm({ alias: 'is-positive', pref: '1.0.0' }, {})
const resolveResult = await resolveFromNpm({ alias: 'is-positive', pref: '1.0.0' }, { calcSpecifier: true })
expect(resolveResult!.resolvedVia).toBe('npm-registry')
expect(resolveResult!.id).toBe('is-positive@1.0.0')
expect(resolveResult!.specifier).toBe('1.0.0')
expect(resolveResult!.latest!.split('.').length).toBe(3)
expect(resolveResult!.resolution).toStrictEqual({
integrity: 'sha512-9cI+DmhNhA8ioT/3EJFnt0s1yehnAECyIOXdT+2uQGzcEEBaj8oNmVWj33+ZjPndMIFRQh8JeJlEu1uv5/J7pQ==',
@@ -273,8 +274,10 @@ test('can resolve aliased dependency w/o version specifier to default tag', asyn
})
const resolveResult = await resolveFromNpm({ alias: 'positive', pref: 'npm:is-positive' }, {
defaultTag: 'stable',
calcSpecifier: true,
})
expect(resolveResult!.id).toBe('is-positive@3.0.0')
expect(resolveResult!.specifier).toBe('npm:is-positive@^3.0.0')
})
test('can resolve aliased scoped dependency', async () => {
@@ -940,7 +943,12 @@ test('resolve when tarball URL is requested from the registry', async () => {
cacheDir,
registries,
})
const resolveResult = await resolveFromNpm({ alias: 'is-positive', pref: `${registries.default}is-positive/-/is-positive-1.0.0.tgz` }, {})
const resolveResult = await resolveFromNpm({
alias: 'is-positive',
pref: `${registries.default}is-positive/-/is-positive-1.0.0.tgz`,
}, {
calcSpecifier: true,
})
expect(resolveResult!.resolvedVia).toBe('npm-registry')
expect(resolveResult!.id).toBe('is-positive@1.0.0')
@@ -952,7 +960,7 @@ test('resolve when tarball URL is requested from the registry', async () => {
expect(resolveResult!.manifest).toBeTruthy()
expect(resolveResult!.manifest!.name).toBe('is-positive')
expect(resolveResult!.manifest!.version).toBe('1.0.0')
expect(resolveResult!.normalizedPref).toBe(`${registries.default}is-positive/-/is-positive-1.0.0.tgz`)
expect(resolveResult!.specifier).toBe(`${registries.default}is-positive/-/is-positive-1.0.0.tgz`)
// The resolve function does not wait for the package meta cache file to be saved
// so we must delay for a bit in order to read it
@@ -972,7 +980,7 @@ test('resolve when tarball URL is requested from the registry and alias is not s
cacheDir,
registries,
})
const resolveResult = await resolveFromNpm({ pref: `${registries.default}is-positive/-/is-positive-1.0.0.tgz` }, {})
const resolveResult = await resolveFromNpm({ pref: `${registries.default}is-positive/-/is-positive-1.0.0.tgz` }, { calcSpecifier: true })
expect(resolveResult!.resolvedVia).toBe('npm-registry')
expect(resolveResult!.id).toBe('is-positive@1.0.0')
@@ -984,7 +992,7 @@ test('resolve when tarball URL is requested from the registry and alias is not s
expect(resolveResult!.manifest).toBeTruthy()
expect(resolveResult!.manifest!.name).toBe('is-positive')
expect(resolveResult!.manifest!.version).toBe('1.0.0')
expect(resolveResult!.normalizedPref).toBe(`${registries.default}is-positive/-/is-positive-1.0.0.tgz`)
expect(resolveResult!.specifier).toBe(`${registries.default}is-positive/-/is-positive-1.0.0.tgz`)
// The resolve function does not wait for the package meta cache file to be saved
// so we must delay for a bit in order to read it

View File

@@ -1,4 +1,4 @@
import { whichVersionIsPinned } from '@pnpm/which-version-is-pinned'
import { whichVersionIsPinned } from '../lib/whichVersionIsPinned'
test.each([
['^1.0.0', 'major'],

View File

@@ -2,6 +2,7 @@ import {
type ProjectRootDir,
type DependencyManifest,
type PkgResolutionId,
type PinnedVersion,
} from '@pnpm/types'
export { type PkgResolutionId }
@@ -42,9 +43,9 @@ export interface ResolveResult {
latest?: string
publishedAt?: string
manifest?: DependencyManifest
normalizedPref?: string // is null for npm-hosted dependencies
resolution: Resolution
resolvedVia: 'npm-registry' | 'git-repository' | 'local-filesystem' | 'workspace' | 'url' | string
specifier?: string
}
/**
@@ -99,10 +100,13 @@ export interface ResolveOptions {
workspacePackages?: WorkspacePackages
update?: false | 'compatible' | 'latest'
injectWorkspacePackages?: boolean
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
}
export type WantedDependency = {
injected?: boolean
prevSpecifier?: string
} & ({
alias?: string
pref: string

View File

@@ -16,7 +16,7 @@ export async function resolveFromTarball (
return {
id: resolvedUrl as PkgResolutionId,
normalizedPref: resolvedUrl,
specifier: resolvedUrl,
resolution: {
tarball: resolvedUrl,
},

View File

@@ -11,7 +11,7 @@ test('tarball from npm registry', async () => {
expect(resolutionResult).toStrictEqual({
id: 'https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz',
normalizedPref: 'https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz',
specifier: 'https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz',
resolution: {
tarball: 'https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz',
},
@@ -26,7 +26,7 @@ test('tarball from URL that contain port number', async () => {
expect(resolutionResult).toStrictEqual({
id: 'http://buildserver.mycompany.com:81/my-private-package-0.1.6.tgz',
normalizedPref: 'http://buildserver.mycompany.com:81/my-private-package-0.1.6.tgz',
specifier: 'http://buildserver.mycompany.com:81/my-private-package-0.1.6.tgz',
resolution: {
tarball: 'http://buildserver.mycompany.com:81/my-private-package-0.1.6.tgz',
},
@@ -39,7 +39,7 @@ test('tarball not from npm registry', async () => {
expect(resolutionResult).toStrictEqual({
id: 'https://codeload.github.com/hegemonic/taffydb/legacy.tar.gz/refs/heads/master',
normalizedPref: 'https://codeload.github.com/hegemonic/taffydb/legacy.tar.gz/refs/heads/master',
specifier: 'https://codeload.github.com/hegemonic/taffydb/legacy.tar.gz/refs/heads/master',
resolution: {
tarball: 'https://codeload.github.com/hegemonic/taffydb/legacy.tar.gz/refs/heads/master',
},
@@ -52,7 +52,7 @@ test('tarballs from GitHub (is-negative)', async () => {
expect(resolutionResult).toStrictEqual({
id: 'https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
normalizedPref: 'https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
specifier: 'https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
resolution: {
tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
},

View File

@@ -37,6 +37,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
| 'registries'
| 'registrySupportsTimeField'
| 'resolutionMode'
| 'saveWorkspaceProtocol'
| 'strictSsl'
| 'unsafePerm'
| 'userAgent'
@@ -90,6 +91,7 @@ export async function createNewStoreController (
gitShallowHosts: opts.gitShallowHosts,
resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs,
includeOnlyPackageFiles: !opts.deployAllFiles,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
})
await fs.mkdir(opts.storeDir, { recursive: true })
return {

View File

@@ -17,6 +17,7 @@ import {
type SupportedArchitectures,
type DependencyManifest,
type PackageManifest,
type PinnedVersion,
} from '@pnpm/types'
export type { PackageFileInfo, PackageFilesResponse, ImportPackageFunction, ImportPackageFunctionAsync }
@@ -131,6 +132,8 @@ export interface RequestPackageOptions {
supportedArchitectures?: SupportedArchitectures
onFetchError?: OnFetchError
injectWorkspacePackages?: boolean
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
}
export type BundledManifestFunction = () => Promise<BundledManifest | undefined>
@@ -144,7 +147,7 @@ export interface PackageResponse {
resolution: Resolution
manifest?: PackageManifest
id: PkgResolutionId
normalizedPref?: string
specifier?: string
updated: boolean
publishedAt?: string
resolvedVia?: string