├── .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 | You need to enable JavaScript to run this app.
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 |
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 |
37 |
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 | [](https://travis-ci.org/jubianchi/semver-check)
4 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------