diff --git a/.env.example b/.env.example index 5f6532d36c15..50914541a617 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -EXPENSIFY_URL_COM=https://www.expensify.com.dev/ EXPENSIFY_URL_CASH=https://expensify.cash/ -EXPENSIFY_PARTNER_NAME=android -EXPENSIFY_PARTNER_PASSWORD=c3a9ac418ea3f152aae2 +EXPENSIFY_URL_COM=https://www.expensify.com.dev/ +EXPENSIFY_PARTNER_NAME=chat-expensify-com +EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 PUSHER_APP_KEY=ac6d22b891daae55283a NGROK_URL=https://expensify-user.ngrok.io/ USE_NGROK=false diff --git a/.github/actions/createOrUpdateStagingDeploy/index.js b/.github/actions/createOrUpdateStagingDeploy/index.js index 4fc7cbf8fb28..80b4fea16754 100644 --- a/.github/actions/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/createOrUpdateStagingDeploy/index.js @@ -480,6 +480,19 @@ class GithubUtils { }); } + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + /** * Generate the URL of an Expensify.cash pull request given the PR number. * diff --git a/.github/actions/getReleaseBody/action.yml b/.github/actions/getReleaseBody/action.yml new file mode 100644 index 000000000000..f682257b33a1 --- /dev/null +++ b/.github/actions/getReleaseBody/action.yml @@ -0,0 +1,12 @@ +name: 'Get Release Body' +description: 'Generate the body of a production release' +inputs: + PR_LIST: + description: JSON array of pull request numbers (string) + required: true +outputs: + RELEASE_BODY: + description: String body of a production release. +runs: + using: 'node12' + main: './index.js' diff --git a/.github/actions/getReleaseBody/getReleaseBody.js b/.github/actions/getReleaseBody/getReleaseBody.js new file mode 100644 index 000000000000..80dc354e9fbf --- /dev/null +++ b/.github/actions/getReleaseBody/getReleaseBody.js @@ -0,0 +1,12 @@ +const _ = require('underscore'); +const core = require('@actions/core'); +const GithubUtils = require('../../libs/GithubUtils'); + +// Parse the stringified JSON array of PR numbers, and cast each from String -> Number +const PRList = _.map(JSON.parse(core.getInput('PR_LIST', {required: true})), Number); +console.log(`Got PR list: ${PRList}`); + +const releaseBody = GithubUtils.getReleaseBody(PRList); +console.log(`Generated release body: ${releaseBody}`); + +core.setOutput('RELEASE_BODY', releaseBody); diff --git a/.github/actions/getReleaseBody/index.js b/.github/actions/getReleaseBody/index.js new file mode 100644 index 000000000000..5f5f27dd4186 --- /dev/null +++ b/.github/actions/getReleaseBody/index.js @@ -0,0 +1,5077 @@ +/** + * NOTE: This is a compiled file. DO NOT directly edit this file. + */ +module.exports = +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 547: +/***/ ((__unused_webpack_module, __unused_webpack_exports, __nccwpck_require__) => { + +const _ = __nccwpck_require__(987); +const core = __nccwpck_require__(186); +const GithubUtils = __nccwpck_require__(999); + +// Parse the stringified JSON array of PR numbers, and cast each from String -> Number +const PRList = _.map(JSON.parse(core.getInput('PR_LIST', {required: true})), Number); +console.log(`Got PR list: ${PRList}`); + +const releaseBody = GithubUtils.getReleaseBody(PRList); +console.log(`Generated release body: ${releaseBody}`); + +core.setOutput('RELEASE_BODY', releaseBody); + + +/***/ }), + +/***/ 999: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const _ = __nccwpck_require__(987); +const semverParse = __nccwpck_require__(925); +const semverSatisfies = __nccwpck_require__(55); + +const GITHUB_OWNER = 'Expensify'; +const EXPENSIFY_CASH_REPO = 'Expensify.cash'; +const EXPENSIFY_CASH_URL = 'https://github.com/Expensify/Expensify.cash'; + +const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); +const PULL_REQUEST_REGEX = new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/pull/([0-9]+).*`); +const ISSUE_REGEX = new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/issues/([0-9]+).*`); +const ISSUE_OR_PULL_REQUEST_REGEX = new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/(?:pull|issues)/([0-9]+).*`); + +const APPLAUSE_BOT = 'applausebot'; +const STAGING_DEPLOY_CASH_LABEL = 'StagingDeployCash'; + +class GithubUtils { + /** + * @param {Octokit} octokit - Authenticated Octokit object https://octokit.github.io/rest.js + */ + constructor(octokit) { + this.octokit = octokit; + } + + /** + * Finds one open `StagingDeployCash` issue via GitHub octokit library. + * + * @returns {Promise} + */ + getStagingDeployCash() { + return this.octokit.issues.listForRepo({ + owner: GITHUB_OWNER, + repo: EXPENSIFY_CASH_REPO, + labels: STAGING_DEPLOY_CASH_LABEL, + state: 'open', + }) + .then(({data}) => { + if (!data.length) { + const error = new Error(`Unable to find ${STAGING_DEPLOY_CASH_LABEL} issue.`); + error.code = 404; + throw error; + } + + if (data.length > 1) { + const error = new Error(`Found more than one ${STAGING_DEPLOY_CASH_LABEL} issue.`); + error.code = 500; + throw error; + } + + return this.getStagingDeployCashData(data[0]); + }); + } + + /** + * Takes in a GitHub issue object and returns the data we want. + * + * @param {Object} issue + * @returns {Object} + */ + getStagingDeployCashData(issue) { + try { + const versionRegex = new RegExp('([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9]+))?', 'g'); + const tag = issue.body.match(versionRegex)[0].replace(/`/g, ''); + + // eslint-disable-next-line max-len + const compareURLRegex = new RegExp(`${EXPENSIFY_CASH_URL}/compare/${versionRegex.source}\\.\\.\\.${versionRegex.source}`, 'g'); + const comparisonURL = issue.body.match(compareURLRegex)[0]; + + return { + title: issue.title, + url: issue.url, + labels: issue.labels, + PRList: this.getStagingDeployCashPRList(issue), + deployBlockers: this.getStagingDeployCashDeployBlockers(issue), + tag, + comparisonURL, + }; + } catch (exception) { + throw new Error(`Unable to find ${STAGING_DEPLOY_CASH_LABEL} issue with correct data.`); + } + } + + /** + * Parse the PRList section of the StagingDeployCash issue body. + * + * @private + * + * @param {Object} issue + * @returns {Array} - [{url: String, number: Number, isVerified: Boolean}] + */ + getStagingDeployCashPRList(issue) { + const PRListSection = issue.body.match(/pull requests:\*\*\r\n((?:.*\r\n)+)\r\n/)[1]; + const unverifiedPRs = _.map( + [...PRListSection.matchAll(new RegExp(`- \\[ ] (${PULL_REQUEST_REGEX.source})`, 'g'))], + match => ({ + url: match[1], + number: GithubUtils.getPullRequestNumberFromURL(match[1]), + isVerified: false, + }), + ); + const verifiedPRs = _.map( + [...PRListSection.matchAll(new RegExp(`- \\[x] (${PULL_REQUEST_REGEX.source})`, 'g'))], + match => ({ + url: match[1], + number: GithubUtils.getPullRequestNumberFromURL(match[1]), + isVerified: true, + }), + ); + return _.sortBy( + _.union(unverifiedPRs, verifiedPRs), + 'number', + ); + } + + /** + * Parse DeployBlocker section of the StagingDeployCash issue body. + * + * @private + * + * @param {Object} issue + * @returns {Array} - [{URL: String, number: Number, isResolved: Boolean}] + */ + getStagingDeployCashDeployBlockers(issue) { + let deployBlockerSection = issue.body.match(/Deploy Blockers:\*\*\r\n((?:.*\r\n)+)/) || []; + if (deployBlockerSection.length !== 2) { + return []; + } + deployBlockerSection = deployBlockerSection[1]; + const unresolvedDeployBlockers = _.map( + [...deployBlockerSection.matchAll(new RegExp(`- \\[ ] (${ISSUE_OR_PULL_REQUEST_REGEX.source})`, 'g'))], + match => ({ + url: match[1], + number: GithubUtils.getIssueOrPullRequestNumberFromURL(match[1]), + isResolved: false, + }), + ); + const resolvedDeployBlockers = _.map( + [...deployBlockerSection.matchAll(new RegExp(`- \\[x] (${ISSUE_OR_PULL_REQUEST_REGEX.source})`, 'g'))], + match => ({ + url: match[1], + number: GithubUtils.getIssueOrPullRequestNumberFromURL(match[1]), + isResolved: true, + }), + ); + return _.sortBy( + _.union(unresolvedDeployBlockers, resolvedDeployBlockers), + 'number', + ); + } + + /** + * Generate a comparison URL between two versions following the semverLevel passed + * + * @param {String} repoSlug - The slug of the repository: / + * @param {String} tag - The tag to compare first the previous semverLevel + * @param {String} semverLevel - The semantic versioning MAJOR, MINOR, PATCH and BUILD + * @return {Promise} the url generated + * @throws {Error} If the request to the Github API fails. + */ + generateVersionComparisonURL(repoSlug, tag, semverLevel) { + return new Promise((resolve, reject) => { + const getComparisonURL = (previousTag, currentTag) => ( + `${EXPENSIFY_CASH_URL}/compare/${previousTag}...${currentTag}` + ); + + const [repoOwner, repoName] = repoSlug.split('/'); + const tagSemver = semverParse(tag); + + return this.octokit.repos.listTags({ + owner: repoOwner, + repo: repoName, + }) + .then(githubResponse => githubResponse.data.some(({name: repoTag}) => { + if (semverLevel === 'MAJOR' + && semverSatisfies(repoTag, `<${tagSemver.major}.x.x`, {includePrerelease: true}) + ) { + resolve(getComparisonURL(repoTag, tagSemver)); + return true; + } + + if (semverLevel === 'MINOR' + && semverSatisfies( + repoTag, + `<${tagSemver.major}.${tagSemver.minor}.x`, + {includePrerelease: true}, + ) + ) { + resolve(getComparisonURL(repoTag, tagSemver)); + return true; + } + + if (semverLevel === 'PATCH' + && semverSatisfies(repoTag, `<${tagSemver}`, {includePrerelease: true}) + ) { + resolve(getComparisonURL(repoTag, tagSemver)); + return true; + } + + if (semverLevel === 'BUILD' + && repoTag !== tagSemver.version + && semverSatisfies( + repoTag, + `<=${tagSemver.major}.${tagSemver.minor}.${tagSemver.patch}`, + {includePrerelease: true}, + ) + ) { + resolve(getComparisonURL(repoTag, tagSemver)); + return true; + } + return false; + })) + .catch(githubError => reject(githubError)); + }); + } + + /** + * Creates a new StagingDeployCash issue. + * + * @param {String} title + * @param {String} tag + * @param {Array} PRList + * @returns {Promise} + */ + createNewStagingDeployCash(title, tag, PRList) { + return this.generateStagingDeployCashBody(tag, PRList) + .then(body => this.octokit.issues.create({ + owner: GITHUB_OWNER, + repo: EXPENSIFY_CASH_REPO, + labels: [STAGING_DEPLOY_CASH_LABEL], + assignees: [APPLAUSE_BOT], + title, + body, + })); + } + + /** + * Updates the existing open StagingDeployCash issue. + * + * @param {String} [newTag] + * @param {Array} newPRs + * @param {Array} newDeployBlockers + * @returns {Promise} + * @throws {Error} If the StagingDeployCash could not be found or updated. + */ + updateStagingDeployCash(newTag = '', newPRs, newDeployBlockers) { + let issueNumber; + return this.getStagingDeployCash() + .then(({ + url, + tag: oldTag, + PRList: oldPRs, + deployBlockers: oldDeployBlockers, + }) => { + issueNumber = GithubUtils.getIssueNumberFromURL(url); + + // If we aren't sent a tag, then use the existing tag + const tag = _.isEmpty(newTag) ? oldTag : newTag; + + const PRList = _.sortBy( + _.union(oldPRs, _.map(newPRs, URL => ({ + url: URL, + number: GithubUtils.getPullRequestNumberFromURL(URL), + isVerified: false, + }))), + 'number', + ); + const deployBlockers = _.sortBy( + _.union(oldDeployBlockers, _.map(newDeployBlockers, URL => ({ + url: URL, + number: GithubUtils.getIssueOrPullRequestNumberFromURL(URL), + isResolved: false, + }))), + 'number', + ); + + return this.generateStagingDeployCashBody( + tag, + _.pluck(PRList, 'url'), + _.pluck(_.where(PRList, {isVerified: true}), 'url'), + _.pluck(deployBlockers, 'url'), + _.pluck(_.where(deployBlockers, {isResolved: true}), 'url'), + ); + }) + .then(updatedBody => this.octokit.issues.update({ + owner: GITHUB_OWNER, + repo: EXPENSIFY_CASH_REPO, + issue_number: issueNumber, + body: updatedBody, + })); + } + + /** + * Generate the issue body for a StagingDeployCash. + * + * @private + * + * @param {String} tag + * @param {Array} PRList - The list of PR URLs which are included in this StagingDeployCash + * @param {Array} [verifiedPRList] - The list of PR URLs which have passed QA. + * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. + * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. + * @returns {Promise} + */ + generateStagingDeployCashBody( + tag, + PRList, + verifiedPRList = [], + deployBlockers = [], + resolvedDeployBlockers = [], + ) { + return this.generateVersionComparisonURL(`${GITHUB_OWNER}/${EXPENSIFY_CASH_REPO}`, tag, 'PATCH') + .then((comparisonURL) => { + const sortedPRList = _.sortBy(_.unique(PRList), URL => GithubUtils.getPullRequestNumberFromURL(URL)); + const sortedDeployBlockers = _.sortBy( + _.unique(deployBlockers), + URL => GithubUtils.getIssueOrPullRequestNumberFromURL(URL), + ); + + // Tag version and comparison URL + let issueBody = `**Release Version:** \`${tag}\`\r\n`; + issueBody += `**Compare Changes:** ${comparisonURL}\r\n`; + + // PR list + if (!_.isEmpty(PRList)) { + issueBody += '\r\n**This release contains changes from the following pull requests:**\r\n'; + _.each(sortedPRList, (URL) => { + issueBody += _.contains(verifiedPRList, URL) ? '- [x]' : '- [ ]'; + issueBody += ` ${URL}\r\n`; + }); + } + + // Deploy blockers + if (!_.isEmpty(deployBlockers)) { + issueBody += '\r\n**Deploy Blockers:**\r\n'; + _.each(sortedDeployBlockers, (URL) => { + issueBody += _.contains(resolvedDeployBlockers, URL) ? '- [x]' : '- [ ]'; + issueBody += ` ${URL}\r\n`; + }); + } + + issueBody += '\r\ncc @Expensify/applauseleads\r\n'; + + return issueBody; + }) + // eslint-disable-next-line no-console + .catch(err => console.warn('Error generating comparison URL, continuing...', err)); + } + + /** + * Create comment on pull request + * + * @param {String} repo - The repo to search for a matching pull request or issue number + * @param {Number} number - The pull request or issue number + * @param {String} messageBody - The comment message + * @returns {Promise} + */ + createComment(repo, number, messageBody) { + console.log(`Writing comment on #${number}`); + return this.octokit.issues.createComment({ + owner: GITHUB_OWNER, + repo, + issue_number: number, + body: messageBody, + }); + } + + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + + /** + * Generate the URL of an Expensify.cash pull request given the PR number. + * + * @param {Number} number + * @returns {String} + */ + static getPullRequestURLFromNumber(number) { + return `${EXPENSIFY_CASH_URL}/pull/${number}`; + } + + /** + * Parse the pull request number from a URL. + * + * @param {String} URL + * @returns {Number} + * @throws {Error} If the URL is not a valid Github Pull Request. + */ + static getPullRequestNumberFromURL(URL) { + const matches = URL.match(PULL_REQUEST_REGEX); + if (!_.isArray(matches) || matches.length !== 2) { + throw new Error(`Provided URL ${URL} is not a Github Pull Request!`); + } + return Number.parseInt(matches[1], 10); + } + + /** + * Parse the issue number from a URL. + * + * @param {String} URL + * @returns {Number} + * @throws {Error} If the URL is not a valid Github Issue. + */ + static getIssueNumberFromURL(URL) { + const matches = URL.match(ISSUE_REGEX); + if (!_.isArray(matches) || matches.length !== 2) { + throw new Error(`Provided URL ${URL} is not a Github Issue!`); + } + return Number.parseInt(matches[1], 10); + } + + /** + * Parse the issue or pull request number from a URL. + * + * @param {String} URL + * @returns {Number} + * @throws {Error} If the URL is not a valid Github Issue or Pull Request. + */ + static getIssueOrPullRequestNumberFromURL(URL) { + const matches = URL.match(ISSUE_OR_PULL_REQUEST_REGEX); + if (!_.isArray(matches) || matches.length !== 2) { + throw new Error(`Provided URL ${URL} is not a valid Github Issue or Pull Request!`); + } + return Number.parseInt(matches[1], 10); + } +} + +module.exports = GithubUtils; +module.exports.GITHUB_OWNER = GITHUB_OWNER; +module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; +module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; + + +/***/ }), + +/***/ 351: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const os = __importStar(__nccwpck_require__(87)); +const utils_1 = __nccwpck_require__(278); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 186: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const command_1 = __nccwpck_require__(351); +const file_command_1 = __nccwpck_require__(717); +const utils_1 = __nccwpck_require__(278); +const os = __importStar(__nccwpck_require__(87)); +const path = __importStar(__nccwpck_require__(622)); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + const delimiter = '_GitHubActionsFileCommandDelimeter_'; + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); + } +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. The value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + */ +function error(message) { + command_1.issue('error', message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds an warning issue + * @param message warning issue message. Errors will be converted to string via toString() + */ +function warning(message) { + command_1.issue('warning', message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + command_1.issueCommand('save-state', { name }, value); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__nccwpck_require__(747)); +const os = __importStar(__nccwpck_require__(87)); +const utils_1 = __nccwpck_require__(278); +function issueCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueCommand = issueCommand; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 278: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 532: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const ANY = Symbol('SemVer ANY') +// hoisted class for cyclic dependency +class Comparator { + static get ANY () { + return ANY + } + constructor (comp, options) { + options = parseOptions(options) + + if (comp instanceof Comparator) { + if (comp.loose === !!options.loose) { + return comp + } else { + comp = comp.value + } + } + + debug('comparator', comp, options) + this.options = options + this.loose = !!options.loose + this.parse(comp) + + if (this.semver === ANY) { + this.value = '' + } else { + this.value = this.operator + this.semver.version + } + + debug('comp', this) + } + + parse (comp) { + const r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + const m = comp.match(r) + + if (!m) { + throw new TypeError(`Invalid comparator: ${comp}`) + } + + this.operator = m[1] !== undefined ? m[1] : '' + if (this.operator === '=') { + this.operator = '' + } + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) { + this.semver = ANY + } else { + this.semver = new SemVer(m[2], this.options.loose) + } + } + + toString () { + return this.value + } + + test (version) { + debug('Comparator.test', version, this.options.loose) + + if (this.semver === ANY || version === ANY) { + return true + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + return cmp(version, this.operator, this.semver, this.options) + } + + intersects (comp, options) { + if (!(comp instanceof Comparator)) { + throw new TypeError('a Comparator is required') + } + + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + if (this.operator === '') { + if (this.value === '') { + return true + } + return new Range(comp.value, options).test(this.value) + } else if (comp.operator === '') { + if (comp.value === '') { + return true + } + return new Range(this.value, options).test(comp.semver) + } + + const sameDirectionIncreasing = + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '>=' || comp.operator === '>') + const sameDirectionDecreasing = + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '<=' || comp.operator === '<') + const sameSemVer = this.semver.version === comp.semver.version + const differentDirectionsInclusive = + (this.operator === '>=' || this.operator === '<=') && + (comp.operator === '>=' || comp.operator === '<=') + const oppositeDirectionsLessThan = + cmp(this.semver, '<', comp.semver, options) && + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '<=' || comp.operator === '<') + const oppositeDirectionsGreaterThan = + cmp(this.semver, '>', comp.semver, options) && + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '>=' || comp.operator === '>') + + return ( + sameDirectionIncreasing || + sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || + oppositeDirectionsGreaterThan + ) + } +} + +module.exports = Comparator + +const parseOptions = __nccwpck_require__(785) +const {re, t} = __nccwpck_require__(523) +const cmp = __nccwpck_require__(98) +const debug = __nccwpck_require__(427) +const SemVer = __nccwpck_require__(88) +const Range = __nccwpck_require__(828) + + +/***/ }), + +/***/ 828: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +// hoisted class for cyclic dependency +class Range { + constructor (range, options) { + options = parseOptions(options) + + if (range instanceof Range) { + if ( + range.loose === !!options.loose && + range.includePrerelease === !!options.includePrerelease + ) { + return range + } else { + return new Range(range.raw, options) + } + } + + if (range instanceof Comparator) { + // just put it in the set and return + this.raw = range.value + this.set = [[range]] + this.format() + return this + } + + this.options = options + this.loose = !!options.loose + this.includePrerelease = !!options.includePrerelease + + // First, split based on boolean or || + this.raw = range + this.set = range + .split(/\s*\|\|\s*/) + // map the range to a 2d array of comparators + .map(range => this.parseRange(range.trim())) + // throw out any comparator lists that are empty + // this generally means that it was not a valid range, which is allowed + // in loose mode, but will still throw if the WHOLE range is invalid. + .filter(c => c.length) + + if (!this.set.length) { + throw new TypeError(`Invalid SemVer Range: ${range}`) + } + + // if we have any that are not the null set, throw out null sets. + if (this.set.length > 1) { + // keep the first one, in case they're all null sets + const first = this.set[0] + this.set = this.set.filter(c => !isNullSet(c[0])) + if (this.set.length === 0) + this.set = [first] + else if (this.set.length > 1) { + // if we have any that are *, then the range is just * + for (const c of this.set) { + if (c.length === 1 && isAny(c[0])) { + this.set = [c] + break + } + } + } + } + + this.format() + } + + format () { + this.range = this.set + .map((comps) => { + return comps.join(' ').trim() + }) + .join('||') + .trim() + return this.range + } + + toString () { + return this.range + } + + parseRange (range) { + range = range.trim() + + // memoize range parsing for performance. + // this is a very hot path, and fully deterministic. + const memoOpts = Object.keys(this.options).join(',') + const memoKey = `parseRange:${memoOpts}:${range}` + const cached = cache.get(memoKey) + if (cached) + return cached + + const loose = this.options.loose + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] + range = range.replace(hr, hyphenReplace(this.options.includePrerelease)) + debug('hyphen replace', range) + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) + debug('comparator trim', range, re[t.COMPARATORTRIM]) + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[t.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[t.COMPARATORLOOSE] : re[t.COMPARATOR] + const rangeList = range + .split(' ') + .map(comp => parseComparator(comp, this.options)) + .join(' ') + .split(/\s+/) + // >=0.0.0 is equivalent to * + .map(comp => replaceGTE0(comp, this.options)) + // in loose mode, throw out any that are not valid comparators + .filter(this.options.loose ? comp => !!comp.match(compRe) : () => true) + .map(comp => new Comparator(comp, this.options)) + + // if any comparators are the null set, then replace with JUST null set + // if more than one comparator, remove any * comparators + // also, don't include the same comparator more than once + const l = rangeList.length + const rangeMap = new Map() + for (const comp of rangeList) { + if (isNullSet(comp)) + return [comp] + rangeMap.set(comp.value, comp) + } + if (rangeMap.size > 1 && rangeMap.has('')) + rangeMap.delete('') + + const result = [...rangeMap.values()] + cache.set(memoKey, result) + return result + } + + intersects (range, options) { + if (!(range instanceof Range)) { + throw new TypeError('a Range is required') + } + + return this.set.some((thisComparators) => { + return ( + isSatisfiable(thisComparators, options) && + range.set.some((rangeComparators) => { + return ( + isSatisfiable(rangeComparators, options) && + thisComparators.every((thisComparator) => { + return rangeComparators.every((rangeComparator) => { + return thisComparator.intersects(rangeComparator, options) + }) + }) + ) + }) + ) + }) + } + + // if ANY of the sets match ALL of its comparators, then pass + test (version) { + if (!version) { + return false + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + for (let i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version, this.options)) { + return true + } + } + return false + } +} +module.exports = Range + +const LRU = __nccwpck_require__(196) +const cache = new LRU({ max: 1000 }) + +const parseOptions = __nccwpck_require__(785) +const Comparator = __nccwpck_require__(532) +const debug = __nccwpck_require__(427) +const SemVer = __nccwpck_require__(88) +const { + re, + t, + comparatorTrimReplace, + tildeTrimReplace, + caretTrimReplace +} = __nccwpck_require__(523) + +const isNullSet = c => c.value === '<0.0.0-0' +const isAny = c => c.value === '' + +// take a set of comparators and determine whether there +// exists a version which can satisfy it +const isSatisfiable = (comparators, options) => { + let result = true + const remainingComparators = comparators.slice() + let testComparator = remainingComparators.pop() + + while (result && remainingComparators.length) { + result = remainingComparators.every((otherComparator) => { + return testComparator.intersects(otherComparator, options) + }) + + testComparator = remainingComparators.pop() + } + + return result +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +const parseComparator = (comp, options) => { + debug('comp', comp, options) + comp = replaceCarets(comp, options) + debug('caret', comp) + comp = replaceTildes(comp, options) + debug('tildes', comp) + comp = replaceXRanges(comp, options) + debug('xrange', comp) + comp = replaceStars(comp, options) + debug('stars', comp) + return comp +} + +const isX = id => !id || id.toLowerCase() === 'x' || id === '*' + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0 +const replaceTildes = (comp, options) => + comp.trim().split(/\s+/).map((comp) => { + return replaceTilde(comp, options) + }).join(' ') + +const replaceTilde = (comp, options) => { + const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + return comp.replace(r, (_, M, m, p, pr) => { + debug('tilde', comp, _, M, m, p, pr) + let ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = `>=${M}.0.0 <${+M + 1}.0.0-0` + } else if (isX(p)) { + // ~1.2 == >=1.2.0 <1.3.0-0 + ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0` + } else if (pr) { + debug('replaceTilde pr', pr) + ret = `>=${M}.${m}.${p}-${pr + } <${M}.${+m + 1}.0-0` + } else { + // ~1.2.3 == >=1.2.3 <1.3.0-0 + ret = `>=${M}.${m}.${p + } <${M}.${+m + 1}.0-0` + } + + debug('tilde return', ret) + return ret + }) +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0-0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0-0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0 +// ^1.2.3 --> >=1.2.3 <2.0.0-0 +// ^1.2.0 --> >=1.2.0 <2.0.0-0 +const replaceCarets = (comp, options) => + comp.trim().split(/\s+/).map((comp) => { + return replaceCaret(comp, options) + }).join(' ') + +const replaceCaret = (comp, options) => { + debug('caret', comp, options) + const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] + const z = options.includePrerelease ? '-0' : '' + return comp.replace(r, (_, M, m, p, pr) => { + debug('caret', comp, _, M, m, p, pr) + let ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0` + } else if (isX(p)) { + if (M === '0') { + ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0` + } else { + ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0` + } + } else if (pr) { + debug('replaceCaret pr', pr) + if (M === '0') { + if (m === '0') { + ret = `>=${M}.${m}.${p}-${pr + } <${M}.${m}.${+p + 1}-0` + } else { + ret = `>=${M}.${m}.${p}-${pr + } <${M}.${+m + 1}.0-0` + } + } else { + ret = `>=${M}.${m}.${p}-${pr + } <${+M + 1}.0.0-0` + } + } else { + debug('no pr') + if (M === '0') { + if (m === '0') { + ret = `>=${M}.${m}.${p + }${z} <${M}.${m}.${+p + 1}-0` + } else { + ret = `>=${M}.${m}.${p + }${z} <${M}.${+m + 1}.0-0` + } + } else { + ret = `>=${M}.${m}.${p + } <${+M + 1}.0.0-0` + } + } + + debug('caret return', ret) + return ret + }) +} + +const replaceXRanges = (comp, options) => { + debug('replaceXRanges', comp, options) + return comp.split(/\s+/).map((comp) => { + return replaceXRange(comp, options) + }).join(' ') +} + +const replaceXRange = (comp, options) => { + comp = comp.trim() + const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] + return comp.replace(r, (ret, gtlt, M, m, p, pr) => { + debug('xRange', comp, ret, gtlt, M, m, p, pr) + const xM = isX(M) + const xm = xM || isX(m) + const xp = xm || isX(p) + const anyX = xp + + if (gtlt === '=' && anyX) { + gtlt = '' + } + + // if we're including prereleases in the match, then we need + // to fix this to -0, the lowest possible prerelease value + pr = options.includePrerelease ? '-0' : '' + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0-0' + } else { + // nothing is forbidden + ret = '*' + } + } else if (gtlt && anyX) { + // we know patch is an x, because we have any x at all. + // replace X with 0 + if (xm) { + m = 0 + } + p = 0 + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + gtlt = '>=' + if (xm) { + M = +M + 1 + m = 0 + p = 0 + } else { + m = +m + 1 + p = 0 + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<' + if (xm) { + M = +M + 1 + } else { + m = +m + 1 + } + } + + if (gtlt === '<') + pr = '-0' + + ret = `${gtlt + M}.${m}.${p}${pr}` + } else if (xm) { + ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0` + } else if (xp) { + ret = `>=${M}.${m}.0${pr + } <${M}.${+m + 1}.0-0` + } + + debug('xRange return', ret) + + return ret + }) +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +const replaceStars = (comp, options) => { + debug('replaceStars', comp, options) + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[t.STAR], '') +} + +const replaceGTE0 = (comp, options) => { + debug('replaceGTE0', comp, options) + return comp.trim() + .replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], '') +} + +// This function is passed to string.replace(re[t.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-0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0-0 +const hyphenReplace = incPr => ($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) => { + if (isX(fM)) { + from = '' + } else if (isX(fm)) { + from = `>=${fM}.0.0${incPr ? '-0' : ''}` + } else if (isX(fp)) { + from = `>=${fM}.${fm}.0${incPr ? '-0' : ''}` + } else if (fpr) { + from = `>=${from}` + } else { + from = `>=${from}${incPr ? '-0' : ''}` + } + + if (isX(tM)) { + to = '' + } else if (isX(tm)) { + to = `<${+tM + 1}.0.0-0` + } else if (isX(tp)) { + to = `<${tM}.${+tm + 1}.0-0` + } else if (tpr) { + to = `<=${tM}.${tm}.${tp}-${tpr}` + } else if (incPr) { + to = `<${tM}.${tm}.${+tp + 1}-0` + } else { + to = `<=${to}` + } + + return (`${from} ${to}`).trim() +} + +const testSet = (set, version, options) => { + for (let i = 0; i < set.length; i++) { + if (!set[i].test(version)) { + return false + } + } + + if (version.prerelease.length && !options.includePrerelease) { + // 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 i = 0; i < set.length; i++) { + debug(set[i].semver) + if (set[i].semver === Comparator.ANY) { + continue + } + + if (set[i].semver.prerelease.length > 0) { + const allowed = set[i].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 +} + + +/***/ }), + +/***/ 88: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const debug = __nccwpck_require__(427) +const { MAX_LENGTH, MAX_SAFE_INTEGER } = __nccwpck_require__(293) +const { re, t } = __nccwpck_require__(523) + +const parseOptions = __nccwpck_require__(785) +const { compareIdentifiers } = __nccwpck_require__(463) +class SemVer { + constructor (version, options) { + options = parseOptions(options) + + if (version instanceof SemVer) { + if (version.loose === !!options.loose && + version.includePrerelease === !!options.includePrerelease) { + return version + } else { + 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` + ) + } + + debug('SemVer', version, options) + this.options = options + this.loose = !!options.loose + // this isn't actually relevant for versions, but keep it so that we + // don't run into trouble passing this.options around. + this.includePrerelease = !!options.includePrerelease + + const m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) + + if (!m) { + throw new TypeError(`Invalid Version: ${version}`) + } + + this.raw = version + + // these are actually numbers + this.major = +m[1] + this.minor = +m[2] + this.patch = +m[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 (!m[4]) { + this.prerelease = [] + } else { + this.prerelease = m[4].split('.').map((id) => { + if (/^[0-9]+$/.test(id)) { + const num = +id + if (num >= 0 && num < MAX_SAFE_INTEGER) { + return num + } + } + return id + }) + } + + this.build = m[5] ? m[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 + } + + compare (other) { + debug('SemVer.compare', this.version, this.options, other) + if (!(other instanceof SemVer)) { + if (typeof other === 'string' && other === this.version) { + return 0 + } + other = new SemVer(other, this.options) + } + + if (other.version === this.version) { + return 0 + } + + return this.compareMain(other) || this.comparePre(other) + } + + compareMain (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + 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.options) + } + + // 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 i = 0 + do { + const a = this.prerelease[i] + const b = other.prerelease[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) + } + + compareBuild (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + let i = 0 + do { + const a = this.build[i] + const b = other.build[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) + } + + // preminor will bump the version up to the next minor release, and immediately + // down to pre-release. premajor and prepatch work the same way. + inc (release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0 + this.patch = 0 + this.minor = 0 + this.major++ + this.inc('pre', identifier) + break + case 'preminor': + this.prerelease.length = 0 + this.patch = 0 + this.minor++ + this.inc('pre', identifier) + break + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0 + this.inc('patch', identifier) + this.inc('pre', identifier) + break + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) { + this.inc('patch', identifier) + } + this.inc('pre', identifier) + break + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if ( + this.minor !== 0 || + this.patch !== 0 || + this.prerelease.length === 0 + ) { + this.major++ + } + this.minor = 0 + this.patch = 0 + this.prerelease = [] + break + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) { + this.minor++ + } + this.patch = 0 + this.prerelease = [] + break + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) { + this.patch++ + } + this.prerelease = [] + break + // This probably shouldn't be used publicly. + // 1.0.0 'pre' would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) { + this.prerelease = [0] + } else { + let i = this.prerelease.length + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++ + i = -2 + } + } + if (i === -1) { + // didn't increment anything + this.prerelease.push(0) + } + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) { + this.prerelease = [identifier, 0] + } + } else { + this.prerelease = [identifier, 0] + } + } + break + + default: + throw new Error(`invalid increment argument: ${release}`) + } + this.format() + this.raw = this.version + return this + } +} + +module.exports = SemVer + + +/***/ }), + +/***/ 98: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const eq = __nccwpck_require__(898) +const neq = __nccwpck_require__(17) +const gt = __nccwpck_require__(123) +const gte = __nccwpck_require__(522) +const lt = __nccwpck_require__(194) +const lte = __nccwpck_require__(520) + +const cmp = (a, op, b, loose) => { + switch (op) { + case '===': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a === b + + case '!==': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a !== b + + case '': + case '=': + case '==': + return eq(a, b, loose) + + case '!=': + return neq(a, b, loose) + + case '>': + return gt(a, b, loose) + + case '>=': + return gte(a, b, loose) + + case '<': + return lt(a, b, loose) + + case '<=': + return lte(a, b, loose) + + default: + throw new TypeError(`Invalid operator: ${op}`) + } +} +module.exports = cmp + + +/***/ }), + +/***/ 309: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const SemVer = __nccwpck_require__(88) +const compare = (a, b, loose) => + new SemVer(a, loose).compare(new SemVer(b, loose)) + +module.exports = compare + + +/***/ }), + +/***/ 898: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const eq = (a, b, loose) => compare(a, b, loose) === 0 +module.exports = eq + + +/***/ }), + +/***/ 123: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const gt = (a, b, loose) => compare(a, b, loose) > 0 +module.exports = gt + + +/***/ }), + +/***/ 522: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const gte = (a, b, loose) => compare(a, b, loose) >= 0 +module.exports = gte + + +/***/ }), + +/***/ 194: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const lt = (a, b, loose) => compare(a, b, loose) < 0 +module.exports = lt + + +/***/ }), + +/***/ 520: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const lte = (a, b, loose) => compare(a, b, loose) <= 0 +module.exports = lte + + +/***/ }), + +/***/ 17: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const compare = __nccwpck_require__(309) +const neq = (a, b, loose) => compare(a, b, loose) !== 0 +module.exports = neq + + +/***/ }), + +/***/ 925: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const {MAX_LENGTH} = __nccwpck_require__(293) +const { re, t } = __nccwpck_require__(523) +const SemVer = __nccwpck_require__(88) + +const parseOptions = __nccwpck_require__(785) +const parse = (version, options) => { + options = parseOptions(options) + + if (version instanceof SemVer) { + return version + } + + if (typeof version !== 'string') { + return null + } + + if (version.length > MAX_LENGTH) { + return null + } + + const r = options.loose ? re[t.LOOSE] : re[t.FULL] + if (!r.test(version)) { + return null + } + + try { + return new SemVer(version, options) + } catch (er) { + return null + } +} + +module.exports = parse + + +/***/ }), + +/***/ 55: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const Range = __nccwpck_require__(828) +const satisfies = (version, range, options) => { + try { + range = new Range(range, options) + } catch (er) { + return false + } + return range.test(version) +} +module.exports = satisfies + + +/***/ }), + +/***/ 293: +/***/ ((module) => { + +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +const SEMVER_SPEC_VERSION = '2.0.0' + +const MAX_LENGTH = 256 +const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || + /* istanbul ignore next */ 9007199254740991 + +// Max safe segment length for coercion. +const MAX_SAFE_COMPONENT_LENGTH = 16 + +module.exports = { + SEMVER_SPEC_VERSION, + MAX_LENGTH, + MAX_SAFE_INTEGER, + MAX_SAFE_COMPONENT_LENGTH +} + + +/***/ }), + +/***/ 427: +/***/ ((module) => { + +const debug = ( + typeof process === 'object' && + process.env && + process.env.NODE_DEBUG && + /\bsemver\b/i.test(process.env.NODE_DEBUG) +) ? (...args) => console.error('SEMVER', ...args) + : () => {} + +module.exports = debug + + +/***/ }), + +/***/ 463: +/***/ ((module) => { + +const numeric = /^[0-9]+$/ +const compareIdentifiers = (a, b) => { + const anum = numeric.test(a) + const bnum = numeric.test(b) + + if (anum && bnum) { + a = +a + b = +b + } + + return a === b ? 0 + : (anum && !bnum) ? -1 + : (bnum && !anum) ? 1 + : a < b ? -1 + : 1 +} + +const rcompareIdentifiers = (a, b) => compareIdentifiers(b, a) + +module.exports = { + compareIdentifiers, + rcompareIdentifiers +} + + +/***/ }), + +/***/ 785: +/***/ ((module) => { + +// parse out just the options we care about so we always get a consistent +// obj with keys in a consistent order. +const opts = ['includePrerelease', 'loose', 'rtl'] +const parseOptions = options => + !options ? {} + : typeof options !== 'object' ? { loose: true } + : opts.filter(k => options[k]).reduce((options, k) => { + options[k] = true + return options + }, {}) +module.exports = parseOptions + + +/***/ }), + +/***/ 523: +/***/ ((module, exports, __nccwpck_require__) => { + +const { MAX_SAFE_COMPONENT_LENGTH } = __nccwpck_require__(293) +const debug = __nccwpck_require__(427) +exports = module.exports = {} + +// The actual regexps go on exports.re +const re = exports.re = [] +const src = exports.src = [] +const t = exports.t = {} +let R = 0 + +const createToken = (name, value, isGlobal) => { + const index = R++ + debug(index, value) + t[name] = index + src[index] = value + re[index] = new RegExp(value, isGlobal ? 'g' : undefined) +} + +// 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. + +createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*') +createToken('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. + +createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*') + +// ## Main Version +// Three dot-separated numeric identifiers. + +createToken('MAINVERSION', `(${src[t.NUMERICIDENTIFIER]})\\.` + + `(${src[t.NUMERICIDENTIFIER]})\\.` + + `(${src[t.NUMERICIDENTIFIER]})`) + +createToken('MAINVERSIONLOOSE', `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.` + + `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.` + + `(${src[t.NUMERICIDENTIFIERLOOSE]})`) + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. + +createToken('PRERELEASEIDENTIFIER', `(?:${src[t.NUMERICIDENTIFIER] +}|${src[t.NONNUMERICIDENTIFIER]})`) + +createToken('PRERELEASEIDENTIFIERLOOSE', `(?:${src[t.NUMERICIDENTIFIERLOOSE] +}|${src[t.NONNUMERICIDENTIFIER]})`) + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. + +createToken('PRERELEASE', `(?:-(${src[t.PRERELEASEIDENTIFIER] +}(?:\\.${src[t.PRERELEASEIDENTIFIER]})*))`) + +createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE] +}(?:\\.${src[t.PRERELEASEIDENTIFIERLOOSE]})*))`) + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+') + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +createToken('BUILD', `(?:\\+(${src[t.BUILDIDENTIFIER] +}(?:\\.${src[t.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. + +createToken('FULLPLAIN', `v?${src[t.MAINVERSION] +}${src[t.PRERELEASE]}?${ + src[t.BUILD]}?`) + +createToken('FULL', `^${src[t.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. +createToken('LOOSEPLAIN', `[v=\\s]*${src[t.MAINVERSIONLOOSE] +}${src[t.PRERELEASELOOSE]}?${ + src[t.BUILD]}?`) + +createToken('LOOSE', `^${src[t.LOOSEPLAIN]}$`) + +createToken('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. +createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`) +createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`) + +createToken('XRANGEPLAIN', `[v=\\s]*(${src[t.XRANGEIDENTIFIER]})` + + `(?:\\.(${src[t.XRANGEIDENTIFIER]})` + + `(?:\\.(${src[t.XRANGEIDENTIFIER]})` + + `(?:${src[t.PRERELEASE]})?${ + src[t.BUILD]}?` + + `)?)?`) + +createToken('XRANGEPLAINLOOSE', `[v=\\s]*(${src[t.XRANGEIDENTIFIERLOOSE]})` + + `(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})` + + `(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})` + + `(?:${src[t.PRERELEASELOOSE]})?${ + src[t.BUILD]}?` + + `)?)?`) + +createToken('XRANGE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAIN]}$`) +createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`) + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +createToken('COERCE', `${'(^|[^\\d])' + + '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + + `(?:$|[^\\d])`) +createToken('COERCERTL', src[t.COERCE], true) + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +createToken('LONETILDE', '(?:~>?)') + +createToken('TILDETRIM', `(\\s*)${src[t.LONETILDE]}\\s+`, true) +exports.tildeTrimReplace = '$1~' + +createToken('TILDE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAIN]}$`) +createToken('TILDELOOSE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAINLOOSE]}$`) + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +createToken('LONECARET', '(?:\\^)') + +createToken('CARETTRIM', `(\\s*)${src[t.LONECARET]}\\s+`, true) +exports.caretTrimReplace = '$1^' + +createToken('CARET', `^${src[t.LONECARET]}${src[t.XRANGEPLAIN]}$`) +createToken('CARETLOOSE', `^${src[t.LONECARET]}${src[t.XRANGEPLAINLOOSE]}$`) + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +createToken('COMPARATORLOOSE', `^${src[t.GTLT]}\\s*(${src[t.LOOSEPLAIN]})$|^$`) +createToken('COMPARATOR', `^${src[t.GTLT]}\\s*(${src[t.FULLPLAIN]})$|^$`) + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +createToken('COMPARATORTRIM', `(\\s*)${src[t.GTLT] +}\\s*(${src[t.LOOSEPLAIN]}|${src[t.XRANGEPLAIN]})`, true) +exports.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. +createToken('HYPHENRANGE', `^\\s*(${src[t.XRANGEPLAIN]})` + + `\\s+-\\s+` + + `(${src[t.XRANGEPLAIN]})` + + `\\s*$`) + +createToken('HYPHENRANGELOOSE', `^\\s*(${src[t.XRANGEPLAINLOOSE]})` + + `\\s+-\\s+` + + `(${src[t.XRANGEPLAINLOOSE]})` + + `\\s*$`) + +// Star ranges basically just allow anything at all. +createToken('STAR', '(<|>)?=?\\s*\\*') +// >=0.0.0 is like a star +createToken('GTE0', '^\\s*>=\\s*0\.0\.0\\s*$') +createToken('GTE0PRE', '^\\s*>=\\s*0\.0\.0-0\\s*$') + + +/***/ }), + +/***/ 196: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// A linked list to keep track of recently-used-ness +const Yallist = __nccwpck_require__(220) + +const MAX = Symbol('max') +const LENGTH = Symbol('length') +const LENGTH_CALCULATOR = Symbol('lengthCalculator') +const ALLOW_STALE = Symbol('allowStale') +const MAX_AGE = Symbol('maxAge') +const DISPOSE = Symbol('dispose') +const NO_DISPOSE_ON_SET = Symbol('noDisposeOnSet') +const LRU_LIST = Symbol('lruList') +const CACHE = Symbol('cache') +const UPDATE_AGE_ON_GET = Symbol('updateAgeOnGet') + +const naiveLength = () => 1 + +// lruList is a yallist where the head is the youngest +// item, and the tail is the oldest. the list contains the Hit +// objects as the entries. +// Each Hit object has a reference to its Yallist.Node. This +// never changes. +// +// cache is a Map (or PseudoMap) that matches the keys to +// the Yallist.Node object. +class LRUCache { + constructor (options) { + if (typeof options === 'number') + options = { max: options } + + if (!options) + options = {} + + if (options.max && (typeof options.max !== 'number' || options.max < 0)) + throw new TypeError('max must be a non-negative number') + // Kind of weird to have a default max of Infinity, but oh well. + const max = this[MAX] = options.max || Infinity + + const lc = options.length || naiveLength + this[LENGTH_CALCULATOR] = (typeof lc !== 'function') ? naiveLength : lc + this[ALLOW_STALE] = options.stale || false + if (options.maxAge && typeof options.maxAge !== 'number') + throw new TypeError('maxAge must be a number') + this[MAX_AGE] = options.maxAge || 0 + this[DISPOSE] = options.dispose + this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false + this[UPDATE_AGE_ON_GET] = options.updateAgeOnGet || false + this.reset() + } + + // resize the cache when the max changes. + set max (mL) { + if (typeof mL !== 'number' || mL < 0) + throw new TypeError('max must be a non-negative number') + + this[MAX] = mL || Infinity + trim(this) + } + get max () { + return this[MAX] + } + + set allowStale (allowStale) { + this[ALLOW_STALE] = !!allowStale + } + get allowStale () { + return this[ALLOW_STALE] + } + + set maxAge (mA) { + if (typeof mA !== 'number') + throw new TypeError('maxAge must be a non-negative number') + + this[MAX_AGE] = mA + trim(this) + } + get maxAge () { + return this[MAX_AGE] + } + + // resize the cache when the lengthCalculator changes. + set lengthCalculator (lC) { + if (typeof lC !== 'function') + lC = naiveLength + + if (lC !== this[LENGTH_CALCULATOR]) { + this[LENGTH_CALCULATOR] = lC + this[LENGTH] = 0 + this[LRU_LIST].forEach(hit => { + hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key) + this[LENGTH] += hit.length + }) + } + trim(this) + } + get lengthCalculator () { return this[LENGTH_CALCULATOR] } + + get length () { return this[LENGTH] } + get itemCount () { return this[LRU_LIST].length } + + rforEach (fn, thisp) { + thisp = thisp || this + for (let walker = this[LRU_LIST].tail; walker !== null;) { + const prev = walker.prev + forEachStep(this, fn, walker, thisp) + walker = prev + } + } + + forEach (fn, thisp) { + thisp = thisp || this + for (let walker = this[LRU_LIST].head; walker !== null;) { + const next = walker.next + forEachStep(this, fn, walker, thisp) + walker = next + } + } + + keys () { + return this[LRU_LIST].toArray().map(k => k.key) + } + + values () { + return this[LRU_LIST].toArray().map(k => k.value) + } + + reset () { + if (this[DISPOSE] && + this[LRU_LIST] && + this[LRU_LIST].length) { + this[LRU_LIST].forEach(hit => this[DISPOSE](hit.key, hit.value)) + } + + this[CACHE] = new Map() // hash of items by key + this[LRU_LIST] = new Yallist() // list of items in order of use recency + this[LENGTH] = 0 // length of items in the list + } + + dump () { + return this[LRU_LIST].map(hit => + isStale(this, hit) ? false : { + k: hit.key, + v: hit.value, + e: hit.now + (hit.maxAge || 0) + }).toArray().filter(h => h) + } + + dumpLru () { + return this[LRU_LIST] + } + + set (key, value, maxAge) { + maxAge = maxAge || this[MAX_AGE] + + if (maxAge && typeof maxAge !== 'number') + throw new TypeError('maxAge must be a number') + + const now = maxAge ? Date.now() : 0 + const len = this[LENGTH_CALCULATOR](value, key) + + if (this[CACHE].has(key)) { + if (len > this[MAX]) { + del(this, this[CACHE].get(key)) + return false + } + + const node = this[CACHE].get(key) + const item = node.value + + // dispose of the old one before overwriting + // split out into 2 ifs for better coverage tracking + if (this[DISPOSE]) { + if (!this[NO_DISPOSE_ON_SET]) + this[DISPOSE](key, item.value) + } + + item.now = now + item.maxAge = maxAge + item.value = value + this[LENGTH] += len - item.length + item.length = len + this.get(key) + trim(this) + return true + } + + const hit = new Entry(key, value, len, now, maxAge) + + // oversized objects fall out of cache automatically. + if (hit.length > this[MAX]) { + if (this[DISPOSE]) + this[DISPOSE](key, value) + + return false + } + + this[LENGTH] += hit.length + this[LRU_LIST].unshift(hit) + this[CACHE].set(key, this[LRU_LIST].head) + trim(this) + return true + } + + has (key) { + if (!this[CACHE].has(key)) return false + const hit = this[CACHE].get(key).value + return !isStale(this, hit) + } + + get (key) { + return get(this, key, true) + } + + peek (key) { + return get(this, key, false) + } + + pop () { + const node = this[LRU_LIST].tail + if (!node) + return null + + del(this, node) + return node.value + } + + del (key) { + del(this, this[CACHE].get(key)) + } + + load (arr) { + // reset the cache + this.reset() + + const now = Date.now() + // A previous serialized cache has the most recent items first + for (let l = arr.length - 1; l >= 0; l--) { + const hit = arr[l] + const expiresAt = hit.e || 0 + if (expiresAt === 0) + // the item was created without expiration in a non aged cache + this.set(hit.k, hit.v) + else { + const maxAge = expiresAt - now + // dont add already expired items + if (maxAge > 0) { + this.set(hit.k, hit.v, maxAge) + } + } + } + } + + prune () { + this[CACHE].forEach((value, key) => get(this, key, false)) + } +} + +const get = (self, key, doUse) => { + const node = self[CACHE].get(key) + if (node) { + const hit = node.value + if (isStale(self, hit)) { + del(self, node) + if (!self[ALLOW_STALE]) + return undefined + } else { + if (doUse) { + if (self[UPDATE_AGE_ON_GET]) + node.value.now = Date.now() + self[LRU_LIST].unshiftNode(node) + } + } + return hit.value + } +} + +const isStale = (self, hit) => { + if (!hit || (!hit.maxAge && !self[MAX_AGE])) + return false + + const diff = Date.now() - hit.now + return hit.maxAge ? diff > hit.maxAge + : self[MAX_AGE] && (diff > self[MAX_AGE]) +} + +const trim = self => { + if (self[LENGTH] > self[MAX]) { + for (let walker = self[LRU_LIST].tail; + self[LENGTH] > self[MAX] && walker !== null;) { + // We know that we're about to delete this one, and also + // what the next least recently used key will be, so just + // go ahead and set it now. + const prev = walker.prev + del(self, walker) + walker = prev + } + } +} + +const del = (self, node) => { + if (node) { + const hit = node.value + if (self[DISPOSE]) + self[DISPOSE](hit.key, hit.value) + + self[LENGTH] -= hit.length + self[CACHE].delete(hit.key) + self[LRU_LIST].removeNode(node) + } +} + +class Entry { + constructor (key, value, length, now, maxAge) { + this.key = key + this.value = value + this.length = length + this.now = now + this.maxAge = maxAge || 0 + } +} + +const forEachStep = (self, fn, node, thisp) => { + let hit = node.value + if (isStale(self, hit)) { + del(self, node) + if (!self[ALLOW_STALE]) + hit = undefined + } + if (hit) + fn.call(thisp, hit.value, hit.key, self) +} + +module.exports = LRUCache + + +/***/ }), + +/***/ 327: +/***/ ((module) => { + +"use strict"; + +module.exports = function (Yallist) { + Yallist.prototype[Symbol.iterator] = function* () { + for (let walker = this.head; walker; walker = walker.next) { + yield walker.value + } + } +} + + +/***/ }), + +/***/ 220: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +module.exports = Yallist + +Yallist.Node = Node +Yallist.create = Yallist + +function Yallist (list) { + var self = this + if (!(self instanceof Yallist)) { + self = new Yallist() + } + + self.tail = null + self.head = null + self.length = 0 + + if (list && typeof list.forEach === 'function') { + list.forEach(function (item) { + self.push(item) + }) + } else if (arguments.length > 0) { + for (var i = 0, l = arguments.length; i < l; i++) { + self.push(arguments[i]) + } + } + + return self +} + +Yallist.prototype.removeNode = function (node) { + if (node.list !== this) { + throw new Error('removing node which does not belong to this list') + } + + var next = node.next + var prev = node.prev + + if (next) { + next.prev = prev + } + + if (prev) { + prev.next = next + } + + if (node === this.head) { + this.head = next + } + if (node === this.tail) { + this.tail = prev + } + + node.list.length-- + node.next = null + node.prev = null + node.list = null + + return next +} + +Yallist.prototype.unshiftNode = function (node) { + if (node === this.head) { + return + } + + if (node.list) { + node.list.removeNode(node) + } + + var head = this.head + node.list = this + node.next = head + if (head) { + head.prev = node + } + + this.head = node + if (!this.tail) { + this.tail = node + } + this.length++ +} + +Yallist.prototype.pushNode = function (node) { + if (node === this.tail) { + return + } + + if (node.list) { + node.list.removeNode(node) + } + + var tail = this.tail + node.list = this + node.prev = tail + if (tail) { + tail.next = node + } + + this.tail = node + if (!this.head) { + this.head = node + } + this.length++ +} + +Yallist.prototype.push = function () { + for (var i = 0, l = arguments.length; i < l; i++) { + push(this, arguments[i]) + } + return this.length +} + +Yallist.prototype.unshift = function () { + for (var i = 0, l = arguments.length; i < l; i++) { + unshift(this, arguments[i]) + } + return this.length +} + +Yallist.prototype.pop = function () { + if (!this.tail) { + return undefined + } + + var res = this.tail.value + this.tail = this.tail.prev + if (this.tail) { + this.tail.next = null + } else { + this.head = null + } + this.length-- + return res +} + +Yallist.prototype.shift = function () { + if (!this.head) { + return undefined + } + + var res = this.head.value + this.head = this.head.next + if (this.head) { + this.head.prev = null + } else { + this.tail = null + } + this.length-- + return res +} + +Yallist.prototype.forEach = function (fn, thisp) { + thisp = thisp || this + for (var walker = this.head, i = 0; walker !== null; i++) { + fn.call(thisp, walker.value, i, this) + walker = walker.next + } +} + +Yallist.prototype.forEachReverse = function (fn, thisp) { + thisp = thisp || this + for (var walker = this.tail, i = this.length - 1; walker !== null; i--) { + fn.call(thisp, walker.value, i, this) + walker = walker.prev + } +} + +Yallist.prototype.get = function (n) { + for (var i = 0, walker = this.head; walker !== null && i < n; i++) { + // abort out of the list early if we hit a cycle + walker = walker.next + } + if (i === n && walker !== null) { + return walker.value + } +} + +Yallist.prototype.getReverse = function (n) { + for (var i = 0, walker = this.tail; walker !== null && i < n; i++) { + // abort out of the list early if we hit a cycle + walker = walker.prev + } + if (i === n && walker !== null) { + return walker.value + } +} + +Yallist.prototype.map = function (fn, thisp) { + thisp = thisp || this + var res = new Yallist() + for (var walker = this.head; walker !== null;) { + res.push(fn.call(thisp, walker.value, this)) + walker = walker.next + } + return res +} + +Yallist.prototype.mapReverse = function (fn, thisp) { + thisp = thisp || this + var res = new Yallist() + for (var walker = this.tail; walker !== null;) { + res.push(fn.call(thisp, walker.value, this)) + walker = walker.prev + } + return res +} + +Yallist.prototype.reduce = function (fn, initial) { + var acc + var walker = this.head + if (arguments.length > 1) { + acc = initial + } else if (this.head) { + walker = this.head.next + acc = this.head.value + } else { + throw new TypeError('Reduce of empty list with no initial value') + } + + for (var i = 0; walker !== null; i++) { + acc = fn(acc, walker.value, i) + walker = walker.next + } + + return acc +} + +Yallist.prototype.reduceReverse = function (fn, initial) { + var acc + var walker = this.tail + if (arguments.length > 1) { + acc = initial + } else if (this.tail) { + walker = this.tail.prev + acc = this.tail.value + } else { + throw new TypeError('Reduce of empty list with no initial value') + } + + for (var i = this.length - 1; walker !== null; i--) { + acc = fn(acc, walker.value, i) + walker = walker.prev + } + + return acc +} + +Yallist.prototype.toArray = function () { + var arr = new Array(this.length) + for (var i = 0, walker = this.head; walker !== null; i++) { + arr[i] = walker.value + walker = walker.next + } + return arr +} + +Yallist.prototype.toArrayReverse = function () { + var arr = new Array(this.length) + for (var i = 0, walker = this.tail; walker !== null; i++) { + arr[i] = walker.value + walker = walker.prev + } + return arr +} + +Yallist.prototype.slice = function (from, to) { + to = to || this.length + if (to < 0) { + to += this.length + } + from = from || 0 + if (from < 0) { + from += this.length + } + var ret = new Yallist() + if (to < from || to < 0) { + return ret + } + if (from < 0) { + from = 0 + } + if (to > this.length) { + to = this.length + } + for (var i = 0, walker = this.head; walker !== null && i < from; i++) { + walker = walker.next + } + for (; walker !== null && i < to; i++, walker = walker.next) { + ret.push(walker.value) + } + return ret +} + +Yallist.prototype.sliceReverse = function (from, to) { + to = to || this.length + if (to < 0) { + to += this.length + } + from = from || 0 + if (from < 0) { + from += this.length + } + var ret = new Yallist() + if (to < from || to < 0) { + return ret + } + if (from < 0) { + from = 0 + } + if (to > this.length) { + to = this.length + } + for (var i = this.length, walker = this.tail; walker !== null && i > to; i--) { + walker = walker.prev + } + for (; walker !== null && i > from; i--, walker = walker.prev) { + ret.push(walker.value) + } + return ret +} + +Yallist.prototype.splice = function (start, deleteCount, ...nodes) { + if (start > this.length) { + start = this.length - 1 + } + if (start < 0) { + start = this.length + start; + } + + for (var i = 0, walker = this.head; walker !== null && i < start; i++) { + walker = walker.next + } + + var ret = [] + for (var i = 0; walker && i < deleteCount; i++) { + ret.push(walker.value) + walker = this.removeNode(walker) + } + if (walker === null) { + walker = this.tail + } + + if (walker !== this.head && walker !== this.tail) { + walker = walker.prev + } + + for (var i = 0; i < nodes.length; i++) { + walker = insert(this, walker, nodes[i]) + } + return ret; +} + +Yallist.prototype.reverse = function () { + var head = this.head + var tail = this.tail + for (var walker = head; walker !== null; walker = walker.prev) { + var p = walker.prev + walker.prev = walker.next + walker.next = p + } + this.head = tail + this.tail = head + return this +} + +function insert (self, node, value) { + var inserted = node === self.head ? + new Node(value, null, node, self) : + new Node(value, node, node.next, self) + + if (inserted.next === null) { + self.tail = inserted + } + if (inserted.prev === null) { + self.head = inserted + } + + self.length++ + + return inserted +} + +function push (self, item) { + self.tail = new Node(item, self.tail, null, self) + if (!self.head) { + self.head = self.tail + } + self.length++ +} + +function unshift (self, item) { + self.head = new Node(item, null, self.head, self) + if (!self.tail) { + self.tail = self.head + } + self.length++ +} + +function Node (value, prev, next, list) { + if (!(this instanceof Node)) { + return new Node(value, prev, next, list) + } + + this.list = list + this.value = value + + if (prev) { + prev.next = this + this.prev = prev + } else { + this.prev = null + } + + if (next) { + next.prev = this + this.next = next + } else { + this.next = null + } +} + +try { + // add if support for Symbol.iterator is present + __nccwpck_require__(327)(Yallist) +} catch (er) {} + + +/***/ }), + +/***/ 987: +/***/ (function(module) { + +(function (global, factory) { + true ? module.exports = factory() : + 0; +}(this, (function () { + // Underscore.js 1.11.0 + // https://underscorejs.org + // (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + // Underscore may be freely distributed under the MIT license. + + // Current version. + var VERSION = '1.11.0'; + + // Establish the root object, `window` (`self`) in the browser, `global` + // on the server, or `this` in some virtual machines. We use `self` + // instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global || + Function('return this')() || + {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // Modern feature detection. + var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined'; + + // All **ECMAScript 5+** native function implementations that we hope to use + // are declared here. + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create, + nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; + + // Create references to these builtin functions because we override them. + var _isNaN = isNaN, + _isFinite = isFinite; + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + // The largest integer that can be represented exactly. + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + // Some functions take a variable number of arguments, or a few expected + // arguments at the beginning and then a variable number of values to operate + // on. This helper accumulates all remaining arguments past the function’s + // argument length (or an explicit `startIndex`), into an array that becomes + // the last argument. Similar to ES6’s "rest parameter". + function restArguments(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + } + + // Is a given variable an object? + function isObject(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + } + + // Is a given value equal to null? + function isNull(obj) { + return obj === null; + } + + // Is a given variable undefined? + function isUndefined(obj) { + return obj === void 0; + } + + // Is a given value a boolean? + function isBoolean(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + } + + // Is a given value a DOM element? + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + + // Internal function for creating a `toString`-based type tester. + function tagTester(name) { + return function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + } + + var isString = tagTester('String'); + + var isNumber = tagTester('Number'); + + var isDate = tagTester('Date'); + + var isRegExp = tagTester('RegExp'); + + var isError = tagTester('Error'); + + var isSymbol = tagTester('Symbol'); + + var isMap = tagTester('Map'); + + var isWeakMap = tagTester('WeakMap'); + + var isSet = tagTester('Set'); + + var isWeakSet = tagTester('WeakSet'); + + var isArrayBuffer = tagTester('ArrayBuffer'); + + var isDataView = tagTester('DataView'); + + // Is a given value an array? + // Delegates to ECMA5's native `Array.isArray`. + var isArray = nativeIsArray || tagTester('Array'); + + var isFunction = tagTester('Function'); + + // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old + // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). + var nodelist = root.document && root.document.childNodes; + if ( true && typeof Int8Array != 'object' && typeof nodelist != 'function') { + isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + var isFunction$1 = isFunction; + + // Internal function to check whether `key` is an own property name of `obj`. + function has(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + } + + var isArguments = tagTester('Arguments'); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + (function() { + if (!isArguments(arguments)) { + isArguments = function(obj) { + return has(obj, 'callee'); + }; + } + }()); + + var isArguments$1 = isArguments; + + // Is a given object a finite number? + function isFinite$1(obj) { + return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); + } + + // Is the given value `NaN`? + function isNaN$1(obj) { + return isNumber(obj) && _isNaN(obj); + } + + // Predicate-generating function. Often useful outside of Underscore. + function constant(value) { + return function() { + return value; + }; + } + + // Common internal logic for `isArrayLike` and `isBufferLike`. + function createSizePropertyCheck(getSizeProperty) { + return function(collection) { + var sizeProperty = getSizeProperty(collection); + return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; + } + } + + // Internal helper to generate a function to obtain property `key` from `obj`. + function shallowProperty(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + } + + // Internal helper to obtain the `byteLength` property of an object. + var getByteLength = shallowProperty('byteLength'); + + // Internal helper to determine whether we should spend extensive checks against + // `ArrayBuffer` et al. + var isBufferLike = createSizePropertyCheck(getByteLength); + + // Is a given value a typed array? + var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; + function isTypedArray(obj) { + // `ArrayBuffer.isView` is the most future-proof, so use it when available. + // Otherwise, fall back on the above regular expression. + return nativeIsView ? (nativeIsView(obj) && !isDataView(obj)) : + isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); + } + + var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); + + // Internal helper to obtain the `length` property of an object. + var getLength = shallowProperty('length'); + + // Internal helper for collection methods to determine whether a collection + // should be iterated as an array or as an object. + // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var isArrayLike = createSizePropertyCheck(getLength); + + // Internal helper to create a simple lookup structure. + // `collectNonEnumProps` used to depend on `_.contains`, but this led to + // circular imports. `emulatedSet` is a one-off solution that only works for + // arrays of strings. + function emulatedSet(keys) { + var hash = {}; + for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; + return { + contains: function(key) { return hash[key]; }, + push: function(key) { + hash[key] = true; + return keys.push(key); + } + }; + } + + // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't + // be iterated by `for key in ...` and thus missed. Extends `keys` in place if + // needed. + function collectNonEnumProps(obj, keys) { + keys = emulatedSet(keys); + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = isFunction$1(constructor) && constructor.prototype || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (has(obj, prop) && !keys.contains(prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys`. + function keys(obj) { + if (!isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + function isEmpty(obj) { + if (obj == null) return true; + // Skip the more expensive `toString`-based type checks if `obj` has no + // `.length`. + if (isArrayLike(obj) && (isArray(obj) || isString(obj) || isArguments$1(obj))) return obj.length === 0; + return keys(obj).length === 0; + } + + // Returns whether an object has a given set of `key:value` pairs. + function isMatch(object, attrs) { + var _keys = keys(attrs), length = _keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = _keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + } + + // If Underscore is called as a function, it returns a wrapped object that can + // be used OO-style. This wrapper holds altered versions of all functions added + // through `_.mixin`. Wrapped objects may be chained. + function _(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + } + + _.VERSION = VERSION; + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxies for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return String(this._wrapped); + }; + + // Internal recursive comparison function for `_.isEqual`. + function eq(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + } + + // Internal recursive comparison function for `_.isEqual`. + function deepEq(a, b, aStack, bStack) { + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // These types are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + case '[object ArrayBuffer]': + // Coerce to `DataView` so we can fall through to the next case. + return deepEq(new DataView(a), new DataView(b), aStack, bStack); + case '[object DataView]': + var byteLength = getByteLength(a); + if (byteLength !== getByteLength(b)) { + return false; + } + while (byteLength--) { + if (a.getUint8(byteLength) !== b.getUint8(byteLength)) { + return false; + } + } + return true; + } + + if (isTypedArray$1(a)) { + // Coerce typed arrays to `DataView`. + return deepEq(new DataView(a.buffer), new DataView(b.buffer), aStack, bStack); + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + } + + // Perform a deep comparison to check if two objects are equal. + function isEqual(a, b) { + return eq(a, b); + } + + // Retrieve all the enumerable property names of an object. + function allKeys(obj) { + if (!isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Retrieve the values of an object's properties. + function values(obj) { + var _keys = keys(obj); + var length = _keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[_keys[i]]; + } + return values; + } + + // Convert an object into a list of `[key, value]` pairs. + // The opposite of `_.object` with one argument. + function pairs(obj) { + var _keys = keys(obj); + var length = _keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [_keys[i], obj[_keys[i]]]; + } + return pairs; + } + + // Invert the keys and values of an object. The values must be serializable. + function invert(obj) { + var result = {}; + var _keys = keys(obj); + for (var i = 0, length = _keys.length; i < length; i++) { + result[obj[_keys[i]]] = _keys[i]; + } + return result; + } + + // Return a sorted list of the function names available on the object. + function functions(obj) { + var names = []; + for (var key in obj) { + if (isFunction$1(obj[key])) names.push(key); + } + return names.sort(); + } + + // An internal function for creating assigner functions. + function createAssigner(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + } + + // Extend a given object with all the properties in passed-in object(s). + var extend = createAssigner(allKeys); + + // Assigns a given object with all the own properties in the passed-in + // object(s). + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var extendOwn = createAssigner(keys); + + // Fill in a given object with default properties. + var defaults = createAssigner(allKeys, true); + + // Create a naked function reference for surrogate-prototype-swapping. + function ctor() { + return function(){}; + } + + // An internal function for creating a new object that inherits from another. + function baseCreate(prototype) { + if (!isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + var Ctor = ctor(); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + } + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + function create(prototype, props) { + var result = baseCreate(prototype); + if (props) extendOwn(result, props); + return result; + } + + // Create a (shallow-cloned) duplicate of an object. + function clone(obj) { + if (!isObject(obj)) return obj; + return isArray(obj) ? obj.slice() : extend({}, obj); + } + + // Invokes `interceptor` with the `obj` and then returns `obj`. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + function tap(obj, interceptor) { + interceptor(obj); + return obj; + } + + // Shortcut function for checking if an object has a given property directly on + // itself (in other words, not on a prototype). Unlike the internal `has` + // function, this public version can also traverse nested properties. + function has$1(obj, path) { + if (!isArray(path)) { + return has(obj, path); + } + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (obj == null || !hasOwnProperty.call(obj, key)) { + return false; + } + obj = obj[key]; + } + return !!length; + } + + // Keep the identity function around for default iteratees. + function identity(value) { + return value; + } + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + function matcher(attrs) { + attrs = extendOwn({}, attrs); + return function(obj) { + return isMatch(obj, attrs); + }; + } + + // Internal function to obtain a nested property in `obj` along `path`. + function deepGet(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + } + + // Creates a function that, when passed an object, will traverse that object’s + // properties down the given `path`, specified as an array of keys or indices. + function property(path) { + if (!isArray(path)) { + return shallowProperty(path); + } + return function(obj) { + return deepGet(obj, path); + }; + } + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + function optimizeCb(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + // The 2-argument case is omitted because we’re not using it. + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + } + + // An internal function to generate callbacks that can be applied to each + // element in a collection, returning the desired result — either `_.identity`, + // an arbitrary callback, a property matcher, or a property accessor. + function baseIteratee(value, context, argCount) { + if (value == null) return identity; + if (isFunction$1(value)) return optimizeCb(value, context, argCount); + if (isObject(value) && !isArray(value)) return matcher(value); + return property(value); + } + + // External wrapper for our callback generator. Users may customize + // `_.iteratee` if they want additional predicate/iteratee shorthand styles. + // This abstraction hides the internal-only `argCount` argument. + function iteratee(value, context) { + return baseIteratee(value, context, Infinity); + } + _.iteratee = iteratee; + + // The function we call internally to generate a callback. It invokes + // `_.iteratee` if overridden, otherwise `baseIteratee`. + function cb(value, context, argCount) { + if (_.iteratee !== iteratee) return _.iteratee(value, context); + return baseIteratee(value, context, argCount); + } + + // Returns the results of applying the `iteratee` to each element of `obj`. + // In contrast to `_.map` it returns an object. + function mapObject(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = keys(obj), + length = _keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = _keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Predicate-generating function. Often useful outside of Underscore. + function noop(){} + + // Generates a function for a given object that returns a given property. + function propertyOf(obj) { + if (obj == null) { + return function(){}; + } + return function(path) { + return !isArray(path) ? obj[path] : deepGet(obj, path); + }; + } + + // Run a function **n** times. + function times(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + } + + // Return a random integer between `min` and `max` (inclusive). + function random(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + + // A (possibly faster) way to get the current timestamp as an integer. + var now = Date.now || function() { + return new Date().getTime(); + }; + + // Internal helper to generate functions for escaping and unescaping strings + // to/from HTML interpolation. + function createEscaper(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + } + + // Internal list of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + // Function for escaping strings to HTML interpolation. + var _escape = createEscaper(escapeMap); + + // Internal list of HTML entities for unescaping. + var unescapeMap = invert(escapeMap); + + // Function for unescaping strings from HTML interpolation. + var _unescape = createEscaper(unescapeMap); + + // By default, Underscore uses ERB-style template delimiters. Change the + // following template settings to use alternative delimiters. + var templateSettings = _.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + + // When customizing `_.templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + function escapeChar(match) { + return '\\' + escapes[match]; + } + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + } + + // Traverses the children of `obj` along `path`. If a child is a function, it + // is invoked with its parent as context. Returns the value of the final + // child, or `fallback` if any child is undefined. + function result(obj, path, fallback) { + if (!isArray(path)) path = [path]; + var length = path.length; + if (!length) { + return isFunction$1(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = isFunction$1(prop) ? prop.call(obj) : prop; + } + return obj; + } + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + + // Start chaining a wrapped Underscore object. + function chain(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + } + + // Internal function to execute `sourceFunc` bound to `context` with optional + // `args`. Determines whether to execute a function as a constructor or as a + // normal function. + function executeBound(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (isObject(result)) return result; + return self; + } + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. `_` acts + // as a placeholder by default, allowing any combination of arguments to be + // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. + var partial = restArguments(function(func, boundArgs) { + var placeholder = partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + partial.placeholder = _; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). + var bind = restArguments(function(func, context, args) { + if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + + // Internal implementation of a recursive `flatten` function. + function flatten(input, depth, strict, output) { + output = output || []; + if (!depth && depth !== 0) { + depth = Infinity; + } else if (depth <= 0) { + return output.concat(input); + } + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { + // Flatten current level of array or arguments object. + if (depth > 1) { + flatten(value, depth - 1, strict, output); + idx = output.length; + } else { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + } + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + var bindAll = restArguments(function(obj, keys) { + keys = flatten(keys, false, false); + var index = keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = keys[index]; + obj[key] = bind(obj[key], obj); + } + return obj; + }); + + // Memoize an expensive function by storing its results. + function memoize(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + } + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + var delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + var defer = partial(delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + function throttle(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var _now = now(); + if (!previous && options.leading === false) previous = _now; + var remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + } + + // When a sequence of calls of the returned function ends, the argument + // function is triggered. The end of a sequence is defined by the `wait` + // parameter. If `immediate` is passed, the argument function will be + // triggered at the beginning of the sequence instead of at the end. + function debounce(func, wait, immediate) { + var timeout, result; + + var later = function(context, args) { + timeout = null; + if (args) result = func.apply(context, args); + }; + + var debounced = restArguments(function(args) { + if (timeout) clearTimeout(timeout); + if (immediate) { + var callNow = !timeout; + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(this, args); + } else { + timeout = delay(later, wait, this, args); + } + + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = null; + }; + + return debounced; + } + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + function wrap(func, wrapper) { + return partial(wrapper, func); + } + + // Returns a negated version of the passed-in predicate. + function negate(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + } + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + function compose() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + } + + // Returns a function that will only be executed on and after the Nth call. + function after(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + } + + // Returns a function that will only be executed up to (but not including) the + // Nth call. + function before(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + } + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + var once = partial(before, 2); + + // Returns the first key on an object that passes a truth test. + function findKey(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = keys(obj), key; + for (var i = 0, length = _keys.length; i < length; i++) { + key = _keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + } + + // Internal function to generate `_.findIndex` and `_.findLastIndex`. + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a truth test. + var findIndex = createPredicateIndexFinder(1); + + // Returns the last index on an array-like that passes a truth test. + var findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + function sortedIndex(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + } + + // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), isNaN$1); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + var indexOf = createIndexFinder(1, findIndex, sortedIndex); + + // Return the position of the last occurrence of an item in an array, + // or -1 if the item is not included in the array. + var lastIndexOf = createIndexFinder(-1, findLastIndex); + + // Return the first value which passes a truth test. + function find(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? findIndex : findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + } + + // Convenience version of a common use case of `_.find`: getting the first + // object containing specific `key:value` pairs. + function findWhere(obj, attrs) { + return find(obj, matcher(attrs)); + } + + // The cornerstone for collection functions, an `each` + // implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + function each(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var _keys = keys(obj); + for (i = 0, length = _keys.length; i < length; i++) { + iteratee(obj[_keys[i]], _keys[i], obj); + } + } + return obj; + } + + // Return the results of applying the iteratee to each element. + function map(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Internal helper to create a reducing function, iterating left or right. + function createReduce(dir) { + // Wrap code that reassigns argument variables in a separate function than + // the one that accesses `arguments.length` to avoid a perf hit. (#1991) + var reducer = function(obj, iteratee, memo, initial) { + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[_keys ? _keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = _keys ? _keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + var reduce = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + var reduceRight = createReduce(-1); + + // Return all the elements that pass a truth test. + function filter(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + } + + // Return all the elements for which a truth test fails. + function reject(obj, predicate, context) { + return filter(obj, negate(cb(predicate)), context); + } + + // Determine whether all of the elements pass a truth test. + function every(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + } + + // Determine if at least one element in the object passes a truth test. + function some(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + } + + // Determine if the array or object contains a given item (using `===`). + function contains(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return indexOf(obj, item, fromIndex) >= 0; + } + + // Invoke a method (with arguments) on every item in a collection. + var invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (isFunction$1(path)) { + func = path; + } else if (isArray(path)) { + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + + // Convenience version of a common use case of `_.map`: fetching a property. + function pluck(obj, key) { + return map(obj, property(key)); + } + + // Convenience version of a common use case of `_.filter`: selecting only + // objects containing specific `key:value` pairs. + function where(obj, attrs) { + return filter(obj, matcher(attrs)); + } + + // Return the maximum element (or element-based computation). + function max(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Return the minimum element (or element-based computation). + function min(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Sample **n** random values from a collection using the modern version of the + // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `_.map`. + function sample(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = values(obj); + return obj[random(obj.length - 1)]; + } + var sample = isArrayLike(obj) ? clone(obj) : values(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + } + + // Shuffle a collection. + function shuffle(obj) { + return sample(obj, Infinity); + } + + // Sort the object's values by a criterion produced by an iteratee. + function sortBy(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return pluck(map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + } + + // An internal function used for aggregate "group by" operations. + function group(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + } + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + var groupBy = group(function(result, value, key) { + if (has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `_.groupBy`, but for + // when you know that your index values will be unique. + var indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + var countBy = group(function(result, value, key) { + if (has(result, key)) result[key]++; else result[key] = 1; + }); + + // Split a collection into two arrays: one whose elements all pass the given + // truth test, and one whose elements all do not pass the truth test. + var partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + + // Safely create a real, live array from anything iterable. + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + function toArray(obj) { + if (!obj) return []; + if (isArray(obj)) return slice.call(obj); + if (isString(obj)) { + // Keep surrogate pair characters together. + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return map(obj, identity); + return values(obj); + } + + // Return the number of elements in a collection. + function size(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : keys(obj).length; + } + + // Internal `_.pick` helper function to determine whether `key` is an enumerable + // property name of `obj`. + function keyInObj(value, key, obj) { + return key in obj; + } + + // Return a copy of the object only containing the allowed properties. + var pick = restArguments(function(obj, keys) { + var result = {}, iteratee = keys[0]; + if (obj == null) return result; + if (isFunction$1(iteratee)) { + if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); + keys = allKeys(obj); + } else { + iteratee = keyInObj; + keys = flatten(keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + + // Return a copy of the object without the disallowed properties. + var omit = restArguments(function(obj, keys) { + var iteratee = keys[0], context; + if (isFunction$1(iteratee)) { + iteratee = negate(iteratee); + if (keys.length > 1) context = keys[1]; + } else { + keys = map(flatten(keys, false, false), String); + iteratee = function(value, key) { + return !contains(keys, key); + }; + } + return pick(obj, iteratee, context); + }); + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + function initial(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + } + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. The **guard** check allows it to work with `_.map`. + function first(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[0]; + return initial(array, array.length - n); + } + + // Returns everything but the first entry of the `array`. Especially useful on + // the `arguments` object. Passing an **n** will return the rest N values in the + // `array`. + function rest(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + } + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + function last(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return rest(array, Math.max(0, array.length - n)); + } + + // Trim out all falsy values from an array. + function compact(array) { + return filter(array, Boolean); + } + + // Flatten out an array, either recursively (by default), or up to `depth`. + // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. + function flatten$1(array, depth) { + return flatten(array, depth, false); + } + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + var difference = restArguments(function(array, rest) { + rest = flatten(rest, true, true); + return filter(array, function(value){ + return !contains(rest, value); + }); + }); + + // Return a version of the array that does not contain the specified value(s). + var without = restArguments(function(array, otherArrays) { + return difference(array, otherArrays); + }); + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // The faster algorithm will not work with an iteratee if the iteratee + // is not a one-to-one function, so providing an iteratee will disable + // the faster algorithm. + function uniq(array, isSorted, iteratee, context) { + if (!isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!contains(result, value)) { + result.push(value); + } + } + return result; + } + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + var union = restArguments(function(arrays) { + return uniq(flatten(arrays, true, true)); + }); + + // Produce an array that contains every item shared between all the + // passed-in arrays. + function intersection(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + } + + // Complement of zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices. + function unzip(array) { + var length = array && max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = pluck(array, index); + } + return result; + } + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + var zip = restArguments(unzip); + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. Passing by pairs is the reverse of `_.pairs`. + function object(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + } + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](https://docs.python.org/library/functions.html#range). + function range(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + } + + // Chunk a single array into multiple arrays, each containing `count` or fewer + // items. + function chunk(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + } + + // Helper function to continue chaining intermediate results. + function chainResult(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + } + + // Add your own custom functions to the Underscore object. + function mixin(obj) { + each(functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_, args)); + }; + }); + return _; + } + + // Add all mutator `Array` functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) { + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) { + delete obj[0]; + } + } + return chainResult(this, obj); + }; + }); + + // Add all accessor `Array` functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) obj = method.apply(obj, arguments); + return chainResult(this, obj); + }; + }); + + // Named Exports + + var allExports = { + __proto__: null, + VERSION: VERSION, + restArguments: restArguments, + isObject: isObject, + isNull: isNull, + isUndefined: isUndefined, + isBoolean: isBoolean, + isElement: isElement, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isRegExp: isRegExp, + isError: isError, + isSymbol: isSymbol, + isMap: isMap, + isWeakMap: isWeakMap, + isSet: isSet, + isWeakSet: isWeakSet, + isArrayBuffer: isArrayBuffer, + isDataView: isDataView, + isArray: isArray, + isFunction: isFunction$1, + isArguments: isArguments$1, + isFinite: isFinite$1, + isNaN: isNaN$1, + isTypedArray: isTypedArray$1, + isEmpty: isEmpty, + isMatch: isMatch, + isEqual: isEqual, + keys: keys, + allKeys: allKeys, + values: values, + pairs: pairs, + invert: invert, + functions: functions, + methods: functions, + extend: extend, + extendOwn: extendOwn, + assign: extendOwn, + defaults: defaults, + create: create, + clone: clone, + tap: tap, + has: has$1, + mapObject: mapObject, + identity: identity, + constant: constant, + noop: noop, + property: property, + propertyOf: propertyOf, + matcher: matcher, + matches: matcher, + times: times, + random: random, + now: now, + escape: _escape, + unescape: _unescape, + templateSettings: templateSettings, + template: template, + result: result, + uniqueId: uniqueId, + chain: chain, + iteratee: iteratee, + partial: partial, + bind: bind, + bindAll: bindAll, + memoize: memoize, + delay: delay, + defer: defer, + throttle: throttle, + debounce: debounce, + wrap: wrap, + negate: negate, + compose: compose, + after: after, + before: before, + once: once, + findKey: findKey, + findIndex: findIndex, + findLastIndex: findLastIndex, + sortedIndex: sortedIndex, + indexOf: indexOf, + lastIndexOf: lastIndexOf, + find: find, + detect: find, + findWhere: findWhere, + each: each, + forEach: each, + map: map, + collect: map, + reduce: reduce, + foldl: reduce, + inject: reduce, + reduceRight: reduceRight, + foldr: reduceRight, + filter: filter, + select: filter, + reject: reject, + every: every, + all: every, + some: some, + any: some, + contains: contains, + includes: contains, + include: contains, + invoke: invoke, + pluck: pluck, + where: where, + max: max, + min: min, + shuffle: shuffle, + sample: sample, + sortBy: sortBy, + groupBy: groupBy, + indexBy: indexBy, + countBy: countBy, + partition: partition, + toArray: toArray, + size: size, + pick: pick, + omit: omit, + first: first, + head: first, + take: first, + initial: initial, + last: last, + rest: rest, + tail: rest, + drop: rest, + compact: compact, + flatten: flatten$1, + without: without, + uniq: uniq, + unique: uniq, + union: union, + intersection: intersection, + difference: difference, + unzip: unzip, + transpose: unzip, + zip: zip, + object: object, + range: range, + chunk: chunk, + mixin: mixin, + 'default': _ + }; + + // Default Export + + // Add all of the Underscore functions to the wrapper object. + var _$1 = mixin(allExports); + // Legacy Node.js API. + _$1._ = _$1; + + return _$1; + +}))); +//# sourceMappingURL=underscore.js.map + + +/***/ }), + +/***/ 747: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs");; + +/***/ }), + +/***/ 87: +/***/ ((module) => { + +"use strict"; +module.exports = require("os");; + +/***/ }), + +/***/ 622: +/***/ ((module) => { + +"use strict"; +module.exports = require("path");; + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ if(__webpack_module_cache__[moduleId]) { +/******/ return __webpack_module_cache__[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ __nccwpck_require__.ab = __dirname + "/";/************************************************************************/ +/******/ // module exports must be returned from runtime so entry inlining is disabled +/******/ // startup +/******/ // Load entry module and return exports +/******/ return __nccwpck_require__(547); +/******/ })() +; diff --git a/.github/actions/getReleasePullRequestList/getReleasePullRequestList.js b/.github/actions/getReleasePullRequestList/getReleasePullRequestList.js index 7c696e462c75..9c3169dee14e 100644 --- a/.github/actions/getReleasePullRequestList/getReleasePullRequestList.js +++ b/.github/actions/getReleasePullRequestList/getReleasePullRequestList.js @@ -7,25 +7,32 @@ const octokit = github.getOctokit(core.getInput('GITHUB_TOKEN', {required: true} const inputTag = core.getInput('TAG', {required: true}); console.log('Fetching release list from github...'); -octokit.repos.listTags({ +octokit.repos.listReleases({ owner: github.context.repo.owner, repo: github.context.repo.repo, }) .catch(githubError => core.setFailed(githubError)) .then(({data}) => { - const tags = _.pluck(data, 'name'); - + const tags = _.pluck(data, 'tag_name'); const priorTagIndex = _.indexOf(tags, inputTag) + 1; - if (priorTagIndex === 0 || priorTagIndex === tags.length) { - core.setFailed('Given tag is not found in the last 30 release tags.'); + + if (priorTagIndex === 0) { + console.log(`No release was found for input tag ${inputTag}. Comparing it to latest release ${tags[0]}`); + } + + if (priorTagIndex === tags.length) { + const err = new Error('Somehow, the input tag was at the end of the paginated result, ' + + "so we don't have the prior tag."); + console.error(err.message); + core.setFailed(err); return; } + const priorTag = tags[priorTagIndex]; console.log(`Given Release Tag: ${inputTag}`); console.log(`Prior Release Tag: ${priorTag}`); - const gitUtils = new GitUtils(); - return gitUtils.getPullRequestsMergedBetween(priorTag, inputTag); + return GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); }) .then(pullRequestList => core.setOutput('PR_LIST', pullRequestList)) .catch(error => core.setFailed(error)); diff --git a/.github/actions/getReleasePullRequestList/index.js b/.github/actions/getReleasePullRequestList/index.js index 0cdff738ab98..cf1a30f951e5 100644 --- a/.github/actions/getReleasePullRequestList/index.js +++ b/.github/actions/getReleasePullRequestList/index.js @@ -17,25 +17,32 @@ const octokit = github.getOctokit(core.getInput('GITHUB_TOKEN', {required: true} const inputTag = core.getInput('TAG', {required: true}); console.log('Fetching release list from github...'); -octokit.repos.listTags({ +octokit.repos.listReleases({ owner: github.context.repo.owner, repo: github.context.repo.repo, }) .catch(githubError => core.setFailed(githubError)) .then(({data}) => { - const tags = _.pluck(data, 'name'); - + const tags = _.pluck(data, 'tag_name'); const priorTagIndex = _.indexOf(tags, inputTag) + 1; - if (priorTagIndex === 0 || priorTagIndex === tags.length) { - core.setFailed('Given tag is not found in the last 30 release tags.'); + + if (priorTagIndex === 0) { + console.log(`No release was found for input tag ${inputTag}. Comparing it to latest release ${tags[0]}`); + } + + if (priorTagIndex === tags.length) { + const err = new Error('Somehow, the input tag was at the end of the paginated result, ' + + "so we don't have the prior tag."); + console.error(err.message); + core.setFailed(err); return; } + const priorTag = tags[priorTagIndex]; console.log(`Given Release Tag: ${inputTag}`); console.log(`Prior Release Tag: ${priorTag}`); - const gitUtils = new GitUtils(); - return gitUtils.getPullRequestsMergedBetween(priorTag, inputTag); + return GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); }) .then(pullRequestList => core.setOutput('PR_LIST', pullRequestList)) .catch(error => core.setFailed(error)); diff --git a/.github/actions/isPullRequestMergeable/index.js b/.github/actions/isPullRequestMergeable/index.js index 3f4a6469ff1d..2966fd94887d 100644 --- a/.github/actions/isPullRequestMergeable/index.js +++ b/.github/actions/isPullRequestMergeable/index.js @@ -428,6 +428,19 @@ class GithubUtils { }); } + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + /** * Generate the URL of an Expensify.cash pull request given the PR number. * diff --git a/.github/actions/isStagingDeployLocked/index.js b/.github/actions/isStagingDeployLocked/index.js index 39a47d553a54..658fb3990c76 100644 --- a/.github/actions/isStagingDeployLocked/index.js +++ b/.github/actions/isStagingDeployLocked/index.js @@ -406,6 +406,19 @@ class GithubUtils { }); } + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + /** * Generate the URL of an Expensify.cash pull request given the PR number. * diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js index 16f50a2e15ec..188ead40240d 100644 --- a/.github/actions/markPullRequestsAsDeployed/index.js +++ b/.github/actions/markPullRequestsAsDeployed/index.js @@ -411,6 +411,19 @@ class GithubUtils { }); } + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + /** * Generate the URL of an Expensify.cash pull request given the PR number. * diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index d28b221fd858..4e6984e91bfd 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -364,6 +364,19 @@ class GithubUtils { }); } + /** + * Generate the well-formatted body of a production release. + * + * @param {Array} pullRequests + * @returns {String} + */ + static getReleaseBody(pullRequests) { + return _.map( + pullRequests, + number => `- ${this.getPullRequestURLFromNumber(number)}`, + ).join('\r\n'); + } + /** * Generate the URL of an Expensify.cash pull request given the PR number. * diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index e10671d849f9..bdcff5ab7882 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -11,6 +11,7 @@ ACTIONS_DIR="$(dirname "$(dirname "$0")")/actions" declare -r GITHUB_ACTIONS=( "$ACTIONS_DIR/bumpVersion/bumpVersion.js" "$ACTIONS_DIR/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js" + "$ACTIONS_DIR/getReleaseBody/getReleaseBody.js" "$ACTIONS_DIR/getReleasePullRequestList/getReleasePullRequestList.js" "$ACTIONS_DIR/isPullRequestMergeable/isPullRequestMergeable.js" "$ACTIONS_DIR/isStagingDeployLocked/isStagingDeployLocked.js" diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index dfed77104a17..2785310c5574 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,9 +13,7 @@ jobs: if: github.actor == 'OSBotify' runs-on: ubuntu-latest env: -# TODO: Uncomment when we'd like to deploy to production -# SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} - SHOULD_DEPLOY_PRODUCTION: ${{ false }} + SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} steps: - uses: actions/checkout@v2 @@ -47,10 +45,13 @@ jobs: LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - name: Run Fastlane beta + if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'false' }} run: bundle exec fastlane android beta + # TODO: uncomment when we want to release iOS to production - name: Run Fastlane production - if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }} +# if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }} + if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' && 'false' == 'true' }} run: bundle exec fastlane android production env: VERSION: ${{ env.VERSION }} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 51f10ebe3c6e..098b00e26223 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -9,9 +9,10 @@ on: jobs: getPullRequestMergeability: + if: github.actor == 'OSBotify' && github.event.label.name == 'automerge' runs-on: ubuntu-latest outputs: - isMergeable: ${{ steps.isPullRequestMergeable.IS_MERGEABLE }} + isMergeable: ${{ steps.isPullRequestMergeable.outputs.IS_MERGEABLE }} steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f with: @@ -25,6 +26,31 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + - name: Fail workflow if PR is not mergeable + if: ${{ steps.isPullRequestMergeable.outputs.IS_MERGEABLE == 'false' }} + run: exit 1 + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + master: runs-on: ubuntu-latest needs: getPullRequestMergeability @@ -42,12 +68,11 @@ jobs: uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd with: github-token: ${{ secrets.GITHUB_TOKEN }} - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && steps.changed.outputs.files_updated == 'android/app/build.gradle ios/ExpensifyCash/Info.plist ios/ExpensifyCashTests/Info.plist package-lock.json package.json' && steps.changed.outputs.files_created == '' && steps.changed.outputs.files_deleted == '' + if: steps.changed.outputs.files_updated == 'android/app/build.gradle ios/ExpensifyCash/Info.plist ios/ExpensifyCashTests/Info.plist package-lock.json package.json' && steps.changed.outputs.files_created == '' && steps.changed.outputs.files_deleted == '' - name: Check for an auto merge # Version: 0.12.0 uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && github.event.pull_request.mergeable_state == 'clean' env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -82,15 +107,11 @@ jobs: uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd with: github-token: ${{ secrets.GITHUB_TOKEN }} - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && github.event.pull_request.head.ref == 'master' - - - name: Check PR mergable states - run: echo "Mergeable - ${{ github.event.pull_request.mergeable }} Clean - ${{ github.event.pull_request.mergeable_state }}" + if: github.event.pull_request.head.ref == 'master' - name: Check for an auto merge # Version: 0.12.0 uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && github.event.pull_request.mergeable_state == 'clean' env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -125,12 +146,11 @@ jobs: uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd with: github-token: ${{ secrets.GITHUB_TOKEN }} - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && github.event.pull_request.head.ref == 'staging' + if: github.event.pull_request.head.ref == 'staging' - name: Check for an auto merge # Version: 0.12.0 uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 - if: needs.getPullRequestMergeability.outputs.isMergeable == 'true' && github.event.pull_request.mergeable_state == 'clean' env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d342c52797f7..aac552bdd88b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,13 +27,36 @@ jobs: if: ${{ needs.validate.outputs.isAutomergePR == 'true' && github.ref == 'refs/heads/production' }} steps: - - name: Checkout production - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f with: - ref: production + fetch-depth: 0 token: ${{ secrets.OS_BOTIFY_TOKEN }} + - name: Checkout production branch + run: git checkout production + + - name: Get current app version + run: echo "PRODUCTION_VERSION=$(npm run print-version --silent)" >> $GITHUB_ENV + + - name: Get Release Pull Request List + id: getReleasePRList + uses: Expensify/Expensify.cash/.github/actions/getReleasePullRequestList@master + with: + TAG: ${{ env.PRODUCTION_VERSION }} + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Generate Release Body + id: getReleaseBody + uses: Expensify/Expensify.cash/.github/actions/getReleaseBody@master + with: + PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} + - name: 🚀 Create release to trigger production deploy 🚀 - run: echo "Create release with version $(npm run print-version --silent)" + uses: softprops/action-gh-release@affa18ef97bc9db20076945705aba8c516139abd + with: + tag_name: ${{ env.PRODUCTION_VERSION }} + body: ${{ steps.getReleaseBody.outputs.RELEASE_BODY }} + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 523b17acac0f..9685e74a56f7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,7 +34,11 @@ jobs: run: npm install -g detox-cli - name: Install cocoapods - run: cd ios && pod install --repo-update + uses: nick-invision/retry@7c68161adf97a48beb850a595b8784ec57a98cbb + with: + timeout_minutes: 15 + max_attempts: 3 + command: cd ios && pod install --repo-update - name: Install brew depdencies run: | diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml new file mode 100644 index 000000000000..afa5496bb2d5 --- /dev/null +++ b/.github/workflows/finishReleaseCycle.yml @@ -0,0 +1,142 @@ +name: Prepare production deploy + +on: + issues: + types: [closed] + +# The updateProduction and createNewStagingDeployCash jobs are executed when a StagingDeployCash is closed. +jobs: + # Update the production branch to trigger the production deploy. + updateProduction: + runs-on: ubuntu-latest + + # Note: Anyone with Triage access to the Expensify.cash repo can trigger a production deploy + if: contains(github.event.issue.labels.*.name, 'StagingDeployCash') + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + ref: staging + fetch-depth: 0 + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Set up git config + run: git config user.name OSBotify + + - name: Set new version for production branch + run: echo "PRODUCTION_VERSION=$(npm run print-version --silent)" >> $GITHUB_ENV + + - name: Create Pull Request (production) + # Version: 2.4.3 + uses: repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d + with: + source_branch: staging + destination_branch: production + pr_label: automerge + github_token: ${{ secrets.OS_BOTIFY_TOKEN }} + pr_title: Update version to ${{ env.PRODUCTION_VERSION }} on production + pr_body: Update version to ${{ env.PRODUCTION_VERSION }} + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + # Deploy deferred PRs to staging and create a new StagingDeployCash for the next release cycle. + createNewStagingDeployCash: + runs-on: macos-latest + if: contains(github.event.issue.labels.*.name, 'StagingDeployCash') + steps: + # Version: 2.3.4 + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + ref: master + fetch-depth: 0 + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - uses: softprops/turnstyle@8db075d65b19bf94e6e8687b504db69938dc3c65 + with: + poll-interval-seconds: 10 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create a new branch off master + run: | + git checkout -b version-bump-${{ github.sha }} + git push --set-upstream origin version-bump-${{ github.sha }} + + - name: Generate a new version + id: bumpVersion + uses: Expensify/Expensify.cash/.github/actions/bumpVersion@master + with: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Commit new version + run: | + git add \ + ./package.json \ + ./package-lock.json \ + ./android/app/build.gradle \ + ./ios/ExpensifyCash/Info.plist \ + ./ios/ExpensifyCashTests/Info.plist + git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" + + - name: Create Pull Request (master) + uses: peter-evans/create-pull-request@09b9ac155b0d5ad7d8d157ed32158c1b73689109 + with: + token: ${{ secrets.OS_BOTIFY_TOKEN }} + author: OSBotify + base: master + branch: version-bump-${{ github.sha }} + title: Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }} on master + body: Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }} + labels: automerge + + - name: Tag version + run: git tag ${{ steps.bumpVersion.outputs.NEW_VERSION }} + + #TODO: Once we add cherry picking, we will need run this from elsewhere + - name: 🚀 Push tags to trigger staging deploy 🚀 + run: git push --tags + + - name: Create new StagingDeployCash + uses: Expensify/Expensify.cash/.github/actions/createOrUpdateStagingDeploy@master + with: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + NPM_VERSION: ${{ steps.bumpVersion.outputs.NEW_VERSION }} + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index dee19d46c5e5..627e0bec28bd 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -16,9 +16,7 @@ jobs: if: github.actor == 'OSBotify' runs-on: macos-latest env: -# TODO: Uncomment when we'd like to deploy to production7 -# SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} - SHOULD_DEPLOY_PRODUCTION: ${{ false }} + SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} steps: - uses: actions/checkout@v2 @@ -63,6 +61,7 @@ jobs: LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - name: Run Fastlane + if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'false' }} run: bundle exec fastlane ios beta env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} @@ -70,8 +69,10 @@ jobs: APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} + # TODO: uncomment when we want to release iOS to production - name: Run Fastlane for App Store release - if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }} +# if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }} + if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' && 'false' == 'true' }} run: bundle exec fastlane ios production env: VERSION: ${{ env.NEW_IOS_VERSION }} diff --git a/.github/workflows/lockDeploys.yml b/.github/workflows/lockDeploys.yml new file mode 100644 index 000000000000..b4c9a84ad75f --- /dev/null +++ b/.github/workflows/lockDeploys.yml @@ -0,0 +1,111 @@ +name: Lock staging deploys during QA + +on: + issues: + types: [labeled] + +jobs: + lockStagingDeploys: + if: ${{ github.event.label.name == '🔐 LockCashDeploys 🔐' && contains(github.event.issue.labels.*.name, 'StagingDeployCash') && github.actor != 'OSBotify' }} + runs-on: macos-latest + steps: + # Version: 2.3.4 + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + ref: master + fetch-depth: 0 + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Wait for any preDeploy version jobs to finish + uses: tomchv/wait-my-workflow@2da0b8a92211e6d7c9964602b99a7052080a1d61 + with: + token: ${{ secrets.GITHUB_TOKEN }} + checkName: version + intervalSeconds: 10 + timeoutSeconds: 360 + + - name: Wait for any automerge jobs to start + run: sleep 60 + + - name: Wait for any automerge-master jobs to finish + uses: tomchv/wait-my-workflow@2da0b8a92211e6d7c9964602b99a7052080a1d61 + with: + token: ${{ secrets.GITHUB_TOKEN }} + checkName: master + intervalSeconds: 10 + timeoutSeconds: 150 + + - uses: softprops/turnstyle@8db075d65b19bf94e6e8687b504db69938dc3c65 + with: + poll-interval-seconds: 10 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create a new branch + run: | + git config user.name OSBotify + git pull origin master + git checkout -b version-patch-${{ github.sha }} + git push --set-upstream origin version-patch-${{ github.sha }} + + - name: Generate version + id: bumpVersion + uses: Expensify/Expensify.cash/.github/actions/bumpVersion@master + with: + SEMVER_LEVEL: PATCH + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Commit new version + run: | + git add \ + ./package.json \ + ./package-lock.json \ + ./android/app/build.gradle \ + ./ios/ExpensifyCash/Info.plist \ + ./ios/ExpensifyCashTests/Info.plist + git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" + + - name: Create Pull Request (master) + uses: peter-evans/create-pull-request@09b9ac155b0d5ad7d8d157ed32158c1b73689109 + with: + token: ${{ secrets.OS_BOTIFY_TOKEN }} + author: OSBotify + base: master + branch: version-patch-${{ github.sha }} + title: Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }} on master + body: Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }} + labels: automerge + + - name: Tag version + run: git tag ${{ steps.bumpVersion.outputs.NEW_VERSION }} + + # TODO: Once we add cherry picking, we will need run this from the deploy workflow + - name: 🚀 Push tags to trigger staging deploy 🚀 + run: git push --tags + + - name: Update StagingDeployCash + uses: Expensify/Expensify.cash/.github/actions/createOrUpdateStagingDeploy@master + with: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + NPM_VERSION: ${{ steps.bumpVersion.outputs.NEW_VERSION }} + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index d0bc8c57ebd4..a61096adc7b7 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -107,6 +107,14 @@ jobs: body: Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }} labels: automerge + - name: Tag version + run: git tag ${{ steps.bumpVersion.outputs.NEW_VERSION }} + + #TODO: Once we add cherry picking, we will need run this from elsewhere + - name: 🚀 Push tags to trigger staging deploy 🚀 + if: ${{ needs.chooseDeployActions.outputs.isStagingDeployLocked == 'false' }} + run: git push --tags + - name: Update StagingDeployCash uses: Expensify/Expensify.cash/.github/actions/createOrUpdateStagingDeploy@master with: @@ -114,13 +122,6 @@ jobs: NPM_VERSION: ${{ steps.bumpVersion.outputs.NEW_VERSION }} NEW_PULL_REQUESTS: https://github.com/Expensify/Expensify.cash/pull/${{ needs.chooseDeployActions.outputs.mergedPullRequest }} - #TODO: Once we add cherry picking, we will need run this from elsewhere - - name: Tag version - run: git tag ${{ steps.bumpVersion.outputs.NEW_VERSION }} - - - name: 🚀 Push tags to trigger staging deploy 🚀 - run: git push --tags - # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all # the other workflows with the same change - uses: 8398a7/action-slack@v3 @@ -159,7 +160,7 @@ jobs: - name: Set Staging Version run: echo "STAGING_VERSION=$(npm run print-version --silent)" >> $GITHUB_ENV - - name: Create Pull Request + - name: Create Pull Request (staging) # Version: 2.4.3 uses: repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb00f06c95de..432d980d7119 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,7 @@ We are currently managing payment via Upwork. If you'd like to be paid for your 1. CLA - You must sign our [Contributor License Agreement](https://github.com/Expensify/Expensify.cash/blob/master/CLA.md) by following the CLA bot instructions that will be posted on your PR 1. Tests - All tests must pass before a merge of a pull request 1. Lint - All code must pass lint checks before a merge of a pull request +1. Please never force push when a PR review has already started (because this messes with the PR review history) #### Testing Upon submission of a PR, please include a numbered list of explicit testing steps for each platform (Web, Desktop, iOS, and Android) to confirm the fix works as expected and there are no regressions. diff --git a/Gemfile.lock b/Gemfile.lock index 4e0a01222f40..8bedb55fa944 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,16 +15,16 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.1.1) - aws-partitions (1.431.1) - aws-sdk-core (3.112.1) + aws-partitions (1.434.0) + aws-sdk-core (3.113.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.42.0) + aws-sdk-kms (1.43.0) aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.89.0) + aws-sdk-s3 (1.92.0) aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -97,7 +97,7 @@ GEM faraday_middleware (1.0.0) faraday (~> 1.0) fastimage (2.2.3) - fastlane (2.176.0) + fastlane (2.178.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) artifactory (~> 3.0) @@ -147,7 +147,7 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-apis-core (0.2.1) + google-apis-core (0.3.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.14) httpclient (>= 2.8.1, < 3.0) @@ -157,17 +157,17 @@ GEM rexml signet (~> 0.14) webrick - google-apis-iamcredentials_v1 (0.1.0) + google-apis-iamcredentials_v1 (0.2.0) google-apis-core (~> 0.1) - google-apis-storage_v1 (0.2.0) + google-apis-storage_v1 (0.3.0) google-apis-core (~> 0.1) - google-cloud-core (1.5.0) + google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.4.0) + google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.0.1) - google-cloud-storage (1.30.0) + google-cloud-errors (1.1.0) + google-cloud-storage (1.31.0) addressable (~> 2.5) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) diff --git a/android/app/build.gradle b/android/app/build.gradle index 59bbf728e10a..5f76659cd80a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -148,8 +148,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001000271 - versionName "1.0.2-71" + versionCode 1001000504 + versionName "1.0.5-4" } splits { abi { diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist index fbe1a735c445..8a09ebcea1e1 100644 --- a/ios/ExpensifyCash/Info.plist +++ b/ios/ExpensifyCash/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.2 + 1.0.5 CFBundleSignature ???? CFBundleVersion - 1.0.2.71 + 1.0.5.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist index b3663c7c9655..896ccc168a8b 100644 --- a/ios/ExpensifyCashTests/Info.plist +++ b/ios/ExpensifyCashTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.0.2 + 1.0.5 CFBundleSignature ???? CFBundleVersion - 1.0.2.71 + 1.0.5.4 diff --git a/package-lock.json b/package-lock.json index 98b24e8d9ea5..55270dbd72db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.2-71", + "version": "1.0.5-4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 88bc87878000..92149bd3ef0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.2-71", + "version": "1.0.5-4", "author": "Expensify, Inc.", "homepage": "https://expensify.cash", "description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.js b/src/CONST.js index 6e4841f27de4..906d90effbc9 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -20,7 +20,12 @@ const CONST = { }, REPORT: { MAXIMUM_PARTICIPANTS: 8, - REPORT_ACTIONS_LIMIT: 50, + ACTIONS: { + LIMIT: 50, + TYPE: { + IOU: 'IOU', + }, + }, }, MODAL: { MODAL_TYPE: { @@ -66,6 +71,9 @@ const CONST = { TIMEZONE: 'timeZone', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, + + // at least 8 characters, 1 capital letter, 1 lowercase number, 1 number + PASSWORD_COMPLEXITY_REGEX_STRING: '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$', }; export default CONST; diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 8edd302fb8c6..883047058594 100644 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -35,9 +35,6 @@ export default { // Contains all the personalDetails the user has access to PERSONAL_DETAILS: 'personalDetails', - // Contains the user preference for the LHN priority mode - PRIORITY_MODE: 'priorityMode', - // Indicates whether an update is available and ready to beinstalled. UPDATE_AVAILABLE: 'updateAvailable', @@ -53,13 +50,18 @@ export default { BETAS: 'betas', // NVP keys + // Contains the user's payPalMe address NVP_PAYPAL_ME_ADDRESS: 'nvp_paypalMeAddress', + // Contains the user preference for the LHN priority mode + NVP_PRIORITY_MODE: 'nvp_priorityMode', + // Collection Keys COLLECTION: { REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', + REPORT_IOUS: 'reportIOUs_', }, }; diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js new file mode 100644 index 000000000000..3f12fdacaa62 --- /dev/null +++ b/src/components/BigNumberPad.js @@ -0,0 +1,75 @@ +import React, {PureComponent} from 'react'; +import { + Text, TouchableOpacity, View, +} from 'react-native'; +import PropTypes from 'prop-types'; +import styles from '../styles/styles'; + +const propTypes = { + // Callback to inform parent modal with key pressed + numberPressed: PropTypes.func.isRequired, +}; + +const padNumbers = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', '8', '9'], + ['.', '0', '<'], +]; + +class BigNumberPad extends PureComponent { + /** + * Creates set of buttons for given row + * + * @param {number} row + * @returns {View} + */ + createNumberPadRow(row) { + const self = this; + const numberPadRow = padNumbers[row].map((column, index) => self.createNumberPadButton(row, index)); + return ( + + {numberPadRow} + + ); + } + + /** + * Creates a button for given row and column + * + * @param {number} row + * @param {number} column + * @returns {View} + */ + createNumberPadButton(row, column) { + // Adding margin between buttons except first column to + // avoid unccessary space before the first column. + const marginLeft = column > 0 ? styles.ml3 : {}; + return ( + this.props.numberPressed(padNumbers[row][column])} + > + + {padNumbers[row][column]} + + + ); + } + + render() { + const self = this; + const numberPad = padNumbers.map((row, index) => self.createNumberPadRow(index)); + return ( + + {numberPad} + + ); + } +} + +BigNumberPad.propTypes = propTypes; +BigNumberPad.displayName = 'BigNumberPad'; + +export default BigNumberPad; diff --git a/src/components/ButtonWithLoader.js b/src/components/ButtonWithLoader.js index ce3dc7f545ad..915fbdadab12 100644 --- a/src/components/ButtonWithLoader.js +++ b/src/components/ButtonWithLoader.js @@ -13,19 +13,23 @@ const propTypes = { // Indicates whether the button should be disabled and in the loading state isLoading: PropTypes.bool, + // Indicates whether the button should be disabled + isDisabled: PropTypes.bool, + // A function that is called when the button is clicked on onClick: PropTypes.func.isRequired, }; const defaultProps = { isLoading: false, + isDisabled: false, }; const ButtonWithLoader = props => ( {props.isLoading ? ( diff --git a/src/components/Hoverable/HoverablePropTypes.js b/src/components/Hoverable/HoverablePropTypes.js index 2d2842be19c5..9125e9c8669a 100644 --- a/src/components/Hoverable/HoverablePropTypes.js +++ b/src/components/Hoverable/HoverablePropTypes.js @@ -7,6 +7,10 @@ const propTypes = { PropTypes.func, ]).isRequired, + // Styles to be assigned to the Hoverable Container + // eslint-disable-next-line react/forbid-prop-types + containerStyle: PropTypes.object, + // Function that executes when the mouse moves over the children. onHoverIn: PropTypes.func, @@ -15,6 +19,7 @@ const propTypes = { }; const defaultProps = { + containerStyle: {}, onHoverIn: () => {}, onHoverOut: () => {}, }; diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index da6463561b2b..c67b26c3f2de 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -57,6 +57,7 @@ class Hoverable extends Component { render() { return ( this.wrapperView = el} onMouseEnter={() => this.setIsHovered(true)} onMouseLeave={() => this.setIsHovered(false)} diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js index 7f759d705788..b783cca19866 100644 --- a/src/components/OptionsList.js +++ b/src/components/OptionsList.js @@ -61,6 +61,9 @@ const propTypes = { PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(SectionList)}), ]), + + // Whether to show the title tooltip + showTitleTooltip: PropTypes.bool, }; const defaultProps = { @@ -77,6 +80,7 @@ const defaultProps = { onSelectRow: () => {}, headerMessage: '', innerRef: null, + showTitleTooltip: false, }; class OptionsList extends Component { @@ -142,6 +146,7 @@ class OptionsList extends Component { return ( ); diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index bef213b31d27..f89ea10a211e 100644 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -110,6 +110,7 @@ class TextInputFocusable extends React.Component { } if (prevProps.defaultValue !== this.props.defaultValue) { this.updateNumberOfLines(); + this.moveCursorToEnd(); } } @@ -198,6 +199,19 @@ class TextInputFocusable extends React.Component { }); } + /** + * Move cursor to end by setting start and end + * to length of the input value. + */ + moveCursorToEnd() { + this.setState({ + selection: { + start: this.props.defaultValue.length, + end: this.props.defaultValue.length, + }, + }); + } + focusInput() { this.textInput.focus(); } diff --git a/src/components/Tooltip/TooltipPropTypes.js b/src/components/Tooltip/TooltipPropTypes.js new file mode 100644 index 000000000000..699c6c643fb3 --- /dev/null +++ b/src/components/Tooltip/TooltipPropTypes.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import {windowDimensionsPropTypes} from '../withWindowDimensions'; + +const propTypes = { + // The text to display in the tooltip. + text: PropTypes.string.isRequired, + + // Styles to be assigned to the Tooltip wrapper views + containerStyle: PropTypes.object, + + // Children to wrap with Tooltip. + children: PropTypes.node.isRequired, + + // Props inherited from withWindowDimensions + ...windowDimensionsPropTypes, + + // Any additional amount to manually adjust the horizontal position of the tooltip. + // A positive value shifts the tooltip to the right, and a negative value shifts it to the left. + shiftHorizontal: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + + // Any additional amount to manually adjust the vertical position of the tooltip. + // A positive value shifts the tooltip down, and a negative value shifts it up. + shiftVertical: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), +}; + +const defaultProps = { + shiftHorizontal: 0, + shiftVertical: 0, +}; + +export { + propTypes, + defaultProps, +}; diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js new file mode 100644 index 000000000000..1611147cc08b --- /dev/null +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -0,0 +1,62 @@ +import React, {memo} from 'react'; +import PropTypes from 'prop-types'; +import {Animated, Text, View} from 'react-native'; +import ReactDOM from 'react-dom'; + +const propTypes = { + // Style for Animation + // eslint-disable-next-line react/forbid-prop-types + animationStyle: PropTypes.object.isRequired, + + // Syle for Tooltip wrapper + // eslint-disable-next-line react/forbid-prop-types + tooltipWrapperStyle: PropTypes.object.isRequired, + + // Style for the text rendered inside tooltip + // eslint-disable-next-line react/forbid-prop-types + tooltipTextStyle: PropTypes.object.isRequired, + + // Style for the Tooltip pointer Wrapper + // eslint-disable-next-line react/forbid-prop-types + pointerWrapperStyle: PropTypes.object.isRequired, + + // Style for the Tooltip pointer + // eslint-disable-next-line react/forbid-prop-types + pointerStyle: PropTypes.object.isRequired, + + // Callback to set the Ref to the Tooltip + setTooltipRef: PropTypes.func.isRequired, + + // Text to be shown in the tooltip + text: PropTypes.string.isRequired, + + // Callback to be used to calulate the width and height of tooltip + measureTooltip: PropTypes.func.isRequired, +}; + +const defaultProps = {}; + +const TooltipRenderedOnPageBody = props => ReactDOM.createPortal( + + {props.text} + + + + , + document.querySelector('body'), +); + +TooltipRenderedOnPageBody.propTypes = propTypes; +TooltipRenderedOnPageBody.defaultProps = defaultProps; +TooltipRenderedOnPageBody.displayName = 'TooltipRenderedOnPageBody'; + +// Props will change frequently. +// On every tooltip hover, we update the position in state which will result in re-rendering. +// We also update the state on layout changes which will be triggered often. +// There will be n number of tooltip components in the page. +// Its good to memorize this one. +export default memo(TooltipRenderedOnPageBody); diff --git a/src/components/Tooltip.js b/src/components/Tooltip/index.js similarity index 61% rename from src/components/Tooltip.js rename to src/components/Tooltip/index.js index c72df808e47e..cd38fc3ce0fc 100644 --- a/src/components/Tooltip.js +++ b/src/components/Tooltip/index.js @@ -1,33 +1,11 @@ +import _ from 'underscore'; import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {Animated, Text, View} from 'react-native'; -import Hoverable from './Hoverable'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; -import getTooltipStyles from '../styles/getTooltipStyles'; - -const propTypes = { - // The text to display in the tooltip. - text: PropTypes.string.isRequired, - - // Children to wrap with Tooltip. - children: PropTypes.node.isRequired, - - // Props inherited from withWindowDimensions - ...windowDimensionsPropTypes, - - // Any additional amount to manually adjust the horizontal position of the tooltip. - // A positive value shifts the tooltip to the right, and a negative value shifts it to the left. - shiftHorizontal: PropTypes.number, - - // Any additional amount to manually adjust the vertical position of the tooltip. - // A positive value shifts the tooltip down, and a negative value shifts it up. - shiftVertical: PropTypes.number, -}; - -const defaultProps = { - shiftHorizontal: 0, - shiftVertical: 0, -}; +import {Animated, View} from 'react-native'; +import TooltipRenderedOnPageBody from './TooltipRenderedOnPageBody'; +import Hoverable from '../Hoverable'; +import withWindowDimensions from '../withWindowDimensions'; +import getTooltipStyles from '../../styles/getTooltipStyles'; +import {propTypes, defaultProps} from './TooltipPropTypes'; class Tooltip extends PureComponent { constructor(props) { @@ -56,6 +34,7 @@ class Tooltip extends PureComponent { this.tooltip = null; this.isComponentMounted = false; + this.shouldStartShowAnimation = false; this.animation = new Animated.Value(0); this.getWrapperPosition = this.getWrapperPosition.bind(this); @@ -102,9 +81,13 @@ class Tooltip extends PureComponent { return new Promise(((resolve) => { // Make sure the wrapper is mounted before attempting to measure it. if (this.wrapperView) { - this.wrapperView.measureInWindow((x, y) => resolve({x, y})); + this.wrapperView.measureInWindow((x, y, width, height) => resolve({ + x, y, width, height, + })); } else { - resolve({x: 0, y: 0}); + resolve({ + x: 0, y: 0, width: 0, height: 0, + }); } })); } @@ -144,16 +127,37 @@ class Tooltip extends PureComponent { * Display the tooltip in an animation. */ showTooltip() { - Animated.timing(this.animation, { - toValue: 1, - duration: 140, - }).start(); + this.shouldStartShowAnimation = true; + + // We have to dynamically calculate the position here as tooltip could have been rendered on some elments + // that has changed its position + this.getWrapperPosition() + .then(({ + x, y, width, height, + }) => { + this.setState({ + wrapperWidth: width, + wrapperHeight: height, + xOffset: x, + yOffset: y, + }); + + // We may need this check due to the reason that the animation start will fire async + // and hideTooltip could fire before it thus keeping the Tooltip visible + if (this.shouldStartShowAnimation) { + Animated.timing(this.animation, { + toValue: 1, + duration: 140, + }).start(); + } + }); } /** * Hide the tooltip in an animation. */ hideTooltip() { + this.shouldStartShowAnimation = false; Animated.timing(this.animation, { toValue: 0, duration: 140, @@ -176,34 +180,35 @@ class Tooltip extends PureComponent { this.state.wrapperHeight, this.state.tooltipWidth, this.state.tooltipHeight, - this.props.shiftHorizontal, - this.props.shiftVertical, + _.result(this.props, 'shiftHorizontal'), + _.result(this.props, 'shiftVertical'), ); - return ( - - this.wrapperView = el} - onLayout={this.measureWrapperAndGetPosition} + <> + this.tooltip = el} + measureTooltip={this.measureTooltip} + text={this.props.text} + /> + - - this.tooltip = el} - onLayout={this.measureTooltip} - style={tooltipWrapperStyle} - > - {this.props.text} - - - - - - {this.props.children} - - + this.wrapperView = el} + onLayout={this.measureWrapperAndGetPosition} + style={this.props.containerStyle} + > + {this.props.children} + + + ); } } diff --git a/src/components/Tooltip/index.native.js b/src/components/Tooltip/index.native.js new file mode 100644 index 000000000000..d5092883bbad --- /dev/null +++ b/src/components/Tooltip/index.native.js @@ -0,0 +1,15 @@ +// We can't use the common component for the Tooltip as Web implementation uses DOM specific method to +// render the View which is not present on the Mobile. +import {propTypes, defaultProps} from './TooltipPropTypes'; + +/** + * There is no native support for the Hover on the Mobile platform so we just return the enclosing childrens + * @param {propTypes} props + * @returns {ReactNodeLike} + */ +const Tooltip = props => props.children; + +Tooltip.propTypes = propTypes; +Tooltip.defaultProps = defaultProps; +Tooltip.displayName = 'Tooltip'; +export default Tooltip; diff --git a/src/libs/API.js b/src/libs/API.js index 852dee8497d8..d3b7d9378519 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -605,7 +605,6 @@ function SetNameValuePair(parameters) { } /** - * * @param {Object} parameters * @param {String[]} data * @returns {Promise} @@ -621,6 +620,17 @@ function Mobile_GetConstants(parameters) { return Network.post(commandName, finalParameters); } +/** + * @param {Object} parameters + * @param {String} parameters.debtorEmail + * @returns {Promise} + */ +function GetIOUReport(parameters) { + const commandName = 'GetIOUReport'; + requireParameters(['debtorEmail'], parameters, commandName); + return Network.post(commandName, parameters); +} + export { getAuthToken, Authenticate, @@ -630,6 +640,7 @@ export { DeleteLogin, Get, GetAccountStatus, + GetIOUReport, GetRequestCountryCode, Graphite_Timer, Log, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 397345801370..d89e0375b422 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -23,7 +23,7 @@ import CONFIG from '../../../CONFIG'; import {fetchCountryCodeByRequestIP} from '../../actions/GeoLocation'; import KeyboardShortcut from '../../KeyboardShortcut'; import Navigation from '../Navigation'; -import {getBetas} from '../../actions/User'; +import * as User from '../../actions/User'; import NameValuePair from '../../actions/NameValuePair'; // Main drawer navigator @@ -69,10 +69,10 @@ class AuthScreens extends React.Component { }).then(subscribeToReportCommentEvents); // Fetch some data we need on initialization - NameValuePair.get(CONST.NVP.PRIORITY_MODE, ONYXKEYS.PRIORITY_MODE, 'default'); + NameValuePair.get(CONST.NVP.PRIORITY_MODE, ONYXKEYS.NVP_PRIORITY_MODE, 'default'); PersonalDetails.fetch(); - PersonalDetails.fetchTimezone(); - getBetas(); + User.fetch(); + User.getBetas(); fetchAllReports(true, true); fetchCountryCodeByRequestIP(); UnreadIndicatorUpdater.listenForReportChanges(); @@ -85,8 +85,8 @@ class AuthScreens extends React.Component { return; } PersonalDetails.fetch(); - PersonalDetails.fetchTimezone(); - getBetas(); + User.fetch(); + User.getBetas(); }, 1000 * 60 * 30); Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f6546c64a3e7..e919f280e3db 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -7,6 +7,7 @@ import Str from 'expensify-common/lib/str'; import {getDefaultAvatar} from './actions/PersonalDetails'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; +import {getReportParticipantsTitle} from './reportUtils'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -92,11 +93,14 @@ function createOption(personalDetailList, report, draftComments, activeReportID, : '') + _.unescape(report.lastMessageText) : ''; + const tooltipText = getReportParticipantsTitle(lodashGet(report, ['participants'], [])); return { text: report ? report.reportName : personalDetail.displayName, alternateText: (showChatPreviewLine && lastMessageText) ? lastMessageText : personalDetail.login, icons: report ? report.icons : [personalDetail.avatar], + tooltipText, + participantsList: personalDetailList, // It doesn't make sense to provide a login in the case of a report with multiple participants since // there isn't any one single login to refer to for a report. @@ -424,4 +428,5 @@ export { getNewGroupOptions, getSidebarOptions, getHeaderMessage, + getPersonalDetailsForLogins, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 292873979bdf..4faccd19f686 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -88,6 +88,7 @@ function formatPersonalDetails(personalDetailsList) { const avatar = getAvatar(personalDetailsResponse, login); const displayName = getDisplayName(login, personalDetailsResponse); const pronouns = lodashGet(personalDetailsResponse, 'pronouns', ''); + const timezone = lodashGet(personalDetailsResponse, 'timeZone', CONST.DEFAULT_TIME_ZONE); return { ...finalObject, @@ -96,26 +97,12 @@ function formatPersonalDetails(personalDetailsList) { avatar, displayName, pronouns, + timezone, }, }; }, {}); } -/** - * Get the timezone of the logged in user - */ -function fetchTimezone() { - API.Get({ - returnValueList: 'nameValuePairs', - name: 'timeZone', - }) - .then((response) => { - const timezone = lodashGet(response.nameValuePairs, [CONST.NVP.TIMEZONE], CONST.DEFAULT_TIME_ZONE); - Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, {timezone}); - }) - .catch(error => console.debug('Error fetching user timezone', error)); -} - /** * Get the personal details for our organization */ @@ -240,7 +227,6 @@ NetworkConnection.onReconnect(fetch); export { fetch, - fetchTimezone, getFromReportParticipants, getDisplayName, getDefaultAvatar, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 11988ae99b21..668000529b7e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -152,6 +152,102 @@ function getSimplifiedReportObject(report) { }; } +/** + * Get a simplified version of an IOU report + * + * @param {Object} reportData + * @param {Number} reportData.transactionID + * @param {Number} reportData.amount + * @param {String} reportData.currency + * @param {String} reportData.created + * @param {String} reportData.comment + * @param {Object[]} reportData.transactionList + * @param {String} reportData.ownerEmail + * @param {String} reportData.managerEmail + * @param {Number} reportData.reportID + * @returns {Object} + */ +function getSimplifiedIOUReport(reportData) { + const transactions = _.map(reportData.transactionList, transaction => ({ + transactionID: transaction.transactionID, + amount: transaction.amount, + currency: transaction.currency, + created: transaction.created, + comment: transaction.comment, + })); + + return { + reportID: reportData.reportID, + ownerEmail: reportData.ownerEmail, + managerEmail: reportData.managerEmail, + currency: reportData.currency, + transactions, + }; +} + +/** + * Fetches the updated data for an IOU Report and updates the IOU collection in Onyx + * + * @param {Object} report + * @param {Object[]} report.reportActionList + * @param {Number} report.reportID + */ +function updateIOUReportData(report) { + const reportActionList = report.reportActionList || []; + const containsIOUAction = _.any(reportActionList, + reportAction => reportAction.action === CONST.REPORT.ACTIONS.TYPE.IOU); + + // If there aren't any IOU actions, we don't need to fetch any additional data + if (!containsIOUAction) { + return; + } + + // If we don't have one participant (other than the current user), this is not an IOU + const participants = getParticipantEmailsFromReport(report); + if (participants.length !== 1) { + Log.alert('[Report] Report with IOU action has more than 2 participants', true, { + reportID: report.reportID, + participants, + }); + return; + } + + // Since the Chat and the IOU are different reports with different reportIDs, and GetIOUReport only returns the + // IOU's reportID, keep track of the IOU's reportID so we can use it to get the IOUReport data via `GetReportStuff` + let iouReportID = 0; + API.GetIOUReport({ + debtorEmail: participants[0], + }).then((response) => { + iouReportID = response.reportID || 0; + if (response.jsonCode !== 200) { + throw new Error(response.message); + } else if (iouReportID === 0) { + throw new Error('GetIOUReport returned a reportID of 0, not fetching IOU report data'); + } + + return API.Get({ + returnValueList: 'reportStuff', + reportIDList: iouReportID, + shouldLoadOptionalKeys: true, + includePinnedReports: true, + }); + }).then((response) => { + if (response.jsonCode !== 200) { + throw new Error(response.message); + } + + const iouReportData = response.reports[iouReportID]; + if (!iouReportData) { + throw new Error(`No iouReportData found for reportID ${iouReportID}`); + } + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportID}`, + getSimplifiedIOUReport(iouReportData)); + }).catch((error) => { + console.debug(`[Report] Failed to populate IOU Collection: ${error.message}`); + }); +} + /** * Fetches chat reports when provided a list of * chat report IDs @@ -178,7 +274,9 @@ function fetchChatReportsByIDs(chatList) { const simplifiedReports = {}; _.each(fetchedReports, (report) => { const key = `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`; - simplifiedReports[key] = getSimplifiedReportObject(report); + const simplifiedReport = getSimplifiedReportObject(report); + simplifiedReports[key] = simplifiedReport; + updateIOUReportData(report); }); // We use mergeCollection such that it updates ONYXKEYS.COLLECTION.REPORT in one go. @@ -518,7 +616,7 @@ function fetchActions(reportID, offset) { return API.Report_GetHistory({ reportID, reportActionsOffset, - reportActionsLimit: CONST.REPORT.REPORT_ACTIONS_LIMIT, + reportActionsLimit: CONST.REPORT.ACTIONS.LIMIT, }) .then((data) => { // We must remove all optimistic actions so there will not be any stuck comments. At this point, we should @@ -651,7 +749,7 @@ function addAction(reportID, text, file) { API.Report_AddComment({ reportID, - reportComment: htmlForNewComment, + reportComment: commentText, file, clientID: optimisticReportActionID, diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 1fba0d94f5c0..a7a3b4074ebc 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -3,7 +3,6 @@ import lodashGet from 'lodash.get'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; -import {signIn} from './Session'; import CONST from '../../CONST'; /** @@ -11,15 +10,23 @@ import CONST from '../../CONST'; * * @param {String} oldPassword * @param {String} password - * @param {String} [twoFactorAuthCode] + * @returns {Promise} */ -function changePassword(oldPassword, password, twoFactorAuthCode) { - API.ChangePassword({oldPassword, password}).then((response) => { - // If we've successfully authenticated the user, ensure we sign them in so they don't get booted out - if (response.jsonCode === 200) { - signIn(password, twoFactorAuthCode); - } - }); +function changePassword(oldPassword, password) { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: '', loading: true}); + + return API.ChangePassword({oldPassword, password}) + .then((response) => { + if (response.jsonCode !== 200) { + const error = lodashGet(response, 'message', 'Unable to change password. Please try again.'); + Onyx.merge(ONYXKEYS.ACCOUNT, {error}); + } + return response; + }) + .finally((response) => { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); + return response; + }); } function getBetas() { diff --git a/src/libs/hasEllipsis/index.js b/src/libs/hasEllipsis/index.js new file mode 100644 index 000000000000..d18af237be35 --- /dev/null +++ b/src/libs/hasEllipsis/index.js @@ -0,0 +1,11 @@ +/** + * Does an elment have ellipsis + * + * @param {HTMLElement} el Element to check + * @returns {Boolean} + */ +function hasEllipsis(el) { + return el.offsetWidth < el.scrollWidth; +} + +export default hasEllipsis; diff --git a/src/libs/hasEllipsis/index.native.js b/src/libs/hasEllipsis/index.native.js new file mode 100644 index 000000000000..2d1ec238274a --- /dev/null +++ b/src/libs/hasEllipsis/index.native.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index a93c50bb6455..8b0d428076c2 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -1,4 +1,5 @@ import RenameActiveClientsKey from './migrations/RenameActiveClientsKey'; +import RenamePriorityModeKey from './migrations/RenamePriorityModeKey'; export default function () { const startTime = Date.now(); @@ -8,6 +9,7 @@ export default function () { // Add all migrations to an array so they are executed in order const migrationPromises = [ RenameActiveClientsKey, + RenamePriorityModeKey, ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the diff --git a/src/libs/migrations/RenamePriorityModeKey.js b/src/libs/migrations/RenamePriorityModeKey.js new file mode 100644 index 000000000000..c80c1058bbae --- /dev/null +++ b/src/libs/migrations/RenamePriorityModeKey.js @@ -0,0 +1,33 @@ +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import ONYXKEYS from '../../ONYXKEYS'; + +// This migration changes the name of the Onyx key NVP_PRIORITY_MODE from priorityMode to nvp_priorityMode +export default function () { + return new Promise((resolve) => { + // Connect to the old key in Onyx to get the old value of priorityMode + // then set the new key nvp_priorityMode to hold the old data + // finally remove the old key by setting the value to null + const connectionID = Onyx.connect({ + key: 'priorityMode', + callback: (oldPriorityMode) => { + Onyx.disconnect(connectionID); + + // Fail early here because there is nothing to migrate + if (_.isEmpty(oldPriorityMode)) { + console.debug('[Migrate Onyx] Skipped migration RenamePriorityModeKey'); + return resolve(); + } + + Onyx.multiSet({ + priorityMode: null, + [ONYXKEYS.NVP_PRIORITY_MODE]: oldPriorityMode, + }) + .then(() => { + console.debug('[Migrate Onyx] Ran migration RenamePriorityModeKey'); + resolve(); + }); + }, + }); + }); +} diff --git a/src/libs/reportUtils.js b/src/libs/reportUtils.js new file mode 100644 index 000000000000..538076d6c183 --- /dev/null +++ b/src/libs/reportUtils.js @@ -0,0 +1,17 @@ +import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; + +/** + * Returns the concatenated title for the PrimaryLogins of a report + * + * @param {Array} logins + * @returns {string} + */ +function getReportParticipantsTitle(logins) { + return _.map(logins, login => Str.removeSMSDomain(login)).join(', '); +} + +export { + // eslint-disable-next-line import/prefer-default-export + getReportParticipantsTitle, +}; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index d4b4be1c0712..a63ba1a6b924 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -108,6 +108,8 @@ const ProfilePage = ({personalDetails, route}) => { {moment().tz(profileDetails.timezone.selected).format('LT')} + {' '} + {moment().tz(profileDetails.timezone.selected).zoneAbbr()} ) : null} diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 76a06bf34e46..08be33ce2af5 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -165,6 +165,7 @@ class SearchPage extends Component { headerMessage={headerMessage} hideSectionHeaders hideAdditionalOptionStates + showTitleTooltip /> diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index bd2a0fcf4fac..4716530e07b6 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -2,7 +2,7 @@ import React from 'react'; import {View, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import Header from '../../components/Header'; +import lodashGet from 'lodash.get'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; import themeColors from '../../styles/themes/default'; @@ -14,6 +14,10 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/ import MultipleAvatars from '../../components/MultipleAvatars'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; +import {getReportParticipantsTitle} from '../../libs/reportUtils'; +import OptionRowTitle from './sidebar/OptionRowTitle'; +import {getPersonalDetailsForLogins} from '../../libs/OptionsListUtils'; +import {participantPropTypes} from './sidebar/optionPropTypes'; const propTypes = { // Toggles the navigationMenu open and closed @@ -25,6 +29,9 @@ const propTypes = { // Name of the report reportName: PropTypes.string, + // List of primarylogins of participants of the report + participants: PropTypes.arrayOf(PropTypes.string), + // ID of the report reportID: PropTypes.number, @@ -32,6 +39,9 @@ const propTypes = { isPinned: PropTypes.bool, }), + // Personal details of all the users + personalDetails: PropTypes.arrayOf(participantPropTypes).isRequired, + ...windowDimensionsPropTypes, }; @@ -39,51 +49,66 @@ const defaultProps = { report: null, }; -const HeaderView = props => ( - - - {props.isSmallScreenWidth && ( - - - - )} - {props.report && props.report.reportName ? ( - +const HeaderView = (props) => { + const participants = lodashGet(props.report, 'participants', []); + const reportOption = { + text: lodashGet(props.report, 'reportName', ''), + tooltipText: getReportParticipantsTitle(participants), + participantsList: getPersonalDetailsForLogins(participants, props.personalDetails), + }; + + return ( + + + {props.isSmallScreenWidth && ( { - const {participants} = props.report; - if (participants.length === 1) { - Navigation.navigate(ROUTES.getProfileRoute(participants[0])); - } - }} + onPress={props.onNavigationMenuButtonClicked} + style={[styles.LHNToggle]} > - + -
- + )} + {props.report && props.report.reportName && ( + togglePinnedState(props.report)} - style={[styles.touchableButtonImage, styles.mr0]} + onPress={() => { + if (participants.length === 1) { + Navigation.navigate(ROUTES.getProfileRoute(participants[0])); + } + }} + style={[styles.flexRow, styles.alignItemsCenter]} > - + + + + + + togglePinnedState(props.report)} + style={[styles.touchableButtonImage, styles.mr0]} + > + + + - - ) : null} + )} + - -); - + ); +}; HeaderView.propTypes = propTypes; HeaderView.displayName = 'HeaderView'; HeaderView.defaultProps = defaultProps; @@ -94,5 +119,8 @@ export default compose( report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, }), )(HeaderView); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3525cd4a8770..a0d920a60185 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -6,8 +6,10 @@ import {withOnyx} from 'react-native-onyx'; import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; import ReportActionPropTypes from './ReportActionPropTypes'; -import styles from '../../../styles/styles'; -import getReportActionItemStyles from '../../../styles/getReportActionItemStyles'; +import { + getReportActionItemStyle, + getMiniReportActionContextMenuWrapperStyle, +} from '../../../styles/getReportActionItemStyles'; import PressableWithSecondaryInteraction from '../../../components/PressableWithSecondaryInteraction'; import Hoverable from '../../../components/Hoverable'; import PopoverWithMeasuredContent from '../../../components/PopoverWithMeasuredContent'; @@ -107,12 +109,12 @@ class ReportActionItem extends Component { {hovered => ( - + {!this.props.displayAsGroup ? : } - + - {Str.htmlDecode(fragment.text)} - + + + {Str.htmlDecode(fragment.text)} + + ); case 'LINK': return LINK; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index ccd9ea1df280..bf40be5e8cda 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -31,6 +31,7 @@ const ReportActionItemSingle = ({action}) => { diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 4d626ebba79b..9fc3d0a42542 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -209,9 +209,9 @@ class ReportActionsView extends React.Component { } this.setState({isLoadingMoreChats: true}, () => { - // Retrieve the next REPORT_ACTIONS_LIMIT sized page of comments, unless we're near the beginning, in which + // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments, unless we're near the beginning, in which // case just get everything starting from 0. - const offset = Math.max(minSequenceNumber - CONST.REPORT.REPORT_ACTIONS_LIMIT, 0); + const offset = Math.max(minSequenceNumber - CONST.REPORT.ACTIONS.LIMIT, 0); fetchActions(this.props.reportID, offset) .then(() => this.setState({isLoadingMoreChats: false})); }); diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js index 641109bc45c6..c31d128f5d8d 100644 --- a/src/pages/home/sidebar/OptionRow.js +++ b/src/pages/home/sidebar/OptionRow.js @@ -8,12 +8,13 @@ import { StyleSheet, } from 'react-native'; import styles from '../../../styles/styles'; -import optionPropTypes from './optionPropTypes'; +import {optionPropTypes} from './optionPropTypes'; import Icon from '../../../components/Icon'; import {Pencil, PinCircle, Checkmark} from '../../../components/Icon/Expensicons'; import MultipleAvatars from '../../../components/MultipleAvatars'; import themeColors from '../../../styles/themes/default'; import Hoverable from '../../../components/Hoverable'; +import OptionRowTitle from './OptionRowTitle'; const propTypes = { // Style for hovered state @@ -40,6 +41,9 @@ const propTypes = { // Force the text style to be the unread style forceTextUnreadStyle: PropTypes.bool, + + // Whether to show the title tooltip + showTitleTooltip: PropTypes.bool, }; const defaultProps = { @@ -48,6 +52,7 @@ const defaultProps = { showSelectedState: false, isSelected: false, forceTextUnreadStyle: false, + showTitleTooltip: false, }; const OptionRow = ({ @@ -59,6 +64,7 @@ const OptionRow = ({ showSelectedState, isSelected, forceTextUnreadStyle, + showTitleTooltip, }) => { const textStyle = optionIsFocused ? styles.sidebarLinkActiveText @@ -105,9 +111,13 @@ const OptionRow = ({ ) } - - {option.text} - + + {option.alternateText ? ( containerRight ? -(tooltipX - newToolX) : 0; + } + + + render() { + const { + option, style, tooltipEnabled, numberOfLines, + } = this.props; + + if (!tooltipEnabled) { + return {option.text}; + } + return ( + + // Tokenization of string only support 1 numberofLines on Web + this.containerRef = el} + > + {_.map(option.participantsList, (participant, index) => ( + + this.getTooltipShiftX(index)} + > + {/* // We need to get the refs to all the names which will be used to correct + the horizontal position of the tooltip */} + this.childRefs[index] = el}> + {participant.displayName} + + + {index < option.participantsList.length - 1 && } + + ))} + {option.participantsList.length > 1 && this.state.isEllipsisActive + && ( + + + {/* There is some Gap for real ellipsis so we are adding 4 `.` to cover */} + .... + + + )} + + ); + } +} +OptionRowTitle.propTypes = propTypes; +OptionRowTitle.defaultProps = defaultProps; +OptionRowTitle.displayName = 'OptionRowTitle'; + +export default OptionRowTitle; diff --git a/src/pages/home/sidebar/OptionRowTitle/index.native.js b/src/pages/home/sidebar/OptionRowTitle/index.native.js new file mode 100644 index 000000000000..b863a115579a --- /dev/null +++ b/src/pages/home/sidebar/OptionRowTitle/index.native.js @@ -0,0 +1,20 @@ +// As we don't have to show tooltips of the Native platform so we simply render the option title which wraps. +import React from 'react'; +import {Text} from 'react-native'; +import {propTypes, defaultProps} from './OptionRowTitleProps'; + +const OptionRowTitle = ({ + style, + option, + numberOfLines, +}) => ( + + {option.text} + +); + +OptionRowTitle.propTypes = propTypes; +OptionRowTitle.defaultProps = defaultProps; +OptionRowTitle.displayName = 'OptionRowTitle'; + +export default OptionRowTitle; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index c4215b28e658..0a9a0d9fdac3 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -18,6 +18,7 @@ import {getSidebarOptions} from '../../../libs/OptionsListUtils'; import {getDefaultAvatar} from '../../../libs/actions/PersonalDetails'; import KeyboardSpacer from '../../../components/KeyboardSpacer'; import CONST from '../../../CONST'; +import {participantPropTypes} from './optionPropTypes'; const propTypes = { // Toggles the navigation menu open and closed @@ -41,11 +42,7 @@ const propTypes = { draftComments: PropTypes.objectOf(PropTypes.string), // List of users' personal details - personalDetails: PropTypes.objectOf(PropTypes.shape({ - login: PropTypes.string.isRequired, - avatar: PropTypes.string.isRequired, - displayName: PropTypes.string.isRequired, - })), + personalDetails: PropTypes.objectOf(participantPropTypes), // The personal details of the person who is logged in myPersonalDetails: PropTypes.shape({ @@ -149,6 +146,7 @@ class SidebarLinks extends React.Component { this.props.onLinkClick(); }} hideSectionHeaders + showTitleTooltip disableFocusOptions={this.props.isSmallScreenWidth} /> @@ -181,7 +179,7 @@ export default compose( key: ONYXKEYS.CURRENTLY_VIEWED_REPORTID, }, priorityMode: { - key: ONYXKEYS.PRIORITY_MODE, + key: ONYXKEYS.NVP_PRIORITY_MODE, }, }), )(SidebarLinks); diff --git a/src/pages/home/sidebar/optionPropTypes.js b/src/pages/home/sidebar/optionPropTypes.js index 2d1d8d999dc1..ec2881f790b5 100644 --- a/src/pages/home/sidebar/optionPropTypes.js +++ b/src/pages/home/sidebar/optionPropTypes.js @@ -1,12 +1,26 @@ import PropTypes from 'prop-types'; -export default PropTypes.shape({ +const participantPropTypes = PropTypes.shape({ + // Primary login of participant + login: PropTypes.string, + + // Display Name of participant + displayName: PropTypes.string, + + // Avatar url of participant + avatar: PropTypes.string, +}); + +const optionPropTypes = PropTypes.shape({ // The full name of the user if available, otherwise the login (email/phone number) of the user text: PropTypes.string.isRequired, // Subtitle to show under report displayName, mostly lastMessageText of the report alternateText: PropTypes.string.isRequired, + // List of participants of the report + participantsList: PropTypes.arrayOf(participantPropTypes).isRequired, + // The array URLs of the person's avatar icon: PropTypes.arrayOf(PropTypes.string), @@ -15,4 +29,12 @@ export default PropTypes.shape({ // The option key provided to FlatList keyExtractor keyForList: PropTypes.string, + + // Text to show for tooltip + tooltipText: PropTypes.string, }); + +export { + participantPropTypes, + optionPropTypes, +}; diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index 79760ed0b472..593addfda470 100644 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -39,12 +39,16 @@ class IOUModal extends Component { this.navigateToPreviousStep = this.navigateToPreviousStep.bind(this); this.navigateToNextStep = this.navigateToNextStep.bind(this); + this.updateAmount = this.updateAmount.bind(this); + this.currencySelected = this.currencySelected.bind(this); this.addParticipants = this.addParticipants.bind(this); this.state = { currentStepIndex: 0, participants: [], - iouAmount: 42, + amount: '', + selectedCurrency: 'USD', + isAmountPageNextButtonDisabled: true, }; } @@ -60,7 +64,10 @@ class IOUModal extends Component { getTitleForStep() { if (this.state.currentStepIndex === 1) { - return `${this.props.hasMultipleParticipants ? 'Split' : 'Request'} $${this.state.iouAmount}`; + return `${this.props.hasMultipleParticipants ? 'Split' : 'Request'} $${this.state.amount}`; + } + if (steps[this.state.currentStepIndex] === Steps.IOUAmount) { + return this.props.hasMultipleParticipants ? 'Split Bill' : 'Request Money'; } return steps[this.state.currentStepIndex] || ''; } @@ -95,6 +102,42 @@ class IOUModal extends Component { }); } + /** + * Update amount with number or Backspace pressed. + * Validate new amount with decimal number regex up to 6 digits and 2 decimal digit + * + * @param {String} buttonPressed + */ + updateAmount(buttonPressed) { + // Backspace button is pressed + if (buttonPressed === '<' || buttonPressed === 'Backspace') { + if (this.state.amount.length > 0) { + this.setState(prevState => ({ + amount: prevState.amount.substring(0, prevState.amount.length - 1), + isAmountPageNextButtonDisabled: prevState.amount.length === 1, + })); + } + } else { + const decimalNumberRegex = new RegExp(/^\d{1,6}(\.\d{0,2})?$/, 'i'); + const amount = this.state.amount + buttonPressed; + if (decimalNumberRegex.test(amount)) { + this.setState({ + amount, + isAmountPageNextButtonDisabled: false, + }); + } + } + } + + /** + * Update the currency state + * + * @param {String} selectedCurrency + */ + currencySelected(selectedCurrency) { + this.setState({selectedCurrency}); + } + render() { const currentStep = steps[this.state.currentStepIndex]; return ( @@ -130,7 +173,14 @@ class IOUModal extends Component { {currentStep === Steps.IOUAmount && ( - + )} {currentStep === Steps.IOUParticipants && ( console.debug('create IOU report')} participants={this.state.participants} - iouAmount={42} + iouAmount={this.state.amount} /> )} diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index aa2369729612..4d0c2e99d67e 100644 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -10,11 +10,33 @@ import {withOnyx} from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; import styles from '../../../styles/styles'; import themeColors from '../../../styles/themes/default'; +import BigNumberPad from '../../../components/BigNumberPad'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; +import TextInputFocusable from '../../../components/TextInputFocusable'; const propTypes = { // Callback to inform parent modal of success onStepComplete: PropTypes.func.isRequired, + // Callback to inform parent modal with key pressed + numberPressed: PropTypes.func.isRequired, + + // Currency selection will be implemented later + // eslint-disable-next-line react/no-unused-prop-types + currencySelected: PropTypes.func.isRequired, + + // User's currency preference + selectedCurrency: PropTypes.string.isRequired, + + // Amount value entered by user + amount: PropTypes.string.isRequired, + + // To disable/enable Next button based on amount validity + isNextButtonDisabled: PropTypes.bool.isRequired, + + /* Window Dimensions Props */ + ...windowDimensionsPropTypes, + /* Onyx Props */ // Holds data related to IOU view state, rather than the underlying IOU data. @@ -28,25 +50,83 @@ const propTypes = { const defaultProps = { iou: {}, }; +class IOUAmountPage extends React.Component { + constructor(props) { + super(props); + + this.state = { + textInputWidth: 0, + }; + } -const IOUAmountPage = props => ( - - {props.iou.loading && } - - - Next - - - -); + componentDidMount() { + if (this.textInput) { + this.textInput.focus(); + } + } + render() { + return ( + + {this.props.iou.loading && } + + + {this.props.selectedCurrency} + + {this.props.isSmallScreenWidth + ? {this.props.amount} + : ( + + { + this.props.numberPressed(event.key); + event.preventDefault(); + }} + ref={el => this.textInput = el} + defaultValue={this.props.amount} + textAlign="left" + /> + this.setState({textInputWidth: e.nativeEvent.layout.width})} + > + {this.props.amount} + + + )} + + + {this.props.isSmallScreenWidth + ? + : } + + + Next + + + + + ); + } +} IOUAmountPage.displayName = 'IOUAmountPage'; IOUAmountPage.propTypes = propTypes; IOUAmountPage.defaultProps = defaultProps; -export default withOnyx({ +export default withWindowDimensions(withOnyx({ iou: {key: ONYXKEYS.IOU}, -})(IOUAmountPage); +})(IOUAmountPage)); diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index 7ed238f3596c..f3f99cb79116 100644 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -1,20 +1,149 @@ -import React from 'react'; +import React, {Component} from 'react'; +import {View, TextInput} from 'react-native'; +import Onyx, {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import {isEmpty} from 'underscore'; + import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import ScreenWrapper from '../../components/ScreenWrapper'; +import Text from '../../components/Text'; +import styles from '../../styles/styles'; +import ONYXKEYS from '../../ONYXKEYS'; +import CONST from '../../CONST'; +import ButtonWithLoader from '../../components/ButtonWithLoader'; +import {changePassword} from '../../libs/actions/User'; + +const propTypes = { + /* Onyx Props */ + // Holds information about the users account that is logging in + account: PropTypes.shape({ + // An error message to display to the user + error: PropTypes.string, + + // Whether or not a sign on form is loading (being submitted) + loading: PropTypes.bool, + }), +}; + +const defaultProps = { + account: {}, +}; +class PasswordPage extends Component { + constructor(props) { + super(props); + + this.state = { + currentPassword: '', + newPassword: '', + confirmNewPassword: '', + isPasswordRequirementsVisible: false, + }; + + this.handleChangePassword = this.handleChangePassword.bind(this); + } + + componentWillUnmount() { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: ''}); + } + + handleChangePassword() { + changePassword(this.state.currentPassword, this.state.newPassword) + .then((response) => { + if (response.jsonCode === 200) { + Navigation.navigate(ROUTES.SETTINGS); + } + }); + } -const PasswordPage = () => ( - - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - -); + render() { + return ( + + Navigation.navigate(ROUTES.SETTINGS)} + onCloseButtonPress={Navigation.dismissModal} + /> + + + + Changing your password will update your password for both your Expensify.com + and Expensify.cash accounts. + + + Current Password* + this.setState({currentPassword})} + /> + + + New Password* + this.setState({newPassword})} + onFocus={() => this.setState({isPasswordRequirementsVisible: true})} + onBlur={() => this.setState({isPasswordRequirementsVisible: false})} + /> + {this.state.isPasswordRequirementsVisible && ( + + New password must be different than your old password, have at least 8 characters, + 1 capital letter, 1 lowercase letter, 1 number. + + )} + + + Confirm New Password* + this.setState({confirmNewPassword})} + onSubmitEditing={this.handleChangePassword} + /> + + {!isEmpty(this.props.account.error) && ( + + {this.props.account.error} + + )} + + + + + + + ); + } +} PasswordPage.displayName = 'PasswordPage'; +PasswordPage.propTypes = propTypes; +PasswordPage.defaultProps = defaultProps; -export default PasswordPage; +export default withOnyx({ + account: { + key: ONYXKEYS.ACCOUNT, + }, +})(PasswordPage); diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js index b964cb75dceb..f3da3c89fa83 100644 --- a/src/pages/settings/PreferencesPage.js +++ b/src/pages/settings/PreferencesPage.js @@ -56,7 +56,7 @@ const PreferencesPage = ({priorityMode}) => ( {/* placeholder from appearing as a selection option. */} NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.PRIORITY_MODE) + mode => NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.NVP_PRIORITY_MODE) } items={Object.values(priorityModes)} style={styles.picker} @@ -80,6 +80,6 @@ PreferencesPage.displayName = 'PreferencesPage'; export default withOnyx({ priorityMode: { - key: ONYXKEYS.PRIORITY_MODE, + key: ONYXKEYS.NVP_PRIORITY_MODE, }, })(PreferencesPage); diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index a393ecd0b925..68aca6d584ae 100644 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -88,6 +88,7 @@ class PasswordForm extends React.Component { placeholderTextColor={themeColors.textSupporting} onChangeText={text => this.setState({twoFactorAuthCode: text})} onSubmitEditing={this.validateAndSubmitForm} + keyboardType="numeric" /> )} diff --git a/src/styles/getReportActionItemStyles.js b/src/styles/getReportActionItemStyles.js index e4ac54a3886c..20054eb0eef0 100644 --- a/src/styles/getReportActionItemStyles.js +++ b/src/styles/getReportActionItemStyles.js @@ -1,4 +1,5 @@ import themeColors from './themes/default'; +import positioning from './utilities/positioning'; /** * Generate the styles for the ReportActionItem wrapper view. @@ -6,7 +7,7 @@ import themeColors from './themes/default'; * @param {Boolean} [isHovered] * @returns {Object} */ -export default function (isHovered = false) { +export function getReportActionItemStyle(isHovered = false) { return { display: 'flex', justifyContent: 'space-between', @@ -14,3 +15,17 @@ export default function (isHovered = false) { cursor: 'default', }; } + +/** + * Generate the wrapper styles for the mini ReportActionContextMenu. + * + * @param {Boolean} isReportActionItemGrouped + * @returns {Object} + */ +export function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGrouped) { + return { + ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), + ...positioning.r4, + position: 'absolute', + }; +} diff --git a/src/styles/getTooltipStyles.js b/src/styles/getTooltipStyles.js index 29b6c3c02c97..21699e5428d1 100644 --- a/src/styles/getTooltipStyles.js +++ b/src/styles/getTooltipStyles.js @@ -111,19 +111,24 @@ export default function getTooltipStyles( return { animationStyle: { + // remember Transform causes a new Local cordinate system + // https://drafts.csswg.org/css-transforms-1/#transform-rendering + // so Position fixed children will be relative to this new Local cordinate system transform: [{ scale: currentSize, }], }, tooltipWrapperStyle: { - position: 'absolute', + position: 'fixed', backgroundColor: themeColors.heading, borderRadius: variables.componentBorderRadiusSmall, ...tooltipVerticalPadding, ...spacing.ph2, + zIndex: variables.tooltipzIndex, - // Because it uses absolute positioning, the top-left corner of the tooltip is aligned - // with the top-left corner of the wrapped component by default. + // Because it uses fixed positioning, the top-left corner of the tooltip is aligned + // with the top-left corner of the window by default. + // we will use yOffset to position the tooltip relative to the Wrapped Component // So we need to shift the tooltip vertically and horizontally to position it correctly. // // First, we'll position it vertically. @@ -132,12 +137,13 @@ export default function getTooltipStyles( top: shouldShowBelow // We need to shift the tooltip down below the component. So shift the tooltip down (+) by... - ? componentHeight + POINTER_HEIGHT + manualShiftVertical + ? yOffset + componentHeight + POINTER_HEIGHT + manualShiftVertical // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... - : -(tooltipHeight + POINTER_HEIGHT) + manualShiftVertical, + : (yOffset - (tooltipHeight + POINTER_HEIGHT)) + manualShiftVertical, // Next, we'll position it horizontally. + // we will use xOffset to position the tooltip relative to the Wrapped Component // To shift the tooltip right, we'll give `left` a positive value. // To shift the tooltip left, we'll give `left` a negative value. // @@ -148,7 +154,7 @@ export default function getTooltipStyles( // so the tooltip's center lines up with the center of the wrapped component. // 3) Add the horizontal shift (left or right) computed above to keep it out of the gutters. // 4) Lastly, add the manual horizontal shift passed in as a parameter. - left: ((componentWidth / 2) - (tooltipWidth / 2)) + horizontalShift + manualShiftHorizontal, + left: xOffset + ((componentWidth / 2) - (tooltipWidth / 2)) + horizontalShift + manualShiftHorizontal, }, tooltipTextStyle: { color: themeColors.textReversed, @@ -156,14 +162,13 @@ export default function getTooltipStyles( fontSize: tooltipFontSize, }, pointerWrapperStyle: { - position: 'absolute', + position: 'fixed', - - // By default, the pointer's top-left will align with the top-left of the wrapped component. + // By default, the pointer's top-left will align with the top-left of the wrapped tooltip. // // To align it vertically, we'll: // - // Shift the pointer up (-) by its height, so that the bottom of the pointer lines up + // Shift the pointer up (-) by component's height, so that the bottom of the pointer lines up // with the top of the wrapped component. // // OR if it should show below: @@ -172,14 +177,16 @@ export default function getTooltipStyles( // so that the top of the pointer aligns with the bottom of the component. // // Always add the manual vertical shift passed in as a parameter. - top: shouldShowBelow ? componentHeight + manualShiftVertical : manualShiftVertical - POINTER_HEIGHT, + top: shouldShowBelow ? manualShiftVertical - POINTER_HEIGHT : tooltipHeight + manualShiftVertical, // To align it horizontally, we'll: - // 1) Shift the pointer to the right (+) by the half the component's width, - // so the left edge of the pointer lines up with the component's center. + // 1) Shift the pointer to the right (+) by the half the tooltipWidth's width, + // so the left edge of the pointer lines up with the tooltipWidth's center. // 2) To the left (-) by half the pointer's width, - // so the pointer's center lines up with the component's center. - left: (componentWidth / 2) - (POINTER_WIDTH / 2), + // so the pointer's center lines up with the tooltipWidth's center. + // 3) Due to the tip start from the left edge of wrapper Tooltip so we have to remove the + // horizontalShift which is added to adjust it into the Window + left: -horizontalShift + ((tooltipWidth / 2) - (POINTER_WIDTH / 2)), }, pointerStyle: { width: 0, diff --git a/src/styles/styles.js b/src/styles/styles.js index a63bf3039626..916b12be0b75 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -10,7 +10,6 @@ import sizing from './utilities/sizing'; import flex from './utilities/flex'; import display from './utilities/display'; import overflow from './utilities/overflow'; -import positioning from './utilities/positioning'; import whiteSpace from './utilities/whiteSpace'; import wordBreak from './utilities/wordBreak'; import textInputAlignSelf from './utilities/textInputAlignSelf'; @@ -136,6 +135,10 @@ const styles = { borderWidth: 0, }, + buttonSuccessDisabled: { + opacity: 0.5, + }, + buttonSuccessHovered: { backgroundColor: themeColors.buttonSuccessHoveredBG, borderWidth: 0, @@ -322,6 +325,12 @@ const styles = { marginBottom: 4, }, + formHint: { + color: themeColors.textSupporting, + fontSize: variables.fontSizeLabel, + lineHeight: 18, + }, + signInPage: { backgroundColor: themeColors.sidebar, padding: 20, @@ -555,6 +564,17 @@ const styles = { ...whiteSpace.noWrap, }, + optionDisplayNameTooltipWrapper: { + position: 'relative', + }, + + optionDisplayNameTooltipEllipsis: { + position: 'absolute', + opacity: 0, + right: 0, + bottom: 0, + }, + optionAlternateText: { color: themeColors.textSupporting, fontFamily: fontFamily.GTA, @@ -943,12 +963,6 @@ const styles = { borderColor: colors.transparent, }, - miniReportActionContextMenuWrapperStyle: { - ...positioning.tn4, - ...positioning.r4, - position: 'absolute', - }, - reportActionContextMenuText: { color: themeColors.heading, fontFamily: fontFamily.GTA_BOLD, @@ -1092,6 +1106,18 @@ const styles = { height: 24, lineHeight: 20, }, + + iouAmountText: { + fontFamily: fontFamily.GTA_BOLD, + fontWeight: fontWeightBold, + fontSize: variables.iouAmountTextSize, + }, + + iouAmountTextInput: addOutlineWidth({ + fontFamily: fontFamily.GTA_BOLD, + fontWeight: fontWeightBold, + fontSize: variables.iouAmountTextSize, + }, 0), }; const baseCodeTagStyles = { diff --git a/src/styles/utilities/display.js b/src/styles/utilities/display.js index 275ac345883e..a16a62694af8 100644 --- a/src/styles/utilities/display.js +++ b/src/styles/utilities/display.js @@ -16,4 +16,8 @@ export default { dNone: { display: 'none', }, + + dInline: { + display: 'inline', + }, }; diff --git a/src/styles/utilities/positioning.js b/src/styles/utilities/positioning.js index ed3164a03c85..9514b8f55091 100644 --- a/src/styles/utilities/positioning.js +++ b/src/styles/utilities/positioning.js @@ -6,6 +6,9 @@ export default { tn4: { top: -16, }, + tn8: { + top: -32, + }, r4: { right: 16, }, diff --git a/src/styles/variables.js b/src/styles/variables.js index 28e0f37585f7..6463935aa836 100644 --- a/src/styles/variables.js +++ b/src/styles/variables.js @@ -14,8 +14,10 @@ export default { iconSizeSmall: 16, iconSizeNormal: 20, iconSizeLarge: 24, + iouAmountTextSize: 40, mobileResponsiveWidthBreakpoint: 800, safeInsertPercentage: 0.7, sideBarWidth: 375, pdfPageMaxWidth: 992, + tooltipzIndex: 10050, }; diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js index 3a5a3bd409fe..6197eb692fa4 100644 --- a/tests/unit/GithubUtilsTest.js +++ b/tests/unit/GithubUtilsTest.js @@ -544,4 +544,15 @@ describe('GithubUtils', () => { expect(GithubUtils.getPullRequestURLFromNumber(input)).toBe(expectedOutput) )); }); + + describe('getReleaseBody', () => { + test.each([ + // eslint-disable-next-line max-len + [[1, 2, 3], '- https://github.com/Expensify/Expensify.cash/pull/1\r\n- https://github.com/Expensify/Expensify.cash/pull/2\r\n- https://github.com/Expensify/Expensify.cash/pull/3'], + [[], ''], + [[12345], '- https://github.com/Expensify/Expensify.cash/pull/12345'], + ])('getReleaseBody("%s")', (input, expectedOutput) => ( + expect(GithubUtils.getReleaseBody(input)).toBe(expectedOutput) + )); + }); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 7c1ecf410b7d..42a07b2bf747 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -71,10 +71,7 @@ function fetchPersonalDetailsForTestUser(accountID, email, personalDetailsList) accountID, email, personalDetailsList, - })) - - // fetchTimezone - .mockImplementationOnce(() => Promise.resolve({})); + })); fetchPersonalDetails(); return waitForPromisesToResolve();