From 1c851f2a6bf24bee58bbce768753a26ea10f1901 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 22 Jan 2021 11:30:25 +0200 Subject: [PATCH] fix(headless): skip optional dep that cannot be installed (#3091) close #3090 --- .changeset/empty-snails-grin.md | 5 + packages/headless/src/index.ts | 29 +++-- .../has-nonexistent-optional-dep/package.json | 14 +++ .../pnpm-lock.yaml | 103 ++++++++++++++++++ packages/headless/test/index.ts | 22 ++++ 5 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 .changeset/empty-snails-grin.md create mode 100644 packages/headless/test/fixtures/has-nonexistent-optional-dep/package.json create mode 100644 packages/headless/test/fixtures/has-nonexistent-optional-dep/pnpm-lock.yaml diff --git a/.changeset/empty-snails-grin.md b/.changeset/empty-snails-grin.md new file mode 100644 index 0000000000..3fbdd9f374 --- /dev/null +++ b/.changeset/empty-snails-grin.md @@ -0,0 +1,5 @@ +--- +"@pnpm/headless": patch +--- + +A failing optional dependency should not cause a crash of headless installation. diff --git a/packages/headless/src/index.ts b/packages/headless/src/index.ts index 7f6397101e..4fd1a66e98 100644 --- a/packages/headless/src/index.ts +++ b/packages/headless/src/index.ts @@ -50,6 +50,7 @@ import packageIsInstallable from '@pnpm/package-is-installable' import { fromDir as readPackageFromDir } from '@pnpm/read-package-json' import { readProjectManifestOnly, safeReadProjectManifestOnly } from '@pnpm/read-project-manifest' import { + FetchPackageToStoreFunction, PackageFilesResponse, StoreController, } from '@pnpm/store-controller-types' @@ -592,13 +593,19 @@ async function lockfileToDepGraph ( requester: opts.lockfileDir, status: 'resolved', }) - let fetchResponse = opts.storeController.fetchPackage({ - force: false, - lockfileDir: opts.lockfileDir, - pkgId: packageId, - resolution, - }) - if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse + let fetchResponse!: ReturnType + try { + fetchResponse = opts.storeController.fetchPackage({ + force: false, + lockfileDir: opts.lockfileDir, + pkgId: packageId, + resolution, + }) + if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse + } catch (err) { + if (pkgSnapshot.optional) return + throw err + } fetchResponse.files() // eslint-disable-line .then(({ fromStore }) => { progressLogger.debug({ @@ -741,7 +748,13 @@ function linkAllPkgs ( ) { return Promise.all( depNodes.map(async (depNode) => { - const filesResponse = await depNode.fetchingFiles() + let filesResponse!: PackageFilesResponse + try { + filesResponse = await depNode.fetchingFiles() + } catch (err) { + if (depNode.optional) return + throw err + } const { importMethod, isBuilt } = await storeController.importPackage(depNode.dir, { filesResponse, diff --git a/packages/headless/test/fixtures/has-nonexistent-optional-dep/package.json b/packages/headless/test/fixtures/has-nonexistent-optional-dep/package.json new file mode 100644 index 0000000000..4f9e06f5a7 --- /dev/null +++ b/packages/headless/test/fixtures/has-nonexistent-optional-dep/package.json @@ -0,0 +1,14 @@ +{ + "name": "has-nonexistent-optional-dep", + "version": "1.0.0", + "dependencies": { + "is-positive": "^1.0.0", + "rimraf": "^2.6.2" + }, + "devDependencies": { + "is-negative": "^2.1.0" + }, + "optionalDependencies": { + "@zkochan/not-exists": "1.2.0" + } +} diff --git a/packages/headless/test/fixtures/has-nonexistent-optional-dep/pnpm-lock.yaml b/packages/headless/test/fixtures/has-nonexistent-optional-dep/pnpm-lock.yaml new file mode 100644 index 0000000000..ea3a35ceff --- /dev/null +++ b/packages/headless/test/fixtures/has-nonexistent-optional-dep/pnpm-lock.yaml @@ -0,0 +1,103 @@ +dependencies: + is-positive: 1.0.0 + rimraf: 2.7.1 +devDependencies: + is-negative: 2.1.0 +lockfileVersion: 5.1 +optionalDependencies: + '@zkochan/not-exists': 1.2.0 +packages: + /balanced-match/1.0.0: + dev: false + resolution: + integrity: sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + /brace-expansion/1.1.11: + dependencies: + balanced-match: 1.0.0 + concat-map: 0.0.1 + dev: false + resolution: + integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + /@zkochan/not-exists/1.2.0: + dev: false + engines: + node: '>=0.1.90' + optional: true + resolution: + integrity: sha512-lweugcX5nailCqZBttArTojZZpHGWhmFJX78KJHlxwhM8tLAy5QCgRgRxrubrksdvA+2Y3inWG5TToyyjL82BQ== + /concat-map/0.0.1: + dev: false + resolution: + integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + /fs.realpath/1.0.0: + dev: false + resolution: + integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + /glob/7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.0.4 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + resolution: + integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + /inflight/1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + resolution: + integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + /inherits/2.0.4: + dev: false + resolution: + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + /is-negative/2.1.0: + dev: true + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-8Nhjd6oVpkw0lh84rCqb4rQKEYc= + /is-positive/1.0.0: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-iACYVrZKLx632LsBeUGEJK4EUss= + /minimatch/3.0.4: + dependencies: + brace-expansion: 1.1.11 + dev: false + resolution: + integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + /once/1.4.0: + dependencies: + wrappy: 1.0.2 + dev: false + resolution: + integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + /path-is-absolute/1.0.1: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + /rimraf/2.7.1: + dependencies: + glob: 7.1.6 + dev: false + hasBin: true + resolution: + integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + /wrappy/1.0.2: + dev: false + resolution: + integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +specifiers: + '@zkochan/not-exists': 1.2.0 + is-negative: ^2.1.0 + is-positive: ^1.0.0 + rimraf: ^2.6.2 diff --git a/packages/headless/test/index.ts b/packages/headless/test/index.ts index 661c24b129..4b1125978a 100644 --- a/packages/headless/test/index.ts +++ b/packages/headless/test/index.ts @@ -233,6 +233,28 @@ test('not installing optional deps', async () => { await project.has('pkg-with-good-optional') }) +test('skipping optional dependency if it cannot be fetched', async () => { + const prefix = path.join(fixtures, 'has-nonexistent-optional-dep') + const reporter = sinon.spy() + + await headless(await testDefaults({ + lockfileDir: prefix, + reporter, + }, { + retry: { + retries: 0, + }, + })) + + const project = assertProject(prefix) + expect(project.requireModule('is-positive')).toBeTruthy() + expect(project.requireModule('rimraf')).toBeTruthy() + expect(project.requireModule('is-negative')).toBeTruthy() + + expect(await project.readCurrentLockfile()).toBeTruthy() + expect(await project.readModulesManifest()).toBeTruthy() +}) + test('run pre/postinstall scripts', async () => { const prefix = path.join(fixtures, 'deps-have-lifecycle-scripts') const outputJsonPath = path.join(prefix, 'output.json')