From 2e8ebabb20a61a642aa32988d5898cba59f2d0fa Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Wed, 29 Apr 2020 11:19:46 +0300 Subject: [PATCH] fix: prefer headless installation when aliases are used PR #2510 --- .changeset/angry-weeks-collect.md | 5 + .../src/install/allProjectsAreUpToDate.ts | 107 +++++++++++++++ packages/supi/src/install/index.ts | 64 +-------- .../supi/test/allProjectsAreUpToDate.test.ts | 129 ++++++++++++++++++ packages/supi/test/index.ts | 1 + 5 files changed, 244 insertions(+), 62 deletions(-) create mode 100644 .changeset/angry-weeks-collect.md create mode 100644 packages/supi/src/install/allProjectsAreUpToDate.ts create mode 100644 packages/supi/test/allProjectsAreUpToDate.test.ts diff --git a/.changeset/angry-weeks-collect.md b/.changeset/angry-weeks-collect.md new file mode 100644 index 0000000000..9a753259d0 --- /dev/null +++ b/.changeset/angry-weeks-collect.md @@ -0,0 +1,5 @@ +--- +"supi": patch +--- + +Headless installation should be preferred when local dependencies that use aliases are up-to-date. diff --git a/packages/supi/src/install/allProjectsAreUpToDate.ts b/packages/supi/src/install/allProjectsAreUpToDate.ts new file mode 100644 index 0000000000..d8dea3ac66 --- /dev/null +++ b/packages/supi/src/install/allProjectsAreUpToDate.ts @@ -0,0 +1,107 @@ +import { ProjectOptions } from '@pnpm/get-context' +import { + Lockfile, + ProjectSnapshot, +} from '@pnpm/lockfile-file' +import { satisfiesPackageManifest } from '@pnpm/lockfile-utils' +import { safeReadPackageFromDir as safeReadPkgFromDir } from '@pnpm/read-package-json' +import { WorkspacePackages } from '@pnpm/resolver-base' +import { + DEPENDENCIES_FIELDS, + DependencyManifest, + ProjectManifest, +} from '@pnpm/types' +import pEvery from 'p-every' +import path = require('path') +import R = require('ramda') +import semver = require('semver') + +export default async function allProjectsAreUpToDate ( + projects: Array, + opts: { + wantedLockfile: Lockfile, + workspacePackages: WorkspacePackages, + }, +) { + const manifestsByDir = opts.workspacePackages ? getWorkspacePackagesByDirectory(opts.workspacePackages) : {} + const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, opts.wantedLockfile) + const _linkedPackagesAreUpToDate = linkedPackagesAreUpToDate.bind(null, manifestsByDir, opts.workspacePackages) + return pEvery(projects, async (project) => { + const importer = opts.wantedLockfile.importers[project.id] + return importer && !hasLocalTarballDepsInRoot(importer) && + _satisfiesPackageManifest(project.manifest, project.id) && + _linkedPackagesAreUpToDate(project.manifest, importer, project.rootDir) + }) +} + +function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages) { + const workspacePackagesByDirectory = {} + Object.keys(workspacePackages || {}).forEach((pkgName) => { + Object.keys(workspacePackages[pkgName] || {}).forEach((pkgVersion) => { + workspacePackagesByDirectory[workspacePackages[pkgName][pkgVersion].dir] = workspacePackages[pkgName][pkgVersion].manifest + }) + }) + return workspacePackagesByDirectory +} + +async function linkedPackagesAreUpToDate ( + manifestsByDir: Record, + workspacePackages: WorkspacePackages, + manifest: ProjectManifest, + projectSnapshot: ProjectSnapshot, + projectDir: string, +) { + for (const depField of DEPENDENCIES_FIELDS) { + const lockfileDeps = projectSnapshot[depField] + const manifestDeps = manifest[depField] + if (!lockfileDeps || !manifestDeps) continue + const depNames = Object.keys(lockfileDeps) + for (const depName of depNames) { + const currentSpec = manifestDeps[depName] + if (!currentSpec) continue + const lockfileRef = lockfileDeps[depName] + const isLinked = lockfileRef.startsWith('link:') + if ( + isLinked && + ( + currentSpec.startsWith('link:') || + currentSpec.startsWith('file:') + ) + ) { + continue + } + const linkedDir = isLinked + ? path.join(projectDir, lockfileRef.substr(5)) + : workspacePackages?.[depName]?.[lockfileRef]?.dir + if (!linkedDir) continue + const linkedPkg = manifestsByDir[linkedDir] ?? await safeReadPkgFromDir(linkedDir) + const availableRange = getVersionRange(currentSpec) + // This should pass the same options to semver as @pnpm/npm-resolver + const localPackageSatisfiesRange = availableRange === '*' || + linkedPkg && semver.satisfies(linkedPkg.version, availableRange, { loose: true }) + if (isLinked !== localPackageSatisfiesRange) return false + } + } + return true +} + +function getVersionRange (spec: string) { + if (spec.startsWith('workspace:')) return spec.substr(10) + if (spec.startsWith('npm:')) { + spec = spec.substr(4) + const index = spec.indexOf('@', 1) + if (index === -1) return '*' + return spec.substr(index + 1) || '*' + } + return spec +} + +function hasLocalTarballDepsInRoot (importer: ProjectSnapshot) { + return R.any(refIsLocalTarball, Object.values(importer.dependencies ?? {})) + || R.any(refIsLocalTarball, Object.values(importer.devDependencies ?? {})) + || R.any(refIsLocalTarball, Object.values(importer.optionalDependencies ?? {})) +} + +function refIsLocalTarball (ref: string) { + return ref.startsWith('file:') && (ref.endsWith('.tgz') || ref.endsWith('.tar.gz') || ref.endsWith('.tar')) +} diff --git a/packages/supi/src/install/index.ts b/packages/supi/src/install/index.ts index b1b4c89c46..3f61116a2e 100644 --- a/packages/supi/src/install/index.ts +++ b/packages/supi/src/install/index.ts @@ -17,13 +17,11 @@ import { } from '@pnpm/lifecycle' import linkBins from '@pnpm/link-bins' import { - Lockfile, ProjectSnapshot, writeCurrentLockfile, writeLockfiles, writeWantedLockfile, } from '@pnpm/lockfile-file' -import { satisfiesPackageManifest } from '@pnpm/lockfile-utils' import logger, { streamParser } from '@pnpm/logger' import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils' import { write as writeModulesYaml } from '@pnpm/modules-yaml' @@ -48,18 +46,17 @@ import rimraf = require('@zkochan/rimraf') import * as dp from 'dependency-path' import isInnerLink = require('is-inner-link') import isSubdir = require('is-subdir') -import pEvery from 'p-every' import pFilter = require('p-filter') import pLimit from 'p-limit' import path = require('path') import R = require('ramda') -import semver = require('semver') import getSpecFromPackageManifest from '../getSpecFromPackageManifest' import lock from '../lock' import parseWantedDependencies from '../parseWantedDependencies' import safeIsInnerLink from '../safeIsInnerLink' import removeDeps from '../uninstall/removeDeps' import { updateProjectManifest } from '../utils/getPref' +import allProjectsAreUpToDate from './allProjectsAreUpToDate' import extendOptions, { InstallOptions, StrictInstallOptions, @@ -190,11 +187,7 @@ export async function mutateModules ( (!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === ctx.projects.length) && ctx.existsWantedLockfile && ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION && - await pEvery(ctx.projects, async (project) => - !hasLocalTarballDepsInRoot(ctx.wantedLockfile, project.id) && - satisfiesPackageManifest(ctx.wantedLockfile, project.manifest, project.id) && - linkedPackagesAreUpToDate(project.manifest, ctx.wantedLockfile.importers[project.id], project.rootDir, opts.workspacePackages), - ) + await allProjectsAreUpToDate(ctx.projects, { wantedLockfile: ctx.wantedLockfile, workspacePackages: opts.workspacePackages }) ) ) { if (!ctx.existsWantedLockfile) { @@ -523,59 +516,6 @@ function forgetResolutionsOfPrevWantedDeps (importer: ProjectSnapshot, wantedDep } } -async function linkedPackagesAreUpToDate ( - manifest: ProjectManifest, - projectSnapshot: ProjectSnapshot, - prefix: string, - workspacePackages?: WorkspacePackages, -) { - const workspacePackagesByDirectory = workspacePackages ? getWorkspacePackagesByDirectory(workspacePackages) : {} - for (const depField of DEPENDENCIES_FIELDS) { - const importerDeps = projectSnapshot[depField] - const pkgDeps = manifest[depField] - if (!importerDeps || !pkgDeps) continue - const depNames = Object.keys(importerDeps) - for (const depName of depNames) { - if (!pkgDeps[depName]) continue - const isLinked = importerDeps[depName].startsWith('link:') - if (isLinked && (pkgDeps[depName].startsWith('link:') || pkgDeps[depName].startsWith('file:'))) continue - const dir = isLinked - ? path.join(prefix, importerDeps[depName].substr(5)) - : workspacePackages?.[depName]?.[importerDeps[depName]]?.dir - if (!dir) continue - const linkedPkg = workspacePackagesByDirectory[dir] || await safeReadPkgFromDir(dir) - const availableRange = pkgDeps[depName].startsWith('workspace:') ? pkgDeps[depName].substr(10) : pkgDeps[depName] - // This should pass the same options to semver as @pnpm/npm-resolver - const localPackageSatisfiesRange = availableRange === '*' || - linkedPkg && semver.satisfies(linkedPkg.version, availableRange, { loose: true }) - if (isLinked !== localPackageSatisfiesRange) return false - } - } - return true -} - -function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages) { - const workspacePackagesByDirectory = {} - Object.keys(workspacePackages || {}).forEach((pkgName) => { - Object.keys(workspacePackages[pkgName] || {}).forEach((pkgVersion) => { - workspacePackagesByDirectory[workspacePackages[pkgName][pkgVersion].dir] = workspacePackages[pkgName][pkgVersion].manifest - }) - }) - return workspacePackagesByDirectory -} - -function hasLocalTarballDepsInRoot (lockfile: Lockfile, importerId: string) { - const importer = lockfile.importers?.[importerId] - if (!importer) return false - return R.any(refIsLocalTarball, R.values(importer.dependencies || {})) - || R.any(refIsLocalTarball, R.values(importer.devDependencies || {})) - || R.any(refIsLocalTarball, R.values(importer.optionalDependencies || {})) -} - -function refIsLocalTarball (ref: string) { - return ref.startsWith('file:') && (ref.endsWith('.tgz') || ref.endsWith('.tar.gz') || ref.endsWith('.tar')) -} - export async function addDependenciesToPackage ( manifest: ProjectManifest, dependencySelectors: string[], diff --git a/packages/supi/test/allProjectsAreUpToDate.test.ts b/packages/supi/test/allProjectsAreUpToDate.test.ts new file mode 100644 index 0000000000..615c5be2b4 --- /dev/null +++ b/packages/supi/test/allProjectsAreUpToDate.test.ts @@ -0,0 +1,129 @@ +import allProjectsAreUpToDate from 'supi/lib/install/allProjectsAreUpToDate' +import tape = require('tape') +import promisifyTape from 'tape-promise' + +const test = promisifyTape(tape) + +const fooManifest = { + name: 'foo', + version: '1.0.0', +} +const workspacePackages = { + foo: { + '1.0.0': { + dir: 'foo', + manifest: fooManifest, + }, + }, +} + +test('allProjectsAreUpToDate(): works with aliased local dependencies', async (t: tape.Test) => { + t.ok(await allProjectsAreUpToDate([ + { + id: 'bar', + manifest: { + dependencies: { + alias: 'npm:foo', + }, + }, + rootDir: 'bar', + }, + { + id: 'foo', + manifest: fooManifest, + rootDir: 'foo', + }, + ], { + wantedLockfile: { + importers: { + bar: { + dependencies: { + alias: 'link:../foo', + }, + specifiers: { + alias: 'npm:foo', + }, + }, + foo: { + specifiers: {}, + }, + }, + lockfileVersion: 5, + }, + workspacePackages, + })) +}) + +test('allProjectsAreUpToDate(): works with aliased local dependencies that specify versions', async (t: tape.Test) => { + t.ok(await allProjectsAreUpToDate([ + { + id: 'bar', + manifest: { + dependencies: { + alias: 'npm:foo@1', + }, + }, + rootDir: 'bar', + }, + { + id: 'foo', + manifest: fooManifest, + rootDir: 'foo', + }, + ], { + wantedLockfile: { + importers: { + bar: { + dependencies: { + alias: 'link:../foo', + }, + specifiers: { + alias: 'npm:foo@1', + }, + }, + foo: { + specifiers: {}, + }, + }, + lockfileVersion: 5, + }, + workspacePackages, + })) +}) + +test('allProjectsAreUpToDate(): returns false if the aliased dependency version is out of date', async (t: tape.Test) => { + t.notOk(await allProjectsAreUpToDate([ + { + id: 'bar', + manifest: { + dependencies: { + alias: 'npm:foo@0', + }, + }, + rootDir: 'bar', + }, + { + id: 'foo', + manifest: fooManifest, + rootDir: 'foo', + }, + ], { + wantedLockfile: { + importers: { + bar: { + dependencies: { + alias: 'link:../foo', + }, + specifiers: { + alias: 'npm:foo@0', + }, + }, + foo: { + specifiers: {}, + }, + }, + lockfileVersion: 5, + }, + workspacePackages, + })) +}) diff --git a/packages/supi/test/index.ts b/packages/supi/test/index.ts index 591eced2e9..d233505e69 100644 --- a/packages/supi/test/index.ts +++ b/packages/supi/test/index.ts @@ -1,4 +1,5 @@ /// +import './allProjectsAreUpToDate.test' import './api' import './breakingChanges' import './cache'