From ae32d313e49c7c83cfe57b640d3b78624bacfac6 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Wed, 8 Dec 2021 13:27:05 +0200 Subject: [PATCH] refactor: `@pnpm/resolve-dependencies` and `@pnpm/core` (#4085) --- .changeset/afraid-bikes-sip.md | 5 + .changeset/hungry-seas-itch.md | 5 + packages/core/package.json | 3 +- .../core/src/install/getPreferredVersions.ts | 33 +--- packages/core/src/install/index.ts | 141 ++---------------- packages/core/src/install/link.ts | 4 +- packages/core/src/parseWantedDependencies.ts | 6 +- packages/core/tsconfig.json | 3 + packages/resolve-dependencies/package.json | 3 + .../src}/getWantedDependencies.ts | 4 +- packages/resolve-dependencies/src/index.ts | 22 ++- .../src/resolveDependencyTree.ts | 14 +- .../src/safeIsInnerLink.ts | 3 +- .../src/toResolveImporter.ts | 141 ++++++++++++++++++ packages/resolve-dependencies/tsconfig.json | 3 + packages/which-version-is-pinned/README.md | 22 +++ .../which-version-is-pinned/jest.config.js | 1 + packages/which-version-is-pinned/package.json | 34 +++++ .../src/index.ts} | 2 +- .../which-version-is-pinned/test/index.ts | 11 ++ .../which-version-is-pinned/tsconfig.json | 12 ++ .../tsconfig.lint.json | 8 + pnpm-lock.yaml | 21 ++- 23 files changed, 316 insertions(+), 185 deletions(-) create mode 100644 .changeset/afraid-bikes-sip.md create mode 100644 .changeset/hungry-seas-itch.md rename packages/{core/src/install => resolve-dependencies/src}/getWantedDependencies.ts (93%) rename packages/{core => resolve-dependencies}/src/safeIsInnerLink.ts (89%) create mode 100644 packages/resolve-dependencies/src/toResolveImporter.ts create mode 100644 packages/which-version-is-pinned/README.md create mode 100644 packages/which-version-is-pinned/jest.config.js create mode 100644 packages/which-version-is-pinned/package.json rename packages/{core/src/guessPinnedVersionFromExistingSpec.ts => which-version-is-pinned/src/index.ts} (86%) create mode 100644 packages/which-version-is-pinned/test/index.ts create mode 100644 packages/which-version-is-pinned/tsconfig.json create mode 100644 packages/which-version-is-pinned/tsconfig.lint.json diff --git a/.changeset/afraid-bikes-sip.md b/.changeset/afraid-bikes-sip.md new file mode 100644 index 0000000000..57579b4196 --- /dev/null +++ b/.changeset/afraid-bikes-sip.md @@ -0,0 +1,5 @@ +--- +"@pnpm/which-version-is-pinned": major +--- + +Initial release. diff --git a/.changeset/hungry-seas-itch.md b/.changeset/hungry-seas-itch.md new file mode 100644 index 0000000000..eed4a5a0a3 --- /dev/null +++ b/.changeset/hungry-seas-itch.md @@ -0,0 +1,5 @@ +--- +"@pnpm/resolve-dependencies": major +--- + +Breaking changes to the API. New required options added: `defaultUpdateDepth` and `preferredVersions`. diff --git a/packages/core/package.json b/packages/core/package.json index 9661bc4f22..e9f83032da 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,12 +46,12 @@ "@pnpm/store-controller-types": "workspace:11.0.7", "@pnpm/symlink-dependency": "workspace:4.0.8", "@pnpm/types": "workspace:7.6.0", + "@pnpm/which-version-is-pinned": "workspace:0.0.0", "@zkochan/npm-package-arg": "^2.0.1", "@zkochan/rimraf": "^2.1.1", "dependency-path": "workspace:8.0.6", "graph-sequencer": "2.0.0", "is-inner-link": "^4.0.0", - "is-subdir": "^1.1.1", "load-json-file": "^6.2.0", "normalize-path": "^3.0.0", "p-every": "^2.0.0", @@ -62,7 +62,6 @@ "ramda": "^0.27.1", "run-groups": "^3.0.1", "semver": "^7.3.4", - "semver-utils": "^1.1.4", "version-selector-type": "^3.0.0" }, "devDependencies": { diff --git a/packages/core/src/install/getPreferredVersions.ts b/packages/core/src/install/getPreferredVersions.ts index a58d40e08d..ec3278767b 100644 --- a/packages/core/src/install/getPreferredVersions.ts +++ b/packages/core/src/install/getPreferredVersions.ts @@ -1,8 +1,7 @@ import { nameVerFromPkgSnapshot, PackageSnapshots } from '@pnpm/lockfile-utils' import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils' import { PreferredVersions } from '@pnpm/resolver-base' -import { Dependencies, DependencyManifest, ProjectManifest } from '@pnpm/types' -import getVerSelType from 'version-selector-type' +import { DependencyManifest } from '@pnpm/types' export function getAllUniqueSpecs (manifests: DependencyManifest[]) { const allSpecs: Record = {} @@ -22,36 +21,6 @@ export function getAllUniqueSpecs (manifests: DependencyManifest[]) { return allSpecs } -export default function getPreferredVersionsFromPackage ( - pkg: Pick -): PreferredVersions { - return getVersionSpecsByRealNames(getAllDependenciesFromManifest(pkg)) -} - -function getVersionSpecsByRealNames (deps: Dependencies) { - return Object.keys(deps) - .reduce((acc, depName) => { - if (deps[depName].startsWith('npm:')) { - const pref = deps[depName].substr(4) - const index = pref.lastIndexOf('@') - const spec = pref.substr(index + 1) - const selector = getVerSelType(spec) - if (selector != null) { - const pkgName = pref.substr(0, index) - acc[pkgName] = acc[pkgName] || {} - acc[pkgName][selector.normalized] = selector.type - } - } else if (!deps[depName].includes(':')) { // we really care only about semver specs - const selector = getVerSelType(deps[depName]) - if (selector != null) { - acc[depName] = acc[depName] || {} - acc[depName][selector.normalized] = selector.type - } - } - return acc - }, {}) -} - export function getPreferredVersionsFromLockfile (snapshots: PackageSnapshots): PreferredVersions { const preferredVersions: PreferredVersions = {} for (const [depPath, snapshot] of Object.entries(snapshots)) { diff --git a/packages/core/src/install/index.ts b/packages/core/src/install/index.ts index cdf0722557..e40f16c34f 100644 --- a/packages/core/src/install/index.ts +++ b/packages/core/src/install/index.ts @@ -38,9 +38,12 @@ import resolveDependencies, { DependenciesGraph, DependenciesGraphNode, } from '@pnpm/resolve-dependencies' +import getWantedDependencies, { + PinnedVersion, + WantedDependency, +} from '@pnpm/resolve-dependencies/lib/getWantedDependencies' import { PreferredVersions, - WorkspacePackages, } from '@pnpm/resolver-base' import { DependenciesField, @@ -61,7 +64,6 @@ import pipeWith from 'ramda/src/pipeWith' import props from 'ramda/src/props' import unnest from 'ramda/src/unnest' import parseWantedDependencies from '../parseWantedDependencies' -import safeIsInnerLink from '../safeIsInnerLink' import removeDeps from '../uninstall/removeDeps' import allProjectsAreUpToDate from './allProjectsAreUpToDate' import createPackageExtender from './createPackageExtender' @@ -70,11 +72,7 @@ import extendOptions, { InstallOptions, StrictInstallOptions, } from './extendInstallOptions' -import getPreferredVersionsFromPackage, { getPreferredVersionsFromLockfile, getAllUniqueSpecs } from './getPreferredVersions' -import getWantedDependencies, { - PinnedVersion, - WantedDependency, -} from './getWantedDependencies' +import { getPreferredVersionsFromLockfile, getAllUniqueSpecs } from './getPreferredVersions' import linkPackages from './link' const BROKEN_LOCKFILE_INTEGRITY_ERRORS = new Set([ @@ -561,51 +559,6 @@ function pkgHasDependencies (manifest: ProjectManifest) { ) } -async function partitionLinkedPackages ( - dependencies: WantedDependency[], - opts: { - projectDir: string - lockfileOnly: boolean - modulesDir: string - storeDir: string - virtualStoreDir: string - workspacePackages?: WorkspacePackages - } -) { - const nonLinkedDependencies: WantedDependency[] = [] - const linkedAliases = new Set() - for (const dependency of dependencies) { - if ( - !dependency.alias || - opts.workspacePackages?.[dependency.alias] != null || - dependency.pref.startsWith('workspace:') - ) { - nonLinkedDependencies.push(dependency) - continue - } - const isInnerLink = await safeIsInnerLink(opts.modulesDir, dependency.alias, { - hideAlienModules: !opts.lockfileOnly, - projectDir: opts.projectDir, - storeDir: opts.storeDir, - virtualStoreDir: opts.virtualStoreDir, - }) - if (isInnerLink === true) { - nonLinkedDependencies.push(dependency) - continue - } - // This info-log might be better to be moved to the reporter - logger.info({ - message: `${dependency.alias} is linked to ${opts.modulesDir} from ${isInnerLink}`, - prefix: opts.projectDir, - }) - linkedAliases.add(dependency.alias) - } - return { - linkedAliases, - nonLinkedDependencies, - } -} - // If the specifier is new, the old resolution probably does not satisfy it anymore. // By removing these resolutions we ensure that they are resolved again using the new specs. function forgetResolutionsOfPrevWantedDeps (importer: ProjectSnapshot, wantedDeps: WantedDependency[]) { @@ -664,7 +617,7 @@ export type ImporterToUpdate = { pruneDirectDependencies: boolean removePackages?: string[] updatePackageManifest: boolean - wantedDependencies: Array + wantedDependencies: Array } & DependenciesMutation type InstallFunction = ( @@ -736,16 +689,6 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { opts.force || opts.needsFullResolution || ctx.lockfileHadConflicts - const _toResolveImporter = toResolveImporter.bind(null, { - defaultUpdateDepth: (opts.update || (opts.updateMatching != null)) ? opts.depth : -1, - lockfileOnly: opts.lockfileOnly, - preferredVersions, - storeDir: ctx.storeDir, - updateAll: Boolean(opts.updateMatching), - virtualStoreDir: ctx.virtualStoreDir, - workspacePackages: opts.workspacePackages, - }) - const projectsToResolve = await Promise.all(projects.map(async (project) => _toResolveImporter(project))) // Ignore some fields when fixing lockfile, so these fields can be regenerated // and make sure it's up-to-date @@ -776,9 +719,10 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, } = await resolveDependencies( - projectsToResolve, + projects, { currentLockfile: ctx.currentLockfile, + defaultUpdateDepth: (opts.update || (opts.updateMatching != null)) ? opts.depth : -1, dryRun: opts.lockfileOnly, engineStrict: opts.engineStrict, force: opts.force, @@ -790,6 +734,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { nodeVersion: opts.nodeVersion, pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '', preferWorkspacePackages: opts.preferWorkspacePackages, + preferredVersions, preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol, registries: ctx.registries, saveWorkspaceProtocol: opts.saveWorkspaceProtocol, @@ -819,7 +764,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { const lockfileOpts = { forceSharedFormat: opts.forceSharedLockfile } if (!opts.lockfileOnly && opts.enableModulesDir) { const result = await linkPackages( - projectsToResolve, + projects, dependenciesGraph, { currentLockfile: ctx.currentLockfile, @@ -910,7 +855,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { }) } - await Promise.all(projectsToResolve.map(async (project, index) => { + await Promise.all(projects.map(async (project, index) => { let linkedPackages!: string[] if (ctx.publicHoistPattern?.length && path.relative(project.rootDir, opts.lockfileDir) === '') { const nodeExecPathByAlias = Object.entries(project.manifest.dependenciesMeta ?? {}) @@ -1018,7 +963,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { return { newLockfile, - projects: projectsToResolve.map(({ manifest, rootDir }) => ({ rootDir, manifest })), + projects: projects.map(({ manifest, rootDir }) => ({ rootDir, manifest })), } } @@ -1040,68 +985,6 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { } } -async function toResolveImporter ( - opts: { - defaultUpdateDepth: number - lockfileOnly: boolean - preferredVersions?: PreferredVersions - storeDir: string - updateAll: boolean - virtualStoreDir: string - workspacePackages: WorkspacePackages - }, - project: ImporterToUpdate -) { - const allDeps = getWantedDependencies(project.manifest) - const { nonLinkedDependencies } = await partitionLinkedPackages(allDeps, { - lockfileOnly: opts.lockfileOnly, - modulesDir: project.modulesDir, - projectDir: project.rootDir, - storeDir: opts.storeDir, - virtualStoreDir: opts.virtualStoreDir, - workspacePackages: opts.workspacePackages, - }) - const existingDeps = nonLinkedDependencies - .filter(({ alias }) => !project.wantedDependencies.some((wantedDep) => wantedDep.alias === alias)) - let wantedDependencies!: Array - if (!project.manifest) { - wantedDependencies = [ - ...project.wantedDependencies, - ...existingDeps, - ] - .map((dep) => ({ - ...dep, - updateDepth: opts.defaultUpdateDepth, - })) - } else { - // Direct local tarballs are always checked, - // so their update depth should be at least 0 - const updateLocalTarballs = (dep: WantedDependency) => ({ - ...dep, - updateDepth: opts.updateAll - ? opts.defaultUpdateDepth - : (prefIsLocalTarball(dep.pref) ? 0 : -1), - }) - wantedDependencies = [ - ...project.wantedDependencies.map( - opts.defaultUpdateDepth < 0 - ? updateLocalTarballs - : (dep) => ({ ...dep, updateDepth: opts.defaultUpdateDepth })), - ...existingDeps.map(updateLocalTarballs), - ] - } - return { - ...project, - hasRemovedDependencies: Boolean(project.removePackages?.length), - preferredVersions: opts.preferredVersions ?? (project.manifest && getPreferredVersionsFromPackage(project.manifest)) ?? {}, - wantedDependencies, - } -} - -function prefIsLocalTarball (pref: string) { - return pref.startsWith('file:') && pref.endsWith('.tgz') -} - const limitLinking = pLimit(16) async function linkAllBins ( diff --git a/packages/core/src/install/link.ts b/packages/core/src/install/link.ts index 77eccf35ae..a61cf84074 100644 --- a/packages/core/src/install/link.ts +++ b/packages/core/src/install/link.ts @@ -19,7 +19,6 @@ import { DependenciesGraph, DependenciesGraphNode, LinkedDependency, - ImporterToResolve, } from '@pnpm/resolve-dependencies' import { StoreController } from '@pnpm/store-controller-types' import symlinkDependency, { symlinkDirectRootDependency } from '@pnpm/symlink-dependency' @@ -34,11 +33,12 @@ import equals from 'ramda/src/equals' import difference from 'ramda/src/difference' import omit from 'ramda/src/omit' import props from 'ramda/src/props' +import { ImporterToUpdate } from './index' const brokenModulesLogger = logger('_broken_node_modules') export default async function linkPackages ( - projects: ImporterToResolve[], + projects: ImporterToUpdate[], depGraph: DependenciesGraph, opts: { currentLockfile: Lockfile diff --git a/packages/core/src/parseWantedDependencies.ts b/packages/core/src/parseWantedDependencies.ts index a0ac748691..86977ea637 100644 --- a/packages/core/src/parseWantedDependencies.ts +++ b/packages/core/src/parseWantedDependencies.ts @@ -1,7 +1,7 @@ import parseWantedDependency from '@pnpm/parse-wanted-dependency' import { Dependencies } from '@pnpm/types' -import guessPinnedVersionFromExistingSpec from './guessPinnedVersionFromExistingSpec' -import { PinnedVersion, WantedDependency } from './install/getWantedDependencies' +import whichVersionIsPinned from '@pnpm/which-version-is-pinned' +import { PinnedVersion, WantedDependency } from '@pnpm/resolve-dependencies/lib/getWantedDependencies' export default function parseWantedDependencies ( rawWantedDependencies: string[], @@ -34,7 +34,7 @@ export default function parseWantedDependencies ( ? 'workspace:*' : opts.currentPrefs[alias] } - pinnedVersion = guessPinnedVersionFromExistingSpec(opts.currentPrefs[alias]) + pinnedVersion = whichVersionIsPinned(opts.currentPrefs[alias]) } const result = { alias, diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index fa425698d4..8b2cbf15ae 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -125,6 +125,9 @@ }, { "path": "../types" + }, + { + "path": "../which-version-is-pinned" } ] } diff --git a/packages/resolve-dependencies/package.json b/packages/resolve-dependencies/package.json index 55ed2e6d72..ff6a201e8a 100644 --- a/packages/resolve-dependencies/package.json +++ b/packages/resolve-dependencies/package.json @@ -42,9 +42,12 @@ "@pnpm/resolver-base": "workspace:8.1.1", "@pnpm/store-controller-types": "workspace:11.0.7", "@pnpm/types": "workspace:7.6.0", + "@pnpm/which-version-is-pinned": "workspace:0.0.0", "dependency-path": "workspace:8.0.6", "encode-registry": "^3.0.0", "get-npm-tarball-url": "^2.0.3", + "is-inner-link": "^4.0.0", + "is-subdir": "^1.1.1", "path-exists": "^4.0.0", "ramda": "^0.27.1", "replace-string": "^3.1.0", diff --git a/packages/core/src/install/getWantedDependencies.ts b/packages/resolve-dependencies/src/getWantedDependencies.ts similarity index 93% rename from packages/core/src/install/getWantedDependencies.ts rename to packages/resolve-dependencies/src/getWantedDependencies.ts index 2637af7189..8645837ea9 100644 --- a/packages/core/src/install/getWantedDependencies.ts +++ b/packages/resolve-dependencies/src/getWantedDependencies.ts @@ -5,7 +5,7 @@ import { IncludedDependencies, ProjectManifest, } from '@pnpm/types' -import guessPinnedVersionFromExistingSpec from '../guessPinnedVersionFromExistingSpec' +import whichVersionIsPinned from '@pnpm/which-version-is-pinned' export type PinnedVersion = 'major' | 'minor' | 'patch' | 'none' @@ -71,7 +71,7 @@ function getWantedDependenciesFromGivenSet ( injected: opts.dependenciesMeta[alias]?.injected, optional: depType === 'optional', nodeExecPath: opts.nodeExecPath ?? opts.dependenciesMeta[alias]?.node, - pinnedVersion: guessPinnedVersionFromExistingSpec(deps[alias]), + pinnedVersion: whichVersionIsPinned(deps[alias]), pref, raw: `${alias}@${pref}`, } diff --git a/packages/resolve-dependencies/src/index.ts b/packages/resolve-dependencies/src/index.ts index c795f028c8..d8972ebaba 100644 --- a/packages/resolve-dependencies/src/index.ts +++ b/packages/resolve-dependencies/src/index.ts @@ -33,6 +33,7 @@ import resolvePeers, { GenericDependenciesGraph, GenericDependenciesGraphNode, } from './resolvePeers' +import toResolveImporter from './toResolveImporter' import updateLockfile from './updateLockfile' import updateProjectManifest from './updateProjectManifest' @@ -73,27 +74,37 @@ export type ImporterToResolve = Importer<{ export default async function ( importers: ImporterToResolve[], opts: ResolveDependenciesOptions & { + defaultUpdateDepth: number preserveWorkspaceProtocol: boolean saveWorkspaceProtocol: boolean strictPeerDependencies: boolean } ) { + const _toResolveImporter = toResolveImporter.bind(null, { + defaultUpdateDepth: opts.defaultUpdateDepth, + lockfileOnly: opts.dryRun, + preferredVersions: opts.preferredVersions, + updateAll: Boolean(opts.updateMatching), + virtualStoreDir: opts.virtualStoreDir, + workspacePackages: opts.workspacePackages, + }) + const projectsToResolve = await Promise.all(importers.map(async (project) => _toResolveImporter(project))) const { dependenciesTree, outdatedDependencies, resolvedImporters, resolvedPackagesByDepPath, wantedToBeSkippedPackageIds, - } = await resolveDependencyTree(importers, opts) + } = await resolveDependencyTree(projectsToResolve, opts) const linkedDependenciesByProjectId: Record = {} - const projectsToLink = await Promise.all(importers.map(async (project, index) => { + const projectsToLink = await Promise.all(projectsToResolve.map(async (project, index) => { const resolvedImporter = resolvedImporters[project.id] linkedDependenciesByProjectId[project.id] = resolvedImporter.linkedDependencies let updatedManifest: ProjectManifest | undefined = project.manifest let updatedOriginalManifest: ProjectManifest | undefined = project.originalManifest if (project.updatePackageManifest) { - const manifests = await updateProjectManifest(importers[index], { + const manifests = await updateProjectManifest(project, { directDependencies: resolvedImporter.directDependencies, preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol, saveWorkspaceProtocol: opts.saveWorkspaceProtocol, @@ -130,13 +141,14 @@ export default async function ( ) : [] - project.manifest = updatedOriginalManifest ?? project.originalManifest ?? project.manifest + const manifest = updatedOriginalManifest ?? project.originalManifest ?? project.manifest + importers[index].manifest = manifest return { binsDir: project.binsDir, directNodeIdsByAlias: resolvedImporter.directNodeIdsByAlias, id: project.id, linkedDependencies: resolvedImporter.linkedDependencies, - manifest: project.manifest, + manifest, modulesDir: project.modulesDir, rootDir: project.rootDir, topParents, diff --git a/packages/resolve-dependencies/src/resolveDependencyTree.ts b/packages/resolve-dependencies/src/resolveDependencyTree.ts index 37f60d12b1..5991e20eeb 100644 --- a/packages/resolve-dependencies/src/resolveDependencyTree.ts +++ b/packages/resolve-dependencies/src/resolveDependencyTree.ts @@ -2,6 +2,7 @@ import { Lockfile } from '@pnpm/lockfile-types' import { PreferredVersions, Resolution, WorkspacePackages } from '@pnpm/resolver-base' import { StoreController } from '@pnpm/store-controller-types' import { + ProjectManifest, ReadPackageHook, Registries, } from '@pnpm/types' @@ -37,10 +38,16 @@ export interface ResolvedDirectDependency { export interface Importer { id: string - hasRemovedDependencies?: boolean + manifest: ProjectManifest modulesDir: string - preferredVersions?: PreferredVersions + removePackages?: string[] rootDir: string + wantedDependencies: Array +} + +export interface ImporterToResolveGeneric extends Importer { + hasRemovedDependencies?: boolean + preferredVersions?: PreferredVersions wantedDependencies: Array } @@ -57,6 +64,7 @@ export interface ResolveDependenciesOptions { nodeVersion: string registries: Registries pnpmVersion: string + preferredVersions?: PreferredVersions preferWorkspacePackages?: boolean updateMatching?: (pkgName: string) => boolean linkWorkspacePackagesDepth?: number @@ -69,7 +77,7 @@ export interface ResolveDependenciesOptions { } export default async function ( - importers: Array>, + importers: Array>, opts: ResolveDependenciesOptions ) { const directDepsByImporterId = {} as {[id: string]: Array} diff --git a/packages/core/src/safeIsInnerLink.ts b/packages/resolve-dependencies/src/safeIsInnerLink.ts similarity index 89% rename from packages/core/src/safeIsInnerLink.ts rename to packages/resolve-dependencies/src/safeIsInnerLink.ts index c63d8e3c08..eb55a315d6 100644 --- a/packages/core/src/safeIsInnerLink.ts +++ b/packages/resolve-dependencies/src/safeIsInnerLink.ts @@ -10,7 +10,6 @@ export default async function safeIsInnerLink ( opts: { hideAlienModules: boolean projectDir: string - storeDir: string virtualStoreDir: string } ): Promise { @@ -19,7 +18,7 @@ export default async function safeIsInnerLink ( if (link.isInner) return true - if (isSubdir(opts.virtualStoreDir, link.target) || isSubdir(opts.storeDir, link.target)) return true + if (isSubdir(opts.virtualStoreDir, link.target)) return true return link.target as string } catch (err: any) { // eslint-disable-line diff --git a/packages/resolve-dependencies/src/toResolveImporter.ts b/packages/resolve-dependencies/src/toResolveImporter.ts new file mode 100644 index 0000000000..0fdde4e2cd --- /dev/null +++ b/packages/resolve-dependencies/src/toResolveImporter.ts @@ -0,0 +1,141 @@ +import logger from '@pnpm/logger' +import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils' +import { + PreferredVersions, + WorkspacePackages, +} from '@pnpm/resolver-base' +import { Dependencies, ProjectManifest } from '@pnpm/types' +import getVerSelType from 'version-selector-type' +import { ImporterToResolve } from '.' +import getWantedDependencies, { WantedDependency } from './getWantedDependencies' +import safeIsInnerLink from './safeIsInnerLink' + +export default async function toResolveImporter ( + opts: { + defaultUpdateDepth: number + lockfileOnly: boolean + preferredVersions?: PreferredVersions + updateAll: boolean + virtualStoreDir: string + workspacePackages: WorkspacePackages + }, + project: ImporterToResolve +) { + const allDeps = getWantedDependencies(project.manifest) + const nonLinkedDependencies = await partitionLinkedPackages(allDeps, { + lockfileOnly: opts.lockfileOnly, + modulesDir: project.modulesDir, + projectDir: project.rootDir, + virtualStoreDir: opts.virtualStoreDir, + workspacePackages: opts.workspacePackages, + }) + const existingDeps = nonLinkedDependencies + .filter(({ alias }) => !project.wantedDependencies.some((wantedDep) => wantedDep.alias === alias)) + let wantedDependencies!: Array + if (!project.manifest) { + wantedDependencies = [ + ...project.wantedDependencies, + ...existingDeps, + ] + .map((dep) => ({ + ...dep, + updateDepth: opts.defaultUpdateDepth, + })) + } else { + // Direct local tarballs are always checked, + // so their update depth should be at least 0 + const updateLocalTarballs = (dep: WantedDependency) => ({ + ...dep, + updateDepth: opts.updateAll + ? opts.defaultUpdateDepth + : (prefIsLocalTarball(dep.pref) ? 0 : -1), + }) + wantedDependencies = [ + ...project.wantedDependencies.map( + opts.defaultUpdateDepth < 0 + ? updateLocalTarballs + : (dep) => ({ ...dep, updateDepth: opts.defaultUpdateDepth })), + ...existingDeps.map(updateLocalTarballs), + ] + } + return { + ...project, + hasRemovedDependencies: Boolean(project.removePackages?.length), + preferredVersions: opts.preferredVersions ?? (project.manifest && getPreferredVersionsFromPackage(project.manifest)) ?? {}, + wantedDependencies, + } +} + +function prefIsLocalTarball (pref: string) { + return pref.startsWith('file:') && pref.endsWith('.tgz') +} + +async function partitionLinkedPackages ( + dependencies: WantedDependency[], + opts: { + projectDir: string + lockfileOnly: boolean + modulesDir: string + virtualStoreDir: string + workspacePackages?: WorkspacePackages + } +): Promise { + const nonLinkedDependencies: WantedDependency[] = [] + const linkedAliases = new Set() + for (const dependency of dependencies) { + if ( + !dependency.alias || + opts.workspacePackages?.[dependency.alias] != null || + dependency.pref.startsWith('workspace:') + ) { + nonLinkedDependencies.push(dependency) + continue + } + const isInnerLink = await safeIsInnerLink(opts.modulesDir, dependency.alias, { + hideAlienModules: !opts.lockfileOnly, + projectDir: opts.projectDir, + virtualStoreDir: opts.virtualStoreDir, + }) + if (isInnerLink === true) { + nonLinkedDependencies.push(dependency) + continue + } + // This info-log might be better to be moved to the reporter + logger.info({ + message: `${dependency.alias} is linked to ${opts.modulesDir} from ${isInnerLink}`, + prefix: opts.projectDir, + }) + linkedAliases.add(dependency.alias) + } + return nonLinkedDependencies +} + +function getPreferredVersionsFromPackage ( + pkg: Pick +): PreferredVersions { + return getVersionSpecsByRealNames(getAllDependenciesFromManifest(pkg)) +} + +function getVersionSpecsByRealNames (deps: Dependencies) { + return Object.keys(deps) + .reduce((acc, depName) => { + if (deps[depName].startsWith('npm:')) { + const pref = deps[depName].substr(4) + const index = pref.lastIndexOf('@') + const spec = pref.substr(index + 1) + const selector = getVerSelType(spec) + if (selector != null) { + const pkgName = pref.substr(0, index) + acc[pkgName] = acc[pkgName] || {} + acc[pkgName][selector.normalized] = selector.type + } + } else if (!deps[depName].includes(':')) { // we really care only about semver specs + const selector = getVerSelType(deps[depName]) + if (selector != null) { + acc[depName] = acc[depName] || {} + acc[depName][selector.normalized] = selector.type + } + } + return acc + }, {}) +} diff --git a/packages/resolve-dependencies/tsconfig.json b/packages/resolve-dependencies/tsconfig.json index 1d0fbfef50..e9a9f1dbaf 100644 --- a/packages/resolve-dependencies/tsconfig.json +++ b/packages/resolve-dependencies/tsconfig.json @@ -50,6 +50,9 @@ }, { "path": "../types" + }, + { + "path": "../which-version-is-pinned" } ] } diff --git a/packages/which-version-is-pinned/README.md b/packages/which-version-is-pinned/README.md new file mode 100644 index 0000000000..51815a2262 --- /dev/null +++ b/packages/which-version-is-pinned/README.md @@ -0,0 +1,22 @@ +# @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) diff --git a/packages/which-version-is-pinned/jest.config.js b/packages/which-version-is-pinned/jest.config.js new file mode 100644 index 0000000000..50200e3d7c --- /dev/null +++ b/packages/which-version-is-pinned/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config') diff --git a/packages/which-version-is-pinned/package.json b/packages/which-version-is-pinned/package.json new file mode 100644 index 0000000000..c216e3d398 --- /dev/null +++ b/packages/which-version-is-pinned/package.json @@ -0,0 +1,34 @@ +{ + "name": "@pnpm/which-version-is-pinned", + "description": "Takes a version spec and returns which version is pinned by it", + "version": "0.0.0", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib", + "!*.map" + ], + "keywords": [ + "pnpm6" + ], + "license": "MIT", + "engines": { + "node": ">=12.17" + }, + "repository": "https://github.com/pnpm/pnpm/blob/master/packages/which-version-is-pinned", + "scripts": { + "_test": "jest", + "test": "pnpm run compile && pnpm run _test", + "lint": "eslint src/**/*.ts test/**/*.ts", + "prepublishOnly": "pnpm run compile", + "compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix" + }, + "homepage": "https://github.com/pnpm/pnpm/blob/master/packages/which-version-is-pinned#readme", + "funding": "https://opencollective.com/pnpm", + "dependencies": { + "semver-utils": "^1.1.4" + } +} diff --git a/packages/core/src/guessPinnedVersionFromExistingSpec.ts b/packages/which-version-is-pinned/src/index.ts similarity index 86% rename from packages/core/src/guessPinnedVersionFromExistingSpec.ts rename to packages/which-version-is-pinned/src/index.ts index a15a20c601..73351b2cc0 100644 --- a/packages/core/src/guessPinnedVersionFromExistingSpec.ts +++ b/packages/which-version-is-pinned/src/index.ts @@ -1,6 +1,6 @@ import { parseRange } from 'semver-utils' -export default function guessPinnedVersionFromExistingSpec (spec: string) { +export default function whichVersionIsPinned (spec: string) { if (spec.startsWith('workspace:')) spec = spec.substr('workspace:'.length) if (spec === '*') return 'none' const parsedRange = parseRange(spec) diff --git a/packages/which-version-is-pinned/test/index.ts b/packages/which-version-is-pinned/test/index.ts new file mode 100644 index 0000000000..8c084f122e --- /dev/null +++ b/packages/which-version-is-pinned/test/index.ts @@ -0,0 +1,11 @@ +import whichVersionIsPinned from '@pnpm/which-version-is-pinned' + +test.each([ + ['^1.0.0', 'major'], + ['~1.0.0', 'minor'], + ['1.0.0', 'patch'], + ['*', 'none'], + ['workspace:^1.0.0', 'major'], +])('whichVersionIsPinned()', (spec, expectedResult) => { + expect(whichVersionIsPinned(spec)).toEqual(expectedResult) +}) diff --git a/packages/which-version-is-pinned/tsconfig.json b/packages/which-version-is-pinned/tsconfig.json new file mode 100644 index 0000000000..41aa731a88 --- /dev/null +++ b/packages/which-version-is-pinned/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@pnpm/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts", + "../../typings/**/*.d.ts" + ], + "references": [] +} diff --git a/packages/which-version-is-pinned/tsconfig.lint.json b/packages/which-version-is-pinned/tsconfig.lint.json new file mode 100644 index 0000000000..0dc5add6b7 --- /dev/null +++ b/packages/which-version-is-pinned/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../typings/**/*.d.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fad1fef359..6ce6163316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -414,6 +414,7 @@ importers: '@pnpm/symlink-dependency': workspace:4.0.8 '@pnpm/test-fixtures': workspace:* '@pnpm/types': workspace:7.6.0 + '@pnpm/which-version-is-pinned': workspace:0.0.0 '@types/fs-extra': ^9.0.5 '@types/is-ci': ^3.0.0 '@types/is-windows': ^1.0.0 @@ -432,7 +433,6 @@ importers: graph-sequencer: 2.0.0 is-ci: ^3.0.0 is-inner-link: ^4.0.0 - is-subdir: ^1.1.1 is-windows: ^1.0.2 load-json-file: ^6.2.0 ncp: ^2.0.0 @@ -449,7 +449,6 @@ importers: resolve-link-target: ^2.0.0 run-groups: ^3.0.1 semver: ^7.3.4 - semver-utils: ^1.1.4 sinon: ^11.1.1 symlink-dir: ^5.0.0 version-selector-type: ^3.0.0 @@ -487,12 +486,12 @@ importers: '@pnpm/store-controller-types': link:../store-controller-types '@pnpm/symlink-dependency': link:../symlink-dependency '@pnpm/types': link:../types + '@pnpm/which-version-is-pinned': link:../which-version-is-pinned '@zkochan/npm-package-arg': 2.0.1 '@zkochan/rimraf': 2.1.1 dependency-path: link:../dependency-path graph-sequencer: 2.0.0 is-inner-link: 4.0.0 - is-subdir: 1.2.0 load-json-file: 6.2.0 normalize-path: 3.0.0 p-every: 2.0.0 @@ -503,7 +502,6 @@ importers: ramda: 0.27.1 run-groups: 3.0.1 semver: 7.3.5 - semver-utils: 1.1.4 version-selector-type: 3.0.0 devDependencies: '@pnpm/assert-project': link:../../privatePackages/assert-project @@ -3067,11 +3065,14 @@ importers: '@pnpm/resolver-base': workspace:8.1.1 '@pnpm/store-controller-types': workspace:11.0.7 '@pnpm/types': workspace:7.6.0 + '@pnpm/which-version-is-pinned': workspace:0.0.0 '@types/ramda': 0.27.39 '@types/semver': ^7.3.4 dependency-path: workspace:8.0.6 encode-registry: ^3.0.0 get-npm-tarball-url: ^2.0.3 + is-inner-link: ^4.0.0 + is-subdir: ^1.1.1 path-exists: ^4.0.0 ramda: ^0.27.1 replace-string: ^3.1.0 @@ -3091,9 +3092,12 @@ importers: '@pnpm/resolver-base': link:../resolver-base '@pnpm/store-controller-types': link:../store-controller-types '@pnpm/types': link:../types + '@pnpm/which-version-is-pinned': link:../which-version-is-pinned dependency-path: link:../dependency-path encode-registry: 3.0.0 get-npm-tarball-url: 2.0.3 + is-inner-link: 4.0.0 + is-subdir: 1.2.0 path-exists: 4.0.0 ramda: 0.27.1 replace-string: 3.1.0 @@ -3305,6 +3309,15 @@ importers: devDependencies: '@pnpm/types': 'link:' + packages/which-version-is-pinned: + specifiers: + '@pnpm/which-version-is-pinned': 'link:' + semver-utils: ^1.1.4 + dependencies: + semver-utils: 1.1.4 + devDependencies: + '@pnpm/which-version-is-pinned': 'link:' + packages/write-project-manifest: specifiers: '@pnpm/types': workspace:7.6.0