├── .gitignore ├── .prettierrc ├── .github └── workflows │ └── ci.yml ├── .eslintrc.js ├── LICENSE.md ├── package.json ├── index.d.ts ├── RELEASE.md ├── CHANGELOG.md ├── index.js ├── README.md └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 16.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:node/recommended', 8 | 'plugin:prettier/recommended', 9 | ], 10 | plugins: ['prettier', 'node'], 11 | parserOptions: { 12 | ecmaVersion: 2017, 13 | sourceType: 'script', 14 | }, 15 | env: { 16 | node: true, 17 | }, 18 | overrides: [ 19 | { 20 | files: ['jest.setup.js', '**/*.test.js'], 21 | env: { 22 | jest: true, 23 | }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Robert Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "validate-peer-dependencies", 4 | "version": "2.2.0", 5 | "description": "Validate that the peerDependencies of a given package.json have been satisfied.", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:rwjblue/validate-peer-dependencies.git" 9 | }, 10 | "license": "MIT", 11 | "author": "Robert Jackson ", 12 | "main": "index.js", 13 | "types": "index.d.ts", 14 | "files": [ 15 | "index.js", 16 | "index.d.ts" 17 | ], 18 | "scripts": { 19 | "lint": "eslint .", 20 | "test": "npm-run-all lint test:jest", 21 | "test:jest": "jest" 22 | }, 23 | "dependencies": { 24 | "resolve-package-path": "^4.0.3", 25 | "semver": "^7.3.8" 26 | }, 27 | "engines": { 28 | "node": ">= 12" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^8.32.0", 32 | "eslint-config-prettier": "^8.6.0", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-prettier": "^4.2.1", 35 | "fixturify-project": "^5.2.0", 36 | "jest": "^26.6.0", 37 | "npm-run-all": "^4.1.5", 38 | "prettier": "^2.8.3", 39 | "release-it": "^15.6.0", 40 | "@release-it-plugins/lerna-changelog": "^5.0.0" 41 | }, 42 | "publishConfig": { 43 | "registry": "https://registry.npmjs.org" 44 | }, 45 | "release-it": { 46 | "plugins": { 47 | "@release-it-plugins/lerna-changelog": { 48 | "infile": "CHANGELOG.md", 49 | "launchEditor": true 50 | } 51 | }, 52 | "git": { 53 | "tagName": "v${version}" 54 | }, 55 | "github": { 56 | "release": true, 57 | "tokenRef": "GITHUB_AUTH" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type Options = { 2 | /** 3 | Set to `false` to disabling caching or provide a `Map` to control cache 4 | sharing. The contents of the map are opaque. 5 | */ 6 | cache?: boolean | Map; 7 | /** 8 | Specify a custom handler for errors. This can be useful to provide better 9 | errors for your particular context. 10 | */ 11 | handleFailure?: (result: unknown) => void; 12 | /** 13 | The path that should be used as the starting point for resolving 14 | `peerDependencies` from 15 | */ 16 | resolvePeerDependenciesFrom?: string; 17 | }; 18 | 19 | declare const validatePeerDependencies: { 20 | /** 21 | Validate peer dependencies for the package rooted at `parentRoot`. 22 | 23 | @example 24 | ```typescript 25 | validatePeerDependencies(__dirname); 26 | ``` 27 | 28 | @param parentRoot - 29 | The directory containing the `package.json` to validate. Typically this 30 | should be specified via `__dirname`. 31 | 32 | @param [options] 33 | @param {boolean | Map} options.cache - 34 | Set to `false` to disabling caching or provide a `Map` to control cache 35 | sharing. The contents of the map are opaque. 36 | @param [options.handleFailure] - 37 | Specify a custom handler for errors. This can be useful to provide better 38 | errors for your particular context. 39 | @param [options.resolvePeerDependenciesFrom] - 40 | The path that should be used as the starting point for resolving 41 | `peerDependencies` from. 42 | 43 | */ 44 | (parentRoot: string, options?: Options): void; 45 | 46 | /** 47 | Treat `pkg` as being present, overriding whatever is found in `node_modules`. 48 | 49 | This is often useful during development when linking to a library that has peer dependencies. 50 | */ 51 | assumeProvided(pkg: { name?: string; version?: string }): void; 52 | }; 53 | 54 | export = validatePeerDependencies; 55 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | * breaking - Used when the PR is considered a breaking change. 21 | * enhancement - Used when the PR adds a new feature or enhancement. 22 | * bug - Used when the PR fixes a bug included in a previous release. 23 | * documentation - Used when the PR adds or updates documentation. 24 | * internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | * First, ensure that you have installed your projects dependencies: 32 | 33 | ```sh 34 | npm install 35 | ``` 36 | 37 | * Second, ensure that you have obtained a 38 | [GitHub personal access token][generate-token] with the `repo` scope (no 39 | other permissions are needed). Make sure the token is available as the 40 | `GITHUB_AUTH` environment variable. 41 | 42 | For instance: 43 | 44 | ```bash 45 | export GITHUB_AUTH=abc123def456 46 | ``` 47 | 48 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 49 | 50 | * And last (but not least 😁) do your release. 51 | 52 | ```sh 53 | npx release-it 54 | ``` 55 | 56 | [release-it](https://github.com/release-it/release-it/) manages the actual 57 | release process. It will prompt you to to choose the version number after which 58 | you will have the chance to hand tweak the changelog to be used (for the 59 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 60 | pushing the tag and commits, etc. 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## v2.2.0 (2023-01-23) 4 | 5 | #### :rocket: Enhancement 6 | * [#24](https://github.com/rwjblue/validate-peer-dependencies/pull/24) Add environment variables for overrides ([@rwjblue](https://github.com/rwjblue)) 7 | 8 | #### :house: Internal 9 | * [#23](https://github.com/rwjblue/validate-peer-dependencies/pull/23) Update release-it and lerna-changelog generation ([@rwjblue](https://github.com/rwjblue)) 10 | * [#22](https://github.com/rwjblue/validate-peer-dependencies/pull/22) Update in range deps/devDeps ([@rwjblue](https://github.com/rwjblue)) 11 | * [#21](https://github.com/rwjblue/validate-peer-dependencies/pull/21) Update fixturify-project to latest ([@rwjblue](https://github.com/rwjblue)) 12 | * [#19](https://github.com/rwjblue/validate-peer-dependencies/pull/19) Update linting related devDependencies ([@rwjblue](https://github.com/rwjblue)) 13 | * [#18](https://github.com/rwjblue/validate-peer-dependencies/pull/18) Update dependencies to latest ([@rwjblue](https://github.com/rwjblue)) 14 | 15 | #### Committers: 1 16 | - Robert Jackson ([@rwjblue](https://github.com/rwjblue)) 17 | 18 | ## v2.1.0 (2022-07-25) 19 | 20 | #### :rocket: Enhancement 21 | * [#16](https://github.com/rwjblue/validate-peer-dependencies/pull/16) Add typescript types ([@antonk52](https://github.com/antonk52)) 22 | 23 | #### :memo: Documentation 24 | * [#15](https://github.com/rwjblue/validate-peer-dependencies/pull/15) docs: update ember-addon example ([@ndekeister-us](https://github.com/ndekeister-us)) 25 | 26 | #### Committers: 3 27 | - Anton ([@antonk52](https://github.com/antonk52)) 28 | - David J. Hamilton ([@hjdivad](https://github.com/hjdivad)) 29 | - Nathanaël Dekeister ([@ndekeister-us](https://github.com/ndekeister-us)) 30 | 31 | 32 | ## v2.0.0 (2021-05-11) 33 | 34 | #### :boom: Breaking Change 35 | * [#13](https://github.com/rwjblue/validate-peer-dependencies/pull/13) Drop support for node v10 ([@hjdivad](https://github.com/hjdivad)) 36 | * [#12](https://github.com/rwjblue/validate-peer-dependencies/pull/12) Upgrades resolve-package-path to 4.0.0 ([@scalvert](https://github.com/scalvert)) 37 | 38 | #### :house: Internal 39 | * [#10](https://github.com/rwjblue/validate-peer-dependencies/pull/10) Remove yarn.lock ([@rwjblue](https://github.com/rwjblue)) 40 | 41 | #### Committers: 3 42 | - David J. Hamilton ([@hjdivad](https://github.com/hjdivad)) 43 | - Robert Jackson ([@rwjblue](https://github.com/rwjblue)) 44 | - Steve Calvert ([@scalvert](https://github.com/scalvert)) 45 | 46 | ## v1.2.0 (2021-04-01) 47 | 48 | #### :rocket: Enhancement 49 | * [#9](https://github.com/rwjblue/validate-peer-dependencies/pull/9) Add assumeProvided ([@hjdivad](https://github.com/hjdivad)) 50 | 51 | #### Committers: 1 52 | - David J. Hamilton ([@hjdivad](https://github.com/hjdivad)) 53 | 54 | ## v1.1.0 (2020-10-20) 55 | 56 | #### :rocket: Enhancement 57 | * [#8](https://github.com/rwjblue/validate-peer-dependencies/pull/8) Add support for peerDependenciesMeta. ([@rwjblue](https://github.com/rwjblue)) 58 | * [#4](https://github.com/rwjblue/validate-peer-dependencies/pull/4) Add `resolvePeerDependenciesFrom` option. ([@rwjblue](https://github.com/rwjblue)) 59 | 60 | #### :bug: Bug Fix 61 | * [#6](https://github.com/rwjblue/validate-peer-dependencies/pull/6) Error for any packages in peerDependencies **and** dependencies ([@rwjblue](https://github.com/rwjblue)) 62 | 63 | #### :house: Internal 64 | * [#7](https://github.com/rwjblue/validate-peer-dependencies/pull/7) Remove copy/paste mistakes in test harness. ([@rwjblue](https://github.com/rwjblue)) 65 | 66 | #### Committers: 1 67 | - Robert Jackson ([@rwjblue](https://github.com/rwjblue)) 68 | 69 | 70 | ## v1.0.0 (2020-10-20) 71 | 72 | #### :rocket: Enhancement 73 | * [#1](https://github.com/rwjblue/validate-peer-dependencies/pull/1) Implement `validatePeerDependencies` logic. ([@rwjblue](https://github.com/rwjblue) / [@hjdivad](https://github.com/hjdivad)) 74 | 75 | #### :memo: Documentation 76 | * [#2](https://github.com/rwjblue/validate-peer-dependencies/pull/2) Add detailed information to README.md. ([@rwjblue](https://github.com/rwjblue)) 77 | 78 | #### :house: Internal 79 | * [#3](https://github.com/rwjblue/validate-peer-dependencies/pull/3) Add automated release setup. ([@rwjblue](https://github.com/rwjblue)) 80 | 81 | #### Committers: 1 82 | - Robert Jackson ([@rwjblue](https://github.com/rwjblue)) 83 | - David J. Hamilton [@hjdivad](https://github.com/hjdivad)) 84 | 85 | 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const resolvePackagePath = require('resolve-package-path'); 4 | const semver = require('semver'); 5 | const path = require('path'); 6 | 7 | // avoid checking multiple times from the same location 8 | const HasPeerDepsInstalled = new Map(); 9 | 10 | const NullCache = new (class NullCache { 11 | get() {} 12 | set() {} 13 | has() { 14 | return false; 15 | } 16 | })(); 17 | 18 | function throwUsefulError(result) { 19 | let { missingPeerDependencies, incompatibleRanges } = result; 20 | 21 | let missingPeerDependenciesMessage = (missingPeerDependencies || []).reduce( 22 | (message, metadata) => { 23 | return `${message}\n\t* ${metadata.name}: \`${metadata.specifiedPeerDependencyRange}\`; it was not installed`; 24 | }, 25 | '' 26 | ); 27 | 28 | let incompatiblePeerDependenciesMessage = (incompatibleRanges || []).reduce( 29 | (message, metadata) => { 30 | return `${message}\n\t* ${metadata.name}: \`${metadata.specifiedPeerDependencyRange}\`; it was resolved to \`${metadata.version}\``; 31 | }, 32 | '' 33 | ); 34 | 35 | throw new Error( 36 | `${result.pkg.name} has the following unmet peerDependencies:\n${missingPeerDependenciesMessage}${incompatiblePeerDependenciesMessage}` 37 | ); 38 | } 39 | 40 | function resolvePackageVersion( 41 | packageName, 42 | resolvePeerDependenciesFrom, 43 | cache 44 | ) { 45 | let assumedVersion = AssumptionMap.get(packageName); 46 | if (assumedVersion !== undefined) { 47 | return assumedVersion; 48 | } 49 | 50 | let peerDepPackagePath = resolvePackagePath( 51 | packageName, 52 | resolvePeerDependenciesFrom, 53 | cache === NullCache ? false : undefined 54 | ); 55 | 56 | if (peerDepPackagePath === null) { 57 | return null; 58 | } 59 | 60 | return require(peerDepPackagePath).version; 61 | } 62 | 63 | module.exports = function validatePeerDependencies(parentRoot, options = {}) { 64 | if ( 65 | process.env.VALIDATE_PEER_DEPENDENCIES && 66 | process.env.VALIDATE_PEER_DEPENDENCIES.toLowerCase() === 'false' 67 | ) { 68 | // do not validate 69 | return; 70 | } 71 | 72 | let { cache, handleFailure, resolvePeerDependenciesFrom } = options; 73 | 74 | if (cache === false) { 75 | cache = NullCache; 76 | } else if (cache === undefined || cache === true) { 77 | cache = HasPeerDepsInstalled; 78 | } 79 | 80 | if (typeof handleFailure !== 'function') { 81 | handleFailure = throwUsefulError; 82 | } 83 | 84 | let cacheKey = parentRoot; 85 | 86 | if (resolvePeerDependenciesFrom === undefined) { 87 | resolvePeerDependenciesFrom = parentRoot; 88 | } else { 89 | cacheKey += `\0${resolvePeerDependenciesFrom}`; 90 | } 91 | 92 | if (cache.has(cacheKey)) { 93 | let result = cache.get(cacheKey); 94 | if (result !== true) { 95 | handleFailure(result); 96 | } 97 | return; 98 | } 99 | 100 | let packagePath = resolvePackagePath.findUpPackagePath( 101 | parentRoot, 102 | cache === NullCache ? false : undefined 103 | ); 104 | 105 | if (packagePath === null) { 106 | throw new Error( 107 | `validate-peer-dependencies could not find a package.json when resolving upwards from:\n\t${parentRoot}.` 108 | ); 109 | } 110 | 111 | let ignoredPeerDependencies = process.env.IGNORE_PEER_DEPENDENCIES 112 | ? new Set(process.env.IGNORE_PEER_DEPENDENCIES.split(',')) 113 | : new Set(); 114 | 115 | let pkg = require(packagePath); 116 | let { dependencies, peerDependencies, peerDependenciesMeta } = pkg; 117 | let hasDependencies = Boolean(dependencies); 118 | let hasPeerDependenciesMeta = Boolean(peerDependenciesMeta); 119 | 120 | // lazily created as needed 121 | let missingPeerDependencies = null; 122 | let incompatibleRanges = null; 123 | let invalidPackageConfiguration = null; 124 | 125 | for (let packageName in peerDependencies) { 126 | if (hasDependencies && packageName in dependencies) { 127 | if (invalidPackageConfiguration === null) { 128 | invalidPackageConfiguration = []; 129 | } 130 | 131 | invalidPackageConfiguration.push({ 132 | name: packageName, 133 | reason: 'included both as dependency and as a peer dependency', 134 | }); 135 | } 136 | 137 | if (ignoredPeerDependencies.has(packageName)) { 138 | continue; 139 | } 140 | 141 | // foo-package: >= 1.9.0 < 2.0.0 142 | // foo-package: >= 1.9.0 143 | // foo-package: ^1.9.0 144 | let specifiedPeerDependencyRange = peerDependencies[packageName]; 145 | 146 | let foundPackageVersion = resolvePackageVersion( 147 | packageName, 148 | resolvePeerDependenciesFrom, 149 | cache 150 | ); 151 | 152 | if (foundPackageVersion === null) { 153 | if ( 154 | hasPeerDependenciesMeta && 155 | packageName in peerDependenciesMeta && 156 | peerDependenciesMeta[packageName].optional 157 | ) { 158 | continue; 159 | } 160 | 161 | if (missingPeerDependencies === null) { 162 | missingPeerDependencies = []; 163 | } 164 | 165 | missingPeerDependencies.push({ 166 | name: packageName, 167 | specifiedPeerDependencyRange, 168 | }); 169 | 170 | continue; 171 | } 172 | 173 | if ( 174 | !semver.satisfies(foundPackageVersion, specifiedPeerDependencyRange, { 175 | includePrerelease: true, 176 | }) 177 | ) { 178 | if (incompatibleRanges === null) { 179 | incompatibleRanges = []; 180 | } 181 | 182 | incompatibleRanges.push({ 183 | name: packageName, 184 | version: foundPackageVersion, 185 | specifiedPeerDependencyRange, 186 | }); 187 | 188 | continue; 189 | } 190 | } 191 | 192 | if (invalidPackageConfiguration !== null) { 193 | // intentionally throwing an error here (not going through `handleFailure`) because 194 | // this represents a problem with the including package itself that should not be 195 | // squelchable by a custom `handleFailure` 196 | let invalidPackageConfigurationMessage = invalidPackageConfiguration.reduce( 197 | (message, metadata) => 198 | `${message}\n\t* ${metadata.name}: ${metadata.reason}`, 199 | '' 200 | ); 201 | 202 | let relativePath = path.relative(process.cwd(), parentRoot); 203 | 204 | throw new Error( 205 | `${pkg.name} (at \`./${relativePath}\`) is improperly configured:\n${invalidPackageConfigurationMessage}` 206 | ); 207 | } 208 | 209 | let isValid = missingPeerDependencies === null && incompatibleRanges === null; 210 | 211 | let result; 212 | if (isValid) { 213 | result = true; 214 | } else { 215 | result = { 216 | pkg, 217 | packagePath, 218 | incompatibleRanges, 219 | missingPeerDependencies, 220 | }; 221 | } 222 | 223 | cache.set(cacheKey, result); 224 | 225 | if (result !== true) { 226 | handleFailure(result); 227 | } 228 | }; 229 | 230 | let AssumptionMapName = '__ValidatePeerDependenciesAssumeProvided'; 231 | if (!(AssumptionMapName in global)) { 232 | global[AssumptionMapName] = new Map(); 233 | } 234 | 235 | // make sure to re-use the map created by a different instance of 236 | // validate-peer-dependencies 237 | let AssumptionMap = global[AssumptionMapName]; 238 | 239 | module.exports.assumeProvided = function ({ name, version } = {}) { 240 | if (name === undefined || version === undefined) { 241 | throw new Error( 242 | `assumeProvided({ name, version}): name and version are required, but name='${name}' version='${version}'` 243 | ); 244 | } 245 | AssumptionMap.set(name, version); 246 | }; 247 | 248 | Object.defineProperty(module.exports, '__HasPeerDepsInstalled', { 249 | enumerable: false, 250 | configurable: false, 251 | value: HasPeerDepsInstalled, 252 | }); 253 | 254 | module.exports._resetCache = function () { 255 | HasPeerDepsInstalled.clear(); 256 | }; 257 | 258 | module.exports._resetAssumptions = function () { 259 | global[AssumptionMapName].clear(); 260 | }; 261 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # validate-peer-dependencies 2 | 3 | A utility to allow packages to validate that their specified `peerDependencies` are properly satisified. 4 | 5 | ## Why? 6 | 7 | `peerDependencies` are actually a pretty important mechanism when working with 8 | "plugin systems". For example, most of the packages in the `@babel` namespace 9 | will declare a peer dependency on the version of `@babel/core` that they 10 | require to be present. 11 | 12 | Unfortunately, for quite a long time `peerDependencies` were very poorly 13 | supported in the Node ecosystem. Neither `npm` nor `yarn` would automatically 14 | install peer dependencies (`npm@3` `peerDependencies` removed "auto 15 | installation" of `peerDependencies`). They wouldn't even validate that the specified 16 | peer dependency was satisfied (both `npm` and `yarn` would emit a console 17 | warning, which is **very very** often completely ignored). 18 | 19 | Finally now with `npm@7` adding back support for installing `peerDependencies` 20 | automatically we are moving in the right direction. Unfortunately, many of us 21 | have projects that must still support older `npm` versions (or `yarn` versions) 22 | that do not provide that installation support. 23 | 24 | **That** is where this project comes in. It aims to provide a **_fast_** and **_easy_** 25 | way to validate that your required peer dependencies are satisified. 26 | 27 | ## Usage 28 | 29 | The simplest usage of `validatePeerDependencies` would look like the following: 30 | 31 | ```js 32 | require('validate-peer-dependencies')(__dirname); 33 | ``` 34 | 35 | This simple invocation will do the following: 36 | 37 | * find the nearest `package.json` file from the specified path (in this case `__dirname`) 38 | * read that `package.json` to find any specified `peerDependencies` entries 39 | * ensure that *each* of the specified `peerDependencies` are present and that 40 | the installed versions match the semver ranges that were specified 41 | * if any of the `peerDependencies` were not present or if their ranges were not satisified 42 | throw a useful error 43 | 44 | Here is an example error message: 45 | 46 | ``` 47 | test-app has the following unmet peerDependencies: 48 | 49 | * bar: `>= 2`; it was not installed 50 | * foo: `> 1`; it was resolved to `1.0.0` 51 | ``` 52 | 53 | ### Known Issues 54 | 55 | There are no known scenarios where `validate-peer-dependencies` will flag a 56 | peer dependency as missing, when it really is present. However, there are a few known 57 | problem case where `validate-peer-dependencies` cannot properly validate that a 58 | peer dependency is installed (where a error will not be thrown but it should have been): 59 | 60 | To illustrate, let's use the following setup: 61 | 62 | * Package `parent` depends on `child` and `sibling` 63 | * Package `child` has a dev dependency (for local development) and a peer 64 | dependency on `sibling` package 65 | * Package `child` uses `validate-peer-dependencies` to confirm that `sibling` is 66 | provided 67 | 68 | In this case, if `child` has been linked locally (e.g. `npm link`/`yarn link`) into `parent` 69 | when `validate-peer-dependencies` is ran it will incorrectly believe that `parent` has satisfied 70 | the contract, but in fact it _may_ not have. This is a smallish edge case, but still a possible 71 | issue. 72 | 73 | These known issues are mitigated by passing in the 74 | `resolvePeerDependenciesFrom` with the root directory of `parent`. As noted in 75 | the documentation for that option below, you often do not have access to the 76 | correct value for `resolvePeerDependenciesFrom` but in some ecosystems (e.g. 77 | ember-cli addons) you **do**. In scenarios where you can use it, you 78 | **absolutely** should. 79 | 80 | ### Options 81 | 82 | A few custom options are available for use: 83 | 84 | * `cache` - Can be `false` to disable caching, or a `Map` instance to use your own custom cache 85 | * `handleFailure` - A callback function that will be invoked if validation fails 86 | * `resolvePeerDependenciesFrom` - The path that should be used as the starting point for resolving `peerDependencies` from 87 | 88 | #### `cache` 89 | 90 | Pass this option to either prevent caching completely (useful in testing 91 | scenarios), or to provide a custom cache. 92 | 93 | ```js 94 | const validatePeerDependencies = require('validate-peer-dependencies'); 95 | 96 | // completely disable caching 97 | validatePeerDependencies(__dirname, { cache: false }); 98 | 99 | // instruct caching system to leverage your own cache 100 | const cache = new Map(); 101 | validatePeerDependencies(__dirname, { cache }); 102 | ``` 103 | 104 | #### `resolvePeerDependenciesFrom` 105 | 106 | Pass this option if you **know** the base directory (the dir containing the 107 | `package.json`) that should be used as the starting point of peer dependency 108 | resolution. 109 | 110 | For example, given the following dependencies: 111 | 112 | * Package `parent` depends on `child` and `sibling` 113 | * Package `child` has a peer dependency on `sibling` package 114 | * Package `child` uses `validate-peer-dependencies` to confirm that `sibling` is 115 | provided 116 | 117 | _Most_ of the time in the Node ecosystem you can not actually know the path to 118 | `parent` (it could be hoisted / deduplicated to any number of possible 119 | locations), but in some (some what special) circumstances you can. For example, 120 | in the `ember-cli` addon ecosystem an addon is instantiated with access to the 121 | root path of the package that included it (`parent` in the example above). 122 | 123 | The main benefit of specifying `resolvePeerDependenciesFrom` is that while 124 | locally developing `child` you might `npm link`/`yarn link` it into `parent` 125 | manually. In that case the default behavior (using the directory that contains 126 | `child`'s `package.json`) is not correct! When linking (and not specifying 127 | `resolvePeerDependenciesFrom`) the invocation to `validatePeerDependencies` 128 | would **always** find the peer dependencies (even if the `parent` didn't have 129 | them installed) because the locally linked copy of `child` would have specified 130 | them in its `devDependencies` and therefore the peer dependency would be 131 | resolvable from `child`'s on disk location. 132 | 133 | Here is an example of what usage by an ember-cli addon would look like: 134 | 135 | ```javascript 136 | 'use strict'; 137 | 138 | const validatePeerDependencies = require('validate-peer-dependencies'); 139 | 140 | module.exports = { 141 | // ...snip... 142 | init() { 143 | this._super.init.apply(this, arguments); 144 | 145 | validatePeerDependencies(__dirname, { 146 | resolvePeerDependenciesFrom: this.parent.root, 147 | }); 148 | } 149 | }; 150 | ``` 151 | 152 | Or alternatively, if it only makes sense for the addon to validate peer deps 153 | during a build, that would look like: 154 | 155 | ```javascript 156 | 'use strict'; 157 | 158 | const validatePeerDependencies = require('validate-peer-dependencies'); 159 | 160 | module.exports = { 161 | included(parent) { 162 | this._super.included.apply(this, arguments); 163 | 164 | validatePeerDependencies(__dirname, { 165 | resolvePeerDependenciesFrom: parent.root, 166 | }); 167 | 168 | return parent; 169 | } 170 | }; 171 | ``` 172 | 173 | #### `handleFailure` 174 | 175 | By default, `validatePeerDependencies` emits an error that looks like: 176 | 177 | ``` 178 | test-app has the following unmet peerDependencies: 179 | 180 | * bar: `>= 2`; it was not installed 181 | * foo: `> 1`; it was resolved to `1.0.0` 182 | ``` 183 | 184 | If you would like to customize the error message (or handle the failure in a 185 | different way), you can provide a custom `handleFailure` callback. 186 | 187 | The callback will be passed in a result object with the following interface: 188 | 189 | ```ts 190 | interface IncompatibleDependency { 191 | /** 192 | The name of the package that was incompatible. 193 | */ 194 | name: string; 195 | 196 | /** 197 | The peer dependency range that was specified. 198 | */ 199 | specifiedPeerDependencyRange: string; 200 | 201 | /** 202 | The version that was actually found. 203 | */ 204 | version: string; 205 | } 206 | 207 | interface MissingPeerDependency { 208 | /** 209 | The name of the package that was incompatible. 210 | */ 211 | name: string; 212 | 213 | /** 214 | The peer dependency range that was specified. 215 | */ 216 | specifiedPeerDependencyRange: string; 217 | } 218 | 219 | interface Result { 220 | /** 221 | The `package.json` contents that were resolved from the specified root 222 | directory. 223 | */ 224 | pkg: unknown; 225 | 226 | /** 227 | The path to the `package.json` that was resolved from the specified root 228 | directory. 229 | */ 230 | packagePath: string; 231 | 232 | /** 233 | The list of peer dependencies that were not found. 234 | */ 235 | incompatibleRanges: IncompatibleDependency[]; 236 | 237 | /** 238 | The list of peer dependencies that were found, but did not match the 239 | specified semver range. 240 | */ 241 | missingPeerDependencies: MissingPeerDependency[]; 242 | } 243 | ``` 244 | 245 | For example, this is how you might override the default error message to customize: 246 | 247 | ```js 248 | validatePeerDependencies(__dirname, { 249 | handleFailure(result) { 250 | let { missingPeerDependencies, incompatibleRanges } = result; 251 | 252 | let missingPeerDependenciesMessage = (missingPeerDependencies || []).reduce( 253 | (message, metadata) => { 254 | return `${message}\n\t* ${metadata.name}: \`${metadata.specifiedPeerDependencyRange}\`; it was not installed`; 255 | }, 256 | '' 257 | ); 258 | 259 | let incompatiblePeerDependenciesMessage = (incompatibleRanges || []).reduce( 260 | (message, metadata) => { 261 | return `${message}\n\t* ${metadata.name}: \`${metadata.specifiedPeerDependencyRange}\`; it was resolved to \`${metadata.version}\``; 262 | }, 263 | '' 264 | ); 265 | 266 | throw new Error( 267 | `${result.pkg.name} has the following unmet peerDependencies:\n${missingPeerDependenciesMessage}${incompatiblePeerDependenciesMessage}` 268 | ); 269 | }, 270 | }); 271 | ``` 272 | 273 | ### assumeProvided 274 | 275 | It is sometimes desirable to treat a peer dependency as satisfied even when it would not be considered satisfied under the node resolution algorithm. 276 | 277 | For example an ember addon may consider itself to satisfy the peer dependency requirements of one of its own dev dependencies during local development. 278 | 279 | ```js 280 | const assumeProvided = require('validate-peer-dependencies').assumeProvided; 281 | 282 | // subsequent calls to validatePeerDependencies will assume some-package is available and will resolve to version 1.2.3 283 | assumeProvided({ name: 'some-package', version: '1.2.3' }); 284 | 285 | // for the more common case of the package assuming itself to be available during development, the following is the likely preferred invocation 286 | assumeProvided(require('./package.json')); 287 | ``` 288 | 289 | Note that assumptions are global, since peer dependency validation may be occurring in different instances of `validate-peer-dependencies`. 290 | 291 | ### Disabling checks via Environment Variables 292 | 293 | It can be helpful to disable checks when doing certain kinds of testing (e.g. testing a pre-release with breaking changes to see whether any of the changes **actually* break a user). 294 | 295 | This can be done with the environment variables `VALIDATE_PEER_DEPENDENCIES` and `IGNORE_PEER_DEPENDENCIES`. 296 | 297 | * `VALIDATE_PEER_DEPENDENCIES=false` disables all validation. Any other value is ignored 298 | * `IGNORE_PEER_DEPENDENCIES=foo,bar` disablesa peer dependency validatation for `foo` and `bar` 299 | 300 | ## Requirements 301 | 302 | [Active versions](https://nodejs.org/en/about/releases/) of Node are supported. 303 | 304 | ## License 305 | 306 | This project is licensed under the [MIT License](LICENSE.md). 307 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const { Project } = require('fixturify-project'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const fs = require('fs'); 5 | const validatePeerDependencies = require('./index'); 6 | const { assumeProvided, _resetAssumptions } = validatePeerDependencies; 7 | 8 | const ROOT = process.cwd(); 9 | 10 | describe('validate-peer-dependencies', function () { 11 | let project; 12 | 13 | beforeEach(() => { 14 | project = new Project('test-app'); 15 | }); 16 | 17 | afterEach(async () => { 18 | await project.dispose(); 19 | 20 | process.chdir(ROOT); 21 | }); 22 | 23 | it('throws an error when peerDependencies are not present', async () => { 24 | project.pkg.peerDependencies = { 25 | foo: '> 1', 26 | }; 27 | 28 | await project.write(); 29 | 30 | expect(() => validatePeerDependencies(project.baseDir)) 31 | .toThrowErrorMatchingInlineSnapshot(` 32 | "test-app has the following unmet peerDependencies: 33 | 34 | * foo: \`> 1\`; it was not installed" 35 | `); 36 | }); 37 | 38 | it('throws an error when an entry is in peerDependencies **and** in dependencies', async () => { 39 | project.pkg.peerDependencies = { 40 | foo: '>= 1', 41 | }; 42 | project.addDependency('foo', '1.0.0'); 43 | await project.write(); 44 | 45 | process.chdir(project.baseDir); 46 | 47 | expect(() => validatePeerDependencies(project.baseDir)) 48 | .toThrowErrorMatchingInlineSnapshot(` 49 | "test-app (at \`./\`) is improperly configured: 50 | 51 | * foo: included both as dependency and as a peer dependency" 52 | `); 53 | }); 54 | 55 | it('throws an error when peerDependencies are present but at the wrong version', async () => { 56 | project.pkg.peerDependencies = { 57 | foo: '> 1', 58 | }; 59 | 60 | project.addDevDependency('foo', '1.0.0'); 61 | await project.write(); 62 | 63 | expect(() => validatePeerDependencies(project.baseDir)) 64 | .toThrowErrorMatchingInlineSnapshot(` 65 | "test-app has the following unmet peerDependencies: 66 | 67 | * foo: \`> 1\`; it was resolved to \`1.0.0\`" 68 | `); 69 | }); 70 | 71 | it('throws an error when peerDependencies that are optional are present but at the wrong version', async () => { 72 | project.pkg.peerDependencies = { 73 | foo: '> 1', 74 | }; 75 | project.pkg.peerDependenciesMeta = { 76 | foo: { 77 | optional: true, 78 | }, 79 | }; 80 | 81 | project.addDevDependency('foo', '1.0.0'); 82 | await project.write(); 83 | 84 | expect(() => validatePeerDependencies(project.baseDir)) 85 | .toThrowErrorMatchingInlineSnapshot(` 86 | "test-app has the following unmet peerDependencies: 87 | 88 | * foo: \`> 1\`; it was resolved to \`1.0.0\`" 89 | `); 90 | }); 91 | 92 | it('throws if some peerDependencies are met and others are missing', async () => { 93 | project.pkg.peerDependencies = { 94 | foo: '> 1', 95 | bar: '>= 2', 96 | }; 97 | 98 | project.addDevDependency('foo', '2.0.0'); 99 | await project.write(); 100 | 101 | expect(() => validatePeerDependencies(project.baseDir)) 102 | .toThrowErrorMatchingInlineSnapshot(` 103 | "test-app has the following unmet peerDependencies: 104 | 105 | * bar: \`>= 2\`; it was not installed" 106 | `); 107 | }); 108 | 109 | it('throws if some peerDependencies are met and others are on an unsupported version', async () => { 110 | project.pkg.peerDependencies = { 111 | foo: '> 1', 112 | bar: '>= 2', 113 | }; 114 | 115 | project.addDevDependency('foo', '2.0.0'); 116 | project.addDevDependency('bar', '1.0.0'); 117 | await project.write(); 118 | 119 | expect(() => validatePeerDependencies(project.baseDir)) 120 | .toThrowErrorMatchingInlineSnapshot(` 121 | "test-app has the following unmet peerDependencies: 122 | 123 | * bar: \`>= 2\`; it was resolved to \`1.0.0\`" 124 | `); 125 | }); 126 | 127 | it('throws when some peerDependencies are missing and some are outdated', async () => { 128 | project.pkg.peerDependencies = { 129 | foo: '> 1', 130 | bar: '>= 2', 131 | }; 132 | 133 | project.addDevDependency('foo', '1.0.0'); 134 | await project.write(); 135 | 136 | expect(() => validatePeerDependencies(project.baseDir)) 137 | .toThrowErrorMatchingInlineSnapshot(` 138 | "test-app has the following unmet peerDependencies: 139 | 140 | * bar: \`>= 2\`; it was not installed 141 | * foo: \`> 1\`; it was resolved to \`1.0.0\`" 142 | `); 143 | }); 144 | 145 | it('does not throw when peerDependencies are satisfied', async () => { 146 | project.pkg.peerDependencies = { 147 | foo: '>= 1', 148 | }; 149 | 150 | project.addDevDependency('foo', '1.0.0'); 151 | await project.write(); 152 | 153 | validatePeerDependencies(project.baseDir); 154 | }); 155 | 156 | it('does not throw when peerDependencies are optional', async () => { 157 | project.pkg.peerDependencies = { 158 | foo: '>= 1', 159 | }; 160 | 161 | project.pkg.peerDependenciesMeta = { 162 | foo: { 163 | optional: true, 164 | }, 165 | }; 166 | 167 | await project.write(); 168 | 169 | validatePeerDependencies(project.baseDir); 170 | }); 171 | 172 | it('errors with a helpful message when the provided project root does not contain a package.json', async () => { 173 | let tmpdir = await fs.promises.mkdtemp( 174 | path.join(os.tmpdir(), 'fake-project-') 175 | ); 176 | 177 | expect(() => { 178 | validatePeerDependencies(tmpdir); 179 | }).toThrowError( 180 | `validate-peer-dependencies could not find a package.json when resolving upwards from:\n\t${tmpdir}` 181 | ); 182 | }); 183 | 184 | it('allows prerelease ranges that are greater than the specified set', async () => { 185 | project.pkg.peerDependencies = { 186 | foo: '>= 1', 187 | bar: '^2.0.0', 188 | }; 189 | 190 | project.addDevDependency('foo', '1.1.0-beta.1'); 191 | project.addDevDependency('bar', '2.1.0-alpha.1'); 192 | await project.write(); 193 | 194 | validatePeerDependencies(project.baseDir); 195 | }); 196 | 197 | describe('resolvePeerDependenciesFrom', () => { 198 | it('when resolvePeerDependenciesFrom is provided the cached results are independent of usages without resolvePeerDependenciesFrom for the same parentRoot', async () => { 199 | let linkedPackage = new Project('foo'); 200 | 201 | try { 202 | linkedPackage.pkg.peerDependencies = { 203 | bar: '^1.0.0', 204 | }; 205 | await linkedPackage.write(); 206 | 207 | project.addDevDependency('bar', '1.0.0'); 208 | await project.write(); 209 | 210 | validatePeerDependencies(linkedPackage.baseDir, { 211 | resolvePeerDependenciesFrom: project.baseDir, 212 | }); 213 | 214 | expect(() => validatePeerDependencies(linkedPackage.baseDir)) 215 | .toThrowErrorMatchingInlineSnapshot(` 216 | "foo has the following unmet peerDependencies: 217 | 218 | * bar: \`^1.0.0\`; it was not installed" 219 | `); 220 | } finally { 221 | linkedPackage.dispose(); 222 | } 223 | }); 224 | 225 | it('can provide custom base directory for peerDependency resolution (for linking situations)', async () => { 226 | let linkedPackage = new Project('foo'); 227 | 228 | try { 229 | linkedPackage.pkg.peerDependencies = { 230 | bar: '^1.0.0', 231 | }; 232 | await linkedPackage.write(); 233 | 234 | project.addDevDependency('bar', '1.0.0'); 235 | await project.write(); 236 | 237 | validatePeerDependencies(linkedPackage.baseDir, { 238 | resolvePeerDependenciesFrom: project.baseDir, 239 | }); 240 | } finally { 241 | linkedPackage.dispose(); 242 | } 243 | }); 244 | }); 245 | 246 | describe('caching', () => { 247 | it('caches failure to find package.json from parentRoot by default', async () => { 248 | let tmpdir = await fs.promises.mkdtemp( 249 | path.join(os.tmpdir(), 'fake-project-') 250 | ); 251 | 252 | expect(() => { 253 | validatePeerDependencies(tmpdir); 254 | }).toThrowError( 255 | `validate-peer-dependencies could not find a package.json when resolving upwards from:\n\t${tmpdir}` 256 | ); 257 | }); 258 | 259 | it('can prevent caching failure to find package.json from parentRoot with cache: false', async () => { 260 | let tmpdir = await fs.promises.mkdtemp( 261 | path.join(os.tmpdir(), 'fake-project-') 262 | ); 263 | project.baseDir = tmpdir; 264 | 265 | expect(() => { 266 | validatePeerDependencies(project.baseDir, { cache: false }); 267 | }).toThrowError( 268 | `validate-peer-dependencies could not find a package.json when resolving upwards from:\n\t${project.baseDir}` 269 | ); 270 | 271 | await project.write(); 272 | validatePeerDependencies(project.baseDir, { cache: false }); 273 | }); 274 | 275 | it('caches succesfull results by default', async () => { 276 | project.pkg.peerDependencies = { 277 | foo: '>= 1', 278 | }; 279 | 280 | project.addDevDependency('foo', '1.0.0'); 281 | await project.write(); 282 | 283 | validatePeerDependencies(project.baseDir); 284 | 285 | // TODO: expose a public API to fixturify-project to remove deps/devDeps? 286 | delete project._devDependencies.foo; 287 | await project.write(); 288 | 289 | // does not error because it was cached 290 | validatePeerDependencies(project.baseDir); 291 | }); 292 | 293 | it('caches failures by default', async () => { 294 | project.pkg.peerDependencies = { 295 | foo: '>= 1', 296 | }; 297 | 298 | await project.write(); 299 | 300 | expect(() => validatePeerDependencies(project.baseDir)) 301 | .toThrowErrorMatchingInlineSnapshot(` 302 | "test-app has the following unmet peerDependencies: 303 | 304 | * foo: \`>= 1\`; it was not installed" 305 | `); 306 | 307 | project.addDevDependency('foo', '1.0.0'); 308 | await project.write(); 309 | 310 | expect(() => validatePeerDependencies(project.baseDir)) 311 | .toThrowErrorMatchingInlineSnapshot(` 312 | "test-app has the following unmet peerDependencies: 313 | 314 | * foo: \`>= 1\`; it was not installed" 315 | `); 316 | }); 317 | 318 | it('can prevent caching by passing `cache: false` option', async () => { 319 | project.pkg.peerDependencies = { 320 | foo: '>= 1', 321 | }; 322 | 323 | await project.write(); 324 | 325 | expect(() => 326 | validatePeerDependencies(project.baseDir, { 327 | cache: false, 328 | }) 329 | ).toThrowErrorMatchingInlineSnapshot(` 330 | "test-app has the following unmet peerDependencies: 331 | 332 | * foo: \`>= 1\`; it was not installed" 333 | `); 334 | 335 | project.addDevDependency('foo', '1.0.0'); 336 | await project.write(); 337 | 338 | validatePeerDependencies(project.baseDir); 339 | }); 340 | 341 | it('provide its own cache', async () => { 342 | let cache = new Map(); 343 | project.pkg.peerDependencies = { 344 | foo: '>= 1', 345 | }; 346 | 347 | await project.write(); 348 | 349 | expect(() => 350 | validatePeerDependencies(project.baseDir, { 351 | cache, 352 | }) 353 | ).toThrowErrorMatchingInlineSnapshot(` 354 | "test-app has the following unmet peerDependencies: 355 | 356 | * foo: \`>= 1\`; it was not installed" 357 | `); 358 | 359 | expect(cache.size).toEqual(1); 360 | }); 361 | }); 362 | 363 | describe('handleFailure', () => { 364 | it('provide its own handleFailure', async () => { 365 | expect.hasAssertions(); 366 | 367 | project.pkg.peerDependencies = { 368 | foo: '>= 1', 369 | bar: '>= 2', 370 | }; 371 | 372 | project.addDevDependency('bar', '1.0.0'); 373 | await project.write(); 374 | 375 | validatePeerDependencies(project.baseDir, { 376 | handleFailure(result) { 377 | expect(result).toMatchInlineSnapshot( 378 | { 379 | packagePath: expect.stringMatching('package.json'), 380 | }, 381 | ` 382 | Object { 383 | "incompatibleRanges": Array [ 384 | Object { 385 | "name": "bar", 386 | "specifiedPeerDependencyRange": ">= 2", 387 | "version": "1.0.0", 388 | }, 389 | ], 390 | "missingPeerDependencies": Array [ 391 | Object { 392 | "name": "foo", 393 | "specifiedPeerDependencyRange": ">= 1", 394 | }, 395 | ], 396 | "packagePath": StringMatching /package\\.json/, 397 | "pkg": Object { 398 | "dependencies": Object {}, 399 | "devDependencies": Object { 400 | "bar": "1.0.0", 401 | }, 402 | "keywords": Array [], 403 | "name": "test-app", 404 | "peerDependencies": Object { 405 | "bar": ">= 2", 406 | "foo": ">= 1", 407 | }, 408 | "version": "0.0.0", 409 | }, 410 | } 411 | ` 412 | ); 413 | }, 414 | }); 415 | }); 416 | }); 417 | }); 418 | 419 | describe('assumeProvided', function () { 420 | let project; 421 | 422 | beforeEach(() => { 423 | project = new Project('test-app'); 424 | }); 425 | 426 | afterEach(async () => { 427 | _resetAssumptions(); 428 | await project.dispose(); 429 | 430 | process.chdir(ROOT); 431 | }); 432 | 433 | it('throws if passed an object that lacks either name or version', async function () { 434 | expect(() => assumeProvided()).toThrowErrorMatchingInlineSnapshot( 435 | `"assumeProvided({ name, version}): name and version are required, but name='undefined' version='undefined'"` 436 | ); 437 | 438 | expect(() => 439 | assumeProvided({ name: 'best package' }) 440 | ).toThrowErrorMatchingInlineSnapshot( 441 | `"assumeProvided({ name, version}): name and version are required, but name='best package' version='undefined'"` 442 | ); 443 | 444 | expect(() => 445 | assumeProvided({ version: '5.0.1' }) 446 | ).toThrowErrorMatchingInlineSnapshot( 447 | `"assumeProvided({ name, version}): name and version are required, but name='undefined' version='5.0.1'"` 448 | ); 449 | }); 450 | 451 | it('can be used to provide satisfy missing peer dependencies', async function () { 452 | project.pkg.peerDependencies = { 453 | foo: '> 1', 454 | }; 455 | 456 | await project.write(); 457 | 458 | expect(() => validatePeerDependencies(project.baseDir, { cache: false })) 459 | .toThrowErrorMatchingInlineSnapshot(` 460 | "test-app has the following unmet peerDependencies: 461 | 462 | * foo: \`> 1\`; it was not installed" 463 | `); 464 | 465 | assumeProvided({ 466 | name: 'foo', 467 | version: '2.0.0', 468 | }); 469 | 470 | // now it doesn't throw 471 | validatePeerDependencies(project.baseDir, { cache: false }); 472 | }); 473 | 474 | it('uses the last value provided', async function () { 475 | project.pkg.peerDependencies = { 476 | foo: '> 1', 477 | }; 478 | 479 | await project.write(); 480 | 481 | assumeProvided({ 482 | name: 'foo', 483 | version: '0.5.0', 484 | }); 485 | 486 | assumeProvided({ 487 | name: 'foo', 488 | version: '2.0.0', 489 | }); 490 | 491 | validatePeerDependencies(project.baseDir); 492 | }); 493 | 494 | it('supersedes resolution', async function () { 495 | project.pkg.peerDependencies = { 496 | foo: '>= 1', 497 | }; 498 | 499 | project.addDevDependency('foo', '1.0.0'); 500 | await project.write(); 501 | 502 | validatePeerDependencies(project.baseDir, { cache: false }); 503 | 504 | assumeProvided({ 505 | name: 'foo', 506 | version: '0.5.0', 507 | }); 508 | 509 | // the assumption takes priority over anything resolvable, so now we 510 | // consider the peer dependency unmet 511 | expect(() => validatePeerDependencies(project.baseDir, { cache: false })) 512 | .toThrowErrorMatchingInlineSnapshot(` 513 | "test-app has the following unmet peerDependencies: 514 | 515 | * foo: \`>= 1\`; it was resolved to \`0.5.0\`" 516 | `); 517 | }); 518 | 519 | it('does not prevent other peer dependencies from being validated', async function () { 520 | project.pkg.peerDependencies = { 521 | foo: '>= 1', 522 | bar: '> 41', 523 | }; 524 | 525 | project.addDevDependency('foo', '1.0.0'); 526 | await project.write(); 527 | 528 | assumeProvided({ 529 | name: 'bar', 530 | version: '42.0.1', 531 | }); 532 | 533 | // making assumptions about bar does not interfere with resolving foo 534 | validatePeerDependencies(project.baseDir, { cache: false }); 535 | }); 536 | 537 | it('does not throw an error when peerDependencies are not present with process.env.VALIDATE_PEER_DEPENDENCIES=false', async () => { 538 | try { 539 | process.env.VALIDATE_PEER_DEPENDENCIES = 'false'; 540 | 541 | project.pkg.peerDependencies = { 542 | foo: '> 1', 543 | }; 544 | 545 | await project.write(); 546 | 547 | // should not throw an error 548 | validatePeerDependencies(project.baseDir); 549 | } finally { 550 | delete process.env.VALIDATE_PEER_DEPENDENCIES; 551 | } 552 | }); 553 | 554 | it('does not throw an error for incorrect specified peerDependencies process.env.IGNORE_PEER_DEPENDENCIES=foo', async () => { 555 | try { 556 | process.env.IGNORE_PEER_DEPENDENCIES = 'bar,foo'; 557 | 558 | project.pkg.peerDependencies = { 559 | foo: '> 1', 560 | }; 561 | 562 | project.addDevDependency('foo', '2.0.0'); 563 | await project.write(); 564 | 565 | // should not throw an error 566 | validatePeerDependencies(project.baseDir); 567 | } finally { 568 | delete process.env.IGNORE_PEER_DEPENDENCIES; 569 | } 570 | }); 571 | }); 572 | --------------------------------------------------------------------------------