├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── CI.yml │ └── Deploy.yml ├── src ├── components │ ├── Card.scss │ ├── Quote.scss │ ├── App.scss │ ├── Footer.scss │ ├── Header.js │ ├── Card.js │ ├── ExplainConstraintHyphen.js │ ├── Alert.js │ ├── ExplainConstraintCaret.js │ ├── Quote.js │ ├── Explain.js │ ├── ExplainConstraintPessimistic.js │ ├── ExplainConstraintStrict.js │ ├── ConstraintType.js │ ├── Form.js │ ├── App.js │ ├── CopyUrl.js │ ├── Explain.spec.js │ ├── ExplainConstraintWildcard.js │ ├── WhatYouGet.js │ ├── ExplainConstraintRange.js │ ├── ExplainConstraintTilde.js │ ├── Why.js │ ├── WhyLoose.js │ ├── ExplainVersion.js │ ├── Footer.js │ ├── Version.js │ ├── Constraint.js │ ├── Implementations.js │ ├── Version.spec.js │ ├── Constraint.spec.js │ ├── Router.js │ ├── WhyStrict.js │ ├── ExplainConstraint.js │ └── ExplainConstraint.spec.js ├── index.css ├── history.js ├── setupTests.js ├── index.js ├── actions.js ├── store.js ├── semver.js ├── serviceWorker.js └── semver.spec.js ├── public ├── google9b0e39e61d9ceee7.html ├── favicon.ico ├── manifest.json └── index.html ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jubianchi] 2 | -------------------------------------------------------------------------------- /src/components/Card.scss: -------------------------------------------------------------------------------- 1 | .card :last-child { 2 | margin-bottom: 0; 3 | } 4 | -------------------------------------------------------------------------------- /public/google9b0e39e61d9ceee7.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google9b0e39e61d9ceee7.html 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jubianchi/semver-check/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Quote.scss: -------------------------------------------------------------------------------- 1 | .blockquote { 2 | border-left: 5px solid #dee2e6; 3 | font-size: 1rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/App.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap.scss'; 2 | 3 | code { 4 | color: currentColor; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | padding-top: 5%; 5 | font-family: sans-serif; 6 | } 7 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from 'history'; 2 | 3 | export default createHashHistory({ 4 | hashType: 'slash', 5 | }); 6 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme/build'; 2 | import Adapter from 'enzyme-adapter-react-16/build'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/components/Footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | font-size: 0.6rem; 3 | } 4 | 5 | footer h3 { 6 | font-size: 0.8rem; 7 | } 8 | 9 | footer img { 10 | border: 5px solid #dee2e6; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default props => ( 4 |
5 |
6 |

Semver check

7 |
8 |
9 | ); 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | assignees: 9 | - 'jubianchi' 10 | versioning-strategy: 'lockfile-only' 11 | pull-request-branch-name: 12 | separator: '-' 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # css 24 | src/components/*.css 25 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Online Semver checker", 3 | "name": "Online Semver checker", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Online Semver checker 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Card.scss'; 4 | 5 | const Card = props => ( 6 |
7 |
{props.children}
8 |
9 | ); 10 | 11 | Card.propTypes = { 12 | className: PropTypes.string, 13 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, 14 | }; 15 | 16 | Card.defaultProps = { 17 | className: '', 18 | }; 19 | 20 | export default Card; 21 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintHyphen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ExplainConstraintHyphen = props => ( 5 |

6 | {props.constraint.constraint} is a hyphen constraint. It means that it will match{' '} 7 | several versions. 8 |

9 | ); 10 | 11 | ExplainConstraintHyphen.propTypes = { 12 | className: PropTypes.string, 13 | constraint: PropTypes.object.isRequired, 14 | }; 15 | 16 | export default ExplainConstraintHyphen; 17 | -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Alert = props => { 5 | const type = props.warning === true ? 'warning' : props.error === true ? 'danger' : 'info'; 6 | 7 | return
{props.children}
; 8 | }; 9 | 10 | Alert.propTypes = { 11 | className: PropTypes.string, 12 | warning: PropTypes.bool, 13 | error: PropTypes.bool, 14 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, 15 | }; 16 | 17 | Alert.defaultProps = { 18 | warning: false, 19 | error: false, 20 | }; 21 | 22 | export default Alert; 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 0.1.0 (2018-09-06) 4 | 5 | ### Bug Fixes 6 | 7 | - Constraint verification is correct ([2babd1e](https://github.com/jubianchi/semver-check/commit/2babd1e)), closes [#37](https://github.com/jubianchi/semver-check/issues/37) [#38](https://github.com/jubianchi/semver-check/issues/38) 8 | - Incomplete (X, X.Y) ranges (caret and tilde) are correctly handled ([1cf3000](https://github.com/jubianchi/semver-check/commit/1cf3000)), closes [#39](https://github.com/jubianchi/semver-check/issues/39) 9 | - Strict constraint are correctly matched ([166313a](https://github.com/jubianchi/semver-check/commit/166313a)), closes [#36](https://github.com/jubianchi/semver-check/issues/36) 10 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintCaret.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import WhatYouGet from './WhatYouGet'; 4 | 5 | const ExplainConstraintCaret = props => ( 6 | 7 |

8 | {props.constraint.constraint} is a caret constraint. It means that it will 9 | match several versions. 10 |

11 | 12 | 13 |
14 | ); 15 | 16 | ExplainConstraintCaret.propTypes = { 17 | className: PropTypes.string, 18 | constraint: PropTypes.object.isRequired, 19 | }; 20 | 21 | export default ExplainConstraintCaret; 22 | -------------------------------------------------------------------------------- /src/components/Quote.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Quote.scss'; 4 | 5 | const Quote = props => ( 6 |
7 | {props.children} 8 | 9 | 12 |
13 | ); 14 | 15 | Quote.propTypes = { 16 | className: PropTypes.string, 17 | author: PropTypes.string.isRequired, 18 | source: PropTypes.string.isRequired, 19 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, 20 | }; 21 | 22 | Quote.defaultProps = { 23 | className: '', 24 | }; 25 | 26 | export default Quote; 27 | -------------------------------------------------------------------------------- /src/components/Explain.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import ExplainConstraint from './ExplainConstraint'; 5 | import ExplainVersion from './ExplainVersion'; 6 | 7 | export const Explain = props => ( 8 |
9 |
{props.constraint.semver !== null && }
10 |
{props.version.semver !== null && }
11 |
12 | ); 13 | 14 | Explain.propTypes = { 15 | className: PropTypes.string, 16 | constraint: PropTypes.object, 17 | version: PropTypes.object, 18 | }; 19 | 20 | export default connect(state => state)(Explain); 21 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintPessimistic.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | 5 | const ExplainConstraintPessimistic = props => ( 6 | 7 |

8 | {props.constraint.constraint} is a pessimistic constraint. It means that it 9 | will match several versions. 10 |

11 | 12 | 13 | This is a special notation only supported by Bundler. 14 | 15 |
16 | ); 17 | 18 | ExplainConstraintPessimistic.propTypes = { 19 | className: PropTypes.string, 20 | constraint: PropTypes.object.isRequired, 21 | }; 22 | 23 | export default ExplainConstraintPessimistic; 24 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintStrict.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | 5 | const ExplainConstraintStrict = props => ( 6 | 7 |

8 | {props.constraint.constraint} is a strict constraint. It means that it will 9 | match a single version. 10 |

11 | 12 | 13 | This constraint is too strict which means you won't even get bug fixes. 14 | 15 |
16 | ); 17 | 18 | ExplainConstraintStrict.propTypes = { 19 | className: PropTypes.string, 20 | constraint: PropTypes.object.isRequired, 21 | }; 22 | 23 | export default ExplainConstraintStrict; 24 | -------------------------------------------------------------------------------- /src/components/ConstraintType.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | const type = semver => { 4 | if (semver.caret) { 5 | return 'Caret'; 6 | } 7 | 8 | if (semver.tilde) { 9 | return 'Tilde'; 10 | } 11 | 12 | if (semver.strict) { 13 | return 'Strict'; 14 | } 15 | 16 | if (semver.hyphen) { 17 | return 'Hyphen'; 18 | } 19 | 20 | if (semver.wildcard) { 21 | return 'X-Range'; 22 | } 23 | 24 | if (semver.pessimistic) { 25 | return 'Pessimistic'; 26 | } 27 | 28 | if (semver.rangeSet) { 29 | return 'Range-set'; 30 | } 31 | 32 | if (semver.range) { 33 | return 'Range'; 34 | } 35 | 36 | return 'Weird'; 37 | }; 38 | 39 | const ConstraintType = props => type(props.constraint.semver); 40 | 41 | ConstraintType.propTypes = { 42 | className: PropTypes.string, 43 | constraint: PropTypes.object.isRequired, 44 | }; 45 | 46 | export default ConstraintType; 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import ReactGA from 'react-ga'; 5 | import { ConnectedRouter } from 'connected-react-router'; 6 | import './index.css'; 7 | import App from './components/App'; 8 | import * as serviceWorker from './serviceWorker'; 9 | import store from './store'; 10 | import history from './history'; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root'), 21 | ); 22 | 23 | if (process.env.NODE_ENV !== 'production') { 24 | serviceWorker.unregister(); 25 | } else { 26 | ReactGA.initialize('UA-56445984-1', { 27 | debug: false, 28 | }); 29 | ReactGA.ga('send', 'pageview'); 30 | 31 | serviceWorker.register(); 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Cache node_modules 19 | id: yarn-cache 20 | uses: actions/cache@v2 21 | with: 22 | path: node_modules 23 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-node- 26 | 27 | - name: Install dependencies 28 | if: steps.yarn-cache.outputs.cache-hit != 'true' 29 | run: yarn 30 | 31 | - name: Coding style 32 | run: yarn cs -l 33 | 34 | - name: Tests 35 | run: yarn test 36 | 37 | - name: Build 38 | run: yarn build 39 | -------------------------------------------------------------------------------- /.github/workflows/Deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Cache node_modules 16 | id: yarn-cache 17 | uses: actions/cache@v2 18 | with: 19 | path: node_modules 20 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-node- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn 27 | 28 | - name: Build 29 | run: yarn build 30 | 31 | - name: Deploy 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./build 36 | enable_jekyll: false 37 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Constraint from './Constraint'; 5 | import Version from './Version'; 6 | import { pushVersion, pushConstraint } from '../actions'; 7 | 8 | export const Form = props => ( 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | ); 18 | 19 | Form.propTypes = { 20 | className: PropTypes.string, 21 | onConstraint: PropTypes.func.isRequired, 22 | onVersion: PropTypes.func.isRequired, 23 | constraint: PropTypes.object, 24 | version: PropTypes.object, 25 | }; 26 | 27 | export default connect( 28 | state => state, 29 | dispatch => ({ 30 | onConstraint: value => dispatch(pushConstraint(value)), 31 | onVersion: value => dispatch(pushVersion(value)), 32 | }), 33 | )(Form); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 @overnetcity, @jubianchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.scss'; 3 | import Header from './Header'; 4 | import Form from './Form'; 5 | import Explain from './Explain'; 6 | import Why from './Why'; 7 | import Implementations from './Implementations'; 8 | import WhyStrict from './WhyStrict'; 9 | import WhyLoose from './WhyLoose'; 10 | import Footer from './Footer'; 11 | import Router from './Router'; 12 | import CopyUrl from './CopyUrl'; 13 | 14 | class App extends Component { 15 | render() { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
38 | ); 39 | } 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/components/CopyUrl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Octicon from 'react-octicon'; 5 | import Clipboard from 'react-clipboard.js'; 6 | 7 | export const CopyUrl = props => 8 | ((props.constraint || {}).semver || (props.version || {}).semver) && ( 9 |
10 |
11 |
COPY THE URL TO THIS CHECK
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | ); 24 | 25 | CopyUrl.propTypes = { 26 | className: PropTypes.string, 27 | constraint: PropTypes.object, 28 | version: PropTypes.object, 29 | }; 30 | 31 | export default connect(state => state)(CopyUrl); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online SemVer Checker 2 | 3 | [![Build Status](https://travis-ci.org/jubianchi/semver-check.svg?branch=master)](https://travis-ci.org/jubianchi/semver-check) 4 | [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 5 | 6 | A basic web app coded with ReactJS to check a version against a SemVer constraint. 7 | 8 | Check it online here: [http://jubianchi.github.io/semver-check](http://jubianchi.github.io/semver-check) 9 | 10 | ## SemVer checker... Why? 11 | 12 | > In the world of software management there exists a dread place called "dependency hell." 13 | 14 | > The bigger your system grows and the more packages you integrate into your software, the more likely you are to find yourself, one day, in this pit of despair. 15 | 16 | More and more projects try to follow [Semantic Versioning](http://semver.org/) to reduce package versioning nightmare and every dependency manager implement its own semantic versioner. 17 | Composer and NPM for example don't handle version constraints the same way. It's hard sometimes to be sure how some library version will behave against some constraint. 18 | 19 | This tiny webapp checks if a given version satisfies another given constraint in the NPM world. 20 | 21 | ## Run it! 22 | 23 | ``` 24 | yarn start 25 | ``` 26 | 27 | This will start the build and open your web browser. 28 | -------------------------------------------------------------------------------- /src/components/Explain.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Explain } from './Explain'; 4 | import ExplainVersion from './ExplainVersion'; 5 | import ExplainConstraint from './ExplainConstraint'; 6 | 7 | describe('Explain', () => { 8 | it('should render version explanation', () => { 9 | const props = { 10 | version: { 11 | semver: {}, 12 | }, 13 | constraint: {}, 14 | }; 15 | 16 | var node = shallow(); 17 | expect(node.find(ExplainVersion).length).toBe(1); 18 | }); 19 | 20 | it('should render constraint explanation', () => { 21 | const props = { 22 | version: {}, 23 | constraint: { 24 | semver: {}, 25 | }, 26 | }; 27 | 28 | var node = shallow(); 29 | expect(node.find(ExplainConstraint).length).toBe(1); 30 | }); 31 | 32 | it('should render both', () => { 33 | const props = { 34 | version: { 35 | semver: {}, 36 | }, 37 | constraint: { 38 | semver: {}, 39 | }, 40 | }; 41 | 42 | var node = shallow(); 43 | expect(node.find(ExplainVersion).length).toBe(1); 44 | expect(node.find(ExplainConstraint).length).toBe(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintWildcard.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | import WhatYouGet from './WhatYouGet'; 5 | 6 | const ExplainConstraintWildcard = props => ( 7 | 8 |

9 | {props.constraint.constraint} is a x-range constraint. It means that it will 10 | match several versions. 11 |

12 | 13 | 22 | 23 | {props.constraint.semver.major === '*' && ( 24 | 25 | This constraint is too loose which means{' '} 26 | you will probably get unexpected breaking changes. 27 | 28 | )} 29 |
30 | ); 31 | 32 | ExplainConstraintWildcard.propTypes = { 33 | className: PropTypes.string, 34 | constraint: PropTypes.object.isRequired, 35 | }; 36 | 37 | export default ExplainConstraintWildcard; 38 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { push } from 'connected-react-router'; 2 | 3 | export const VERSION = 'VERSION'; 4 | export const CONSTRAINT = 'CONSTRAINT'; 5 | 6 | export const version = version => ({ type: VERSION, version }); 7 | export const constraint = constraint => ({ type: CONSTRAINT, constraint }); 8 | 9 | export const pushVersion = version => (dispatch, getState) => { 10 | const state = getState(); 11 | 12 | if (version && state.constraint.constraint) { 13 | dispatch(push(`/${encodeURIComponent(state.constraint.constraint)}/${encodeURIComponent(version)}`)); 14 | } else if (version) { 15 | dispatch(push(`/version/${encodeURIComponent(version)}`)); 16 | } else if (state.constraint.constraint) { 17 | dispatch(push(`/constraint/${encodeURIComponent(state.constraint.constraint)}`)); 18 | } else { 19 | dispatch(push(`/`)); 20 | } 21 | }; 22 | 23 | export const pushConstraint = constraint => (dispatch, getState) => { 24 | const state = getState(); 25 | 26 | if (constraint && state.version.version) { 27 | dispatch(push(`/${encodeURIComponent(constraint)}/${encodeURIComponent(state.version.version)}`)); 28 | } else if (constraint) { 29 | dispatch(push(`/constraint/${encodeURIComponent(constraint)}`)); 30 | } else if (state.version.version) { 31 | dispatch(push(`/version/${encodeURIComponent(state.version.version)}`)); 32 | } else { 33 | dispatch(push(`/`)); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/WhatYouGet.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const WhatYouGet = props => { 5 | if (props.major === false && props.minor === false && props.patch === false) { 6 | return null; 7 | } 8 | 9 | return ( 10 | 11 |

Given the constraint you entered, you will get:

12 | 13 |
    14 | {props.major && ( 15 |
  • 16 | The next major releases which may introduce breaking changes 17 |
  • 18 | )} 19 | {props.minor && ( 20 |
  • 21 | The next minor releases which will provide new features 22 |
  • 23 | )} 24 | {props.patch && ( 25 |
  • 26 | The next patch releases which will fix bugs 27 |
  • 28 | )} 29 |
30 |
31 | ); 32 | }; 33 | 34 | WhatYouGet.propTypes = { 35 | major: PropTypes.bool, 36 | minor: PropTypes.bool, 37 | patch: PropTypes.bool, 38 | }; 39 | 40 | WhatYouGet.defaultProps = { 41 | major: false, 42 | minor: false, 43 | patch: false, 44 | }; 45 | 46 | export default WhatYouGet; 47 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintRange.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Alert from './Alert'; 4 | 5 | const ExplainConstraintRange = props => { 6 | const bound = props.constraint.semver.raw.split(' ').length > 1; 7 | 8 | return ( 9 | 10 |

11 | {props.constraint.constraint} is a range constraint. It means that it will 12 | match several versions. 13 |

14 | 15 | {bound === false && 16 | (['>', '>='].indexOf(props.constraint.semver.operator) > -1 ? ( 17 | 18 | This constraint does not provide an upper bound which means{' '} 19 | you will probably get unexpected breaking changes. 20 | 21 | ) : ( 22 | 23 | This constraint does not provide a lower bound which means{' '} 24 | you will probably get unexpected breaking changes. 25 | 26 | ))} 27 |
28 | ); 29 | }; 30 | 31 | ExplainConstraintRange.propTypes = { 32 | className: PropTypes.string, 33 | constraint: PropTypes.object.isRequired, 34 | }; 35 | 36 | export default ExplainConstraintRange; 37 | -------------------------------------------------------------------------------- /src/components/ExplainConstraintTilde.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import WhatYouGet from './WhatYouGet'; 4 | import Alert from './Alert'; 5 | 6 | const ExplainConstraintTilde = props => ( 7 | 8 |

9 | {props.constraint.constraint} is a tilde constraint. It means that it will 10 | match several versions. 11 |

12 | 13 | 14 | 15 | {props.constraint.semver.major && props.constraint.semver.minor && !props.constraint.semver.patch && ( 16 | 17 |

18 | Composer handles tilde constraint differently. Your constraint will translate to{' '} 19 | 20 | >= 21 | {props.constraint.semver.major}.{props.constraint.semver.minor} 22 | .0 < 23 | {parseInt(props.constraint.semver.major, 10) + 1} 24 | .0.0 25 | 26 | . 27 |

28 | 29 | 30 |
31 | )} 32 |
33 | ); 34 | 35 | ExplainConstraintTilde.propTypes = { 36 | className: PropTypes.string, 37 | constraint: PropTypes.object.isRequired, 38 | }; 39 | 40 | export default ExplainConstraintTilde; 41 | -------------------------------------------------------------------------------- /src/components/Why.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Quote from './Quote'; 4 | 5 | const Why = props => ( 6 |
7 |
8 |

SEMVER CHECKER... WHY?

9 | 10 | 11 |

In the world of software management there exists a dread place called "dependency hell."

12 | 13 |

14 | The bigger your system grows and the more packages you integrate into your software, the more likely 15 | you are to find yourself, one day, in this pit of despair. 16 |

17 |
18 | 19 |

20 | More and more projects try to follow Semantic Versioning to reduce package versioning nightmare and 21 | every dependency manager implements its own semantic versioner.{' '} 22 | Composer and NPM for example 23 | don't handle version constraints the same way. It's hard sometimes to be sure how some library version 24 | will behave against some constraint. 25 |

26 | 27 |

This tiny webapp checks if a given version satisfies another given constraint.

28 |
29 |
30 | ); 31 | 32 | Why.propTypes = { 33 | className: PropTypes.string, 34 | }; 35 | 36 | Why.defaultProps = { 37 | className: '', 38 | }; 39 | 40 | export default Why; 41 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux'; 2 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 3 | import thunk from 'redux-thunk'; 4 | import { VERSION, CONSTRAINT } from './actions'; 5 | import semver from './semver'; 6 | import history from './history'; 7 | 8 | const initialState = { 9 | version: { version: '', semver: null }, 10 | constraint: { constraint: '', semver: null }, 11 | }; 12 | 13 | const version = (state = { version: '', semver: null }, action) => { 14 | if (action.type === VERSION) { 15 | const cleaned = semver.coerce(action.version); 16 | 17 | return { 18 | ...state, 19 | version: action.version, 20 | semver: cleaned, 21 | }; 22 | } 23 | 24 | return state; 25 | }; 26 | 27 | const constraint = (state = { constraint: '', semver: null }, action) => { 28 | if (action.type === CONSTRAINT) { 29 | const cleaned = semver.cleanRange(action.constraint); 30 | 31 | return { 32 | ...state, 33 | constraint: action.constraint, 34 | semver: semver.coerceRange(cleaned), 35 | }; 36 | } 37 | 38 | return state; 39 | }; 40 | 41 | const reducers = combineReducers({ 42 | router: connectRouter(history), 43 | version, 44 | constraint, 45 | }); 46 | 47 | const enhancers = [applyMiddleware(routerMiddleware(history), thunk)]; 48 | 49 | if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__) { 50 | enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__()); 51 | } 52 | 53 | export default createStore(reducers, initialState, compose(...enhancers)); 54 | -------------------------------------------------------------------------------- /src/components/WhyLoose.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Quote from './Quote'; 4 | 5 | const WhyLoose = props => ( 6 |
7 |
8 |

WHY USING LOOSE CONSTRAINT IS BAD?

9 | 10 |

11 | Loose constraint are those constraints matching any version greater than a given one. It is a very bad 12 | idea to use them. 13 |

14 | 15 |

16 | Why? Because with them you are only giving a lower bound to your dependency's version, which means every 17 | version greater than the one you chose, be it a patch, minor or major release. If we read Semantic 18 | Versioning carefully: 19 |

20 | 21 | 22 |
    23 |
  1. 24 | Major version X (x.y.z | x > 0) MUST be incremented if any backwards incompatible changes are 25 | introduced to the public API. It MAY include minor and patch level changes. Patch and minor 26 | version MUST be reset to 0 when major version is incremented. 27 |
  2. 28 |
29 |
30 | 31 |

32 | What does this mean? It means that major releases may break backward compatibility. 33 | With a loose constraint you will get those releases and the BC break they introduce. This is likely not 34 | what you want! 35 |

36 |
37 |
38 | ); 39 | 40 | WhyLoose.propTypes = { 41 | className: PropTypes.string, 42 | }; 43 | 44 | WhyLoose.defaultProps = { 45 | className: '', 46 | }; 47 | 48 | export default WhyLoose; 49 | -------------------------------------------------------------------------------- /src/components/ExplainVersion.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import semver from '../semver'; 4 | import Card from './Card'; 5 | 6 | const ExplainVersion = props => { 7 | let satisfies = null; 8 | 9 | if (props.constraint && props.constraint.semver) { 10 | satisfies = semver.satisfies(props.version.semver, props.constraint.semver); 11 | } 12 | 13 | return ( 14 | 15 |
{props.version.version}
16 | {satisfies === true && ( 17 |
18 | {props.version.version} satisfies constraint {props.constraint.constraint} 19 |
20 | )} 21 | 22 | {satisfies === false && ( 23 |
24 | {props.version.version} does not satisfy constraint{' '} 25 | {props.constraint.constraint} 26 |
27 | )} 28 | 29 |

Given the version you entered:

30 | 31 |
    32 | {['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].map(type => ( 33 |
  • 34 | The next {type} release will be{' '} 35 | {semver.inc(props.version.version, type)} 36 |
  • 37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | 43 | ExplainVersion.propTypes = { 44 | version: PropTypes.object.isRequired, 45 | constraint: PropTypes.object, 46 | }; 47 | 48 | ExplainVersion.defaultProps = { 49 | constraint: null, 50 | }; 51 | 52 | export default ExplainVersion; 53 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Footer.scss'; 4 | 5 | const Footer = props => ( 6 | 46 | ); 47 | 48 | Footer.propTypes = { 49 | className: PropTypes.string, 50 | }; 51 | 52 | Footer.defaultProps = { 53 | className: '', 54 | }; 55 | 56 | export default Footer; 57 | -------------------------------------------------------------------------------- /src/components/Version.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DebounceInput } from 'react-debounce-input'; 4 | 5 | export default class Version extends Component { 6 | static propTypes = { 7 | onVersion: PropTypes.func.isRequired, 8 | version: PropTypes.string, 9 | semver: PropTypes.object, 10 | }; 11 | 12 | static defaultProps = { 13 | version: '', 14 | semver: null, 15 | }; 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.handleInput = this.handleInput.bind(this); 21 | } 22 | 23 | handleInput({ target: { value: version } }) { 24 | this.props.onVersion(version); 25 | } 26 | 27 | shouldComponentUpdate(prevProps) { 28 | return prevProps.version !== this.props.version; 29 | } 30 | 31 | render() { 32 | const valid = this.props.semver !== null; 33 | 34 | return ( 35 | 36 |
37 |
38 | 43 | Version 44 | 45 |
46 | 54 |
55 | {valid || This version is invalid.} 56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Constraint.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DebounceInput } from 'react-debounce-input'; 4 | 5 | export default class Constraint extends Component { 6 | static propTypes = { 7 | onConstraint: PropTypes.func.isRequired, 8 | constraint: PropTypes.string, 9 | semver: PropTypes.object, 10 | }; 11 | 12 | static defaultProps = { 13 | constraint: '', 14 | semver: null, 15 | }; 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.handleInput = this.handleInput.bind(this); 21 | } 22 | 23 | handleInput({ target: { value: constraint } }) { 24 | this.props.onConstraint(constraint); 25 | } 26 | 27 | shouldComponentUpdate(prevProps) { 28 | return prevProps.constraint !== this.props.constraint; 29 | } 30 | 31 | render() { 32 | const valid = this.props.semver !== null; 33 | 34 | return ( 35 | 36 |
37 |
38 | 43 | Constraint 44 | 45 |
46 | 54 |
55 | {valid || This constraint is invalid.} 56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Implementations.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Implementations = props => ( 5 |
6 |
7 |

SEMVER CONSTRAINT IMPLEMENTATIONS

8 | 9 |

10 | Semantic Versioning stands as a standard versioning scheme but it does not ( 11 | yet) cover dependency management and how to 12 | express constraint. 13 |

14 | 15 |

16 | Without any formal specification about constraint, dependency managers sometimes handle or express them 17 | differently. For example, the tilde-range constraint (~x.y) does not work the same way in{' '} 18 | NPM and Composer. 19 |

20 | 21 | 39 |
40 |
41 | ); 42 | 43 | Implementations.propTypes = { 44 | className: PropTypes.string, 45 | }; 46 | 47 | Implementations.defaultProps = { 48 | className: '', 49 | }; 50 | 51 | export default Implementations; 52 | -------------------------------------------------------------------------------- /src/components/Version.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, shallow } from 'enzyme'; 3 | import Version from './Version'; 4 | import { DebounceInput } from 'react-debounce-input'; 5 | 6 | describe('Version', () => { 7 | it('should render an input', () => { 8 | const props = { 9 | version: '', 10 | onVersion: () => {}, 11 | }; 12 | 13 | var node = render(); 14 | expect(node.find('input').length).toBe(1); 15 | }); 16 | 17 | it('should render the default value', () => { 18 | const props = { 19 | version: '1.0.0', 20 | onVersion: () => {}, 21 | }; 22 | 23 | var node = render(); 24 | expect(node.find('input').prop('value')).toBe(props.version); 25 | }); 26 | 27 | it('should have invalid style', () => { 28 | const props = { 29 | version: 'x.y.z', 30 | semver: null, 31 | onVersion: () => {}, 32 | }; 33 | 34 | var node = render(); 35 | expect(node.find('input').hasClass('is-invalid')).toBe(true); 36 | }); 37 | 38 | it('should have valid style', () => { 39 | const props = { 40 | version: '1.0.0', 41 | semver: { 42 | raw: '1.0.0', 43 | }, 44 | onVersion: () => {}, 45 | }; 46 | 47 | var node = render(); 48 | expect(node.find('input').hasClass('is-valid')).toBe(true); 49 | }); 50 | 51 | it('should call change handler (debounced)', done => { 52 | const props = { 53 | version: '', 54 | semver: null, 55 | onVersion: jest.fn(), 56 | }; 57 | 58 | const event = { 59 | persist: () => {}, 60 | target: { 61 | value: '1.0.0', 62 | }, 63 | }; 64 | 65 | var node = shallow(); 66 | 67 | node.find(DebounceInput).shallow().find('input').simulate('change', event); 68 | 69 | setTimeout(() => { 70 | expect(props.onVersion).toHaveBeenCalled(); 71 | 72 | done(); 73 | }, 300); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/Constraint.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, shallow } from 'enzyme'; 3 | import Constraint from './Constraint'; 4 | import { DebounceInput } from 'react-debounce-input'; 5 | 6 | describe('Constraint', () => { 7 | it('should render an input', () => { 8 | const props = { 9 | onConstraint: () => {}, 10 | }; 11 | 12 | var node = render(); 13 | expect(node.find('input').length).toBe(1); 14 | }); 15 | 16 | it('should render the default value', () => { 17 | const props = { 18 | constraint: '1.0.0', 19 | onConstraint: () => {}, 20 | }; 21 | 22 | var node = render(); 23 | expect(node.find('input').prop('value')).toBe(props.constraint); 24 | }); 25 | 26 | it('should have invalid style', () => { 27 | const props = { 28 | constraint: 'x.y.z', 29 | semver: null, 30 | onConstraint: () => {}, 31 | }; 32 | 33 | var node = render(); 34 | expect(node.find('input').hasClass('is-invalid')).toBe(true); 35 | }); 36 | 37 | it('should have valid style', () => { 38 | const props = { 39 | constraint: '1.0.0', 40 | semver: { 41 | raw: '1.0.0', 42 | }, 43 | onConstraint: () => {}, 44 | }; 45 | 46 | var node = render(); 47 | expect(node.find('input').hasClass('is-valid')).toBe(true); 48 | }); 49 | 50 | it('should call change handler (debounced)', done => { 51 | const props = { 52 | constraint: '', 53 | semver: null, 54 | onConstraint: jest.fn(), 55 | }; 56 | 57 | const event = { 58 | persist: () => {}, 59 | target: { 60 | value: '1.0.0', 61 | }, 62 | }; 63 | 64 | var node = shallow(); 65 | 66 | node.find(DebounceInput).shallow().find('input').simulate('change', event); 67 | 68 | setTimeout(() => { 69 | expect(props.onConstraint).toHaveBeenCalled(); 70 | 71 | done(); 72 | }, 300); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semver-check", 3 | "description": "A dummy webapp to check your semver compat", 4 | "version": "0.1.0", 5 | "private": true, 6 | "homepage": ".", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/jubianchi/semver-check" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "bootstrap": "^4.1.3", 14 | "connected-react-router": "^6.0.0", 15 | "history": "^4.7.2", 16 | "prop-types": "^15.6.2", 17 | "react": "^16.13.1", 18 | "react-clipboard.js": "^2.0.2", 19 | "react-debounce-input": "^3.2.0", 20 | "react-dom": "^16.13.1", 21 | "react-ga": "^2.5.3", 22 | "react-octicon": "^3.0.1", 23 | "react-redux": "^7.2.0", 24 | "react-router-dom": "^4.3.1", 25 | "redux": "^4.0.0", 26 | "redux-thunk": "^2.3.0", 27 | "semver": "^7.3.0" 28 | }, 29 | "devDependencies": { 30 | "conventional-changelog-cli": "^2.0.34", 31 | "enzyme": "^3.11.0", 32 | "enzyme-adapter-react-16": "^1.5.0", 33 | "node-sass-chokidar": "^1.3.3", 34 | "npm-run-all": "^4.1.3", 35 | "prettier": "^2.0.5", 36 | "react-scripts": "3.4.1" 37 | }, 38 | "scripts": { 39 | "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", 40 | "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", 41 | "start-js": "react-scripts start", 42 | "start": "npm-run-all -p watch-css start-js", 43 | "build-js": "react-scripts build", 44 | "build": "npm-run-all build-css build-js", 45 | "test": "react-scripts test --env=jsdom", 46 | "eject": "react-scripts eject", 47 | "changelog": "conventional-changelog -i CHANGELOG.md -s -p angular", 48 | "cs": "prettier --ignore-path .gitignore --write './**/*.{js,jsx,json,css,scss,sass,md,yml}' '!./**/package.json' '!./**/yarn.json'" 49 | }, 50 | "prettier": { 51 | "printWidth": 120, 52 | "useTabs": false, 53 | "tabWidth": 4, 54 | "semi": true, 55 | "singleQuote": true, 56 | "trailingComma": "all", 57 | "bracketSpacing": true, 58 | "jsxBracketSameLine": false, 59 | "arrowParens": "avoid" 60 | }, 61 | "browserslist": [ 62 | ">0.2%", 63 | "not dead", 64 | "not ie <= 11", 65 | "not op_mini all" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Router.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import connect from 'react-redux/es/connect/connect'; 3 | import { constraint, version } from '../actions'; 4 | import { Route, Switch } from 'react-router-dom'; 5 | import React from 'react'; 6 | 7 | class Router extends Component { 8 | updateHistory() { 9 | const { constraint: stateConstraint, version: stateVersion } = this.props.state; 10 | const { constraint: routerConstraint, version: routerVersion } = this.props.router; 11 | 12 | if (stateConstraint.constraint !== decodeURIComponent(routerConstraint)) { 13 | this.props.onRouterConstraint(decodeURIComponent(routerConstraint)); 14 | } 15 | 16 | if (stateVersion.version !== decodeURIComponent(routerVersion)) { 17 | this.props.onRouterVersion(decodeURIComponent(routerVersion)); 18 | } 19 | } 20 | 21 | shouldComponentUpdate() { 22 | const { constraint: stateConstraint, version: stateVersion } = this.props.state; 23 | const { constraint: routerConstraint, version: routerVersion } = this.props.router; 24 | 25 | return stateConstraint.constraint !== routerConstraint || stateVersion.version !== routerVersion; 26 | } 27 | 28 | componentDidMount() { 29 | this.updateHistory(); 30 | } 31 | 32 | componentDidUpdate() { 33 | this.updateHistory(); 34 | } 35 | 36 | render() { 37 | return null; 38 | } 39 | } 40 | 41 | const ConnectedRouter = connect( 42 | (state, props) => ({ 43 | state: { 44 | version: state.version, 45 | constraint: state.constraint, 46 | }, 47 | router: { 48 | version: props.match.params.version || '', 49 | constraint: props.match.params.constraint || '', 50 | }, 51 | }), 52 | dispatch => ({ 53 | onRouterConstraint: value => { 54 | dispatch(constraint(value)); 55 | }, 56 | onRouterVersion: value => { 57 | dispatch(version(value)); 58 | }, 59 | }), 60 | )(Router); 61 | 62 | export default () => ( 63 | 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | 69 | ); 70 | -------------------------------------------------------------------------------- /src/components/WhyStrict.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const WhyStrict = props => ( 5 |
6 |
7 |

WHY USING STRICT CONSTRAINT IS BAD?

8 | 9 |

10 | Strict constraint (or fully qualified constraint) are those constraints matching only one version. In 11 | most case it is a bad idea to use them. 12 |

13 | 14 |

15 | Why? Because with them you are locking your dependency to a specific patch release which means you won't 16 | ever get bug fixes when updating your dependencies. 17 |

18 | 19 |

20 | Moreover, using strict constraint will make the work of some dependency managers harder: if you are 21 | depending on a package and have a dependency in common, if both of you require this common dependency 22 | strictly, your manager won't be able to choose an appropriate version, satisfying every constraint. 23 |

24 | 25 |

Unless you know exactly what you are doing and why, you should change to a more flexible one like:

26 | 27 |
    28 |
  • 29 | ~x.y.z if your dependency manager supports tilde-range constraints 30 |
  • 31 |
  • 32 | >=x.y.z <x.(y+1).0 if your dependency manager supports range constraints 33 |
  • 34 |
35 | 36 |

37 | Using such constraints, you will allow your dependency manager to pull patch releases letting you get 38 | bug fixes. If the library you are depending on strictly implements Semantic Versioning you should be 39 | able to make your constraint even more flexible by allowing your dependency manager to also pull new 40 | features: 41 |

42 | 43 |
    44 |
  • 45 | ^x.y.z if your dependency manager supports caret-range constraints 46 |
  • 47 |
  • 48 | >=x.y.z <(x+1).0.0 if your dependency manager support range constraints 49 |
  • 50 |
51 |
52 |
53 | ); 54 | 55 | WhyStrict.propTypes = { 56 | className: PropTypes.string, 57 | }; 58 | 59 | WhyStrict.defaultProps = { 60 | className: '', 61 | }; 62 | 63 | export default WhyStrict; 64 | -------------------------------------------------------------------------------- /src/components/ExplainConstraint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ExplainConstraintRange from './ExplainConstraintRange'; 4 | import ConstraintType from './ConstraintType'; 5 | import ExplainConstraintCaret from './ExplainConstraintCaret'; 6 | import ExplainConstraintPessimistic from './ExplainConstraintPessimistic'; 7 | import ExplainConstraintStrict from './ExplainConstraintStrict'; 8 | import ExplainConstraintHyphen from './ExplainConstraintHyphen'; 9 | import ExplainConstraintWildcard from './ExplainConstraintWildcard'; 10 | import ExplainConstraintTilde from './ExplainConstraintTilde'; 11 | import Card from './Card'; 12 | 13 | const ExplainConstraint = props => { 14 | return ( 15 | 24 |
25 | constraint 26 |
27 | 28 | {props.constraint.semver.wildcard === true && props.constraint.semver.major === '*' ? ( 29 |
Constraint will be satisfied by any version.
30 | ) : ( 31 |
32 | Constraint will be satisfied by versions matching {props.constraint.semver.raw}. 33 |
34 | )} 35 | 36 | {props.constraint.semver.caret === true && } 37 | 38 | {props.constraint.semver.tilde === true && } 39 | 40 | {props.constraint.semver.pessimistic === true && ( 41 | 42 | )} 43 | 44 | {props.constraint.semver.strict === true && } 45 | 46 | {props.constraint.semver.hyphen === true && } 47 | 48 | {props.constraint.semver.wildcard === true && } 49 | 50 | {props.constraint.semver.range === true && } 51 |
52 | ); 53 | }; 54 | 55 | ExplainConstraint.propTypes = { 56 | constraint: PropTypes.object.isRequired, 57 | }; 58 | 59 | export default ExplainConstraint; 60 | -------------------------------------------------------------------------------- /src/components/ExplainConstraint.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'enzyme'; 3 | import ExplainConstraint from './ExplainConstraint'; 4 | 5 | describe('ExplainConstraint', () => { 6 | it('should display caret constraints', () => { 7 | const props = { 8 | constraint: { 9 | constraint: '^1.0.0', 10 | semver: { 11 | caret: true, 12 | }, 13 | }, 14 | }; 15 | 16 | var node = render(); 17 | expect(node.text()).toContain(props.constraint.constraint + ' is a caret constraint'); 18 | }); 19 | 20 | it('should display tilde constraints', () => { 21 | const props = { 22 | constraint: { 23 | constraint: '~1.0.0', 24 | semver: { 25 | tilde: true, 26 | }, 27 | }, 28 | }; 29 | 30 | var node = render(); 31 | expect(node.text()).toContain(props.constraint.constraint + ' is a tilde constraint'); 32 | }); 33 | 34 | it('should display pessimistic constraints', () => { 35 | const props = { 36 | constraint: { 37 | constraint: '~>1.0.0', 38 | semver: { 39 | pessimistic: true, 40 | }, 41 | }, 42 | }; 43 | 44 | var node = render(); 45 | expect(node.text()).toContain(props.constraint.constraint + ' is a pessimistic constraint'); 46 | }); 47 | 48 | it('should display strict constraints', () => { 49 | const props = { 50 | constraint: { 51 | constraint: '1.0.0', 52 | semver: { 53 | strict: true, 54 | }, 55 | }, 56 | }; 57 | 58 | var node = render(); 59 | expect(node.text()).toContain(props.constraint.constraint + ' is a strict constraint'); 60 | }); 61 | 62 | it('should display hyphen constraints', () => { 63 | const props = { 64 | constraint: { 65 | constraint: '1.0.0 - 2.0.0', 66 | semver: { 67 | hyphen: true, 68 | }, 69 | }, 70 | }; 71 | 72 | var node = render(); 73 | expect(node.text()).toContain(props.constraint.constraint + ' is a hyphen constraint'); 74 | }); 75 | 76 | it('should display wildcard (x-range) constraints', () => { 77 | const props = { 78 | constraint: { 79 | constraint: '1.0.*', 80 | semver: { 81 | wildcard: true, 82 | }, 83 | }, 84 | }; 85 | 86 | var node = render(); 87 | expect(node.text()).toContain(props.constraint.constraint + ' is a x-range constraint'); 88 | }); 89 | 90 | it('should display range constraints', () => { 91 | const props = { 92 | constraint: { 93 | constraint: '1.0.0 <1.5.0', 94 | semver: { 95 | range: true, 96 | raw: '>=1.0.0 <1.5.0', 97 | }, 98 | }, 99 | }; 100 | 101 | var node = render(); 102 | expect(node.text()).toContain(props.constraint.constraint + ' is a range constraint'); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/semver.js: -------------------------------------------------------------------------------- 1 | import semver from 'semver'; 2 | 3 | const NR = '(?:0|[1-9])[0-9]*'; 4 | const BUILD = NR; 5 | const PRE = `[0-9a-zA-Z\\-]+(?:\\.${NR})?`; 6 | const QUALIFIER = `-(?:${PRE})?(?:\\+${BUILD})?`; 7 | const XR = `(?:[xX\\*]|${NR})`; 8 | const PARTIAL = `${XR}(?:\\.${XR}(?:\\.${XR}(?:${QUALIFIER})?)?)?`; 9 | const CARET = `\\^\\s*${PARTIAL}`; 10 | const TILDE = `~\\s*${PARTIAL}`; 11 | const PRIMITIVE = `(?:<|>|<=|>=)\\s*${PARTIAL}`; 12 | const SIMPLE = `(?:${PRIMITIVE}|${PARTIAL}|${TILDE}|${CARET})`; 13 | const HYPHEN = `${PARTIAL}\\s-\\s${PARTIAL}`; 14 | const RANGE = `(?:${HYPHEN}|${SIMPLE}(?:\\s+${SIMPLE})*)`; 15 | const LOGICAL_OR = `\\s*\\|\\|\\s*`; 16 | const RANGE_SET = `${RANGE}(?:${LOGICAL_OR}${RANGE})+`; 17 | const STRICT = `(?:=\\s*)?${NR}\\.${NR}\\.${NR}(?:${QUALIFIER})?`; 18 | const WILDCARD = `(?:[xX\\*]|${NR}\\.[xX\\*]|${NR}\\.${NR}\\.[xX\\*])`; 19 | const PESSIMISTIC = `~>\\s*${PARTIAL}`; 20 | 21 | const explode = range => { 22 | const exploded = { 23 | major: null, 24 | minor: null, 25 | patch: null, 26 | prerelease: null, 27 | }; 28 | 29 | const [major, minor, patchAndPrerelease, ...rest] = range.split('.'); 30 | 31 | exploded.major = major; 32 | exploded.minor = minor || null; 33 | 34 | if (typeof patchAndPrerelease !== 'undefined') { 35 | const [patch, prerelease] = `${patchAndPrerelease}${rest.length ? `.${rest.join('.')}` : ''}`.split('-'); 36 | 37 | exploded.patch = patch; 38 | exploded.prerelease = prerelease; 39 | } 40 | 41 | return exploded; 42 | }; 43 | 44 | export default { 45 | ...semver, 46 | satisfies: (version, constraint, options = {}) => { 47 | return semver.satisfies(version.raw, constraint.raw, { ...options, includePrerelease: true }); 48 | }, 49 | cleanRange: range => 50 | range 51 | .trim() 52 | .replace(/v(\d+\.)/gi, '$1') 53 | .replace(/(^|\s+|\|\|)([^><]?)=(\d+\.)/g, '$1$2$3'), 54 | coerceRange: range => { 55 | if (!range && range !== 0) { 56 | return null; 57 | } 58 | 59 | const raw = semver.validRange(range.toString()); 60 | 61 | if (raw === null || raw === '') { 62 | return null; 63 | } 64 | 65 | const coerced = { 66 | raw, 67 | caret: false, 68 | tilde: false, 69 | strict: false, 70 | hyphen: false, 71 | wildcard: false, 72 | range: false, 73 | rangeSet: false, 74 | pessimistic: false, 75 | major: null, 76 | minor: null, 77 | patch: null, 78 | prerelease: null, 79 | operator: null, 80 | }; 81 | 82 | if (new RegExp(`^${CARET}$`).exec(range)) { 83 | coerced.caret = true; 84 | coerced.operator = '^'; 85 | 86 | Object.assign(coerced, explode(range.replace(/^\^/, ''))); 87 | } 88 | 89 | if (new RegExp(`^${TILDE}$`).exec(range)) { 90 | coerced.tilde = true; 91 | coerced.operator = '~'; 92 | 93 | Object.assign(coerced, explode(range.replace(/^~/, ''))); 94 | } 95 | 96 | if (new RegExp(`^${PESSIMISTIC}$`).exec(range)) { 97 | coerced.pessimistic = true; 98 | coerced.operator = '~>'; 99 | 100 | Object.assign(coerced, explode(range.replace(/^~>/, ''))); 101 | 102 | if (coerced.major !== null && coerced.minor !== null && coerced.patch !== null) { 103 | coerced.raw = `>=${coerced.major}.${coerced.minor || 0}.${coerced.patch || 0} <${coerced.major}.${ 104 | parseInt(coerced.minor, 10) + 1 105 | }.0`; 106 | } else { 107 | coerced.raw = `>=${coerced.major}.${coerced.minor || 0}.${coerced.patch || 0} <${ 108 | parseInt(coerced.major, 10) + 1 109 | }.0.0`; 110 | } 111 | } 112 | 113 | if (new RegExp(`^${HYPHEN}$`).exec(range)) { 114 | coerced.hyphen = true; 115 | } 116 | 117 | if (new RegExp(`^${STRICT}$`).exec(range.toString())) { 118 | coerced.strict = true; 119 | coerced.operator = '='; 120 | 121 | Object.assign(coerced, explode(range)); 122 | } else if (new RegExp(`^${WILDCARD}$`).exec(range)) { 123 | coerced.wildcard = true; 124 | 125 | Object.assign(coerced, explode(range.replace(/[xX]/, '*'))); 126 | } else if (new RegExp(`^${RANGE}$`).exec(range.toString())) { 127 | coerced.range = true; 128 | 129 | const matches = new RegExp('^(<=|<|>=|>|=)').exec(range); 130 | 131 | if (matches !== null) { 132 | coerced.operator = matches[1] || null; 133 | } 134 | } else if (new RegExp(`^${RANGE_SET}$`).exec(range)) { 135 | coerced.rangeSet = true; 136 | } 137 | 138 | return coerced; 139 | }, 140 | }; 141 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit http://bit.ly/CRA-PWA', 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | 'New content is available and will be used when all ' + 72 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.', 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log('Content is cached for offline use.'); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch(error => { 95 | console.error('Error during service worker registration:', error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl) 102 | .then(response => { 103 | // Ensure service worker exists, and that we really are getting a JS file. 104 | const contentType = response.headers.get('content-type'); 105 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { 106 | // No service worker found. Probably a different app. Reload the page. 107 | navigator.serviceWorker.ready.then(registration => { 108 | registration.unregister().then(() => { 109 | window.location.reload(); 110 | }); 111 | }); 112 | } else { 113 | // Service worker found. Proceed as normal. 114 | registerValidSW(swUrl, config); 115 | } 116 | }) 117 | .catch(() => { 118 | console.log('No internet connection found. App is running in offline mode.'); 119 | }); 120 | } 121 | 122 | export function unregister() { 123 | if ('serviceWorker' in navigator) { 124 | navigator.serviceWorker.ready.then(registration => { 125 | registration.unregister(); 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/semver.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import semver from './semver'; 3 | 4 | describe('semver', () => { 5 | describe('clean range', () => { 6 | it('should remove trailing/leading spaces', () => { 7 | expect(semver.cleanRange(' 1.0.0\t')).toBe('1.0.0'); 8 | expect(semver.cleanRange(' ^1.0.0\t')).toBe('^1.0.0'); 9 | expect(semver.cleanRange('\t~1.0.0\t')).toBe('~1.0.0'); 10 | }); 11 | 12 | it('should remove the v prefix in front of each version number', () => { 13 | const data = { 14 | 'v1.0.0': '1.0.0', 15 | '=v1.0.0': '1.0.0', 16 | '^v1.0.0': '^1.0.0', 17 | '~v1.0.0': '~1.0.0', 18 | '~>v1.0.0': '~>1.0.0', 19 | '>v1.0.0': '>1.0.0', 20 | '>=v1.0.0': '>=1.0.0', 21 | ' { 29 | expect(semver.cleanRange(dirty)).toBe(data[dirty]); 30 | }); 31 | }); 32 | 33 | it('should remove the = prefix', () => { 34 | const data = { 35 | '=1.0.0': '1.0.0', 36 | '>=1.0.0': '>=1.0.0', 37 | '<=1.0.0': '<=1.0.0', 38 | '=1.0.0 || =1.2.0': '1.0.0 || 1.2.0', 39 | }; 40 | 41 | Object.keys(data).forEach(dirty => { 42 | expect(semver.cleanRange(dirty)).toBe(data[dirty]); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('coerceRange', () => { 48 | describe('should return null if range is falsy', () => { 49 | it('no range', () => { 50 | expect(semver.coerceRange()).toBe(null); 51 | }); 52 | 53 | it('empty range', () => { 54 | expect(semver.coerceRange('')).toBe(null); 55 | }); 56 | 57 | it('range is undefined', () => { 58 | expect(semver.coerceRange(undefined)).toBe(null); 59 | }); 60 | 61 | it('range is null', () => { 62 | expect(semver.coerceRange(null)).toBe(null); 63 | }); 64 | 65 | it('range is false', () => { 66 | expect(semver.coerceRange(null)).toBe(null); 67 | }); 68 | }); 69 | 70 | describe('should coerce integer ranges', () => { 71 | it('should not return null if range is 0', () => { 72 | expect(semver.coerceRange(0)).not.toBe(null); 73 | }); 74 | 75 | it('should coerce range 0 as a range constraint', () => { 76 | expect(semver.coerceRange(0)).toHaveProperty('range', true); 77 | }); 78 | }); 79 | 80 | describe('types', () => { 81 | const rangeAndType = { 82 | '>=8.10.0': 'range', 83 | '>=8.11.0': 'range', 84 | }; 85 | 86 | Object.keys(rangeAndType).forEach(range => { 87 | it(`should compute type for ${range}`, () => { 88 | expect(semver.coerceRange(range)[rangeAndType[range]]).toBe(true); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('operators', () => { 94 | const rangeAndOperator = { 95 | '1.0.0': '=', 96 | '^1.0.0': '^', 97 | '~1.0.0': '~', 98 | '~>1.0.0': '~>', 99 | }; 100 | 101 | Object.keys(rangeAndOperator).forEach(range => { 102 | it(`should compute operator for ${range}`, () => { 103 | expect(semver.coerceRange(range).operator).toBe(rangeAndOperator[range]); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('constraint parts', () => { 109 | const rangeAndParts = { 110 | '1.0.0': { major: '1', minor: '0', patch: '0' }, 111 | '^1.0.0': { major: '1', minor: '0', patch: '0' }, 112 | '~1.0.0': { major: '1', minor: '0', patch: '0' }, 113 | '~>1.0.0': { major: '1', minor: '0', patch: '0' }, 114 | }; 115 | 116 | Object.keys(rangeAndParts).forEach(range => { 117 | it(`should compute constraint parts for ${range}`, () => { 118 | expect(semver.coerceRange(range)).toMatchObject(rangeAndParts[range]); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('satisifes', function () { 124 | const ranges = { 125 | '1.2.3': ['1.2.3'], 126 | '1.2.3 - 2.3.4': ['1.2.3', '2.0.0', '2.3.4'], 127 | '1.2 - 2.3.4': ['1.2.3', '2.0.0', '2.3.4'], 128 | '1.2.3 - 2.3': ['1.2.3', '2.0.0', '2.3.4'], 129 | '1.2.3 - 2': ['1.2.3', '2.0.0', '2.3.4'], 130 | '*': ['0.0.1', '1.2.3', '2.3.4', '42.13.37'], 131 | x: ['0.0.1', '1.2.3', '2.3.4', '42.13.37'], 132 | '1.*': ['1.0.0', '1.0.1', '1.1.0', '1.2.3'], 133 | '1.x': ['1.0.0', '1.0.1', '1.1.0', '1.2.3'], 134 | '1.2.*': ['1.2.0', '1.2.1'], 135 | '1.2.x': ['1.2.0', '1.2.1'], 136 | '1': ['1.0.0', '1.0.1', '1.1.0', '1.2.3'], 137 | '1.2': ['1.2.0', '1.2.3'], 138 | '~1.2.3': ['1.2.3', '1.2.42'], 139 | '~1.2': ['1.2.0', '1.2.3', '1.2.42'], 140 | '~1': ['1.0.0', '1.0.1', '1.1.0', '1.2.3'], 141 | '~0.2.3': ['0.2.3', '0.2.42'], 142 | '~0.2': ['0.2.3', '0.2.42'], 143 | '~0': ['0.0.1', '0.2.3'], 144 | '~1.2.3-beta.2': ['1.2.3', '1.2.42', '1.2.5-beta.0'], 145 | '^1.2.3': ['1.2.3', '1.2.42'], 146 | '^0.2.3': ['0.2.3', '0.2.42'], 147 | '~>0.17': ['0.22.0'], 148 | '>=1.12.14 <1.15.0': ['1.12.14', '1.14.0'], 149 | '>=1.0.0': ['1.1.0-snapshot.1'], 150 | }; 151 | 152 | Object.keys(ranges).forEach(range => { 153 | ranges[range].forEach(version => { 154 | it(version + ' should satisfy range ' + range, () => { 155 | const constraint = semver.coerceRange(range); 156 | 157 | expect(semver.satisfies(semver.coerce(version), constraint)).toBe(true); 158 | }); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('does not satisfy', function () { 164 | const ranges = { 165 | '1.2.3': ['0.0.1', '1.2.5', '2.0.0'], 166 | '1.2.3 - 2.3.4': ['0.0.1', '1.0.0', '2.5.0', '3.0.0'], 167 | '1.2 - 2.3.4': ['0.0.1', '1.0.0', '2.5.0', '3.0.0'], 168 | '1.2.3 - 2.3': ['0.0.1', '1.0.0', '2.5.0', '3.0.0'], 169 | '1.2.3 - 2': ['0.0.1', '1.0.0', '3.0.0'], 170 | '1.*': ['0.0.1', '2.0.0'], 171 | '1.x': ['0.0.1', '2.0.0'], 172 | '1.2.*': ['1.0.0', '1.3.1', '2.0.0'], 173 | '1.2.x': ['1.0.0', '1.3.1', '2.0.0'], 174 | '1': ['0.0.1', '2.0.0'], 175 | '1.2': ['1.0.0', '1.3.1', '2.0.0'], 176 | '~1.2.3': ['1.0.0', '2.0.0'], 177 | '~1.2': ['1.0.0', '2.0.0'], 178 | '~1': ['0.0.1', '2.0.0'], 179 | '~0.2.3': ['0.0.1', '1.0.0', '2.0.0'], 180 | '~0.2': ['0.0.1', '1.0.0', '2.0.0'], 181 | '~0': ['1.0.0'], 182 | '>=1.12.14 <1.15.0': ['1.15.0', '99.99.99'], 183 | '>=1.0': ['0.0.1'], 184 | '>1.0': ['0.0.1'], 185 | '>=2.0.0-alpha.1 <=2.0.0-alpha.7': ['2.0.0-alpha.10'], 186 | }; 187 | 188 | Object.keys(ranges).forEach(range => { 189 | ranges[range].forEach(version => { 190 | it(version + ' should not satisfy range ' + range, () => { 191 | const constraint = semver.coerceRange(range); 192 | 193 | expect(semver.satisfies(semver.coerce(version), constraint)).toBe(false); 194 | }); 195 | }); 196 | }); 197 | }); 198 | }); 199 | }); 200 | --------------------------------------------------------------------------------