diff --git a/package.json b/package.json index 8ef8483fe..78ec8a06b 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "lint:javascript": "eslint *.js ./src/**/*.js ./tests/**/*.js ./utils/**/*.js", "lint:style": "stylelint ./css/*", "lint:typescript": "tslint --project . --exclude **/*.d.ts --format stylish src/**/*.ts utils/**/*.ts", + "preinstall": "node ./scripts/engines-check.js", "watch": "webpack --watch --mode production", "watch:dev": "webpack --watch --mode development" }, diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 000000000..21584d6dc --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,867 @@ +module.exports = { + "env": { + "node": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + }, + "rules": { + "accessor-pairs": [ + "error", + { + "setWithoutGet": true, + "getWithoutSet": false + } + ], + "array-bracket-newline": [ + "error" + ], + "array-bracket-spacing": [ + "error" + ], + "array-callback-return": [ + "error" + ], + "array-element-newline": [ + "error" + ], + "arrow-body-style": [ + "error" + ], + "arrow-parens": [ + "error" + ], + "arrow-spacing": [ + "error" + ], + "block-scoped-var": [ + "error" + ], + "block-spacing": [ + "error" + ], + "brace-style": [ + "error" + ], + "callback-return": [ + "error" + ], + "camelcase": [ + "error" + ], + "capitalized-comments": [ + "error" + ], + "class-methods-use-this": [ + "error" + ], + "comma-dangle": [ + "error" + ], + "comma-spacing": [ + "error" + ], + "comma-style": [ + "error", + "last" + ], + "complexity": [ + "error" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-return": [ + "error" + ], + "consistent-this": [ + "error" + ], + "constructor-super": [ + "error" + ], + "curly": [ + "error" + ], + "default-case": [ + "error" + ], + "dot-location": [ + "error", + "property" + ], + "dot-notation": [ + "error" + ], + "eol-last": [ + "error" + ], + "eqeqeq": [ + "error" + ], + "for-direction": [ + "error" + ], + "func-call-spacing": [ + "error" + ], + "func-name-matching": [ + "error" + ], + "func-names": [ + "error", + "never" + ], + "func-style": [ + "error" + ], + "function-paren-newline": [ + "error" + ], + "generator-star-spacing": [ + "error", + "before" + ], + "getter-return": [ + "error", + { + "allowImplicit": false + } + ], + "global-require": [ + "error" + ], + "guard-for-in": [ + "error" + ], + "handle-callback-err": [ + "error" + ], + "id-blacklist": [ + "error" + ], + "id-length": [ + "error" + ], + "id-match": [ + "error" + ], + "implicit-arrow-linebreak": [ + "error", + "beside" + ], + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "init-declarations": [ + "error" + ], + "jsx-quotes": [ + "error" + ], + "key-spacing": [ + "error" + ], + "keyword-spacing": [ + "error" + ], + "line-comment-position": [ + "error" + ], + "linebreak-style": [ + "error", + "windows" + ], + "lines-around-comment": [ + "error" + ], + "lines-between-class-members": [ + "error" + ], + "max-depth": [ + "error" + ], + "max-len": [ + "error", + 160 + ], + "max-lines": [ + "error", + { + "skipBlankLines": true, + "skipComments": true + } + ], + "max-nested-callbacks": [ + "error" + ], + "max-params": [ + "error" + ], + "max-statements": [ + "error" + ], + "max-statements-per-line": [ + "error" + ], + "multiline-comment-style": [ + "off", + "starred-block" + ], + "multiline-ternary": [ + "error", + "never" + ], + "new-cap": [ + "error" + ], + "new-parens": [ + "error" + ], + "newline-before-return": [ + "error" // TODO: configure this... + ], + "newline-per-chained-call": [ + "error" + ], + "no-alert": [ + "error" + ], + "no-array-constructor": [ + "error" + ], + "no-await-in-loop": [ + "error" + ], + "no-bitwise": [ + "error" + ], + "no-buffer-constructor": [ + "error" + ], + "no-caller": [ + "error" + ], + "no-case-declarations": [ + "error" + ], + "no-catch-shadow": [ + "error" + ], + "no-class-assign": [ + "error" + ], + "no-compare-neg-zero": [ + "error" + ], + "no-cond-assign": [ + "error", + "except-parens" + ], + "no-confusing-arrow": [ + "error" + ], + "no-console": [ + "error" + ], + "no-const-assign": [ + "error" + ], + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "no-continue": [ + "off" + ], + "no-control-regex": [ + "error" + ], + "no-debugger": [ + "error" + ], + "no-delete-var": [ + "error" + ], + "no-div-regex": [ + "error" + ], + "no-dupe-args": [ + "error" + ], + "no-dupe-class-members": [ + "error" + ], + "no-dupe-keys": [ + "error" + ], + "no-duplicate-case": [ + "error" + ], + "no-duplicate-imports": [ + "error", + { + "includeExports": true + } + ], + "no-else-return": [ + "error" + ], + "no-empty": [ + "error", + { + "allowEmptyCatch": false + } + ], + "no-empty-character-class": [ + "error" + ], + "no-empty-function": [ + "error" + ], + "no-empty-pattern": [ + "error" + ], + "no-eq-null": [ + "error" + ], + "no-ex-assign": [ + "error" + ], + "no-extra-boolean-cast": [ + "error" + ], + "no-extra-parens": [ + "error", + "all", + { + "conditionalAssign": false + } + ], + "no-extra-semi": [ + "error" + ], + "no-eval": [ + "error" + ], + "no-extend-native": [ + "error" + ], + "no-extra-bind": [ + "error" + ], + "no-extra-label": [ + "error" + ], + "no-extra-parens": [ + "error" + ], + "no-fallthrough": [ + "error" + ], + "no-floating-decimal": [ + "error" + ], + "no-func-assign": [ + "error" + ], + "no-global-assign": [ + "error" + ], + "no-implicit-coercion": [ + "error" + ], + "no-implicit-globals": [ + "error" + ], + "no-implied-eval": [ + "error" + ], + "no-inline-comments": [ + "error" + ], + "no-inner-declarations": [ + "error", + "both" + ], + "no-invalid-regexp": [ + "error" + ], + "no-invalid-this": [ + "error" + ], + "no-irregular-whitespace": [ + "error", + { + "skipStrings": false, + "skipComments": false, + "skipRegExps": false, + "skipTemplates": false + } + ], + "no-iterator": [ + "error" + ], + "no-label-var": [ + "error" + ], + "no-labels": [ + "error" + ], + "no-lone-blocks": [ + "error" + ], + "no-lonely-if": [ + "error" + ], + "no-loop-func": [ + "error" + ], + "no-magic-numbers": [ + "error", + { + "ignore": [ + -1, + 0, + 1 + ], + "ignoreArrayIndexes": true + } + ], + "no-mixed-operators": [ + "error" + ], + "no-mixed-requires": [ + "error" + ], + "no-mixed-spaces-and-tabs": [ + "error" + ], + "no-multi-assign": [ + "error" + ], + "no-multi-spaces": [ + "error" + ], + "no-multi-str": [ + "error" + ], + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "no-native-reassign": [ + "error" + ], + "no-negated-condition": [ + "error" + ], + "no-negated-in-lhs": [ + "error" + ], + "no-nested-ternary": [ + "error" + ], + "no-new": [ + "error" + ], + "no-new-func": [ + "error" + ], + "no-new-object": [ + "error" + ], + "no-new-require": [ + "error" + ], + "no-new-symbol": [ + "error" + ], + "no-new-wrappers": [ + "error" + ], + "no-octal": [ + "error" + ], + "no-octal-escape": [ + "error" + ], + "no-obj-calls": [ + "error" + ], + "no-param-reassign": [ + "error" + ], + "no-path-concat": [ + "error" + ], + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ], + "no-process-env": [ + "error" + ], + "no-process-exit": [ + "error" + ], + "no-proto": [ + "error" + ], + "no-prototype-builtins": [ + "error" + ], + "no-redeclare": [ + "error" + ], + "no-regex-spaces": [ + "error" + ], + "no-restricted-globals": [ + "error" + ], + "no-restricted-imports": [ + "error" + ], + "no-restricted-modules": [ + "error" + ], + "no-restricted-properties": [ + "error", + { + "object": "console", + "property": "log", + "message": "'log' is too general, use an appropriate level when logging." + } + ], + "no-restricted-syntax": [ + "error" + ], + "no-return-assign": [ + "error" + ], + "no-return-await": [ + "error" + ], + "no-script-url": [ + "error" + ], + "no-self-assign": [ + "error", + { + "props": false + } + ], + "no-self-compare": [ + "error" + ], + "no-sequences": [ + "error" + ], + "no-shadow": [ + "error" + ], + "no-shadow-restricted-names": [ + "error" + ], + "no-spaced-func": [ + "error" + ], + "no-sparse-arrays": [ + "error" + ], + "no-sync": [ + "error" + ], + "no-tabs": [ + "error" + ], + "no-template-curly-in-string": [ + "error" + ], + "no-ternary": [ + "off" + ], + "no-this-before-super": [ + "error" + ], + "no-throw-literal": [ + "error" + ], + "no-trailing-spaces": [ + "error" + ], + "no-undef": [ + "error" + ], + "no-undef-init": [ + "error" + ], + "no-undefined": [ + "error" + ], + "no-underscore-dangle": [ + "error" + ], + "no-unexpected-multiline": [ + "error" + ], + "no-unmodified-loop-condition": [ + "error" + ], + "no-unneeded-ternary": [ + "error" + ], + "no-unreachable": [ + "error" + ], + "no-unsafe-finally": [ + "error" + ], + "no-unsafe-negation": [ + "error" + ], + "no-unused-expressions": [ + "error" + ], + "no-unused-labels": [ + "error" + ], + "no-unused-vars": [ + "error" + ], + "no-use-before-define": [ + "error" + ], + "no-useless-call": [ + "error" + ], + "no-useless-computed-key": [ + "error" + ], + "no-useless-concat": [ + "error" + ], + "no-useless-constructor": [ + "error" + ], + "no-useless-escape": [ + "error" + ], + "no-useless-rename": [ + "error", + { + "ignoreDestructuring": false, + "ignoreExport": false, + "ignoreImport": false + } + ], + "no-useless-return": [ + "error" + ], + "no-var": [ + "error" + ], + "no-void": [ + "error" + ], + "no-warning-comments": [ + "error" + ], + "no-whitespace-before-property": [ + "error" + ], + "no-with": [ + "error" + ], + "nonblock-statement-body-position": [ + "error", + "below" + ], + "object-curly-newline": [ + "error" + ], + "object-curly-spacing": [ + "error" + ], + "object-property-newline": [ + "error" + ], + "object-shorthand": [ + "error" + ], + "one-var": [ + "off" + ], + "one-var-declaration-per-line": [ + "error" + ], + "operator-assignment": [ + "error" + ], + "operator-linebreak": [ + "error", + "none" + ], + "padded-blocks": [ + "off" + ], + "padding-line-between-statements": [ + "error" + ], + "prefer-arrow-callback": [ + "error" + ], + "prefer-const": [ + "error" + ], + "prefer-destructuring": [ + "off" + ], + "prefer-numeric-literals": [ + "error" + ], + "prefer-promise-reject-errors": [ + "off" + ], + "prefer-reflect": [ + "error" + ], + "prefer-rest-params": [ + "error" + ], + "prefer-spread": [ + "error" + ], + "prefer-template": [ + "error" + ], + "quote-props": [ + "error" + ], + "quotes": [ + "error" + ], + "radix": [ + "error", + "as-needed" + ], + "require-await": [ + "error" + ], + "require-jsdoc": [ + "off" + ], + "require-yield": [ + "error" + ], + "rest-spread-spacing": [ + "error", + "never" + ], + "semi": [ + "error" + ], + "semi-spacing": [ + "error" + ], + "semi-style": [ + "error", + "last" + ], + "sort-imports": [ + "error" + ], + "sort-keys": [ + "error" + ], + "sort-vars": [ + "error" + ], + "space-before-blocks": [ + "error" + ], + "space-before-function-paren": [ + "off" + ], + "space-in-parens": [ + "error" + ], + "space-infix-ops": [ + "error" + ], + "space-unary-ops": [ + "error" + ], + "spaced-comment": [ + "error" + ], + "strict": [ + "error" + ], + "switch-colon-spacing": [ + "error", + { + "after": true, + "before": false + } + ], + "symbol-description": [ + "error" + ], + "template-curly-spacing": [ + "error" + ], + "template-tag-spacing": [ + "error" + ], + "unicode-bom": [ + "error", + "never" + ], + "use-isnan": [ + "error" + ], + "valid-jsdoc": [ + "error" + ], + "valid-typeof": [ + "error" + ], + "vars-on-top": [ + "error" + ], + "wrap-iife": [ + "error", + "any" + ], + "wrap-regex": [ + "error" + ], + "yield-star-spacing": [ + "error", + "before" + ], + "yoda": [ + "error", + "never" + ] + } +}; diff --git a/scripts/engines-check.js b/scripts/engines-check.js new file mode 100644 index 000000000..4bc2e4066 --- /dev/null +++ b/scripts/engines-check.js @@ -0,0 +1,70 @@ +#!node + +/* +This ultimately is derived from https://github.com/daton89-topperblues/node-engine-strict +Since this needs to run *before* any dependencies are installed, it must be inlined here. +*/ +const path = require("path"); +const exec = require("child_process").exec; +const semver = require("./semver"); + +const getPackageJson = () => new Promise((resolve, reject) => { + try { + /* eslint-disable-next-line global-require */ + resolve(require(path.resolve(process.cwd(), "package.json"))); + } catch (error) { + reject(error); + } +}); + +const getEngines = (data) => new Promise((resolve, reject) => { + let versions = null; + + if (data.engines) { + versions = data.engines; + } + + if (versions) { + resolve(versions); + } else { + reject("Missing or improper 'engines' property in 'package.json'"); + } +}); + +const checkNpmVersion = (engines) => new Promise((resolve, reject) => { + exec("npm -v", (error, stdout, stderr) => { + if (error) { + reject(`Unable to find NPM version\n${stderr}`); + } + + const npmVersion = stdout.trim(); + const engineVersion = engines.npm || ">=0"; + + if (semver.satisfies(npmVersion, engineVersion)) { + resolve(); + } else { + reject(`Incorrect npm version\n'package.json' specifies "${engineVersion}", you are currently running "${npmVersion}".`); + } + }); +}); + +const checkNodeVersion = (engines) => new Promise((resolve, reject) => { + const nodeVersion = process.version.substring(1); + + if (semver.satisfies(nodeVersion, engines.node)) { + resolve(engines); + } else { + reject(`Incorrect node version\n'package.json' specifies "${engines.node}", you are currently running "${process.version}".`); + } +}); + +getPackageJson() + .then(getEngines) + .then(checkNodeVersion) + .then(checkNpmVersion) + .then(() => true, (error) => { + // Specifically disable these as the error message gets lost in the normal unhandled output. + /* eslint-disable no-console, no-process-exit */ + console.error(error); + process.exit(1); + }); diff --git a/scripts/semver.js b/scripts/semver.js new file mode 100644 index 000000000..755318c31 --- /dev/null +++ b/scripts/semver.js @@ -0,0 +1,821 @@ +/* eslint-disable max-lines */ +/* eslint-disable func-style */ +/* eslint-disable function-paren-newline */ +/* eslint-disable max-statements */ +/* eslint-disable no-param-reassign */ +/* eslint-disable complexity */ +/* eslint-disable max-params */ +/* eslint-disable max-depth */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable prefer-arrow-callback */ +/* eslint-disable arrow-body-style */ +/* eslint-disable init-declarations */ + +// This is a heavily stripped down/reformatted version of https://github.com/npm/node-semver/blob/v5.5.1/semver.js +// Originally licensed under ISC (https://github.com/npm/node-semver/blob/v5.5.1/LICENSE) +// Copyright (c) Isaac Z. Schlueter and Contributors + +const MAX_LENGTH = 256; +const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; + +// Max safe segment length for coercion. +const MAX_SAFE_COMPONENT_LENGTH = 16; + +// The actual regexps go on re +/** + * @type {RegExp[]} + */ +const re = []; + +/** + * @type {string[]} + */ +const src = []; +const NUMERICIDENTIFIER = 0; +const NUMERICIDENTIFIERLOOSE = 1; +const NONNUMERICIDENTIFIER = 2; +const MAINVERSION = 3; +const MAINVERSIONLOOSE = 4; +const PRERELEASEIDENTIFIER = 5; +const PRERELEASEIDENTIFIERLOOSE = 6; +const PRERELEASE = 7; +const PRERELEASELOOSE = 8; +const BUILDIDENTIFIER = 9; +const BUILD = 10; +const FULL = 11; +const LOOSE = 12; +const GTLT = 13; +const XRANGEIDENTIFIERLOOSE = 14; +const XRANGEIDENTIFIER = 15; +const XRANGEPLAIN = 16; +const XRANGEPLAINLOOSE = 17; +const XRANGE = 18; +const XRANGELOOSE = 19; +const COERCE = 20; +const LONETILDE = 21; +const TILDETRIM = 22; +const TILDE = 23; +const TILDELOOSE = 24; +const LONECARET = 25; +const CARET = 26; +const CARETLOOSE = 27; +const CARETTRIM = 28; +const COMPARATORLOOSE = 29; +const COMPARATOR = 30; +const COMPARATORTRIM = 31; +const HYPHENRANGE = 32; +const HYPHENRANGELOOSE = 33; +const STAR = 34; + +// The following Regular Expressions can be used for tokenizing, validating, and parsing SemVer version strings. + +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. +/* eslint-disable operator-linebreak */ +src[NUMERICIDENTIFIER] = "0|[1-9]\\d*"; +src[NUMERICIDENTIFIERLOOSE] = "[0-9]+"; + +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or more letters, digits, or hyphens. +src[NONNUMERICIDENTIFIER] = "\\d*[a-zA-Z-][a-zA-Z0-9-]*"; + +// ## Main Version +// Three dot-separated numeric identifiers. +src[MAINVERSION] = `(${src[NUMERICIDENTIFIER]})\\.(${src[NUMERICIDENTIFIER]})\\.(${src[NUMERICIDENTIFIER]})`; +src[MAINVERSIONLOOSE] = `(${src[NUMERICIDENTIFIERLOOSE]})\\.(${src[NUMERICIDENTIFIERLOOSE]})\\.(${src[NUMERICIDENTIFIERLOOSE]})`; + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. +src[PRERELEASEIDENTIFIER] = `(?:${src[NUMERICIDENTIFIER]}|${src[NONNUMERICIDENTIFIER]})`; +src[PRERELEASEIDENTIFIERLOOSE] = `(?:${src[NUMERICIDENTIFIERLOOSE]}|${src[NONNUMERICIDENTIFIER]})`; + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version identifiers. +src[PRERELEASE] = `(?:-(${src[PRERELEASEIDENTIFIER]}(?:\\.${src[PRERELEASEIDENTIFIER]})*))`; +src[PRERELEASELOOSE] = `(?:-?(${src[PRERELEASEIDENTIFIERLOOSE]}(?:\\.${src[PRERELEASEIDENTIFIERLOOSE]})*))`; + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. +src[BUILDIDENTIFIER] = "[0-9A-Za-z-]+"; + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata identifiers. +src[BUILD] = `(?:\\+(${src[BUILDIDENTIFIER]}(?:\\.${src[BUILDIDENTIFIER]})*))`; + +// ## Full Version String +// A main version, followed optionally by a pre-release version and build metadata. + +// Note that the only major, minor, patch, and pre-release sections of the version string are capturing groups. +// The build metadata is not a capturing group, because it should not ever be used in version comparison. +const FULLPLAIN = `v?${src[MAINVERSION]}${src[PRERELEASE]}?${src[BUILD]}?`; +src[FULL] = `^${FULLPLAIN}$`; + +// Like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// Also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty common in the npm registry. +const LOOSEPLAIN = `[v=\\s]*${src[MAINVERSIONLOOSE]}${src[PRERELEASELOOSE]}?${src[BUILD]}?`; +src[LOOSE] = `^${LOOSEPLAIN}$`; +src[GTLT] = "((?:<|>)?=?)"; + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +src[XRANGEIDENTIFIERLOOSE] = `${src[NUMERICIDENTIFIERLOOSE]}|x|X|\\*`; +src[XRANGEIDENTIFIER] = `${src[NUMERICIDENTIFIER]}|x|X|\\*`; + +/* eslint-disable-next-line max-len */ +src[XRANGEPLAIN] = `[v=\\s]*(${src[XRANGEIDENTIFIER]})(?:\\.(${src[XRANGEIDENTIFIER]})(?:\\.(${src[XRANGEIDENTIFIER]})(?:${src[PRERELEASE]})?${src[BUILD]}?)?)?`; + +/* eslint-disable-next-line max-len */ +src[XRANGEPLAINLOOSE] = `[v=\\s]*(${src[XRANGEIDENTIFIERLOOSE]})(?:\\.(${src[XRANGEIDENTIFIERLOOSE]})(?:\\.(${src[XRANGEIDENTIFIERLOOSE]})(?:${src[PRERELEASELOOSE]})?${src[BUILD]}?)?)?`; + +src[XRANGE] = `^${src[GTLT]}\\s*${src[XRANGEPLAIN]}$`; +src[XRANGELOOSE] = `^${src[GTLT]}\\s*${src[XRANGEPLAINLOOSE]}$`; + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +/* eslint-disable-next-line max-len */ +src[COERCE] = `(?:^|[^\\d])(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}})(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?(?:$|[^\\d])`; + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +src[LONETILDE] = "(?:~>?)"; + +src[TILDETRIM] = `(\\s*)${src[LONETILDE]}\\s+`; +re[TILDETRIM] = new RegExp(src[TILDETRIM], "g"); +const tildeTrimReplace = "$1~"; + +src[TILDE] = `^${src[LONETILDE]}${src[XRANGEPLAIN]}$`; +src[TILDELOOSE] = `^${src[LONETILDE]}${src[XRANGEPLAINLOOSE]}$`; + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +src[LONECARET] = "(?:\\^)"; +src[CARETTRIM] = `(\\s*)${src[LONECARET]}\\s+`; +re[CARETTRIM] = new RegExp(src[CARETTRIM], "g"); +const caretTrimReplace = "$1^"; +src[CARET] = `^${src[LONECARET]}${src[XRANGEPLAIN]}$`; +src[CARETLOOSE] = `^${src[LONECARET]}${src[XRANGEPLAINLOOSE]}$`; + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +src[COMPARATORLOOSE] = `^${src[GTLT]}\\s*(${LOOSEPLAIN})$|^$`; + +src[COMPARATOR] = `^${src[GTLT]}\\s*(${FULLPLAIN})$|^$`; + +// An expression to strip any whitespace between the gtlt and the thing it modifies, so that `> 1.2.3` ==> `>1.2.3` +src[COMPARATORTRIM] = `(\\s*)${src[GTLT]}\\s*(${LOOSEPLAIN}|${src[XRANGEPLAIN]})`; + +// This one has to use the /g flag +re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], "g"); +const comparatorTrimReplace = "$1$2$3"; + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be checked against either the strict or loose comparator form later. +src[HYPHENRANGE] = `^\\s*(${src[XRANGEPLAIN]})\\s+-\\s+(${src[XRANGEPLAIN]})\\s*$`; + +src[HYPHENRANGELOOSE] = `^\\s*(${src[XRANGEPLAINLOOSE]})\\s+-\\s+(${src[XRANGEPLAINLOOSE]})\\s*$`; + +// Star ranges basically just allow anything at all. +src[STAR] = "(<|>)?=?\\s*\\*"; +/* eslint-enable operator-linebreak */ + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (let idx = 0; idx <= STAR; idx++) { + if (!re[idx]) { + re[idx] = new RegExp(src[idx]); + } +} + +const ANY = {}; +const isX = (id) => !id || id.toLowerCase() === "x" || id === "*"; + +function compareIdentifiers(left, right) { + const numeric = /^[0-9]+$/; + const leftIsNumeric = numeric.test(left); + const rightIsNumeric = numeric.test(right); + if (leftIsNumeric && !rightIsNumeric) { + return -1; + } + + if (rightIsNumeric && !leftIsNumeric) { + return 1; + } + + if (leftIsNumeric && rightIsNumeric) { + left = Number(left); + right = Number(right); + } + + if (left < right) { + return -1; + } + + if (left > right) { + return 1; + } + + return 0; +} + +// This function is passed to string.replace(re[HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr) { + + if (isX(fM)) { + from = ""; + } else if (isX(fm)) { + from = `>=${fM}.0.0`; + } else if (isX(fp)) { + from = `>=${fM}.${fm}.0`; + } else { + from = `>=${from}`; + } + + if (isX(tM)) { + to = ""; + } else if (isX(tm)) { + to = `<${Number(tM) + 1}.0.0`; + } else if (isX(tp)) { + to = `<${tM}.${Number(tm) + 1}.0`; + } else if (tpr) { + to = `<=${tM}.${tm}.${tp}-${tpr}`; + } else { + to = `<=${to}`; + } + + return `${from} ${to}`.trim(); +} + +function replaceTilde(comp, loose) { + const regex = loose ? re[TILDELOOSE] : re[TILDE]; + + return comp.replace(regex, function(match, major, minor, patch, prerelease) { + let ret; + + if (isX(major)) { + ret = ""; + } else if (isX(minor)) { + ret = `>=${major}.0.0 <${Number(major) + 1}.0.0`; + } else if (isX(patch)) { + // ~1.2 == >=1.2.0 <1.3.0 + ret = `>=${major}.${minor}.0 <${major}.${Number(minor) + 1}.0`; + } else if (prerelease) { + if (prerelease.charAt(0) !== "-") { + prerelease = `-${prerelease}`; + } + ret = `>=${major}.${minor}.${patch}${prerelease} <${major}.${Number(minor) + 1}.0`; + } else { + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = `>=${major}.${minor}.${patch} <${major}.${Number(minor) + 1}.0`; + } + + return ret; + }); +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes(comp, loose) { + return comp + .trim() + .split(/\s+/) + .map((comp1) => replaceTilde(comp1, loose)) + .join(" "); +} + +function replaceCaret(comp, loose) { + const regex = loose ? re[CARETLOOSE] : re[CARET]; + + return comp.replace(regex, function(match, major, minor, patch, prerelease) { + let ret; + + if (isX(major)) { + ret = ""; + } else if (isX(minor)) { + ret = `>=${major}.0.0 <${Number(major) + 1}.0.0`; + } else if (isX(patch)) { + if (major === "0") { + ret = `>=${major}.${minor}.0 <${major}.${Number(minor) + 1}.0`; + } else { + ret = `>=${major}.${minor}.0 <${Number(major) + 1}.0.0`; + } + } else if (prerelease) { + if (prerelease.charAt(0) !== "-") { + prerelease = `-${prerelease}`; + } + if (major === "0") { + if (minor === "0") { + ret = `>=${major}.${minor}.${patch}${prerelease} <${major}.${minor}.${Number(patch) + 1}`; + } else { + ret = `>=${major}.${minor}.${patch}${prerelease} <${major}.${Number(minor) + 1}.0`; + } + } else { + ret = `>=${major}.${minor}.${patch}${prerelease} <${Number(major) + 1}.0.0`; + } + } else if (major === "0") { + if (minor === "0") { + ret = `>=${major}.${minor}.${patch} <${major}.${minor}.${Number(patch) + 1}`; + } else { + ret = `>=${major}.${minor}.${patch} <${major}.${Number(minor) + 1}.0`; + } + } else { + ret = `>=${major}.${minor}.${patch} <${Number(major) + 1}.0.0`; + } + + return ret; + }); +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets(comp, loose) { + return comp + .trim() + .split(/\s+/) + .map((comp1) => replaceCaret(comp1, loose)) + .join(" "); +} + +function replaceXRange(comp, loose) { + comp = comp.trim(); + const regex = loose ? re[XRANGELOOSE] : re[XRANGE]; + + return comp.replace(regex, function(ret, operator, major, minor, patch) { + const xM = isX(major); + const xm = xM || isX(minor); + const xp = xm || isX(patch); + const anyX = xp; + + if (operator === "=" && anyX) { + operator = ""; + } + + if (xM) { + if (operator === ">" || operator === "<") { + // Nothing is allowed + ret = "<0.0.0"; + } else { + // Nothing is forbidden + ret = "*"; + } + } else if (operator && anyX) { + // Replace X with 0 + if (xm) { + minor = 0; + } + if (xp) { + patch = 0; + } + + if (operator === ">") { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + operator = ">="; + if (xm) { + major = Number(major) + 1; + minor = 0; + patch = 0; + } else if (xp) { + minor = Number(minor) + 1; + patch = 0; + } + } else if (operator === "<=") { + // <=0.7.x is actually <0.8.0, since any 0.7.x should pass. Similarly, <=7.x is actually <8.0.0, etc. + operator = "<"; + if (xm) { + major = Number(major) + 1; + } else { + minor = Number(minor) + 1; + } + } + + ret = `${operator}${major}.${minor}.${patch}`; + } else if (xm) { + ret = `>=${major}.0.0 <${Number(major) + 1}.0.0`; + } else if (xp) { + ret = `>=${major}.${minor}.0 <${major}.${Number(minor) + 1}.0`; + } + + return ret; + }); +} + +function replaceXRanges(comp, loose) { + return comp + .split(/\s+/) + .map((comp1) => replaceXRange(comp1, loose)) + .join(" "); +} + +// Because * is AND-ed with everything else in the comparator, and '' means "any version", just remove the *s entirely. +function replaceStars(comp) { + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[STAR], ""); +} + +// Comprised of xranges, tildes, stars, and gtlt's at this point. +// Already replaced the hyphen ranges turn into a set of JUST comparators. +function parseComparator(comp, loose) { + comp = replaceCarets(comp, loose); + comp = replaceTildes(comp, loose); + comp = replaceXRanges(comp, loose); + comp = replaceStars(comp, loose); + + return comp; +} + +class SemVer { + + /** + * A semantic version. + * @param {string} version The version. + * @param {boolean} loose If this is a loose representation of a version. + * @returns {SemVer} a new instance. + */ + constructor(version, loose) { + if (version instanceof SemVer) { + if (version.loose === loose) { + return version; + } + version = version.version; + } else if (typeof version !== "string") { + throw new TypeError(`Invalid Version: ${version}`); + } + if (version.length > MAX_LENGTH) { + throw new TypeError(`version is longer than ${MAX_LENGTH} characters`); + } + if (!(this instanceof SemVer)) { + return new SemVer(version, loose); + } + this.loose = loose; + const matches = version.trim().match(loose ? re[LOOSE] : re[FULL]); + if (!matches) { + throw new TypeError(`Invalid Version: ${version}`); + } + this.raw = version; + // These are actually numbers + this.major = Number(matches[1]); + this.minor = Number(matches[2]); + this.patch = Number(matches[3]); + if (this.major > MAX_SAFE_INTEGER || this.major < 0) { + throw new TypeError("Invalid major version"); + } + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) { + throw new TypeError("Invalid minor version"); + } + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) { + throw new TypeError("Invalid patch version"); + } + // Numberify any prerelease numeric ids + if (matches[4]) { + this.prerelease = matches[4].split(".").map((id) => { + if (/^[0-9]+$/.test(id)) { + const num = Number(id); + if (num >= 0 && num < MAX_SAFE_INTEGER) { + return num; + } + } + + return id; + }); + } else { + this.prerelease = []; + } + this.build = matches[5] ? matches[5].split(".") : []; + this.format(); + } + + format() { + this.version = `${this.major}.${this.minor}.${this.patch}`; + if (this.prerelease.length) { + this.version += `-${this.prerelease.join(".")}`; + } + + return this.version; + } + + toString() { + return this.version; + } + + /** + * Comares the current instance against another instance. + * @param {SemVer} other The SemVer to comare to. + * @returns {0|1|-1} A comparable value for sorting. + */ + compare(other) { + return this.compareMain(other) || this.comparePre(other); + } + + compareMain(other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.loose); + } + + return compareIdentifiers(this.major, other.major) || compareIdentifiers(this.minor, other.minor) || compareIdentifiers(this.patch, other.patch); + } + + comparePre(other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.loose); + } + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) { + return -1; + } else if (!this.prerelease.length && other.prerelease.length) { + return 1; + } else if (!this.prerelease.length && !other.prerelease.length) { + return 0; + } + let idx = 0; + do { + const thisPrelease = this.prerelease[idx]; + const otherPrelease = other.prerelease[idx]; + const thisPreleaseIsUndefined = typeof thisPrelease === "undefined"; + const otherPreleaseIsUndefined = typeof otherPrelease === "undefined"; + if (thisPreleaseIsUndefined && otherPreleaseIsUndefined) { + return 0; + } else if (otherPreleaseIsUndefined) { + return 1; + } else if (thisPreleaseIsUndefined) { + return -1; + } else if (thisPrelease === otherPrelease) { + continue; + } else { + return compareIdentifiers(thisPrelease, otherPrelease); + } + } while ((idx += 1) > 0); + + // Should not hit this point, but assume equal ranking. + return 0; + } +} + +const compare = (leftVersion, rightVersion, loose) => new SemVer(leftVersion, loose).compare(new SemVer(rightVersion, loose)); +const gt = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) > 0; +const lt = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) < 0; +const eq = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) === 0; +const neq = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) !== 0; +const gte = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) >= 0; +const lte = (leftVersion, rightVersion, loose) => compare(leftVersion, rightVersion, loose) <= 0; + +function cmp(left, op, right, loose) { + let ret; + switch (op) { + case "===": + if (typeof left === "object") { + left = left.version; + } + if (typeof right === "object") { + right = right.version; + } + ret = left === right; + break; + case "!==": + if (typeof left === "object") { + left = left.version; + } + if (typeof right === "object") { + right = right.version; + } + ret = left !== right; + break; + case "": + case "=": + case "==": + ret = eq(left, right, loose); + break; + case "!=": + ret = neq(left, right, loose); + break; + case ">": + ret = gt(left, right, loose); + break; + case ">=": + ret = gte(left, right, loose); + break; + case "<": + ret = lt(left, right, loose); + break; + case "<=": + ret = lte(left, right, loose); + break; + default: + throw new TypeError(`Invalid operator: ${op}`); + } + + return ret; +} + +function testSet(set, version) { + for (let idx = 0; idx < set.length; idx++) { + if (!set[idx].test(version)) { + return false; + } + } + + if (version.prerelease.length) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, even though it's within the range set by the comparators. + for (let idx = 0; idx < set.length; idx++) { + if (set[idx].semver !== ANY) { + if (set[idx].semver.prerelease.length > 0) { + const allowed = set[idx].semver; + if (allowed.major === version.major && allowed.minor === version.minor && allowed.patch === version.patch) { + return true; + } + } + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false; + } + + return true; +} +class Comparator { + constructor(comp, loose) { + if (comp instanceof Comparator) { + if (comp.loose === loose) { + return comp; + } + comp = comp.value; + } + if (!(this instanceof Comparator)) { + return new Comparator(comp, loose); + } + this.loose = loose; + this.parse(comp); + if (this.semver === ANY) { + this.value = ""; + } else { + this.value = this.operator + this.semver.version; + } + } + + parse(comp) { + const regex = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + const matches = comp.match(regex); + if (!matches) { + throw new TypeError(`Invalid comparator: ${comp}`); + } + this.operator = matches[1]; + if (this.operator === "=") { + this.operator = ""; + } + // If it literally is just '>' or '' then allow anything. + if (matches[2]) { + this.semver = new SemVer(matches[2], this.loose); + } else { + this.semver = ANY; + } + } + + toString() { + return this.value; + } + + test(version) { + if (this.semver === ANY) { + return true; + } + if (typeof version === "string") { + version = new SemVer(version, this.loose); + } + + return cmp(version, this.operator, this.semver, this.loose); + } +} + +/** + * A range object. + * @param {Range|Comparator|string} range the value to parse to a Range. + * @param {any} loose whether the range is explicit or a loose value. + * @returns {Range} the Range instace. + */ +class Range { + constructor(range, loose) { + if (range instanceof Range) { + if (range.loose === loose) { + return range; + } + + return new Range(range.raw, loose); + } + if (range instanceof Comparator) { + return new Range(range.value, loose); + } + if (!(this instanceof Range)) { + return new Range(range, loose); + } + this.loose = loose; + // First, split based on boolean or || + /** + * @type {string} + */ + this.raw = range; + // Throw out any that are not relevant for whatever reason + const hasLength = (item) => item.length; + this.set = this.raw.split(/\s*\|\|\s*/).map(function (range1) { + return this.parseRange(range1.trim()); + }, this) + .filter(hasLength); + if (!this.set.length) { + throw new TypeError(`Invalid SemVer Range: ${range}`); + } + this.format(); + } + + format() { + this.range = this.set.map((comps) => comps.join(" ").trim()) + .join("||") + .trim(); + + return this.range; + } + + toString() { + return this.range; + } + + parseRange(range) { + const loose = this.loose; + range = range.trim(); + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + const hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]; + range = range.replace(hr, hyphenReplace); + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace); + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[TILDETRIM], tildeTrimReplace); + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[CARETTRIM], caretTrimReplace); + // Normalize spaces + range = range.split(/\s+/).join(" "); + // At this point, the range is completely trimmed and ready to be split into comparators. + const compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + let set = range.split(" ").map((comp) => parseComparator(comp, loose)) + .join(" ") + .split(/\s+/); + if (loose) { + // In loose mode, throw out any that are not valid comparators + set = set.filter((comp) => Boolean(comp.match(compRe))); + } + set = set.map((comp) => new Comparator(comp, loose)); + + return set; + } + + // If ANY of the sets match ALL of its comparators, then pass + test(version) { + if (!version) { + return false; + } + if (typeof version === "string") { + version = new SemVer(version, this.loose); + } + for (let idx = 0; idx < this.set.length; idx++) { + if (testSet(this.set[idx], version)) { + return true; + } + } + + return false; + } +} + +/** + * Checks if the provided version can satisfy the provided range. + * @param {string} version The specific version. + * @param {string} range The range expression. + * @param {any} loose If the range is a loose expression. + * @returns {boolean} Whether the versions successfully satisfies the range. + */ +function satisfies(version, range, loose) { + try { + const rangeObj = new Range(range, loose); + + return rangeObj.test(version); + } catch (er) { + return false; + } +} + +module.exports.satisfies = satisfies;