├── .node-version ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── pnpm-workspace.yaml ├── codecov.yml ├── .prettierrc.json ├── renovate.json ├── LICENSE ├── .lintstagedrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── build.yml ├── lib ├── semantic.d.ts ├── operator.d.ts ├── shared.d.ts ├── version.d.ts ├── specifier.d.ts ├── operator.js ├── semantic.js ├── version.js └── specifier.js ├── .editorconfig ├── README.md ├── tsconfig.json ├── index.d.ts ├── jest.config.js ├── .markdownlint-cli2.jsonc ├── index.js ├── .gitignore ├── LICENSE.BSD ├── test ├── fixture.js ├── operator.test.js ├── version.test.js ├── semantic.test.js ├── version.explain.test.js └── specifier.test.js ├── .releaserc.json ├── package.json ├── eslint.config.mjs └── LICENSE.APACHE /.node-version: -------------------------------------------------------------------------------- 1 | 22.21.1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pnpm lint-staged 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | save-prefix = 3 | provenance = true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /tmp 4 | .DS_Store 5 | .cache 6 | /*.log 7 | /pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | strictPeerDependencies: true 2 | 3 | ignoredBuiltDependencies: 4 | - unrs-resolver 5 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | codecov: 3 | require_ci_to_pass: false 4 | notify: 5 | wait_for_ci: false 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "plugins": ["prettier-plugin-packagejson"] 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>renovatebot/.github"], 3 | "automergeType": "pr", 4 | "prCreation": "immediate" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the licenses 2 | found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made 3 | under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.md": ["markdownlint-cli2 --fix", "prettier --cache --write"], 3 | "*.{ts,js,mjs,cjs}": ["eslint --cache --fix", "prettier --cache --write"], 4 | "!*.{ts,js,mjs,cjs,md}": "prettier --cache --ignore-unknown --write" 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Renovate bot config help 4 | url: https://github.com/renovatebot/renovate/discussions 5 | about: Please ask questions on how to configure your Renovate bot here. 6 | -------------------------------------------------------------------------------- /lib/semantic.d.ts: -------------------------------------------------------------------------------- 1 | export function inc( 2 | input: string, 3 | release?: string, 4 | preReleaseIdentifier?: string, 5 | ): string | null; 6 | export function major(input: string): number; 7 | export function minor(input: string): number; 8 | export function patch(input: string): number; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.{yaml,yml,conf}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pep440 2 | 3 | ![build](https://github.com/renovatebot/pep440/workflows/build/badge.svg) 4 | [![codecov](https://codecov.io/gh/renovatebot/pep440/branch/main/graph/badge.svg?token=OqCFydnm3g)](https://codecov.io/gh/renovatebot/pep4440) 5 | 6 | Python PEP440 implementation in JavaScript. 7 | 8 | This logic has been ported from Python for the purposes of use in [Renovate Bot](https://github.com/renovatebot/renovate). 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "lib": ["ES2023"], 7 | "outDir": "./dist", 8 | /* https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping */ 9 | "target": "ES2022", 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "noEmit": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /lib/operator.d.ts: -------------------------------------------------------------------------------- 1 | export function compare(version: string, other: string): number; 2 | export function eq(version: string, other: string): boolean; 3 | export function ge(version: string, other: string): boolean; 4 | export function gt(version: string, other: string): boolean; 5 | export function le(version: string, other: string): boolean; 6 | export function lt(version: string, other: string): boolean; 7 | export function ne(version: string, other: string): boolean; 8 | export function rcompare(version: string, other: string): number; 9 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type { Pep440Constraint, Pep440Version } from './lib/shared'; 2 | export { clean, explain, valid, parse } from './lib/version'; 3 | 4 | export { 5 | compare, 6 | eq, 7 | ge, 8 | ge as gte, 9 | gt, 10 | le, 11 | le as lte, 12 | lt, 13 | ne, 14 | ne as neq, 15 | rcompare, 16 | } from './lib/operator'; 17 | 18 | export { 19 | filter, 20 | maxSatisfying, 21 | minSatisfying, 22 | RANGE_PATTERN, 23 | satisfies, 24 | validRange, 25 | } from './lib/specifier'; 26 | 27 | export { major, minor, patch, inc } from './lib/semantic'; 28 | -------------------------------------------------------------------------------- /lib/shared.d.ts: -------------------------------------------------------------------------------- 1 | export interface Pep440Constraint { 2 | operator: string; 3 | prefix: string; 4 | version: string; 5 | } 6 | export interface Pep440SpecifierOptions { 7 | prereleases?: boolean; 8 | } 9 | 10 | export interface Pep440Version { 11 | public: string; 12 | base_version: string; 13 | is_prerelease: boolean; 14 | is_devrelease: boolean; 15 | is_postrelease: boolean; 16 | epoch: number; 17 | release: number[]; 18 | pre: (string | number)[]; 19 | post: (string | number)[]; 20 | dev: (string | number)[]; 21 | local: string | null; 22 | } 23 | -------------------------------------------------------------------------------- /lib/version.d.ts: -------------------------------------------------------------------------------- 1 | import type { Pep440Version } from './shared'; 2 | 3 | export const VERSION_PATTERN: string; 4 | export function clean(version: string | null | undefined): string | null; 5 | export function explain( 6 | version: string | null | undefined, 7 | ): Pep440Version | null; 8 | export function parse( 9 | version: string | null | undefined, 10 | regex?: RegExp, 11 | ): Pep440Version | null; 12 | export function stringify( 13 | version: Pep440Version | null | undefined, 14 | ): string | null; 15 | export function valid(version: string | null | undefined): string | null; 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const ci = !!process.env.CI; 2 | 3 | /** @type {import('jest').Config} */ 4 | module.exports = { 5 | roots: ['/test'], 6 | testEnvironment: 'node', 7 | collectCoverage: true, 8 | collectCoverageFrom: ['lib/**/*.js'], 9 | reporters: ci ? ['default', 'jest-junit', 'github-actions'] : ['default'], 10 | coverageProvider: 'v8', 11 | coverageReporters: ci ? ['html', 'json', 'text'] : ['html', 'text'], 12 | coverageThreshold: { 13 | global: { 14 | branches: 100, 15 | functions: 100, 16 | lines: 100, 17 | statements: 100, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You've found a bug 4 | --- 5 | 6 | 10 | 11 | ## Describe the bug 12 | 13 | 14 | 15 | ## Steps to reproduce 16 | 17 | 21 | 22 | ## Additional context 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **What would you like to be able to do?** 7 | 8 | 9 | 10 | **Did you already have any implementation ideas?** 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "extends": "markdownlint/style/prettier", 4 | "list-marker-space": { 5 | "ul_multi": 3, 6 | "ul_single": 3, 7 | }, 8 | "ul-indent": { 9 | "indent": 4, 10 | }, 11 | 12 | // Disable some built-in rules 13 | "first-line-heading": false, 14 | "line-length": false, 15 | "no-emphasis-as-header": false, 16 | "no-inline-html": false, 17 | "single-h1": false, 18 | }, 19 | 20 | // Define glob expressions to use (only valid at root) 21 | // "globs": ["**/*.md"], 22 | 23 | // Define glob expressions to ignore 24 | "ignores": [ 25 | "**/node_modules/**", 26 | "**/TestResults/**", 27 | "**/bin/**", 28 | "**/obj/**", 29 | "coverage/", 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /lib/specifier.d.ts: -------------------------------------------------------------------------------- 1 | import type { Pep440Constraint, Pep440SpecifierOptions } from './shared'; 2 | 3 | export const RANGE_PATTERN: string; 4 | 5 | export function filter( 6 | versions: string[], 7 | specifier: string, 8 | options?: Pep440SpecifierOptions, 9 | ): string[]; 10 | export function maxSatisfying( 11 | versions: string[], 12 | specifier: string, 13 | options?: Pep440SpecifierOptions, 14 | ): string | null; 15 | export function minSatisfying( 16 | versions: string[], 17 | specifier: string, 18 | options?: Pep440SpecifierOptions, 19 | ): string | null; 20 | export function parse(ranges: string): Pep440Constraint[]; // have doubts regarding this which need to be discussed 21 | export function satisfies( 22 | version: string, 23 | specifier: string, 24 | options?: Pep440SpecifierOptions, 25 | ): boolean; 26 | export function validRange(specifier: string): boolean; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { valid, clean, explain, parse } = require('./lib/version'); 2 | 3 | const { lt, le, eq, ne, ge, gt, compare, rcompare } = require('./lib/operator'); 4 | 5 | const { 6 | filter, 7 | maxSatisfying, 8 | minSatisfying, 9 | RANGE_PATTERN, 10 | satisfies, 11 | validRange, 12 | } = require('./lib/specifier'); 13 | 14 | const { major, minor, patch, inc } = require('./lib/semantic'); 15 | 16 | module.exports = { 17 | // version 18 | valid, 19 | clean, 20 | explain, 21 | parse, 22 | 23 | // operator 24 | lt, 25 | le, 26 | lte: le, 27 | eq, 28 | ne, 29 | neq: ne, 30 | ge, 31 | gte: ge, 32 | gt, 33 | compare, 34 | rcompare, 35 | 36 | // range 37 | filter, 38 | maxSatisfying, 39 | minSatisfying, 40 | RANGE_PATTERN, 41 | satisfies, 42 | validRange, 43 | 44 | // semantic 45 | major, 46 | minor, 47 | patch, 48 | inc, 49 | }; 50 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Changes 4 | 5 | 6 | 7 | ## Context 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | /.pnpm-store 61 | -------------------------------------------------------------------------------- /LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) Donald Stufft and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | // This list must be in the correct sorting order 2 | const VERSIONS = [ 3 | // Implicit epoch of 0 4 | '1.0.dev456', 5 | '1.0a1', 6 | '1.0a2.dev456', 7 | '1.0a12.dev456', 8 | '1.0a12', 9 | '1.0b1.dev456', 10 | '1.0b2', 11 | '1.0b2.post345.dev456', 12 | '1.0b2.post345', 13 | '1.0b2-346', 14 | '1.0c1.dev456', 15 | '1.0c1', 16 | '1.0rc2', 17 | '1.0c3', 18 | '1.0', 19 | '1.0.post456.dev34', 20 | '1.0.post456', 21 | '1.1.dev1', 22 | '1.2+123abc', 23 | '1.2+123abc456', 24 | '1.2+abc', 25 | '1.2+abc123', 26 | '1.2+abc123def', 27 | '1.2+1234.abc', 28 | '1.2+123456', 29 | '1.2.r32+123456', 30 | '1.2.rev33+123456', 31 | 32 | // Explicit epoch of 1 33 | '1!1.0.dev456', 34 | '1!1.0a1', 35 | '1!1.0a2.dev456', 36 | '1!1.0a12.dev456', 37 | '1!1.0a12', 38 | '1!1.0b1.dev456', 39 | '1!1.0b2', 40 | '1!1.0b2.post345.dev456', 41 | '1!1.0b2.post345', 42 | '1!1.0b2-346', 43 | '1!1.0c1.dev456', 44 | '1!1.0c1', 45 | '1!1.0rc2', 46 | '1!1.0c3', 47 | '1!1.0', 48 | '1!1.0.post456.dev34', 49 | '1!1.0.post456', 50 | '1!1.1.dev1', 51 | '1!1.2+123abc', 52 | '1!1.2+123abc456', 53 | '1!1.2+abc', 54 | '1!1.2+abc123', 55 | '1!1.2+abc123def', 56 | '1!1.2+1234.abc', 57 | '1!1.2+123456', 58 | '1!1.2.r32+123456', 59 | '1!1.2.rev33+123456', 60 | ]; 61 | 62 | const INVALID_VERSIONS = [ 63 | // Non sensical versions should be invalid 64 | 'french toast', 65 | 66 | // Versions with invalid local versions 67 | '1.0+a+', 68 | '1.0++', 69 | '1.0+_foobar', 70 | '1.0+foo&asd', 71 | '1.0+1+1', 72 | // not valid string, 73 | {}, 74 | '', 75 | false, 76 | true, 77 | [], 78 | null, 79 | undefined, 80 | () => true, 81 | ]; 82 | 83 | module.exports = { 84 | VERSIONS, 85 | INVALID_VERSIONS, 86 | cross, 87 | }; 88 | 89 | function cross(array, fn) { 90 | return [].concat(...array.map(fn)); 91 | } 92 | -------------------------------------------------------------------------------- /test/operator.test.js: -------------------------------------------------------------------------------- 1 | const operator = require('../lib/operator'); 2 | 3 | const { VERSIONS, cross } = require('./fixture'); 4 | 5 | describe('compare(version, other)', () => { 6 | // Below we'll generate every possible combination of VERSIONS that 7 | // should be true/false for the given operator 8 | [ 9 | ...cross(VERSIONS, (left, i) => 10 | cross(VERSIONS.slice(i + 1), (right) => [ 11 | [left, '<', right, true], 12 | [left, '>=', right, false], 13 | ]), 14 | ), 15 | 16 | ...cross(VERSIONS, (left, i) => 17 | cross(VERSIONS.slice(i), (right) => [ 18 | [left, '<=', right, true], 19 | [left, '>', right, false], 20 | ]), 21 | ), 22 | 23 | ...cross(VERSIONS, (version) => [ 24 | [version, '==', version, true], 25 | [version, '!=', version, false], 26 | ]), 27 | 28 | ...cross(VERSIONS, (left) => 29 | cross( 30 | VERSIONS.filter((right) => right !== left), 31 | (right) => [ 32 | [left, '!=', right, true], 33 | [left, '==', right, false], 34 | ], 35 | ), 36 | ), 37 | 38 | ...cross(VERSIONS, (left, i) => 39 | cross(VERSIONS.slice(0, i + 1), (right) => [ 40 | [left, '>=', right, true], 41 | [left, '<', right, false], 42 | ]), 43 | ), 44 | 45 | ...cross(VERSIONS, (left, i) => 46 | cross(VERSIONS.slice(0, i), (right) => [ 47 | [left, '>', right, true], 48 | [left, '<=', right, false], 49 | ]), 50 | ), 51 | ].forEach(([left, op, right, expected]) => { 52 | it(`returns ${expected} for '${left}' ${op} '${right}'`, () => { 53 | expect(operator[op](left, right)).toBe(expected); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('rcompare(version, other)', () => { 59 | [['1.0', '2.0', 1]].forEach(([left, right, expected]) => { 60 | it(`returns ${expected} for '${left}' '${right}'`, () => { 61 | expect(operator.rcompare(left, right)).toBe(expected); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | [ 6 | "@semantic-release/github", 7 | { 8 | "releasedLabels": false, 9 | "successCommentCondition": false 10 | } 11 | ], 12 | "@containerbase/semantic-release-pnpm" 13 | ], 14 | "analyzeCommits": { 15 | "releaseRules": [ 16 | { 17 | "type": "docs", 18 | "scope": "readme.md", 19 | "release": "patch" 20 | }, 21 | { 22 | "type": "build", 23 | "release": "patch" 24 | } 25 | ] 26 | }, 27 | "preset": "conventionalcommits", 28 | "presetConfig": { 29 | "types": [ 30 | { 31 | "type": "feat", 32 | "section": "Features" 33 | }, 34 | { 35 | "type": "feature", 36 | "section": "Features" 37 | }, 38 | { 39 | "type": "fix", 40 | "section": "Bug Fixes" 41 | }, 42 | { 43 | "type": "perf", 44 | "section": "Performance Improvements" 45 | }, 46 | { 47 | "type": "revert", 48 | "section": "Reverts" 49 | }, 50 | { 51 | "type": "docs", 52 | "section": "Documentation" 53 | }, 54 | { 55 | "type": "style", 56 | "section": "Styles" 57 | }, 58 | { 59 | "type": "refactor", 60 | "section": "Code Refactoring" 61 | }, 62 | { 63 | "type": "test", 64 | "section": "Tests" 65 | }, 66 | { 67 | "type": "build", 68 | "section": "Build System" 69 | }, 70 | { 71 | "type": "ci", 72 | "section": "Continuous Integration" 73 | }, 74 | { 75 | "type": "chore", 76 | "section": "Miscellaneous Chores" 77 | } 78 | ] 79 | }, 80 | "tagFormat": "${version}", 81 | "branches": [ 82 | { 83 | "name": "maint/+([0-9])?(.{+([0-9]),x}).x", 84 | "range": "${name.replace(/^maint\\//g, '')}" 85 | }, 86 | { 87 | "name": "main" 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@renovatebot/pep440", 3 | "version": "0.0.0-semantic-release", 4 | "description": "PEP440 implementation in JavaScript", 5 | "keywords": [ 6 | "pep440" 7 | ], 8 | "homepage": "https://github.com/renovatebot/pep440#readme", 9 | "bugs": { 10 | "url": "https://github.com/renovatebot/pep440/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/renovatebot/pep440.git" 15 | }, 16 | "license": "Apache-2.0", 17 | "author": "Ayoub Kaanich", 18 | "main": "index.js", 19 | "files": [ 20 | "index.d.ts", 21 | "index.js", 22 | "lib" 23 | ], 24 | "scripts": { 25 | "eslint": "eslint .", 26 | "eslint-fix": "eslint . --fix", 27 | "jest": "jest", 28 | "lint": "run-s eslint markdown-lint prettier", 29 | "lint-fix": "run-s eslint-fix markdown-lint-fix prettier-fix", 30 | "markdown-lint": "markdownlint-cli2 '**/*.md'", 31 | "markdown-lint-fix": "markdownlint-cli2 --fix '**/*.md'", 32 | "prepare": "husky", 33 | "prettier": "prettier --ignore-unknown --check '**/*.*'", 34 | "prettier-fix": "prettier --ignore-unknown --write '**/*.*'", 35 | "test": "run-s prettier eslint markdown-lint jest" 36 | }, 37 | "devDependencies": { 38 | "@containerbase/eslint-plugin": "1.1.22", 39 | "@containerbase/semantic-release-pnpm": "1.3.9", 40 | "@eslint/js": "9.39.1", 41 | "@types/eslint-config-prettier": "6.11.3", 42 | "@types/jest": "30.0.0", 43 | "@types/node": "20.19.26", 44 | "conventional-changelog-conventionalcommits": "9.1.0", 45 | "eslint": "9.39.1", 46 | "eslint-config-prettier": "10.1.8", 47 | "eslint-formatter-gha": "1.6.0", 48 | "eslint-import-resolver-typescript": "4.4.4", 49 | "eslint-plugin-import": "2.32.0", 50 | "eslint-plugin-jest": "29.2.1", 51 | "eslint-plugin-promise": "7.2.1", 52 | "globals": "16.5.0", 53 | "husky": "9.1.7", 54 | "jest": "30.2.0", 55 | "jest-junit": "16.0.0", 56 | "lint-staged": "16.2.7", 57 | "markdownlint-cli2": "0.20.0", 58 | "npm-run-all2": "8.0.4", 59 | "prettier": "3.7.4", 60 | "prettier-plugin-packagejson": "2.5.20", 61 | "semantic-release": "25.0.2", 62 | "typescript-eslint": "8.49.0" 63 | }, 64 | "packageManager": "pnpm@10.25.0", 65 | "engines": { 66 | "node": "^20.9.0 || ^22.11.0 || ^24", 67 | "pnpm": "^10.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-as-default-member */ 2 | import eslintContainerbase from '@containerbase/eslint-plugin'; 3 | import js from '@eslint/js'; 4 | import eslintConfigPrettier from 'eslint-config-prettier'; 5 | import eslintPluginImport from 'eslint-plugin-import'; 6 | import pluginPromise from 'eslint-plugin-promise'; 7 | import globals from 'globals'; 8 | import tseslint from 'typescript-eslint'; 9 | import eslintJestPlugin from 'eslint-plugin-jest'; 10 | 11 | export default tseslint.config( 12 | { 13 | ignores: [ 14 | '**/.git/', 15 | '**/.vscode', 16 | '**/node_modules/', 17 | '**/dist/', 18 | '**/coverage/', 19 | '**/__fixtures__/**/*', 20 | '**/__mocks__/**/*', 21 | '**/*.d.ts', 22 | '**/*.generated.ts', 23 | 'tools/dist', 24 | 'patches', 25 | ], 26 | }, 27 | { 28 | linterOptions: { 29 | reportUnusedDisableDirectives: true, 30 | }, 31 | }, 32 | js.configs.recommended, 33 | ...tseslint.configs.recommendedTypeChecked.map((config) => ({ 34 | ...config, 35 | files: ['**/*.{ts,js,mjs,cjs}'], 36 | })), 37 | ...tseslint.configs.stylisticTypeChecked.map((config) => ({ 38 | ...config, 39 | files: ['**/*.{ts,js,mjs,cjs}'], 40 | })), 41 | eslintPluginImport.flatConfigs.errors, 42 | eslintPluginImport.flatConfigs.warnings, 43 | eslintPluginImport.flatConfigs.recommended, 44 | eslintPluginImport.flatConfigs.typescript, 45 | eslintJestPlugin.configs['flat/recommended'], 46 | eslintJestPlugin.configs['flat/style'], 47 | pluginPromise.configs['flat/recommended'], 48 | eslintContainerbase.configs.all, 49 | eslintConfigPrettier, 50 | { 51 | files: ['**/*.{ts,js,mjs,cjs}'], 52 | 53 | languageOptions: { 54 | globals: { 55 | ...globals.node, 56 | }, 57 | ecmaVersion: 'latest', 58 | sourceType: 'module', 59 | parserOptions: { 60 | projectService: true, 61 | tsconfigRootDir: import.meta.dirname, 62 | }, 63 | }, 64 | 65 | settings: { 66 | 'import/resolver': { 67 | typescript: { 68 | alwaysTryTypes: true, 69 | }, 70 | }, 71 | }, 72 | }, 73 | { 74 | files: ['**/*.{ts,js,mjs,cjs}'], 75 | rules: { 76 | camelcase: 0, 77 | 'no-param-reassign': 'warn', 78 | 'require-await': 'error', 79 | 'no-use-before-define': 0, 80 | 'no-restricted-syntax': 0, 81 | 'no-await-in-loop': 0, 82 | 'prefer-template': 'off', 83 | 'promise/always-return': 'error', 84 | 'promise/no-return-wrap': 'error', 85 | 'promise/param-names': 'error', 86 | 'promise/catch-or-return': 'error', 87 | 'promise/no-native': 'off', 88 | 'promise/no-nesting': 'warn', 89 | 'promise/no-promise-in-callback': 'warn', 90 | 'promise/no-callback-in-promise': 'warn', 91 | 'promise/avoid-new': 'warn', 92 | 93 | // TODO: fixme 94 | '@typescript-eslint/no-require-imports': 0, 95 | '@typescript-eslint/no-unsafe-assignment': 0, 96 | '@typescript-eslint/no-unsafe-argument': 0, 97 | '@typescript-eslint/no-unsafe-call': 0, 98 | '@typescript-eslint/no-unsafe-member-access': 0, 99 | '@typescript-eslint/no-unsafe-return': 0, 100 | '@typescript-eslint/prefer-nullish-coalescing': 0, 101 | '@typescript-eslint/prefer-optional-chain': 0, 102 | '@typescript-eslint/restrict-template-expressions': 0, 103 | }, 104 | }, 105 | { 106 | files: ['test/**/*.{ts,js,mjs,cjs}'], 107 | languageOptions: { 108 | globals: { 109 | ...globals.jest, 110 | }, 111 | }, 112 | rules: { 113 | 'prefer-destructuring': 0, 114 | 'prefer-promise-reject-errors': 0, 115 | 'import/no-extraneous-dependencies': 0, 116 | 'global-require': 0, 117 | }, 118 | }, 119 | ); 120 | -------------------------------------------------------------------------------- /lib/operator.js: -------------------------------------------------------------------------------- 1 | const { parse } = require('./version'); 2 | 3 | module.exports = { 4 | compare, 5 | rcompare, 6 | lt, 7 | le, 8 | eq, 9 | ne, 10 | ge, 11 | gt, 12 | '<': lt, 13 | '<=': le, 14 | '==': eq, 15 | '!=': ne, 16 | '>=': ge, 17 | '>': gt, 18 | '===': arbitrary, 19 | }; 20 | 21 | function lt(version, other) { 22 | return compare(version, other) < 0; 23 | } 24 | 25 | function le(version, other) { 26 | return compare(version, other) <= 0; 27 | } 28 | 29 | function eq(version, other) { 30 | return compare(version, other) === 0; 31 | } 32 | 33 | function ne(version, other) { 34 | return compare(version, other) !== 0; 35 | } 36 | 37 | function ge(version, other) { 38 | return compare(version, other) >= 0; 39 | } 40 | 41 | function gt(version, other) { 42 | return compare(version, other) > 0; 43 | } 44 | 45 | function arbitrary(version, other) { 46 | return version.toLowerCase() === other.toLowerCase(); 47 | } 48 | 49 | function compare(version, other) { 50 | const parsedVersion = parse(version); 51 | const parsedOther = parse(other); 52 | 53 | const keyVersion = calculateKey(parsedVersion); 54 | const keyOther = calculateKey(parsedOther); 55 | 56 | return pyCompare(keyVersion, keyOther); 57 | } 58 | 59 | function rcompare(version, other) { 60 | return -compare(version, other); 61 | } 62 | 63 | // this logic is buitin in python, but we need to port it to js 64 | // see https://stackoverflow.com/a/5292332/1438522 65 | function pyCompare(elemIn, otherIn) { 66 | let elem = elemIn; 67 | let other = otherIn; 68 | if (elem === other) { 69 | return 0; 70 | } 71 | if (Array.isArray(elem) !== Array.isArray(other)) { 72 | elem = Array.isArray(elem) ? elem : [elem]; 73 | other = Array.isArray(other) ? other : [other]; 74 | } 75 | if (Array.isArray(elem)) { 76 | const len = Math.min(elem.length, other.length); 77 | for (let i = 0; i < len; i += 1) { 78 | const res = pyCompare(elem[i], other[i]); 79 | if (res !== 0) { 80 | return res; 81 | } 82 | } 83 | return elem.length - other.length; 84 | } 85 | if (elem === -Infinity || other === Infinity) { 86 | return -1; 87 | } 88 | if (elem === Infinity || other === -Infinity) { 89 | return 1; 90 | } 91 | return elem < other ? -1 : 1; 92 | } 93 | 94 | function calculateKey(input) { 95 | const { epoch } = input; 96 | let { release, pre, post, local, dev } = input; 97 | // When we compare a release version, we want to compare it with all of the 98 | // trailing zeros removed. So we'll use a reverse the list, drop all the now 99 | // leading zeros until we come to something non zero, then take the rest 100 | // re-reverse it back into the correct order and make it a tuple and use 101 | // that for our sorting key. 102 | release = release.concat(); 103 | release.reverse(); 104 | while (release.length && release[0] === 0) { 105 | release.shift(); 106 | } 107 | release.reverse(); 108 | 109 | // We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. 110 | // We'll do this by abusing the pre segment, but we _only_ want to do this 111 | // if there is !a pre or a post segment. If we have one of those then 112 | // the normal sorting rules will handle this case correctly. 113 | if (!pre && !post && dev) pre = -Infinity; 114 | // Versions without a pre-release (except as noted above) should sort after 115 | // those with one. 116 | else if (!pre) pre = Infinity; 117 | 118 | // Versions without a post segment should sort before those with one. 119 | if (!post) post = -Infinity; 120 | 121 | // Versions without a development segment should sort after those with one. 122 | if (!dev) dev = Infinity; 123 | 124 | if (!local) { 125 | // Versions without a local segment should sort before those with one. 126 | local = -Infinity; 127 | } else { 128 | // Versions with a local segment need that segment parsed to implement 129 | // the sorting rules in PEP440. 130 | // - Alpha numeric segments sort before numeric segments 131 | // - Alpha numeric segments sort lexicographically 132 | // - Numeric segments sort numerically 133 | // - Shorter versions sort before longer versions when the prefixes 134 | // match exactly 135 | local = local.map((i) => 136 | Number.isNaN(Number(i)) ? [-Infinity, i] : [Number(i), ''], 137 | ); 138 | } 139 | 140 | return [epoch, release, pre, post, dev, local]; 141 | } 142 | -------------------------------------------------------------------------------- /test/version.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is dual licensed under the terms of the Apache License, Version 3 | 2.0, and the BSD License. See the LICENSE file in the root of this repository 4 | for complete details. 5 | */ 6 | 7 | const { valid, clean } = require('../lib/version'); 8 | 9 | const { VERSIONS, INVALID_VERSIONS } = require('./fixture'); 10 | 11 | describe('valid(version)', () => { 12 | VERSIONS.forEach((version) => { 13 | it('returns valid for ' + JSON.stringify(version), () => { 14 | expect(valid(version)).toEqual(version); 15 | }); 16 | }); 17 | 18 | INVALID_VERSIONS.forEach((version) => { 19 | it('returns null for ' + JSON.stringify(version), () => { 20 | expect(valid(version)).toBeNull(); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('clean(version)', () => { 26 | INVALID_VERSIONS.forEach((version) => { 27 | it('returns null for ' + JSON.stringify(version), () => { 28 | expect(clean(version)).toBeNull(); 29 | }); 30 | }); 31 | 32 | [ 33 | // Various development release incarnations 34 | ['1.0dev', '1.0.dev0'], 35 | ['1.0.dev', '1.0.dev0'], 36 | ['1.0dev1', '1.0.dev1'], 37 | ['1.0dev', '1.0.dev0'], 38 | ['1.0-dev', '1.0.dev0'], 39 | ['1.0-dev1', '1.0.dev1'], 40 | ['1.0DEV', '1.0.dev0'], 41 | ['1.0.DEV', '1.0.dev0'], 42 | ['1.0DEV1', '1.0.dev1'], 43 | ['1.0DEV', '1.0.dev0'], 44 | ['1.0.DEV1', '1.0.dev1'], 45 | ['1.0-DEV', '1.0.dev0'], 46 | ['1.0-DEV1', '1.0.dev1'], 47 | 48 | // Various alpha incarnations 49 | ['1.0a', '1.0a0'], 50 | ['1.0.a', '1.0a0'], 51 | ['1.0.a1', '1.0a1'], 52 | ['1.0-a', '1.0a0'], 53 | ['1.0-a1', '1.0a1'], 54 | ['1.0alpha', '1.0a0'], 55 | ['1.0.alpha', '1.0a0'], 56 | ['1.0.alpha1', '1.0a1'], 57 | ['1.0-alpha', '1.0a0'], 58 | ['1.0-alpha1', '1.0a1'], 59 | ['1.0A', '1.0a0'], 60 | ['1.0.A', '1.0a0'], 61 | ['1.0.A1', '1.0a1'], 62 | ['1.0-A', '1.0a0'], 63 | ['1.0-A1', '1.0a1'], 64 | ['1.0ALPHA', '1.0a0'], 65 | ['1.0.ALPHA', '1.0a0'], 66 | ['1.0.ALPHA1', '1.0a1'], 67 | ['1.0-ALPHA', '1.0a0'], 68 | ['1.0-ALPHA1', '1.0a1'], 69 | 70 | // Various beta incarnations 71 | ['1.0b', '1.0b0'], 72 | ['1.0.b', '1.0b0'], 73 | ['1.0.b1', '1.0b1'], 74 | ['1.0-b', '1.0b0'], 75 | ['1.0-b1', '1.0b1'], 76 | ['1.0beta', '1.0b0'], 77 | ['1.0.beta', '1.0b0'], 78 | ['1.0.beta1', '1.0b1'], 79 | ['1.0-beta', '1.0b0'], 80 | ['1.0-beta1', '1.0b1'], 81 | ['1.0B', '1.0b0'], 82 | ['1.0.B', '1.0b0'], 83 | ['1.0.B1', '1.0b1'], 84 | ['1.0-B', '1.0b0'], 85 | ['1.0-B1', '1.0b1'], 86 | ['1.0BETA', '1.0b0'], 87 | ['1.0.BETA', '1.0b0'], 88 | ['1.0.BETA1', '1.0b1'], 89 | ['1.0-BETA', '1.0b0'], 90 | ['1.0-BETA1', '1.0b1'], 91 | 92 | // Various release candidate incarnations 93 | ['1.0c', '1.0rc0'], 94 | ['1.0.c', '1.0rc0'], 95 | ['1.0.c1', '1.0rc1'], 96 | ['1.0-c', '1.0rc0'], 97 | ['1.0-c1', '1.0rc1'], 98 | ['1.0rc', '1.0rc0'], 99 | ['1.0.rc', '1.0rc0'], 100 | ['1.0.rc1', '1.0rc1'], 101 | ['1.0-rc', '1.0rc0'], 102 | ['1.0-rc1', '1.0rc1'], 103 | ['1.0C', '1.0rc0'], 104 | ['1.0.C', '1.0rc0'], 105 | ['1.0.C1', '1.0rc1'], 106 | ['1.0-C', '1.0rc0'], 107 | ['1.0-C1', '1.0rc1'], 108 | ['1.0RC', '1.0rc0'], 109 | ['1.0.RC', '1.0rc0'], 110 | ['1.0.RC1', '1.0rc1'], 111 | ['1.0-RC', '1.0rc0'], 112 | ['1.0-RC1', '1.0rc1'], 113 | 114 | // Various post release incarnations 115 | ['1.0post', '1.0.post0'], 116 | ['1.0.post', '1.0.post0'], 117 | ['1.0post1', '1.0.post1'], 118 | ['1.0post', '1.0.post0'], 119 | ['1.0-post', '1.0.post0'], 120 | ['1.0-post1', '1.0.post1'], 121 | ['1.0POST', '1.0.post0'], 122 | ['1.0.POST', '1.0.post0'], 123 | ['1.0POST1', '1.0.post1'], 124 | ['1.0POST', '1.0.post0'], 125 | ['1.0r', '1.0.post0'], 126 | ['1.0rev', '1.0.post0'], 127 | ['1.0.POST1', '1.0.post1'], 128 | ['1.0.r1', '1.0.post1'], 129 | ['1.0.rev1', '1.0.post1'], 130 | ['1.0-POST', '1.0.post0'], 131 | ['1.0-POST1', '1.0.post1'], 132 | ['1.0-5', '1.0.post5'], 133 | ['1.0-r5', '1.0.post5'], 134 | ['1.0-rev5', '1.0.post5'], 135 | 136 | // Local version case insensitivity 137 | ['1.0+AbC', '1.0+abc'], 138 | 139 | // Integer Normalization 140 | ['1.01', '1.1'], 141 | ['1.0a05', '1.0a5'], 142 | ['1.0b07', '1.0b7'], 143 | ['1.0c056', '1.0rc56'], 144 | ['1.0rc09', '1.0rc9'], 145 | ['1.0.post000', '1.0.post0'], 146 | ['1.1.dev09000', '1.1.dev9000'], 147 | ['00!1.2', '1.2'], 148 | ['0100!0.0', '100!0.0'], 149 | 150 | // Various other normalizations 151 | ['v1.0', '1.0'], 152 | [' v1.0\t\n', '1.0'], 153 | ['1.0c1', '1.0rc1'], 154 | ].forEach((tuple) => { 155 | const [version, normalized] = tuple; 156 | it(`normalizes ${JSON.stringify(version)} to ${JSON.stringify( 157 | normalized, 158 | )}`, () => { 159 | expect(clean(version)).toEqual(normalized); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /lib/semantic.js: -------------------------------------------------------------------------------- 1 | const { explain, parse, stringify } = require('./version'); 2 | 3 | // those notation are borrowed from semver 4 | module.exports = { 5 | major, 6 | minor, 7 | patch, 8 | inc, 9 | }; 10 | 11 | function major(input) { 12 | const version = explain(input); 13 | if (!version) { 14 | throw new TypeError('Invalid Version: ' + input); 15 | } 16 | return version.release[0]; 17 | } 18 | 19 | function minor(input) { 20 | const version = explain(input); 21 | if (!version) { 22 | throw new TypeError('Invalid Version: ' + input); 23 | } 24 | if (version.release.length < 2) { 25 | return 0; 26 | } 27 | return version.release[1]; 28 | } 29 | 30 | function patch(input) { 31 | const version = explain(input); 32 | if (!version) { 33 | throw new TypeError('Invalid Version: ' + input); 34 | } 35 | if (version.release.length < 3) { 36 | return 0; 37 | } 38 | return version.release[2]; 39 | } 40 | 41 | function inc(input, release, preReleaseIdentifier) { 42 | let identifier = preReleaseIdentifier || `a`; 43 | const version = parse(input); 44 | 45 | if (!version) { 46 | return null; 47 | } 48 | 49 | if ( 50 | !['a', 'b', 'c', 'rc', 'alpha', 'beta', 'pre', 'preview'].includes( 51 | identifier, 52 | ) 53 | ) { 54 | return null; 55 | } 56 | 57 | switch (release) { 58 | case 'premajor': 59 | { 60 | const [majorVersion] = version.release; 61 | version.release.fill(0); 62 | version.release[0] = majorVersion + 1; 63 | } 64 | version.pre = [identifier, 0]; 65 | delete version.post; 66 | delete version.dev; 67 | delete version.local; 68 | break; 69 | case 'preminor': 70 | { 71 | const [majorVersion, minorVersion = 0] = version.release; 72 | version.release.fill(0); 73 | version.release[0] = majorVersion; 74 | version.release[1] = minorVersion + 1; 75 | } 76 | version.pre = [identifier, 0]; 77 | delete version.post; 78 | delete version.dev; 79 | delete version.local; 80 | break; 81 | case 'prepatch': 82 | { 83 | const [majorVersion, minorVersion = 0, patchVersion = 0] = 84 | version.release; 85 | version.release.fill(0); 86 | version.release[0] = majorVersion; 87 | version.release[1] = minorVersion; 88 | version.release[2] = patchVersion + 1; 89 | } 90 | version.pre = [identifier, 0]; 91 | delete version.post; 92 | delete version.dev; 93 | delete version.local; 94 | break; 95 | case 'prerelease': 96 | if (version.pre === null) { 97 | const [majorVersion, minorVersion = 0, patchVersion = 0] = 98 | version.release; 99 | version.release.fill(0); 100 | version.release[0] = majorVersion; 101 | version.release[1] = minorVersion; 102 | version.release[2] = patchVersion + 1; 103 | version.pre = [identifier, 0]; 104 | } else { 105 | if (preReleaseIdentifier === undefined && version.pre !== null) { 106 | [identifier] = version.pre; 107 | } 108 | 109 | const [letter, number] = version.pre; 110 | if (letter === identifier) { 111 | version.pre = [letter, number + 1]; 112 | } else { 113 | version.pre = [identifier, 0]; 114 | } 115 | } 116 | 117 | delete version.post; 118 | delete version.dev; 119 | delete version.local; 120 | break; 121 | case 'major': 122 | if ( 123 | version.release.slice(1).some((value) => value !== 0) || 124 | version.pre === null 125 | ) { 126 | const [majorVersion] = version.release; 127 | version.release.fill(0); 128 | version.release[0] = majorVersion + 1; 129 | } 130 | delete version.pre; 131 | delete version.post; 132 | delete version.dev; 133 | delete version.local; 134 | break; 135 | case 'minor': 136 | if ( 137 | version.release.slice(2).some((value) => value !== 0) || 138 | version.pre === null 139 | ) { 140 | const [majorVersion, minorVersion = 0] = version.release; 141 | version.release.fill(0); 142 | version.release[0] = majorVersion; 143 | version.release[1] = minorVersion + 1; 144 | } 145 | delete version.pre; 146 | delete version.post; 147 | delete version.dev; 148 | delete version.local; 149 | break; 150 | case 'patch': 151 | if ( 152 | version.release.slice(3).some((value) => value !== 0) || 153 | version.pre === null 154 | ) { 155 | const [majorVersion, minorVersion = 0, patchVersion = 0] = 156 | version.release; 157 | version.release.fill(0); 158 | version.release[0] = majorVersion; 159 | version.release[1] = minorVersion; 160 | version.release[2] = patchVersion + 1; 161 | } 162 | delete version.pre; 163 | delete version.post; 164 | delete version.dev; 165 | delete version.local; 166 | break; 167 | default: 168 | return null; 169 | } 170 | 171 | return stringify(version); 172 | } 173 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | const VERSION_PATTERN = [ 2 | 'v?', 3 | '(?:', 4 | /* */ '(?:(?[0-9]+)!)?', // epoch 5 | /* */ '(?[0-9]+(?:\\.[0-9]+)*)', // release segment 6 | /* */ '(?
', // pre-release
  7 |   /*    */ '[-_\\.]?',
  8 |   /*    */ '(?(a|b|c|rc|alpha|beta|pre|preview))',
  9 |   /*    */ '[-_\\.]?',
 10 |   /*    */ '(?[0-9]+)?',
 11 |   /* */ ')?',
 12 |   /* */ '(?', // post release
 13 |   /*    */ '(?:-(?[0-9]+))',
 14 |   /*    */ '|',
 15 |   /*    */ '(?:',
 16 |   /*        */ '[-_\\.]?',
 17 |   /*        */ '(?post|rev|r)',
 18 |   /*        */ '[-_\\.]?',
 19 |   /*        */ '(?[0-9]+)?',
 20 |   /*    */ ')',
 21 |   /* */ ')?',
 22 |   /* */ '(?', // dev release
 23 |   /*    */ '[-_\\.]?',
 24 |   /*    */ '(?dev)',
 25 |   /*    */ '[-_\\.]?',
 26 |   /*    */ '(?[0-9]+)?',
 27 |   /* */ ')?',
 28 |   ')',
 29 |   '(?:\\+(?[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?', // local version
 30 | ].join('');
 31 | 
 32 | module.exports = {
 33 |   VERSION_PATTERN,
 34 |   valid,
 35 |   clean,
 36 |   explain,
 37 |   parse,
 38 |   stringify,
 39 | };
 40 | 
 41 | const validRegex = new RegExp('^' + VERSION_PATTERN + '$', 'i');
 42 | 
 43 | function valid(version) {
 44 |   return validRegex.test(version) ? version : null;
 45 | }
 46 | 
 47 | const cleanRegex = new RegExp('^\\s*' + VERSION_PATTERN + '\\s*$', 'i');
 48 | function clean(version) {
 49 |   return stringify(parse(version, cleanRegex));
 50 | }
 51 | 
 52 | function parse(version, regex) {
 53 |   // Validate the version and parse it into pieces
 54 |   const { groups } = (regex || validRegex).exec(version) || {};
 55 |   if (!groups) {
 56 |     return null;
 57 |   }
 58 | 
 59 |   // Store the parsed out pieces of the version
 60 |   const parsed = {
 61 |     epoch: Number(groups.epoch ? groups.epoch : 0),
 62 |     release: groups.release.split('.').map(Number),
 63 |     pre: normalize_letter_version(groups.pre_l, groups.pre_n),
 64 |     post: normalize_letter_version(
 65 |       groups.post_l,
 66 |       groups.post_n1 || groups.post_n2,
 67 |     ),
 68 |     dev: normalize_letter_version(groups.dev_l, groups.dev_n),
 69 |     local: parse_local_version(groups.local),
 70 |   };
 71 | 
 72 |   return parsed;
 73 | }
 74 | 
 75 | function stringify(parsed) {
 76 |   if (!parsed) {
 77 |     return null;
 78 |   }
 79 |   const { epoch, release, pre, post, dev, local } = parsed;
 80 |   const parts = [];
 81 | 
 82 |   // Epoch
 83 |   if (epoch !== 0) {
 84 |     parts.push(`${epoch}!`);
 85 |   }
 86 |   // Release segment
 87 |   parts.push(release.join('.'));
 88 | 
 89 |   // Pre-release
 90 |   if (pre) {
 91 |     parts.push(pre.join(''));
 92 |   }
 93 |   // Post-release
 94 |   if (post) {
 95 |     parts.push('.' + post.join(''));
 96 |   }
 97 |   // Development release
 98 |   if (dev) {
 99 |     parts.push('.' + dev.join(''));
100 |   }
101 |   // Local version segment
102 |   if (local) {
103 |     parts.push(`+${local}`);
104 |   }
105 |   return parts.join('');
106 | }
107 | 
108 | function normalize_letter_version(letterIn, numberIn) {
109 |   let letter = letterIn;
110 |   let number = numberIn;
111 |   if (letter) {
112 |     // We consider there to be an implicit 0 in a pre-release if there is
113 |     // not a numeral associated with it.
114 |     if (!number) {
115 |       number = 0;
116 |     }
117 |     // We normalize any letters to their lower case form
118 |     letter = letter.toLowerCase();
119 | 
120 |     // We consider some words to be alternate spellings of other words and
121 |     // in those cases we want to normalize the spellings to our preferred
122 |     // spelling.
123 |     if (letter === 'alpha') {
124 |       letter = 'a';
125 |     } else if (letter === 'beta') {
126 |       letter = 'b';
127 |     } else if (['c', 'pre', 'preview'].includes(letter)) {
128 |       letter = 'rc';
129 |     } else if (['rev', 'r'].includes(letter)) {
130 |       letter = 'post';
131 |     }
132 |     return [letter, Number(number)];
133 |   }
134 |   if (!letter && number) {
135 |     // We assume if we are given a number, but we are not given a letter
136 |     // then this is using the implicit post release syntax (e.g. 1.0-1)
137 |     letter = 'post';
138 | 
139 |     return [letter, Number(number)];
140 |   }
141 |   return null;
142 | }
143 | 
144 | function parse_local_version(local) {
145 |   /*
146 |     Takes a string like abc.1.twelve and turns it into("abc", 1, "twelve").
147 |     */
148 |   if (local) {
149 |     return local
150 |       .split(/[._-]/)
151 |       .map((part) =>
152 |         Number.isNaN(Number(part)) ? part.toLowerCase() : Number(part),
153 |       );
154 |   }
155 |   return null;
156 | }
157 | 
158 | function explain(version) {
159 |   const parsed = parse(version);
160 |   if (!parsed) {
161 |     return parsed;
162 |   }
163 |   const { epoch, release, pre, post, dev, local } = parsed;
164 | 
165 |   let base_version = '';
166 |   if (epoch !== 0) {
167 |     base_version += epoch + '!';
168 |   }
169 |   base_version += release.join('.');
170 | 
171 |   const is_prerelease = Boolean(dev || pre);
172 |   const is_devrelease = Boolean(dev);
173 |   const is_postrelease = Boolean(post);
174 | 
175 |   // return
176 | 
177 |   return {
178 |     epoch,
179 |     release,
180 |     pre,
181 |     post: post ? post[1] : post,
182 |     dev: dev ? dev[1] : dev,
183 |     local: local ? local.join('.') : local,
184 |     public: stringify(parsed).split('+', 1)[0],
185 |     base_version,
186 |     is_prerelease,
187 |     is_devrelease,
188 |     is_postrelease,
189 |   };
190 | }
191 | 


--------------------------------------------------------------------------------
/lib/specifier.js:
--------------------------------------------------------------------------------
  1 | // This file is dual licensed under the terms of the Apache License, Version
  2 | // 2.0, and the BSD License. See the LICENSE file in the root of this repository
  3 | // for complete details.
  4 | 
  5 | const { VERSION_PATTERN, explain: explainVersion } = require('./version');
  6 | 
  7 | const Operator = require('./operator');
  8 | 
  9 | const RANGE_PATTERN = [
 10 |   '(?(===|~=|==|!=|<=|>=|<|>))',
 11 |   '\\s*',
 12 |   '(',
 13 |   /*  */ '(?(?:' + VERSION_PATTERN.replace(/\?<\w+>/g, '?:') + '))',
 14 |   /*  */ '(?\\.\\*)?',
 15 |   /*  */ '|',
 16 |   /*  */ '(?[^,;\\s)]+)',
 17 |   ')',
 18 | ].join('');
 19 | 
 20 | module.exports = {
 21 |   RANGE_PATTERN,
 22 |   parse,
 23 |   satisfies,
 24 |   filter,
 25 |   validRange,
 26 |   maxSatisfying,
 27 |   minSatisfying,
 28 | };
 29 | 
 30 | const isEqualityOperator = (op) => ['==', '!=', '==='].includes(op);
 31 | 
 32 | const rangeRegex = new RegExp('^' + RANGE_PATTERN + '$', 'i');
 33 | 
 34 | function parse(ranges) {
 35 |   if (!ranges.trim()) {
 36 |     return [];
 37 |   }
 38 | 
 39 |   const specifiers = ranges
 40 |     .split(',')
 41 |     .map((range) => rangeRegex.exec(range.trim()) || {})
 42 |     .map(({ groups }) => {
 43 |       if (!groups) {
 44 |         return null;
 45 |       }
 46 | 
 47 |       let { ...spec } = groups;
 48 |       const { operator, version, prefix, legacy } = groups;
 49 | 
 50 |       if (version) {
 51 |         spec = { ...spec, ...explainVersion(version) };
 52 |         if (operator === '~=') {
 53 |           if (spec.release.length < 2) {
 54 |             return null;
 55 |           }
 56 |         }
 57 |         if (!isEqualityOperator(operator) && spec.local) {
 58 |           return null;
 59 |         }
 60 | 
 61 |         if (prefix) {
 62 |           if (!isEqualityOperator(operator) || spec.dev || spec.local) {
 63 |             return null;
 64 |           }
 65 |         }
 66 |       }
 67 |       if (legacy && operator !== '===') {
 68 |         return null;
 69 |       }
 70 | 
 71 |       return spec;
 72 |     });
 73 | 
 74 |   if (specifiers.filter(Boolean).length !== specifiers.length) {
 75 |     return null;
 76 |   }
 77 | 
 78 |   return specifiers;
 79 | }
 80 | 
 81 | function filter(versions, specifier, options = {}) {
 82 |   const filtered = pick(versions, specifier, options);
 83 |   if (filtered.length === 0 && options.prereleases === undefined) {
 84 |     return pick(versions, specifier, { prereleases: true });
 85 |   }
 86 |   return filtered;
 87 | }
 88 | 
 89 | function maxSatisfying(versions, range, options) {
 90 |   const found = filter(versions, range, options).sort(Operator.compare);
 91 |   return found.length === 0 ? null : found[found.length - 1];
 92 | }
 93 | 
 94 | function minSatisfying(versions, range, options) {
 95 |   const found = filter(versions, range, options).sort(Operator.compare);
 96 |   return found.length === 0 ? null : found[0];
 97 | }
 98 | 
 99 | function pick(versions, specifier, options) {
100 |   const parsed = parse(specifier);
101 | 
102 |   if (!parsed) {
103 |     return [];
104 |   }
105 | 
106 |   return versions.filter((version) => {
107 |     const explained = explainVersion(version);
108 | 
109 |     if (!parsed.length) {
110 |       return explained && !(explained.is_prerelease && !options.prereleases);
111 |     }
112 | 
113 |     return parsed.reduce((pass, spec) => {
114 |       if (!pass) {
115 |         return false;
116 |       }
117 |       return contains({ ...spec, ...options }, { version, explained });
118 |     }, true);
119 |   });
120 | }
121 | 
122 | function satisfies(version, specifier, options = {}) {
123 |   const filtered = pick([version], specifier, options);
124 | 
125 |   return filtered.length === 1;
126 | }
127 | 
128 | function arrayStartsWith(array, prefix) {
129 |   if (prefix.length > array.length) {
130 |     return false;
131 |   }
132 | 
133 |   for (let i = 0; i < prefix.length; i += 1) {
134 |     if (prefix[i] !== array[i]) {
135 |       return false;
136 |     }
137 |   }
138 | 
139 |   return true;
140 | }
141 | 
142 | function contains(specifier, input) {
143 |   const { explained } = input;
144 |   let { version } = input;
145 |   const { ...spec } = specifier;
146 | 
147 |   if (spec.prereleases === undefined) {
148 |     spec.prereleases = spec.is_prerelease;
149 |   }
150 | 
151 |   if (explained && explained.is_prerelease && !spec.prereleases) {
152 |     return false;
153 |   }
154 | 
155 |   if (spec.operator === '~=') {
156 |     let compatiblePrefix = spec.release.slice(0, -1).concat('*').join('.');
157 |     if (spec.epoch) {
158 |       compatiblePrefix = spec.epoch + '!' + compatiblePrefix;
159 |     }
160 |     return satisfies(version, `>=${spec.version}, ==${compatiblePrefix}`);
161 |   }
162 | 
163 |   if (spec.prefix) {
164 |     const isMatching =
165 |       explained.epoch === spec.epoch &&
166 |       arrayStartsWith(explained.release, spec.release);
167 |     const isEquality = spec.operator !== '!=';
168 |     return isEquality ? isMatching : !isMatching;
169 |   }
170 | 
171 |   if (explained)
172 |     if (explained.local && spec.version) {
173 |       version = explained.public;
174 |       spec.version = explainVersion(spec.version).public;
175 |     }
176 | 
177 |   if (spec.operator === '<' || spec.operator === '>') {
178 |     // simplified version of https://www.python.org/dev/peps/pep-0440/#exclusive-ordered-comparison
179 |     if (Operator.eq(spec.release.join('.'), explained.release.join('.'))) {
180 |       return false;
181 |     }
182 |   }
183 | 
184 |   const op = Operator[spec.operator];
185 |   return op(version, spec.version || spec.legacy);
186 | }
187 | 
188 | function validRange(specifier) {
189 |   return Boolean(parse(specifier));
190 | }
191 | 


--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
  1 | name: build
  2 | 
  3 | on:
  4 |   pull_request:
  5 |   push:
  6 |     branches-ignore:
  7 |       - gh-readonly-queue/**
  8 | 
  9 |   workflow_dispatch:
 10 |     inputs:
 11 |       dryRun:
 12 |         description: 'Dry-Run'
 13 |         default: 'true'
 14 |         required: false
 15 |   merge_group:
 16 | 
 17 | concurrency:
 18 |   group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
 19 |   cancel-in-progress: ${{ github.ref_name != 'main' && !startsWith(github.ref_name, 'maint/') && github.event_name != 'push' }}
 20 | 
 21 | env:
 22 |   DRY_RUN: ${{ github.ref_name != github.event.repository.default_branch && !startsWith(github.ref_name, 'maint/') }}
 23 | 
 24 | permissions:
 25 |   contents: read
 26 | 
 27 | jobs:
 28 |   setup:
 29 |     runs-on: ubuntu-latest
 30 |     timeout-minutes: 10
 31 | 
 32 |     steps:
 33 |       - name: ⚙️ Setup
 34 |         uses: containerbase/internal-tools/setup@e99c697afb2e3b927c25ce772d5a6b27b9927b6b # v3.14.35
 35 |         with:
 36 |           save-cache: true
 37 | 
 38 |   test:
 39 |     needs:
 40 |       - setup
 41 |     name: ${{ matrix.node-version == 22 && format('test ({0})', matrix.os) || format('test ({0}, node-{1})', matrix.os, matrix.node-version) }}
 42 |     runs-on: ${{ matrix.os }}
 43 | 
 44 |     # tests shouldn't need more time
 45 |     timeout-minutes: 30
 46 | 
 47 |     permissions:
 48 |       id-token: write
 49 | 
 50 |     strategy:
 51 |       matrix:
 52 |         os:
 53 |           - ubuntu-latest
 54 |           - macos-latest
 55 |           - windows-latest
 56 |         node-version:
 57 |           - 20
 58 |           - 22
 59 |           - 24
 60 | 
 61 |     env:
 62 |       coverage: ${{ matrix.os == 'ubuntu-latest' && matrix.node-version == 22 }}
 63 | 
 64 |     steps:
 65 |       - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
 66 |         with:
 67 |           fetch-depth: 10 # required for coverage
 68 |           show-progress: false
 69 |           filter: blob:none # we don't need all blobs
 70 | 
 71 |       - name: ⚙️ Setup
 72 |         uses: containerbase/internal-tools/setup@e99c697afb2e3b927c25ce772d5a6b27b9927b6b # v3.14.35
 73 |         with:
 74 |           checkout: false
 75 |           node-version: ${{ matrix.node-version }}
 76 |           save-cache: true
 77 | 
 78 |       - name: Init platform
 79 |         shell: bash
 80 |         run: |
 81 |           git config --global core.autocrlf false
 82 |           git config --global core.symlinks true
 83 |           echo 'Node $(node --version)'
 84 |           echo 'pnpm $(pnpm --version)'
 85 | 
 86 |       - name: Run tests
 87 |         run: pnpm jest --maxWorkers=2 --ci --coverage ${{ env.coverage }} --logHeapUsage
 88 | 
 89 |       - name: Upload coverage
 90 |         if: env.coverage == 'true' && github.event_name != 'merge_group' && !cancelled()
 91 |         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 92 |         with:
 93 |           name: coverage
 94 |           path: coverage
 95 | 
 96 |       - name: Codecov test results
 97 |         if: github.event_name != 'merge_group' && !cancelled()
 98 |         uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
 99 |         with:
100 |           use_oidc: true
101 |           report_type: test_results
102 |           flags: node${{ matrix.node-version }}-${{ runner.os }}
103 | 
104 |       - name: Codecov coverage result
105 |         if: env.coverage == 'true' && github.event_name != 'merge_group'
106 |         uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
107 |         with:
108 |           use_oidc: true
109 |           flags: node${{ matrix.node-version }}-${{ runner.os }}
110 | 
111 |   lint:
112 |     needs:
113 |       - setup
114 |     runs-on: ubuntu-latest
115 | 
116 |     # lint shouldn't need more than 10 min
117 |     timeout-minutes: 15
118 | 
119 |     steps:
120 |       - name: ⚙️ Setup
121 |         uses: containerbase/internal-tools/setup@e99c697afb2e3b927c25ce772d5a6b27b9927b6b # v3.14.35
122 | 
123 |       - name: Lint
124 |         run: |
125 |           pnpm eslint -f gha
126 |           pnpm markdown-lint
127 |           pnpm prettier
128 | 
129 |       # - name: Type check
130 |       #   run: pnpm type-check
131 | 
132 |   release:
133 |     needs:
134 |       - lint
135 |       - test
136 |     runs-on: ubuntu-latest
137 | 
138 |     # tests shouldn't need more time
139 |     timeout-minutes: 15
140 | 
141 |     permissions:
142 |       contents: write
143 |       id-token: write
144 |       issues: write
145 | 
146 |     steps:
147 |       # full checkout for semantic-release
148 |       - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
149 |         with:
150 |           fetch-depth: 0
151 |           show-progress: false
152 |           filter: blob:none # we don't need all blobs
153 | 
154 |       - name: ⚙️ Setup
155 |         uses: containerbase/internal-tools/setup@e99c697afb2e3b927c25ce772d5a6b27b9927b6b # v3.14.35
156 |         with:
157 |           checkout: false
158 | 
159 |       - name: semantic-release
160 |         run: |
161 |           pnpm semantic-release --dry-run ${{env.DRY_RUN}} --ci ${{env.DRY_RUN != 'true'}}
162 |         env:
163 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
164 | 
165 |   # Catch-all required check
166 |   success:
167 |     needs:
168 |       - setup
169 |       - lint
170 |       - test
171 |       - release
172 |     runs-on: ubuntu-latest
173 | 
174 |     timeout-minutes: 1
175 | 
176 |     if: always()
177 |     steps:
178 |       - name: Fail for failed or skipped setup job
179 |         if: |
180 |           needs.setup.result == 'failure' ||
181 |           needs.setup.result == 'skipped'
182 |         run: exit 1
183 | 
184 |       - name: Fail for failed or skipped lint job
185 |         if: |
186 |           needs.lint.result == 'failure' ||
187 |           needs.lint.result == 'skipped'
188 |         run: exit 1
189 | 
190 |       - name: Fail for failed or skipped lint job
191 |         if: |
192 |           needs.test.result == 'failure' ||
193 |           needs.test.result == 'skipped'
194 |         run: exit 1
195 | 
196 |       - name: Fail for failed or skipped release job
197 |         if: |
198 |           needs.release.result == 'failure' ||
199 |           needs.release.result == 'skipped'
200 |         run: exit 1
201 | 


--------------------------------------------------------------------------------
/LICENSE.APACHE:
--------------------------------------------------------------------------------
  1 | 
  2 |                                  Apache License
  3 |                            Version 2.0, January 2004
  4 |                         http://www.apache.org/licenses/
  5 | 
  6 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7 | 
  8 |    1. Definitions.
  9 | 
 10 |       "License" shall mean the terms and conditions for use, reproduction,
 11 |       and distribution as defined by Sections 1 through 9 of this document.
 12 | 
 13 |       "Licensor" shall mean the copyright owner or entity authorized by
 14 |       the copyright owner that is granting the License.
 15 | 
 16 |       "Legal Entity" shall mean the union of the acting entity and all
 17 |       other entities that control, are controlled by, or are under common
 18 |       control with that entity. For the purposes of this definition,
 19 |       "control" means (i) the power, direct or indirect, to cause the
 20 |       direction or management of such entity, whether by contract or
 21 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 22 |       outstanding shares, or (iii) beneficial ownership of such entity.
 23 | 
 24 |       "You" (or "Your") shall mean an individual or Legal Entity
 25 |       exercising permissions granted by this License.
 26 | 
 27 |       "Source" form shall mean the preferred form for making modifications,
 28 |       including but not limited to software source code, documentation
 29 |       source, and configuration files.
 30 | 
 31 |       "Object" form shall mean any form resulting from mechanical
 32 |       transformation or translation of a Source form, including but
 33 |       not limited to compiled object code, generated documentation,
 34 |       and conversions to other media types.
 35 | 
 36 |       "Work" shall mean the work of authorship, whether in Source or
 37 |       Object form, made available under the License, as indicated by a
 38 |       copyright notice that is included in or attached to the work
 39 |       (an example is provided in the Appendix below).
 40 | 
 41 |       "Derivative Works" shall mean any work, whether in Source or Object
 42 |       form, that is based on (or derived from) the Work and for which the
 43 |       editorial revisions, annotations, elaborations, or other modifications
 44 |       represent, as a whole, an original work of authorship. For the purposes
 45 |       of this License, Derivative Works shall not include works that remain
 46 |       separable from, or merely link (or bind by name) to the interfaces of,
 47 |       the Work and Derivative Works thereof.
 48 | 
 49 |       "Contribution" shall mean any work of authorship, including
 50 |       the original version of the Work and any modifications or additions
 51 |       to that Work or Derivative Works thereof, that is intentionally
 52 |       submitted to Licensor for inclusion in the Work by the copyright owner
 53 |       or by an individual or Legal Entity authorized to submit on behalf of
 54 |       the copyright owner. For the purposes of this definition, "submitted"
 55 |       means any form of electronic, verbal, or written communication sent
 56 |       to the Licensor or its representatives, including but not limited to
 57 |       communication on electronic mailing lists, source code control systems,
 58 |       and issue tracking systems that are managed by, or on behalf of, the
 59 |       Licensor for the purpose of discussing and improving the Work, but
 60 |       excluding communication that is conspicuously marked or otherwise
 61 |       designated in writing by the copyright owner as "Not a Contribution."
 62 | 
 63 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 64 |       on behalf of whom a Contribution has been received by Licensor and
 65 |       subsequently incorporated within the Work.
 66 | 
 67 |    2. Grant of Copyright License. Subject to the terms and conditions of
 68 |       this License, each Contributor hereby grants to You a perpetual,
 69 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 70 |       copyright license to reproduce, prepare Derivative Works of,
 71 |       publicly display, publicly perform, sublicense, and distribute the
 72 |       Work and such Derivative Works in Source or Object form.
 73 | 
 74 |    3. Grant of Patent License. Subject to the terms and conditions of
 75 |       this License, each Contributor hereby grants to You a perpetual,
 76 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 77 |       (except as stated in this section) patent license to make, have made,
 78 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 79 |       where such license applies only to those patent claims licensable
 80 |       by such Contributor that are necessarily infringed by their
 81 |       Contribution(s) alone or by combination of their Contribution(s)
 82 |       with the Work to which such Contribution(s) was submitted. If You
 83 |       institute patent litigation against any entity (including a
 84 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 85 |       or a Contribution incorporated within the Work constitutes direct
 86 |       or contributory patent infringement, then any patent licenses
 87 |       granted to You under this License for that Work shall terminate
 88 |       as of the date such litigation is filed.
 89 | 
 90 |    4. Redistribution. You may reproduce and distribute copies of the
 91 |       Work or Derivative Works thereof in any medium, with or without
 92 |       modifications, and in Source or Object form, provided that You
 93 |       meet the following conditions:
 94 | 
 95 |       (a) You must give any other recipients of the Work or
 96 |           Derivative Works a copy of this License; and
 97 | 
 98 |       (b) You must cause any modified files to carry prominent notices
 99 |           stating that You changed the files; and
100 | 
101 |       (c) You must retain, in the Source form of any Derivative Works
102 |           that You distribute, all copyright, patent, trademark, and
103 |           attribution notices from the Source form of the Work,
104 |           excluding those notices that do not pertain to any part of
105 |           the Derivative Works; and
106 | 
107 |       (d) If the Work includes a "NOTICE" text file as part of its
108 |           distribution, then any Derivative Works that You distribute must
109 |           include a readable copy of the attribution notices contained
110 |           within such NOTICE file, excluding those notices that do not
111 |           pertain to any part of the Derivative Works, in at least one
112 |           of the following places: within a NOTICE text file distributed
113 |           as part of the Derivative Works; within the Source form or
114 |           documentation, if provided along with the Derivative Works; or,
115 |           within a display generated by the Derivative Works, if and
116 |           wherever such third-party notices normally appear. The contents
117 |           of the NOTICE file are for informational purposes only and
118 |           do not modify the License. You may add Your own attribution
119 |           notices within Derivative Works that You distribute, alongside
120 |           or as an addendum to the NOTICE text from the Work, provided
121 |           that such additional attribution notices cannot be construed
122 |           as modifying the License.
123 | 
124 |       You may add Your own copyright statement to Your modifications and
125 |       may provide additional or different license terms and conditions
126 |       for use, reproduction, or distribution of Your modifications, or
127 |       for any such Derivative Works as a whole, provided Your use,
128 |       reproduction, and distribution of the Work otherwise complies with
129 |       the conditions stated in this License.
130 | 
131 |    5. Submission of Contributions. Unless You explicitly state otherwise,
132 |       any Contribution intentionally submitted for inclusion in the Work
133 |       by You to the Licensor shall be under the terms and conditions of
134 |       this License, without any additional terms or conditions.
135 |       Notwithstanding the above, nothing herein shall supersede or modify
136 |       the terms of any separate license agreement you may have executed
137 |       with Licensor regarding such Contributions.
138 | 
139 |    6. Trademarks. This License does not grant permission to use the trade
140 |       names, trademarks, service marks, or product names of the Licensor,
141 |       except as required for reasonable and customary use in describing the
142 |       origin of the Work and reproducing the content of the NOTICE file.
143 | 
144 |    7. Disclaimer of Warranty. Unless required by applicable law or
145 |       agreed to in writing, Licensor provides the Work (and each
146 |       Contributor provides its Contributions) on an "AS IS" BASIS,
147 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 |       implied, including, without limitation, any warranties or conditions
149 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 |       PARTICULAR PURPOSE. You are solely responsible for determining the
151 |       appropriateness of using or redistributing the Work and assume any
152 |       risks associated with Your exercise of permissions under this License.
153 | 
154 |    8. Limitation of Liability. In no event and under no legal theory,
155 |       whether in tort (including negligence), contract, or otherwise,
156 |       unless required by applicable law (such as deliberate and grossly
157 |       negligent acts) or agreed to in writing, shall any Contributor be
158 |       liable to You for damages, including any direct, indirect, special,
159 |       incidental, or consequential damages of any character arising as a
160 |       result of this License or out of the use or inability to use the
161 |       Work (including but not limited to damages for loss of goodwill,
162 |       work stoppage, computer failure or malfunction, or any and all
163 |       other commercial damages or losses), even if such Contributor
164 |       has been advised of the possibility of such damages.
165 | 
166 |    9. Accepting Warranty or Additional Liability. While redistributing
167 |       the Work or Derivative Works thereof, You may choose to offer,
168 |       and charge a fee for, acceptance of support, warranty, indemnity,
169 |       or other liability obligations and/or rights consistent with this
170 |       License. However, in accepting such obligations, You may act only
171 |       on Your own behalf and on Your sole responsibility, not on behalf
172 |       of any other Contributor, and only if You agree to indemnify,
173 |       defend, and hold each Contributor harmless for any liability
174 |       incurred by, or claims asserted against, such Contributor by reason
175 |       of your accepting any such warranty or additional liability.
176 | 
177 |    END OF TERMS AND CONDITIONS


--------------------------------------------------------------------------------
/test/semantic.test.js:
--------------------------------------------------------------------------------
  1 | const { major, minor, patch, inc } = require('..');
  2 | 
  3 | describe('major(version)', () => {
  4 |   it('returns correct value', () => {
  5 |     expect(major('1.2.3')).toBe(1);
  6 |   });
  7 |   it('handles epoch', () => {
  8 |     expect(major('25!1.2.3')).toBe(1);
  9 |   });
 10 |   it('throws when version invalid', () => {
 11 |     expect(() => major('not_version')).toThrow(TypeError);
 12 |   });
 13 | });
 14 | 
 15 | describe('minor(version)', () => {
 16 |   it('returns correct value', () => {
 17 |     expect(minor('1.2.3')).toBe(2);
 18 |   });
 19 |   it('pads zeros', () => {
 20 |     expect(minor('1')).toBe(0);
 21 |   });
 22 |   it('throws when version invalid', () => {
 23 |     expect(() => minor('not_version')).toThrow(TypeError);
 24 |   });
 25 | });
 26 | 
 27 | describe('patch(version)', () => {
 28 |   it('returns correct value', () => {
 29 |     expect(patch('1.2.3')).toBe(3);
 30 |   });
 31 |   it('pads zeros', () => {
 32 |     expect(patch('1.2')).toBe(0);
 33 |   });
 34 |   it('throws when version invalid', () => {
 35 |     expect(() => patch('not_version')).toThrow(TypeError);
 36 |   });
 37 | });
 38 | 
 39 | describe('inc(version, release)', () => {
 40 |   const testCases = [
 41 |     // [ existing version, release type (`major`, `minor`, `patch`), identifier (`a`, `b`, or `rc`.), expected result ]
 42 | 
 43 |     // Semantic Version numbers only.
 44 |     ['0.0.0', 'major', undefined, '1.0.0'],
 45 |     ['1.0.0', 'major', undefined, '2.0.0'],
 46 |     ['1.0.1', 'major', undefined, '2.0.0'],
 47 |     ['1.1.0', 'major', undefined, '2.0.0'],
 48 | 
 49 |     ['0.0.0', 'minor', undefined, '0.1.0'],
 50 |     ['1.0.0', 'minor', undefined, '1.1.0'],
 51 |     ['1.0.1', 'minor', undefined, '1.1.0'],
 52 |     ['1.1.0', 'minor', undefined, '1.2.0'],
 53 | 
 54 |     ['0.0.0', 'patch', undefined, '0.0.1'],
 55 |     ['1.0.0', 'patch', undefined, '1.0.1'],
 56 |     ['1.0.1', 'patch', undefined, '1.0.2'],
 57 |     ['1.1.0', 'patch', undefined, '1.1.1'],
 58 | 
 59 |     ['0.0.0', 'premajor', undefined, '1.0.0a0'],
 60 |     ['1.0.0', 'premajor', undefined, '2.0.0a0'],
 61 |     ['1.0.1', 'premajor', undefined, '2.0.0a0'],
 62 |     ['1.1.0', 'premajor', undefined, '2.0.0a0'],
 63 | 
 64 |     ['0.0.0', 'preminor', undefined, '0.1.0a0'],
 65 |     ['1.0.0', 'preminor', undefined, '1.1.0a0'],
 66 |     ['1.0.1', 'preminor', undefined, '1.1.0a0'],
 67 |     ['1.1.0', 'preminor', undefined, '1.2.0a0'],
 68 | 
 69 |     ['0.0.0', 'prepatch', undefined, '0.0.1a0'],
 70 |     ['1.0.0', 'prepatch', undefined, '1.0.1a0'],
 71 |     ['1.0.1', 'prepatch', undefined, '1.0.2a0'],
 72 |     ['1.1.0', 'prepatch', undefined, '1.1.1a0'],
 73 | 
 74 |     ['0.0.0', 'prerelease', undefined, '0.0.1a0'],
 75 |     ['1.0.0', 'prerelease', undefined, '1.0.1a0'],
 76 |     ['1.0.1', 'prerelease', undefined, '1.0.2a0'],
 77 |     ['1.1.0', 'prerelease', undefined, '1.1.1a0'],
 78 | 
 79 |     ['0.0.0', 'major', `b`, '1.0.0'],
 80 |     ['0.0.0', 'minor', `b`, '0.1.0'],
 81 |     ['0.0.0', 'patch', `b`, '0.0.1'],
 82 |     ['0.0.0', 'premajor', `b`, '1.0.0b0'],
 83 |     ['0.0.0', 'preminor', `b`, '0.1.0b0'],
 84 |     ['0.0.0', 'prepatch', `b`, '0.0.1b0'],
 85 |     ['0.0.0', 'prerelease', `b`, '0.0.1b0'],
 86 | 
 87 |     // Extra release segments, as allowed by PEP440.
 88 |     ['1.1.1.1', 'major', undefined, '2.0.0.0'],
 89 |     ['1.1.1.1', 'minor', undefined, '1.2.0.0'],
 90 |     ['1.1.1.1', 'patch', undefined, '1.1.2.0'],
 91 |     ['1.1.1.1', 'premajor', undefined, '2.0.0.0a0'],
 92 |     ['1.1.1.1', 'preminor', undefined, '1.2.0.0a0'],
 93 |     ['1.1.1.1', 'prepatch', undefined, '1.1.2.0a0'],
 94 |     ['1.1.1.1', 'prerelease', undefined, '1.1.2.0a0'],
 95 | 
 96 |     // Without padding, as allowed by PEP440.
 97 |     ['1', 'major', undefined, '2'],
 98 |     ['1.0', 'major', undefined, '2.0'],
 99 | 
100 |     ['1', 'minor', undefined, '1.1'],
101 |     ['1.0', 'minor', undefined, '1.1'],
102 | 
103 |     ['1', 'patch', undefined, '1.0.1'],
104 |     ['1.0', 'patch', undefined, '1.0.1'],
105 | 
106 |     ['1', 'premajor', undefined, '2a0'],
107 |     ['1.0', 'premajor', undefined, '2.0a0'],
108 | 
109 |     ['1', 'preminor', undefined, '1.1a0'],
110 |     ['1.0', 'preminor', undefined, '1.1a0'],
111 | 
112 |     ['1', 'prepatch', undefined, '1.0.1a0'],
113 |     ['1.0', 'prepatch', undefined, '1.0.1a0'],
114 | 
115 |     ['1', 'prerelease', undefined, '1.0.1a0'],
116 |     ['1.0', 'prerelease', undefined, '1.0.1a0'],
117 | 
118 |     // Pre-release versions.
119 |     ['1.0.0a0', 'major', undefined, '1.0.0'],
120 |     ['1.1.1a0', 'major', undefined, '2.0.0'],
121 |     ['1.1.1rc0', 'major', undefined, '2.0.0'],
122 | 
123 |     ['1.0.0a0', 'minor', undefined, '1.0.0'],
124 |     ['1.1.1a0', 'minor', undefined, '1.2.0'],
125 |     ['1.1.1rc0', 'minor', undefined, '1.2.0'],
126 | 
127 |     ['1.0.0a0', 'patch', undefined, '1.0.0'],
128 |     ['1.1.1a0', 'patch', undefined, '1.1.1'],
129 |     ['1.1.1rc0', 'patch', undefined, '1.1.1'],
130 | 
131 |     ['1.0.0a0', 'premajor', undefined, '2.0.0a0'],
132 |     ['1.1.1a0', 'premajor', undefined, '2.0.0a0'],
133 |     ['1.1.1rc0', 'premajor', undefined, '2.0.0a0'],
134 | 
135 |     ['1.0.0a0', 'preminor', undefined, '1.1.0a0'],
136 |     ['1.1.1a0', 'preminor', undefined, '1.2.0a0'],
137 |     ['1.1.1rc0', 'preminor', undefined, '1.2.0a0'],
138 | 
139 |     ['1.0.0a0', 'prepatch', undefined, '1.0.1a0'],
140 |     ['1.1.1a0', 'prepatch', undefined, '1.1.2a0'],
141 |     ['1.1.1rc0', 'prepatch', undefined, '1.1.2a0'],
142 | 
143 |     ['1.0.0a0', 'prerelease', undefined, '1.0.0a1'],
144 |     ['1.0.0a1', 'prerelease', undefined, '1.0.0a2'],
145 |     ['1.0.0b0', 'prerelease', undefined, '1.0.0b1'],
146 |     ['1.0.0rc0', 'prerelease', undefined, '1.0.0rc1'],
147 | 
148 |     ['1.0.0a0', 'premajor', `b`, '2.0.0b0'],
149 |     ['1.0.0a0', 'preminor', `b`, '1.1.0b0'],
150 |     ['1.0.0a0', 'prepatch', `b`, '1.0.1b0'],
151 |     ['1.0.0a0', 'prerelease', `b`, '1.0.0b0'],
152 | 
153 |     ['1.0.0b0', 'prerelease', `b`, '1.0.0b1'],
154 | 
155 |     // Post-release versions.
156 |     ['1.0.0.post1', 'major', undefined, '2.0.0'],
157 |     ['1.1.1.post1', 'major', undefined, '2.0.0'],
158 | 
159 |     ['1.0.0.post1', 'minor', undefined, '1.1.0'],
160 |     ['1.1.1.post1', 'minor', undefined, '1.2.0'],
161 | 
162 |     ['1.0.0.post1', 'patch', undefined, '1.0.1'],
163 |     ['1.1.1.post1', 'patch', undefined, '1.1.2'],
164 | 
165 |     ['1.0.0.post1', 'premajor', undefined, '2.0.0a0'],
166 |     ['1.1.1.post1', 'premajor', undefined, '2.0.0a0'],
167 | 
168 |     ['1.0.0.post1', 'preminor', undefined, '1.1.0a0'],
169 |     ['1.1.1.post1', 'preminor', undefined, '1.2.0a0'],
170 | 
171 |     ['1.0.0.post1', 'prepatch', undefined, '1.0.1a0'],
172 |     ['1.1.1.post1', 'prepatch', undefined, '1.1.2a0'],
173 | 
174 |     ['1.0.0.post1', 'prerelease', undefined, '1.0.1a0'],
175 |     ['1.1.1.post1', 'prerelease', undefined, '1.1.2a0'],
176 | 
177 |     // Post-releases of pre-release versions.
178 |     ['1.0.0a0.post1', 'major', undefined, '1.0.0'],
179 |     ['1.1.1a0.post1', 'major', undefined, '2.0.0'],
180 |     ['1.1.1rc0.post1', 'major', undefined, '2.0.0'],
181 | 
182 |     ['1.0.0a0.post1', 'minor', undefined, '1.0.0'],
183 |     ['1.1.1a0.post1', 'minor', undefined, '1.2.0'],
184 |     ['1.1.1rc0.post1', 'minor', undefined, '1.2.0'],
185 | 
186 |     ['1.0.0a0.post1', 'patch', undefined, '1.0.0'],
187 |     ['1.1.1a0.post1', 'patch', undefined, '1.1.1'],
188 |     ['1.1.1rc0.post1', 'patch', undefined, '1.1.1'],
189 | 
190 |     ['1.0.0a0.post1', 'premajor', undefined, '2.0.0a0'],
191 |     ['1.1.1a0.post1', 'premajor', undefined, '2.0.0a0'],
192 |     ['1.1.1rc0.post1', 'premajor', undefined, '2.0.0a0'],
193 | 
194 |     ['1.0.0a0.post1', 'preminor', undefined, '1.1.0a0'],
195 |     ['1.1.1a0.post1', 'preminor', undefined, '1.2.0a0'],
196 |     ['1.1.1rc0.post1', 'preminor', undefined, '1.2.0a0'],
197 | 
198 |     ['1.0.0a0.post1', 'prepatch', undefined, '1.0.1a0'],
199 |     ['1.1.1a0.post1', 'prepatch', undefined, '1.1.2a0'],
200 |     ['1.1.1rc0.post1', 'prepatch', undefined, '1.1.2a0'],
201 | 
202 |     ['1.0.0a0.post1', 'prerelease', undefined, '1.0.0a1'],
203 |     ['1.0.0b0.post1', 'prerelease', undefined, '1.0.0b1'],
204 |     ['1.0.0rc0.post1', 'prerelease', undefined, '1.0.0rc1'],
205 | 
206 |     // Development-release versions.
207 |     ['1.0.0.dev1', 'major', undefined, '2.0.0'],
208 |     ['1.1.1.dev1', 'major', undefined, '2.0.0'],
209 | 
210 |     ['1.0.0.dev1', 'minor', undefined, '1.1.0'],
211 |     ['1.1.1.dev1', 'minor', undefined, '1.2.0'],
212 | 
213 |     ['1.0.0.dev1', 'patch', undefined, '1.0.1'],
214 |     ['1.1.1.dev1', 'patch', undefined, '1.1.2'],
215 | 
216 |     ['1.0.0.dev1', 'premajor', undefined, '2.0.0a0'],
217 |     ['1.1.1.dev1', 'premajor', undefined, '2.0.0a0'],
218 | 
219 |     ['1.0.0.dev1', 'preminor', undefined, '1.1.0a0'],
220 |     ['1.1.1.dev1', 'preminor', undefined, '1.2.0a0'],
221 | 
222 |     ['1.0.0.dev1', 'prepatch', undefined, '1.0.1a0'],
223 |     ['1.1.1.dev1', 'prepatch', undefined, '1.1.2a0'],
224 | 
225 |     ['1.0.0.dev1', 'prerelease', undefined, '1.0.1a0'],
226 |     ['1.1.1.dev1', 'prerelease', undefined, '1.1.2a0'],
227 | 
228 |     // Development-releases of pre-release versions.
229 |     ['1.0.0a0.dev1', 'major', undefined, '1.0.0'],
230 |     ['1.1.1a0.dev1', 'major', undefined, '2.0.0'],
231 |     ['1.1.1rc0.dev1', 'major', undefined, '2.0.0'],
232 | 
233 |     ['1.0.0a0.dev1', 'minor', undefined, '1.0.0'],
234 |     ['1.1.1a0.dev1', 'minor', undefined, '1.2.0'],
235 |     ['1.1.1rc0.dev1', 'minor', undefined, '1.2.0'],
236 | 
237 |     ['1.0.0a0.dev1', 'patch', undefined, '1.0.0'],
238 |     ['1.1.1a0.dev1', 'patch', undefined, '1.1.1'],
239 |     ['1.1.1rc0.dev1', 'patch', undefined, '1.1.1'],
240 | 
241 |     ['1.0.0a0.dev1', 'premajor', undefined, '2.0.0a0'],
242 |     ['1.1.1a0.dev1', 'premajor', undefined, '2.0.0a0'],
243 |     ['1.1.1rc0.dev1', 'premajor', undefined, '2.0.0a0'],
244 | 
245 |     ['1.0.0a0.dev1', 'preminor', undefined, '1.1.0a0'],
246 |     ['1.1.1a0.dev1', 'preminor', undefined, '1.2.0a0'],
247 |     ['1.1.1rc0.dev1', 'preminor', undefined, '1.2.0a0'],
248 | 
249 |     ['1.0.0a0.dev1', 'prepatch', undefined, '1.0.1a0'],
250 |     ['1.1.1a0.dev1', 'prepatch', undefined, '1.1.2a0'],
251 |     ['1.1.1rc0.dev1', 'prepatch', undefined, '1.1.2a0'],
252 | 
253 |     ['1.0.0a0.dev1', 'prerelease', undefined, '1.0.0a1'],
254 |     ['1.0.0rc0.dev1', 'prerelease', undefined, '1.0.0rc1'],
255 | 
256 |     // Development-releases of post-release versions.
257 |     ['1.0.0.post1.dev1', 'major', undefined, '2.0.0'],
258 |     ['1.1.1.post1.dev1', 'major', undefined, '2.0.0'],
259 | 
260 |     ['1.0.0.post1.dev1', 'minor', undefined, '1.1.0'],
261 |     ['1.1.1.post1.dev1', 'minor', undefined, '1.2.0'],
262 | 
263 |     ['1.0.0.post1.dev1', 'patch', undefined, '1.0.1'],
264 |     ['1.1.1.post1.dev1', 'patch', undefined, '1.1.2'],
265 | 
266 |     ['1.0.0.post1.dev1', 'premajor', undefined, '2.0.0a0'],
267 |     ['1.1.1.post1.dev1', 'premajor', undefined, '2.0.0a0'],
268 | 
269 |     ['1.0.0.post1.dev1', 'preminor', undefined, '1.1.0a0'],
270 |     ['1.1.1.post1.dev1', 'preminor', undefined, '1.2.0a0'],
271 | 
272 |     ['1.0.0.post1.dev1', 'prepatch', undefined, '1.0.1a0'],
273 |     ['1.1.1.post1.dev1', 'prepatch', undefined, '1.1.2a0'],
274 | 
275 |     ['1.0.0.post1.dev1', 'prerelease', undefined, '1.0.1a0'],
276 | 
277 |     // Development-releases of pre-and-post-release versions.
278 |     ['1.0.0a0.post1.dev1', 'major', undefined, '1.0.0'],
279 |     ['1.1.1a0.post1.dev1', 'major', undefined, '2.0.0'],
280 |     ['1.1.1rc0.post1.dev1', 'major', undefined, '2.0.0'],
281 | 
282 |     ['1.0.0a0.post1.dev1', 'minor', undefined, '1.0.0'],
283 |     ['1.1.1a0.post1.dev1', 'minor', undefined, '1.2.0'],
284 |     ['1.1.1rc0.post1.dev1', 'minor', undefined, '1.2.0'],
285 | 
286 |     ['1.0.0a0.post1.dev1', 'patch', undefined, '1.0.0'],
287 |     ['1.1.1a0.post1.dev1', 'patch', undefined, '1.1.1'],
288 |     ['1.1.1rc0.post1.dev1', 'patch', undefined, '1.1.1'],
289 | 
290 |     ['1.0.0a0.post1.dev1', 'premajor', undefined, '2.0.0a0'],
291 |     ['1.1.1a0.post1.dev1', 'premajor', undefined, '2.0.0a0'],
292 |     ['1.1.1rc0.post1.dev1', 'premajor', undefined, '2.0.0a0'],
293 | 
294 |     ['1.0.0a0.post1.dev1', 'preminor', undefined, '1.1.0a0'],
295 |     ['1.1.1a0.post1.dev1', 'preminor', undefined, '1.2.0a0'],
296 |     ['1.1.1rc0.post1.dev1', 'preminor', undefined, '1.2.0a0'],
297 | 
298 |     ['1.0.0a0.post1.dev1', 'prepatch', undefined, '1.0.1a0'],
299 |     ['1.1.1a0.post1.dev1', 'prepatch', undefined, '1.1.2a0'],
300 |     ['1.1.1rc0.post1.dev1', 'prepatch', undefined, '1.1.2a0'],
301 | 
302 |     ['1.0.0a0.post1.dev1', 'prerelease', undefined, '1.0.0a1'],
303 |     ['1.0.0b0.post1.dev1', 'prerelease', undefined, '1.0.0b1'],
304 |     ['1.0.0rc0.post1.dev1', 'prerelease', undefined, '1.0.0rc1'],
305 |   ];
306 | 
307 |   testCases.forEach((testCase) =>
308 |     it(`handles incrementing ${testCase[0]} using ${testCase[1]} to ${testCase[3]}`, () =>
309 |       expect(inc(testCase[0], testCase[1], testCase[2])).toBe(testCase[3])),
310 |   );
311 | 
312 |   testCases.forEach((testCase) => {
313 |     const epochTestCase = testCase;
314 |     epochTestCase[0] = `1!${epochTestCase[0]}`;
315 |     epochTestCase[3] = `1!${epochTestCase[3]}`;
316 |     it(`handles incrementing ${epochTestCase[0]} using ${epochTestCase[1]} to ${epochTestCase[3]}`, () =>
317 |       expect(inc(epochTestCase[0], epochTestCase[1], epochTestCase[2])).toBe(
318 |         epochTestCase[3],
319 |       ));
320 |   });
321 | 
322 |   // Invalid inputs.
323 |   const invalidCases = [
324 |     ['not_valid', 'major', undefined, null],
325 |     ['1.0.0', 'invalid_release', undefined, null],
326 | 
327 |     [`0.0.0`, `premajor`, `foo`, null],
328 |     [`0.0.0`, `premajor`, 1, null],
329 |     [`1.0.0a0`, `premajor`, `bar`, null],
330 |   ];
331 | 
332 |   invalidCases.forEach((testCase) =>
333 |     it(`handles incrementing ${testCase[0]} using ${testCase[1]} to ${testCase[3]}`, () =>
334 |       expect(inc(testCase[0], testCase[1], testCase[2])).toBe(testCase[3])),
335 |   );
336 | });
337 | 


--------------------------------------------------------------------------------
/test/version.explain.test.js:
--------------------------------------------------------------------------------
  1 | const { explain } = require('../lib/version');
  2 | 
  3 | const { INVALID_VERSIONS } = require('./fixture');
  4 | 
  5 | describe('explain(version)', () => {
  6 |   INVALID_VERSIONS.forEach((version) => {
  7 |     it('returns null for ' + JSON.stringify(version), () => {
  8 |       expect(explain(version)).toBeNull();
  9 |     });
 10 |   });
 11 | });
 12 | 
 13 | describe('explain(version).public', () => {
 14 |   [
 15 |     ['1.0', '1.0'],
 16 |     ['1.0.dev0', '1.0.dev0'],
 17 |     ['1.0.dev6', '1.0.dev6'],
 18 |     ['1.0a1', '1.0a1'],
 19 |     ['1.0a1.post5', '1.0a1.post5'],
 20 |     ['1.0a1.post5.dev6', '1.0a1.post5.dev6'],
 21 |     ['1.0rc4', '1.0rc4'],
 22 |     ['1.0.post5', '1.0.post5'],
 23 |     ['1!1.0', '1!1.0'],
 24 |     ['1!1.0.dev6', '1!1.0.dev6'],
 25 |     ['1!1.0a1', '1!1.0a1'],
 26 |     ['1!1.0a1.post5', '1!1.0a1.post5'],
 27 |     ['1!1.0a1.post5.dev6', '1!1.0a1.post5.dev6'],
 28 |     ['1!1.0rc4', '1!1.0rc4'],
 29 |     ['1!1.0.post5', '1!1.0.post5'],
 30 |     ['1.0+deadbeef', '1.0'],
 31 |     ['1.0.dev6+deadbeef', '1.0.dev6'],
 32 |     ['1.0a1+deadbeef', '1.0a1'],
 33 |     ['1.0a1.post5+deadbeef', '1.0a1.post5'],
 34 |     ['1.0a1.post5.dev6+deadbeef', '1.0a1.post5.dev6'],
 35 |     ['1.0rc4+deadbeef', '1.0rc4'],
 36 |     ['1.0.post5+deadbeef', '1.0.post5'],
 37 |     ['1!1.0+deadbeef', '1!1.0'],
 38 |     ['1!1.0.dev6+deadbeef', '1!1.0.dev6'],
 39 |     ['1!1.0a1+deadbeef', '1!1.0a1'],
 40 |     ['1!1.0a1.post5+deadbeef', '1!1.0a1.post5'],
 41 |     ['1!1.0a1.post5.dev6+deadbeef', '1!1.0a1.post5.dev6'],
 42 |     ['1!1.0rc4+deadbeef', '1!1.0rc4'],
 43 |     ['1!1.0.post5+deadbeef', '1!1.0.post5'],
 44 |   ].forEach(([version, expected]) => {
 45 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
 46 |       version,
 47 |     )}`, () => {
 48 |       expect(explain(version).public).toBe(expected);
 49 |     });
 50 |   });
 51 | });
 52 | 
 53 | describe('explain(version).base_version', () => {
 54 |   [
 55 |     ['1.0', '1.0'],
 56 |     ['1.0.dev0', '1.0'],
 57 |     ['1.0.dev6', '1.0'],
 58 |     ['1.0a1', '1.0'],
 59 |     ['1.0a1.post5', '1.0'],
 60 |     ['1.0a1.post5.dev6', '1.0'],
 61 |     ['1.0rc4', '1.0'],
 62 |     ['1.0.post5', '1.0'],
 63 |     ['1!1.0', '1!1.0'],
 64 |     ['1!1.0.dev6', '1!1.0'],
 65 |     ['1!1.0a1', '1!1.0'],
 66 |     ['1!1.0a1.post5', '1!1.0'],
 67 |     ['1!1.0a1.post5.dev6', '1!1.0'],
 68 |     ['1!1.0rc4', '1!1.0'],
 69 |     ['1!1.0.post5', '1!1.0'],
 70 |     ['1.0+deadbeef', '1.0'],
 71 |     ['1.0.dev6+deadbeef', '1.0'],
 72 |     ['1.0a1+deadbeef', '1.0'],
 73 |     ['1.0a1.post5+deadbeef', '1.0'],
 74 |     ['1.0a1.post5.dev6+deadbeef', '1.0'],
 75 |     ['1.0rc4+deadbeef', '1.0'],
 76 |     ['1.0.post5+deadbeef', '1.0'],
 77 |     ['1!1.0+deadbeef', '1!1.0'],
 78 |     ['1!1.0.dev6+deadbeef', '1!1.0'],
 79 |     ['1!1.0a1+deadbeef', '1!1.0'],
 80 |     ['1!1.0a1.post5+deadbeef', '1!1.0'],
 81 |     ['1!1.0a1.post5.dev6+deadbeef', '1!1.0'],
 82 |     ['1!1.0rc4+deadbeef', '1!1.0'],
 83 |     ['1!1.0.post5+deadbeef', '1!1.0'],
 84 |   ].forEach(([version, expected]) => {
 85 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
 86 |       version,
 87 |     )}`, () => {
 88 |       expect(explain(version).base_version).toBe(expected);
 89 |     });
 90 |   });
 91 | });
 92 | 
 93 | describe('explain(version).epoch', () => {
 94 |   [
 95 |     ['1.0', 0],
 96 |     ['1.0.dev0', 0],
 97 |     ['1.0.dev6', 0],
 98 |     ['1.0a1', 0],
 99 |     ['1.0a1.post5', 0],
100 |     ['1.0a1.post5.dev6', 0],
101 |     ['1.0rc4', 0],
102 |     ['1.0.post5', 0],
103 |     ['1!1.0', 1],
104 |     ['1!1.0.dev6', 1],
105 |     ['1!1.0a1', 1],
106 |     ['1!1.0a1.post5', 1],
107 |     ['1!1.0a1.post5.dev6', 1],
108 |     ['1!1.0rc4', 1],
109 |     ['1!1.0.post5', 1],
110 |     ['1.0+deadbeef', 0],
111 |     ['1.0.dev6+deadbeef', 0],
112 |     ['1.0a1+deadbeef', 0],
113 |     ['1.0a1.post5+deadbeef', 0],
114 |     ['1.0a1.post5.dev6+deadbeef', 0],
115 |     ['1.0rc4+deadbeef', 0],
116 |     ['1.0.post5+deadbeef', 0],
117 |     ['1!1.0+deadbeef', 1],
118 |     ['1!1.0.dev6+deadbeef', 1],
119 |     ['1!1.0a1+deadbeef', 1],
120 |     ['1!1.0a1.post5+deadbeef', 1],
121 |     ['1!1.0a1.post5.dev6+deadbeef', 1],
122 |     ['1!1.0rc4+deadbeef', 1],
123 |     ['1!1.0.post5+deadbeef', 1],
124 |   ].forEach(([version, expected]) => {
125 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
126 |       version,
127 |     )}`, () => {
128 |       expect(explain(version).epoch).toBe(expected);
129 |     });
130 |   });
131 | });
132 | 
133 | describe('explain(version).release', () => {
134 |   [
135 |     ['1.0', [1, 0]],
136 |     ['1.0.dev0', [1, 0]],
137 |     ['1.0.dev6', [1, 0]],
138 |     ['1.0a1', [1, 0]],
139 |     ['1.0a1.post5', [1, 0]],
140 |     ['1.0a1.post5.dev6', [1, 0]],
141 |     ['1.0rc4', [1, 0]],
142 |     ['1.0.post5', [1, 0]],
143 |     ['1!1.0', [1, 0]],
144 |     ['1!1.0.dev6', [1, 0]],
145 |     ['1!1.0a1', [1, 0]],
146 |     ['1!1.0a1.post5', [1, 0]],
147 |     ['1!1.0a1.post5.dev6', [1, 0]],
148 |     ['1!1.0rc4', [1, 0]],
149 |     ['1!1.0.post5', [1, 0]],
150 |     ['1.0+deadbeef', [1, 0]],
151 |     ['1.0.dev6+deadbeef', [1, 0]],
152 |     ['1.0a1+deadbeef', [1, 0]],
153 |     ['1.0a1.post5+deadbeef', [1, 0]],
154 |     ['1.0a1.post5.dev6+deadbeef', [1, 0]],
155 |     ['1.0rc4+deadbeef', [1, 0]],
156 |     ['1.0.post5+deadbeef', [1, 0]],
157 |     ['1!1.0+deadbeef', [1, 0]],
158 |     ['1!1.0.dev6+deadbeef', [1, 0]],
159 |     ['1!1.0a1+deadbeef', [1, 0]],
160 |     ['1!1.0a1.post5+deadbeef', [1, 0]],
161 |     ['1!1.0a1.post5.dev6+deadbeef', [1, 0]],
162 |     ['1!1.0rc4+deadbeef', [1, 0]],
163 |     ['1!1.0.post5+deadbeef', [1, 0]],
164 |   ].forEach(([version, expected]) => {
165 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
166 |       version,
167 |     )}`, () => {
168 |       expect(explain(version).release).toEqual(expected);
169 |     });
170 |   });
171 | });
172 | 
173 | describe('explain(version).local', () => {
174 |   [
175 |     ['1.0', null],
176 |     ['1.0.dev0', null],
177 |     ['1.0.dev6', null],
178 |     ['1.0a1', null],
179 |     ['1.0a1.post5', null],
180 |     ['1.0a1.post5.dev6', null],
181 |     ['1.0rc4', null],
182 |     ['1.0.post5', null],
183 |     ['1!1.0', null],
184 |     ['1!1.0.dev6', null],
185 |     ['1!1.0a1', null],
186 |     ['1!1.0a1.post5', null],
187 |     ['1!1.0a1.post5.dev6', null],
188 |     ['1!1.0rc4', null],
189 |     ['1!1.0.post5', null],
190 |     ['1.0+deadbeef', 'deadbeef'],
191 |     ['1.0.dev6+deadbeef', 'deadbeef'],
192 |     ['1.0a1+deadbeef', 'deadbeef'],
193 |     ['1.0a1.post5+deadbeef', 'deadbeef'],
194 |     ['1.0a1.post5.dev6+deadbeef', 'deadbeef'],
195 |     ['1.0rc4+deadbeef', 'deadbeef'],
196 |     ['1.0.post5+deadbeef', 'deadbeef'],
197 |     ['1!1.0+deadbeef', 'deadbeef'],
198 |     ['1!1.0.dev6+deadbeef', 'deadbeef'],
199 |     ['1!1.0a1+deadbeef', 'deadbeef'],
200 |     ['1!1.0a1.post5+deadbeef', 'deadbeef'],
201 |     ['1!1.0a1.post5.dev6+deadbeef', 'deadbeef'],
202 |     ['1!1.0rc4+deadbeef', 'deadbeef'],
203 |     ['1!1.0.post5+deadbeef', 'deadbeef'],
204 |   ].forEach(([version, expected]) => {
205 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
206 |       version,
207 |     )}`, () => {
208 |       expect(explain(version).local).toBe(expected);
209 |     });
210 |   });
211 | });
212 | 
213 | describe('explain(version).pre', () => {
214 |   [
215 |     ['1.0', null],
216 |     ['1.0.dev0', null],
217 |     ['1.0.dev6', null],
218 |     ['1.0a1', ['a', 1]],
219 |     ['1.0a1.post5', ['a', 1]],
220 |     ['1.0a1.post5.dev6', ['a', 1]],
221 |     ['1.0rc4', ['rc', 4]],
222 |     ['1.0.post5', null],
223 |     ['1!1.0', null],
224 |     ['1!1.0.dev6', null],
225 |     ['1!1.0a1', ['a', 1]],
226 |     ['1!1.0a1.post5', ['a', 1]],
227 |     ['1!1.0a1.post5.dev6', ['a', 1]],
228 |     ['1!1.0rc4', ['rc', 4]],
229 |     ['1!1.0.post5', null],
230 |     ['1.0+deadbeef', null],
231 |     ['1.0.dev6+deadbeef', null],
232 |     ['1.0a1+deadbeef', ['a', 1]],
233 |     ['1.0a1.post5+deadbeef', ['a', 1]],
234 |     ['1.0a1.post5.dev6+deadbeef', ['a', 1]],
235 |     ['1.0rc4+deadbeef', ['rc', 4]],
236 |     ['1.0.post5+deadbeef', null],
237 |     ['1!1.0+deadbeef', null],
238 |     ['1!1.0.dev6+deadbeef', null],
239 |     ['1!1.0a1+deadbeef', ['a', 1]],
240 |     ['1!1.0a1.post5+deadbeef', ['a', 1]],
241 |     ['1!1.0a1.post5.dev6+deadbeef', ['a', 1]],
242 |     ['1!1.0rc4+deadbeef', ['rc', 4]],
243 |     ['1!1.0.post5+deadbeef', null],
244 |   ].forEach(([version, expected]) => {
245 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
246 |       version,
247 |     )}`, () => {
248 |       expect(explain(version).pre).toEqual(expected);
249 |     });
250 |   });
251 | });
252 | 
253 | describe('explain(version).is_prerelease', () => {
254 |   [
255 |     ['1.0.dev0', true],
256 |     ['1.0.dev1', true],
257 |     ['1.0a1.dev1', true],
258 |     ['1.0b1.dev1', true],
259 |     ['1.0c1.dev1', true],
260 |     ['1.0rc1.dev1', true],
261 |     ['1.0a1', true],
262 |     ['1.0b1', true],
263 |     ['1.0c1', true],
264 |     ['1.0rc1', true],
265 |     ['1.0a1.post1.dev1', true],
266 |     ['1.0b1.post1.dev1', true],
267 |     ['1.0c1.post1.dev1', true],
268 |     ['1.0rc1.post1.dev1', true],
269 |     ['1.0a1.post1', true],
270 |     ['1.0b1.post1', true],
271 |     ['1.0c1.post1', true],
272 |     ['1.0rc1.post1', true],
273 |     ['1.0', false],
274 |     ['1.0+dev', false],
275 |     ['1.0.post1', false],
276 |     ['1.0.post1+dev', false],
277 |   ].forEach(([version, expected]) => {
278 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
279 |       version,
280 |     )}`, () => {
281 |       expect(explain(version).is_prerelease).toBe(expected);
282 |     });
283 |   });
284 | });
285 | 
286 | describe('explain(version).dev', () => {
287 |   [
288 |     ['1.0', null],
289 |     ['1.0.dev0', 0],
290 |     ['1.0.dev6', 6],
291 |     ['1.0a1', null],
292 |     ['1.0a1.post5', null],
293 |     ['1.0a1.post5.dev6', 6],
294 |     ['1.0rc4', null],
295 |     ['1.0.post5', null],
296 |     ['1!1.0', null],
297 |     ['1!1.0.dev6', 6],
298 |     ['1!1.0a1', null],
299 |     ['1!1.0a1.post5', null],
300 |     ['1!1.0a1.post5.dev6', 6],
301 |     ['1!1.0rc4', null],
302 |     ['1!1.0.post5', null],
303 |     ['1.0+deadbeef', null],
304 |     ['1.0.dev6+deadbeef', 6],
305 |     ['1.0a1+deadbeef', null],
306 |     ['1.0a1.post5+deadbeef', null],
307 |     ['1.0a1.post5.dev6+deadbeef', 6],
308 |     ['1.0rc4+deadbeef', null],
309 |     ['1.0.post5+deadbeef', null],
310 |     ['1!1.0+deadbeef', null],
311 |     ['1!1.0.dev6+deadbeef', 6],
312 |     ['1!1.0a1+deadbeef', null],
313 |     ['1!1.0a1.post5+deadbeef', null],
314 |     ['1!1.0a1.post5.dev6+deadbeef', 6],
315 |     ['1!1.0rc4+deadbeef', null],
316 |     ['1!1.0.post5+deadbeef', null],
317 |   ].forEach(([version, expected]) => {
318 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
319 |       version,
320 |     )}`, () => {
321 |       expect(explain(version).dev).toBe(expected);
322 |     });
323 |   });
324 | });
325 | 
326 | describe('explain(version).is_devrelease', () => {
327 |   [
328 |     ['1.0', false],
329 |     ['1.0.dev0', true],
330 |     ['1.0.dev6', true],
331 |     ['1.0a1', false],
332 |     ['1.0a1.post5', false],
333 |     ['1.0a1.post5.dev6', true],
334 |     ['1.0rc4', false],
335 |     ['1.0.post5', false],
336 |     ['1!1.0', false],
337 |     ['1!1.0.dev6', true],
338 |     ['1!1.0a1', false],
339 |     ['1!1.0a1.post5', false],
340 |     ['1!1.0a1.post5.dev6', true],
341 |     ['1!1.0rc4', false],
342 |     ['1!1.0.post5', false],
343 |     ['1.0+deadbeef', false],
344 |     ['1.0.dev6+deadbeef', true],
345 |     ['1.0a1+deadbeef', false],
346 |     ['1.0a1.post5+deadbeef', false],
347 |     ['1.0a1.post5.dev6+deadbeef', true],
348 |     ['1.0rc4+deadbeef', false],
349 |     ['1.0.post5+deadbeef', false],
350 |     ['1!1.0+deadbeef', false],
351 |     ['1!1.0.dev6+deadbeef', true],
352 |     ['1!1.0a1+deadbeef', false],
353 |     ['1!1.0a1.post5+deadbeef', false],
354 |     ['1!1.0a1.post5.dev6+deadbeef', true],
355 |     ['1!1.0rc4+deadbeef', false],
356 |     ['1!1.0.post5+deadbeef', false],
357 |   ].forEach(([version, expected]) => {
358 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
359 |       version,
360 |     )}`, () => {
361 |       expect(explain(version).is_devrelease).toBe(expected);
362 |     });
363 |   });
364 | });
365 | 
366 | describe('explain(version).post', () => {
367 |   [
368 |     ['1.0', null],
369 |     ['1.0.dev0', null],
370 |     ['1.0.dev6', null],
371 |     ['1.0a1', null],
372 |     ['1.0a1.post5', 5],
373 |     ['1.0a1.post5.dev6', 5],
374 |     ['1.0rc4', null],
375 |     ['1.0.post5', 5],
376 |     ['1!1.0', null],
377 |     ['1!1.0.dev6', null],
378 |     ['1!1.0a1', null],
379 |     ['1!1.0a1.post5', 5],
380 |     ['1!1.0a1.post5.dev6', 5],
381 |     ['1!1.0rc4', null],
382 |     ['1!1.0.post5', 5],
383 |     ['1.0+deadbeef', null],
384 |     ['1.0.dev6+deadbeef', null],
385 |     ['1.0a1+deadbeef', null],
386 |     ['1.0a1.post5+deadbeef', 5],
387 |     ['1.0a1.post5.dev6+deadbeef', 5],
388 |     ['1.0rc4+deadbeef', null],
389 |     ['1.0.post5+deadbeef', 5],
390 |     ['1!1.0+deadbeef', null],
391 |     ['1!1.0.dev6+deadbeef', null],
392 |     ['1!1.0a1+deadbeef', null],
393 |     ['1!1.0a1.post5+deadbeef', 5],
394 |     ['1!1.0a1.post5.dev6+deadbeef', 5],
395 |     ['1!1.0rc4+deadbeef', null],
396 |     ['1!1.0.post5+deadbeef', 5],
397 |   ].forEach(([version, expected]) => {
398 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
399 |       version,
400 |     )}`, () => {
401 |       expect(explain(version).post).toBe(expected);
402 |     });
403 |   });
404 | });
405 | 
406 | describe('explain(version).is_postrelease', () => {
407 |   [
408 |     ['1.0.dev1', false],
409 |     ['1.0', false],
410 |     ['1.0+foo', false],
411 |     ['1.0.post1.dev1', true],
412 |     ['1.0.post1', true],
413 |   ].forEach(([version, expected]) => {
414 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
415 |       version,
416 |     )}`, () => {
417 |       expect(explain(version).is_postrelease).toBe(expected);
418 |     });
419 |   });
420 | });
421 | 


--------------------------------------------------------------------------------
/test/specifier.test.js:
--------------------------------------------------------------------------------
  1 | const { cross } = require('./fixture');
  2 | 
  3 | const {
  4 |   parse,
  5 |   validRange,
  6 |   satisfies,
  7 |   maxSatisfying,
  8 |   minSatisfying,
  9 |   filter,
 10 | } = require('../lib/specifier');
 11 | 
 12 | const SPECIFIERS = [
 13 |   '~=2.0',
 14 |   '==2.1.*',
 15 |   '==2.1.0.3',
 16 |   '!=2.2.*',
 17 |   '!=2.2.0.5',
 18 |   '<=5',
 19 |   '>=7.9a1',
 20 |   '<1.0.dev1',
 21 |   '>2.0.post1',
 22 |   '===lolwat',
 23 | ];
 24 | 
 25 | const INVALID_SPECIFIERS = [
 26 |   // Operator-less specifier
 27 |   '2.0',
 28 | 
 29 |   // Invalid operator
 30 |   '=>2.0',
 31 | 
 32 |   // Version-less specifier
 33 |   '==',
 34 | 
 35 |   // Local segment on operators which don't support them
 36 |   '~=1.0+5',
 37 |   '>=1.0+deadbeef',
 38 |   '<=1.0+abc123',
 39 |   '>1.0+watwat',
 40 |   '<1.0+1.0',
 41 | 
 42 |   // Prefix matching on operators which don't support them
 43 |   // "~=1.0.*",
 44 |   '>=1.0.*',
 45 |   '<=1.0.*',
 46 |   '>1.0.*',
 47 |   '<1.0.*',
 48 | 
 49 |   // Combination of local and prefix matching on operators which do
 50 |   // support one or the other
 51 |   '==1.0.*+5',
 52 |   '!=1.0.*+deadbeef',
 53 | 
 54 |   // Prefix matching cannot be used inside of a local version
 55 |   '==1.0+5.*',
 56 |   '!=1.0+deadbeef.*',
 57 | 
 58 |   // Prefix matching must appear at the end
 59 |   '==1.0.*.5',
 60 | 
 61 |   // Compatible operator requires 2 digits in the release operator
 62 |   '~=1',
 63 | 
 64 |   // Cannot use a prefix matching after a .devN version
 65 |   '==1.0.dev1.*',
 66 |   '!=1.0.dev1.*',
 67 | ];
 68 | 
 69 | describe('parse(range)', () => {
 70 |   SPECIFIERS.forEach((range) => {
 71 |     it('returns parsed for ' + JSON.stringify(range), () => {
 72 |       expect(parse(range)).not.toBeNull();
 73 |       expect(validRange(range)).toBe(true);
 74 |     });
 75 |   });
 76 |   INVALID_SPECIFIERS.forEach((range) => {
 77 |     it('returns null for ' + JSON.stringify(range), () => {
 78 |       expect(parse(range)).toBeNull();
 79 |       expect(validRange(range)).toBe(false);
 80 |     });
 81 |   });
 82 | 
 83 |   [
 84 |     // Various development release incarnations
 85 |     '1.0dev',
 86 |     '1.0.dev',
 87 |     '1.0dev1',
 88 |     '1.0dev',
 89 |     '1.0-dev',
 90 |     '1.0-dev1',
 91 |     '1.0DEV',
 92 |     '1.0.DEV',
 93 |     '1.0DEV1',
 94 |     '1.0DEV',
 95 |     '1.0.DEV1',
 96 |     '1.0-DEV',
 97 |     '1.0-DEV1',
 98 | 
 99 |     // Various alpha incarnations
100 |     '1.0a',
101 |     '1.0.a',
102 |     '1.0.a1',
103 |     '1.0-a',
104 |     '1.0-a1',
105 |     '1.0alpha',
106 |     '1.0.alpha',
107 |     '1.0.alpha1',
108 |     '1.0-alpha',
109 |     '1.0-alpha1',
110 |     '1.0A',
111 |     '1.0.A',
112 |     '1.0.A1',
113 |     '1.0-A',
114 |     '1.0-A1',
115 |     '1.0ALPHA',
116 |     '1.0.ALPHA',
117 |     '1.0.ALPHA1',
118 |     '1.0-ALPHA',
119 |     '1.0-ALPHA1',
120 | 
121 |     // Various beta incarnations
122 |     '1.0b',
123 |     '1.0.b',
124 |     '1.0.b1',
125 |     '1.0-b',
126 |     '1.0-b1',
127 |     '1.0beta',
128 |     '1.0.beta',
129 |     '1.0.beta1',
130 |     '1.0-beta',
131 |     '1.0-beta1',
132 |     '1.0B',
133 |     '1.0.B',
134 |     '1.0.B1',
135 |     '1.0-B',
136 |     '1.0-B1',
137 |     '1.0BETA',
138 |     '1.0.BETA',
139 |     '1.0.BETA1',
140 |     '1.0-BETA',
141 |     '1.0-BETA1',
142 | 
143 |     // Various release candidate incarnations
144 |     '1.0c',
145 |     '1.0.c',
146 |     '1.0.c1',
147 |     '1.0-c',
148 |     '1.0-c1',
149 |     '1.0rc',
150 |     '1.0.rc',
151 |     '1.0.rc1',
152 |     '1.0-rc',
153 |     '1.0-rc1',
154 |     '1.0C',
155 |     '1.0.C',
156 |     '1.0.C1',
157 |     '1.0-C',
158 |     '1.0-C1',
159 |     '1.0RC',
160 |     '1.0.RC',
161 |     '1.0.RC1',
162 |     '1.0-RC',
163 |     '1.0-RC1',
164 | 
165 |     // Various post release incarnations
166 |     '1.0post',
167 |     '1.0.post',
168 |     '1.0post1',
169 |     '1.0post',
170 |     '1.0-post',
171 |     '1.0-post1',
172 |     '1.0POST',
173 |     '1.0.POST',
174 |     '1.0POST1',
175 |     '1.0POST',
176 |     '1.0.POST1',
177 |     '1.0-POST',
178 |     '1.0-POST1',
179 |     '1.0-5',
180 | 
181 |     // Local version case insensitivity
182 |     '1.0+AbC',
183 | 
184 |     // Integer Normalization
185 |     '1.01',
186 |     '1.0a05',
187 |     '1.0b07',
188 |     '1.0c056',
189 |     '1.0rc09',
190 |     '1.0.post000',
191 |     '1.1.dev09000',
192 |     '00!1.2',
193 |     '0100!0.0',
194 | 
195 |     // Various other normalizations
196 |     'v1.0',
197 |     '  \r \f \v v1.0\t\n',
198 |   ].forEach((version) => {
199 |     it('normalizes ' + JSON.stringify(version), () => {
200 |       const ops = ['==', '!='];
201 |       if (!version.includes('+')) {
202 |         ops.push('~=', '<=', '>=', '<', '>');
203 |       }
204 |       ops.forEach((op) => {
205 |         expect(parse(op + version)).not.toBeNull();
206 |       });
207 |     });
208 |   });
209 | });
210 | 
211 | describe('parse(range).length', () => {
212 |   [
213 |     ['', 0],
214 |     ['==2.0', 1],
215 |     ['>=2.0', 1],
216 |     ['>=2.0,<3', 2],
217 |     ['>=2.0,<3,==2.4', 3],
218 |   ].forEach(([range, length]) => {
219 |     it('returns should be ' + length + ' for ' + JSON.stringify(range), () => {
220 |       expect(parse(range)).toHaveLength(length);
221 |     });
222 |   });
223 | });
224 | 
225 | describe('satisfies(version, specifier)', () => {
226 |   [
227 |     ...[
228 |       // Test the equality operation
229 |       ['2.0', '==2'],
230 |       ['2.0', '==2.0'],
231 |       ['2.0', '==2.0.0'],
232 |       ['2.0+deadbeef', '==2'],
233 |       ['2.0+deadbeef', '==2.0'],
234 |       ['2.0+deadbeef', '==2.0.0'],
235 |       ['2.0+deadbeef', '==2+deadbeef'],
236 |       ['2.0+deadbeef', '==2.0+deadbeef'],
237 |       ['2.0+deadbeef', '==2.0.0+deadbeef'],
238 |       ['2.0+deadbeef.0', '==2.0.0+deadbeef.00'],
239 | 
240 |       // Test the equality operation with a prefix
241 |       ['2.dev1', '==2.*'],
242 |       ['2a1', '==2.*'],
243 |       ['2a1.post1', '==2.*'],
244 |       ['2b1', '==2.*'],
245 |       ['2b1.dev1', '==2.*'],
246 |       ['2c1', '==2.*'],
247 |       ['2c1.post1.dev1', '==2.*'],
248 |       ['2rc1', '==2.*'],
249 |       ['2', '==2.*'],
250 |       ['2.0', '==2.*'],
251 |       ['2.0.0', '==2.*'],
252 |       ['2.0.post1', '==2.0.post1.*'],
253 |       ['2.0.post1.dev1', '==2.0.post1.*'],
254 |       ['2.1+local.version', '==2.1.*'],
255 | 
256 |       // Test the in-equality operation
257 |       ['2.1', '!=2'],
258 |       ['2.1', '!=2.0'],
259 |       ['2.0.1', '!=2'],
260 |       ['2.0.1', '!=2.0'],
261 |       ['2.0.1', '!=2.0.0'],
262 |       ['2.0', '!=2.0+deadbeef'],
263 | 
264 |       // Test the in-equality operation with a prefix
265 |       ['2.0', '!=3.*'],
266 |       ['2.1', '!=2.0.*'],
267 | 
268 |       // Test the greater than equal operation
269 |       ['2.0', '>=2'],
270 |       ['2.0', '>=2.0'],
271 |       ['2.0', '>=2.0.0'],
272 |       ['2.0.post1', '>=2'],
273 |       ['2.0.post1.dev1', '>=2'],
274 |       ['3', '>=2'],
275 | 
276 |       // Test the less than equal operation
277 |       ['2.0', '<=2'],
278 |       ['2.0', '<=2.0'],
279 |       ['2.0', '<=2.0.0'],
280 |       ['2.0.dev1', '<=2'],
281 |       ['2.0a1', '<=2'],
282 |       ['2.0a1.dev1', '<=2'],
283 |       ['2.0b1', '<=2'],
284 |       ['2.0b1.post1', '<=2'],
285 |       ['2.0c1', '<=2'],
286 |       ['2.0c1.post1.dev1', '<=2'],
287 |       ['2.0rc1', '<=2'],
288 |       ['1', '<=2'],
289 | 
290 |       // Test the greater than operation
291 |       ['3', '>2'],
292 |       ['2.1', '>2.0'],
293 |       ['2.0.1', '>2'],
294 |       ['2.1.post1', '>2'],
295 |       ['2.1+local.version', '>2'],
296 | 
297 |       // Test the less than operation
298 |       ['1', '<2'],
299 |       ['2.0', '<2.1'],
300 |       ['2.0.dev0', '<2.1'],
301 | 
302 |       // Test the compatibility operation
303 |       ['1', '~=1.0'],
304 |       ['1.0.1', '~=1.0'],
305 |       ['1.1', '~=1.0'],
306 |       ['1.9999999', '~=1.0'],
307 | 
308 |       // Test that epochs are handled sanely
309 |       ['2!1.0', '~=2!1.0'],
310 |       ['2!1.0', '==2!1.*'],
311 |       ['2!1.0', '==2!1.0'],
312 |       ['2!1.0', '!=1.0'],
313 |       ['1.0', '!=2!1.0'],
314 |       ['1.0', '<=2!0.1'],
315 |       ['2!1.0', '>=2.0'],
316 |       ['1.0', '<2!0.1'],
317 |       ['2!1.0', '>2.0'],
318 |       ['3', '!=3.1.*'],
319 |       ['3.10', '!=3.1.*'],
320 |       ['3.10.0', '!=3.1.*'],
321 |       ['3.10', '==3.10.*'],
322 |       ['3.10.0', '==3.10.*'],
323 |       ['0.1.dev123+deadbeef', '==0.1.dev123+deadbeef'],
324 | 
325 |       // Test some normalization rules
326 |       ['2.0.5', '>2.0dev'],
327 |     ].map(([version, spec]) => [version, spec, true]),
328 |     ...[
329 |       // Test the equality operation
330 |       ['2.1', '==2'],
331 |       ['2.1', '==2.0'],
332 |       ['2.1', '==2.0.0'],
333 |       ['2.0', '==2.0+deadbeef'],
334 | 
335 |       // Test the equality operation with a prefix
336 |       ['2.0', '==3.*'],
337 |       ['2.1', '==2.0.*'],
338 | 
339 |       // Test the in-equality operation
340 |       ['2.0', '!=2'],
341 |       ['2.0', '!=2.0'],
342 |       ['2.0', '!=2.0.0'],
343 |       ['2.0+deadbeef', '!=2'],
344 |       ['2.0+deadbeef', '!=2.0'],
345 |       ['2.0+deadbeef', '!=2.0.0'],
346 |       ['2.0+deadbeef', '!=2+deadbeef'],
347 |       ['2.0+deadbeef', '!=2.0+deadbeef'],
348 |       ['2.0+deadbeef', '!=2.0.0+deadbeef'],
349 |       ['2.0+deadbeef.0', '!=2.0.0+deadbeef.00'],
350 | 
351 |       // Test the in-equality operation with a prefix
352 |       ['2.dev1', '!=2.*'],
353 |       ['2a1', '!=2.*'],
354 |       ['2a1.post1', '!=2.*'],
355 |       ['2b1', '!=2.*'],
356 |       ['2b1.dev1', '!=2.*'],
357 |       ['2c1', '!=2.*'],
358 |       ['2c1.post1.dev1', '!=2.*'],
359 |       ['2rc1', '!=2.*'],
360 |       ['2', '!=2.*'],
361 |       ['2.0', '!=2.*'],
362 |       ['2.0.0', '!=2.*'],
363 |       ['2.0.post1', '!=2.0.post1.*'],
364 |       ['2.0.post1.dev1', '!=2.0.post1.*'],
365 | 
366 |       // Test the greater than equal operation
367 |       ['2.0.dev1', '>=2'],
368 |       ['2.0a1', '>=2'],
369 |       ['2.0a1.dev1', '>=2'],
370 |       ['2.0b1', '>=2'],
371 |       ['2.0b1.post1', '>=2'],
372 |       ['2.0c1', '>=2'],
373 |       ['2.0c1.post1.dev1', '>=2'],
374 |       ['2.0rc1', '>=2'],
375 |       ['1', '>=2'],
376 | 
377 |       // Test the less than equal operation
378 |       ['2.0.post1', '<=2'],
379 |       ['2.0.post1.dev1', '<=2'],
380 |       ['3', '<=2'],
381 | 
382 |       // Test the greater than operation
383 |       ['1', '>2'],
384 |       ['2.0.dev1', '>2'],
385 |       ['2.0a1', '>2'],
386 |       ['2.0a1.post1', '>2'],
387 |       ['2.0b1', '>2'],
388 |       ['2.0b1.dev1', '>2'],
389 |       ['2.0c1', '>2'],
390 |       ['2.0c1.post1.dev1', '>2'],
391 |       ['2.0rc1', '>2'],
392 |       ['2.0', '>2'],
393 |       ['2.0.post1', '>2'],
394 |       ['2.0.post1.dev1', '>2'],
395 |       ['2.0+local.version', '>2'],
396 | 
397 |       // Test the less than operation
398 |       ['2.0.dev1', '<2'],
399 |       ['2.0a1', '<2'],
400 |       ['2.0a1.post1', '<2'],
401 |       ['2.0b1', '<2'],
402 |       ['2.0b2.dev1', '<2'],
403 |       ['2.0c1', '<2'],
404 |       ['2.0c1.post1.dev1', '<2'],
405 |       ['2.0rc1', '<2'],
406 |       ['2.0', '<2'],
407 |       ['2.post1', '<2'],
408 |       ['2.post1.dev1', '<2'],
409 |       ['3', '<2'],
410 |       ['3', '==3.1.*'],
411 |       ['3.10', '==3.1.*'],
412 |       ['3.10.0', '==3.1.*'],
413 |       ['3.10.0', '!=3.10.*'],
414 | 
415 |       // Test the compatibility operation
416 |       ['2.0', '~=1.0'],
417 |       ['1.1.0', '~=1.0.0'],
418 |       ['1.1.post1', '~=1.0.0'],
419 |       ['0.1.dev123+deadbeef', '~=0.1.dev123+deadbeef'],
420 | 
421 |       // Test that epochs are handled sanely
422 |       ['1.0', '~=2!1.0'],
423 |       ['2!1.0', '~=1.0'],
424 |       ['2!1.0', '==1.0'],
425 |       ['1.0', '==2!1.0'],
426 |       ['2!1.0', '==1.*'],
427 |       ['1.0', '==2!1.*'],
428 |       ['2!1.0', '!=2!1.0'],
429 |     ].map(([version, spec]) => [version, spec, false]),
430 | 
431 |     ...[
432 |       // Test identity comparison by itself
433 |       ['lolwat', '===lolwat', true],
434 |       ['Lolwat', '===lolwat', true],
435 |       ['1.0', '===1.0', true],
436 |       ['nope', '===lolwat', false],
437 |       ['1.0.0', '===1.0', false],
438 |       ['1.0.dev0', '===1.0.dev0', true],
439 |     ],
440 |   ].forEach(([version, spec, expected]) => {
441 |     it(`returns ${expected} for ${JSON.stringify(
442 |       version,
443 |     )} satisfies ${JSON.stringify(spec)}`, () => {
444 |       expect(satisfies(version, spec, { prereleases: true })).toBe(expected);
445 |     });
446 |   });
447 | });
448 | 
449 | describe('satisfies([versions], specifier, {prereleases})', () => {
450 |   cross(
451 |     [
452 |       ['>=1.0', '2.0.dev1', false],
453 |       ['>=2.0.dev1', '2.0a1', true],
454 |       ['==2.0.*', '2.0a1.dev1', false],
455 |       ['==2.0a1.*', '2.0a1.dev1', true],
456 |       ['<=2.0', '1.0.dev1', false],
457 |       ['<=2.0.dev1', '1.0a1', true],
458 |     ],
459 |     ([spec, version, result]) => [
460 |       [spec, version, undefined, result],
461 |       [spec, version, !result, !result],
462 |     ],
463 |   ).forEach(([spec, version, prereleases, expected]) => {
464 |     it(`returns ${expected} for ${JSON.stringify(
465 |       version,
466 |     )} satisfies ${JSON.stringify(
467 |       spec,
468 |     )} when prereleases=${prereleases}`, () => {
469 |       expect(satisfies(version, spec, { prereleases })).toBe(expected);
470 |     });
471 |   });
472 | });
473 | 
474 | describe('filter([versions], specifier, {prereleases})', () => {
475 |   [
476 |     // General test of the filter method
477 |     ['', undefined, ['1.0', '2.0a1'], ['1.0']],
478 |     ['>=1.0.dev1', undefined, ['1.0', '2.0a1'], ['1.0', '2.0a1']],
479 |     ['', undefined, ['1.0a1'], ['1.0a1']],
480 |     ['', undefined, ['1.0', '2.0'], ['1.0', '2.0']],
481 |     ['', undefined, ['2.0dog', '1.0'], ['1.0']],
482 | 
483 |     // Test overriding with the prereleases parameter on filter
484 |     ['', false, ['1.0a1'], []],
485 |     ['>=1.0.dev1', false, ['1.0', '2.0a1'], ['1.0']],
486 |     ['', true, ['1.0', '2.0a1'], ['1.0', '2.0a1']],
487 | 
488 |     // Test overriding with the overall specifier
489 |     ['', true, ['1.0', '2.0a1'], ['1.0', '2.0a1']],
490 |     ['', false, ['1.0', '2.0a1'], ['1.0']],
491 |     ['>=1.0.dev1', true, ['1.0', '2.0a1'], ['1.0', '2.0a1']],
492 |     ['>=1.0.dev1', false, ['1.0', '2.0a1'], ['1.0']],
493 |     ['', true, ['1.0a1'], ['1.0a1']],
494 |     ['', false, ['1.0a1'], []],
495 | 
496 |     // Those are not of the original python implimentation
497 |     // but were required for full coverage
498 |     ['wrong range', false, ['1.0'], []],
499 |   ].forEach(([spec, prereleases, versions, expected]) => {
500 |     it(`returns ${JSON.stringify(expected)} for ${JSON.stringify(
501 |       versions,
502 |     )} filter ${JSON.stringify(spec)} when prereleases=${prereleases}`, () => {
503 |       const args = [versions, spec, { prereleases }];
504 |       const filtered = filter(...args);
505 |       expect(filtered).toEqual(expected);
506 |       expect(minSatisfying(...args)).toEqual(filtered[0] || null);
507 |       expect(maxSatisfying(...args)).toEqual(
508 |         filtered[filtered.length - 1] || null,
509 |       );
510 |     });
511 |   });
512 | });
513 | 
514 | it('filter works without options', () => {
515 |   expect(filter([], '')).toEqual([]);
516 | });
517 | 


--------------------------------------------------------------------------------