From f7f32271699a1b322900a4e0a08d091bc1c6d50c Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 4 Nov 2020 13:53:18 -0700 Subject: [PATCH 01/18] Add initial code and dependencies --- .editorconfig | 38 + .eslintrc.js | 3 + .gitignore | 67 ++ README.md | 43 + index.js | 456 +++++++++ lib/Logger.js | 35 + package-lock.json | 2250 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 20 + 8 files changed, 2912 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/Logger.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..212fef118 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,38 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# Howto with your editor: +# Sublime: https://github.com/sindresorhus/editorconfig-sublime + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[**] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 + +# Standard at: https://github.com/felixge/node-style-guide +[**.js, **.json] +trim_trailing_whitespace = true +quote_type = single +curly_bracket_next_line = false +spaces_around_operators = true +space_after_control_statements = true +space_after_anonymous_functions = false +spaces_in_brackets = false + +# No Standard. Please document a standard if different from .js +[**.yml, **.html, **.css] +trim_trailing_whitespace = true + +# No standard. Please document a standard if different from .js +[**.md] + +# Standard at: +[Makefile] + +[package*] +indent_style = space +indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..da9bdd0fa --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: 'expensify', +}; diff --git a/.gitignore b/.gitignore index e69de29bb..8efffec2c 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,67 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +*.cer +*.p12 +*.mobileprovision + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log +dist/ + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore +android/app/android-fastlane-json-key.json + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Local DEV config +/.env diff --git a/README.md b/README.md new file mode 100644 index 000000000..ef0101df2 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# React Ion +This is a persistent storage solution wrapped in a Pub/Sub library. In general that means: + +- Ion stores and retrieves data from persistent storage +- Data is stored as key/value pairs, where the value can be anything from a single piece of data to a complex object +- Collections of data are usually not stored as a single key (eg. an array with multiple objects), but as individual keys+ID (eg. `report_1234`, `report_4567`, etc.). Store collections as individual keys when a component will bind directly to one of those keys. For example: reports are stored as individual keys because `SidebarLink.js` binds to the individual report keys for each link. However, report actions are stored as an array of objects because nothing binds directly to a single report action. +- Ion allows other code to subscribe to changes in data, and then publishes change events whenever data is changed +- Anything needing to read Ion data needs to: + 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage) + 2. Subscribe to changes of the data for a particular key or set of keys. React components use `withIon()` and non-React libs use `Ion.connect()`. + 3. Get initialized with the current value of that key from persistent storage (Ion does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process) +- Subscribing to Ion keys is done using a constant defined in `IONKEYS`. Each Ion key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (eg. display a list of them in the nav menu), then it would subscribe to the key `IONKEYS.COLLECTION.REPORT`. + +### Storage Eviction + +Different platforms come with varying storage capacities and Ion has a way to gracefully fail when those storage limits are encountered. When Ion fails to set or modify a key the following steps are taken: +1. Ion looks at a list of recently accessed keys (access is defined as subscribed to or modified) and locates the key that was least recently accessed +2. It then deletes this key and retries the original operation + +By default, Ion will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise. + +**To flag a key as safe for removal:** +- Add the key to the `safeEvictionKeys` option in `Ion.init(options)` +- Implement `canEvict` in the Ion config for each component subscribing to a key +- The key will only be deleted when all subscribers return `true` for `canEvict` + +e.g. +```js +Ion.init({ + safeEvictionKeys: [IONKEYS.COLLECTION.REPORT_ACTIONS], +}); +``` + +```js +export default withIon({ + reportActions: { + key: ({reportID}) => `${IONKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, + canEvict: props => !props.isActiveReport, + }, +})(ReportActionsView); +``` + +# Deploying diff --git a/index.js b/index.js new file mode 100644 index 000000000..aa3e180d7 --- /dev/null +++ b/index.js @@ -0,0 +1,456 @@ +import _ from 'underscore'; +import AsyncStorage from '@react-native-community/async-storage'; +import Str from 'js-libs/lib/str'; +import {registerLogger, logInfo, logAlert} from './lib/Logger'; + +// Keeps track of the last connectionID that was used so we can keep incrementing it +let lastConnectionID = 0; + +// Holds a mapping of all the react components that want their state subscribed to a store key +const callbackToStateMapping = {}; + +// Holds a list of keys that have been directly subscribed to or recently modified from least to most recent +let recentlyAccessedKeys = []; + +// Holds a list of keys that are safe to remove when we reach max storage +let evictionAllowList = []; + +// Holds a list of keys that we should never remove +const evictionBlocklist = {}; + +/** + * When a key change happens, search for any callbacks matching the regex pattern and trigger those callbacks + * Get some data from the store + * + * @param {string} key + * @returns {Promise<*>} + */ +function get(key) { + return AsyncStorage.getItem(key) + .then(val => JSON.parse(val)) + .catch(err => logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); +} + +/** + * Checks to see if the a subscriber's supplied key + * is associated with a collection of keys. + * + * @param {String} key + * @returns {Boolean} + */ +function isCollectionKey(key) { + // Any key that ends with an underscore is a collection + return Str.endsWith(key, '_'); +} + +/** + * Checks to see if a given key matches with the + * configured key of our connected subscriber + * + * @param {String} configKey + * @param {String} key + * @return {Boolean} + */ +function isKeyMatch(configKey, key) { + return isCollectionKey(configKey) + ? Str.startsWith(key, configKey) + : configKey === key; +} + +/** + * Checks to see if this key has been flagged as + * safe for removal. + * + * @param {String} testKey + * @returns {Boolean} + */ +function isSafeEvictionKey(testKey) { + return _.some(evictionAllowList, key => isKeyMatch(key, testKey)); +} + +/** + * Remove a key from the recently accessed key list. + * + * @param {String} key + */ +function removeLastAccessedKey(key) { + recentlyAccessedKeys = _.without(recentlyAccessedKeys, key); +} + +/** + * Add a key to the list of recently accessed keys. The least + * recently accessed key should be at the head and the most + * recently accessed key at the tail. + * + * @param {String} key + */ +function addLastAccessedKey(key) { + // Only specific keys belong in this list since we cannot remove an entire collection. + if (isCollectionKey(key) || !isSafeEvictionKey(key)) { + return; + } + + removeLastAccessedKey(key); + recentlyAccessedKeys.push(key); +} + +/** + * Removes a key previously added to this list + * which will enable it to be deleted again. + * + * @param {String} key + * @param {Number} connectionID + */ +function removeFromEvictionBlockList(key, connectionID) { + evictionBlocklist[key] = _.without(evictionBlocklist[key] || [], connectionID); + + // Remove the key if there are no more subscribers + if (evictionBlocklist[key].length === 0) { + delete evictionBlocklist[key]; + } +} + +/** + * Keys added to this list can never be deleted. + * + * @param {String} key + * @param {Number} connectionID + */ +function addToEvictionBlockList(key, connectionID) { + removeFromEvictionBlockList(key, connectionID); + + if (!evictionBlocklist[key]) { + evictionBlocklist[key] = []; + } + + evictionBlocklist[key].push(connectionID); +} + +/** + * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks + * + * @param {string} key + * @param {mixed} data + */ +function keyChanged(key, data) { + // Add or remove this key from the recentlyAccessedKeys lists + if (!_.isNull(data)) { + addLastAccessedKey(key); + } else { + removeLastAccessedKey(key); + } + + // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data + _.each(callbackToStateMapping, (subscriber) => { + if (subscriber && isKeyMatch(subscriber.key, key)) { + if (_.isFunction(subscriber.callback)) { + subscriber.callback(data, key); + } + + if (!subscriber.withIonInstance) { + return; + } + + // Check if we are subscribing to a collection key and add this item as a collection + if (isCollectionKey(subscriber.key)) { + subscriber.withIonInstance.setState((prevState) => { + const collection = prevState[subscriber.statePropertyName] || {}; + + // If we have removed the value for this key or it has been + // deleted then remove it from the collection and update + if (_.isNull(data)) { + // We do not have this key in the collection so don't + // bother to update the component state here + if (!collection[key]) { + return; + } + + delete collection[key]; + } else { + collection[key] = data; + } + + return { + [subscriber.statePropertyName]: collection, + }; + }); + } else { + subscriber.withIonInstance.setState({ + [subscriber.statePropertyName]: data, + }); + } + } + }); +} + +/** + * Sends the data obtained from the keys to the connection. It either: + * - sets state on the withIonInstances + * - triggers the callback function + * + * @param {object} config + * @param {object} [config.withIonInstance] + * @param {string} [config.statePropertyName] + * @param {function} [config.callback] + * @param {*|null} val + */ +function sendDataToConnection(config, val) { + if (config.withIonInstance) { + config.withIonInstance.setState({ + [config.statePropertyName]: val, + }); + } else if (_.isFunction(config.callback)) { + config.callback(val); + } +} + +/** + * Subscribes a react component's state directly to a store key + * + * @param {object} mapping the mapping information to connect Ion to the components state + * @param {string} mapping.key + * @param {string} mapping.statePropertyName the name of the property in the state to connect the data to + * @param {object} [mapping.withIonInstance] whose setState() method will be called with any changed data + * This is used by React components to connect to Ion + * @param {object} [mapping.callback] a method that will be called with changed data + * This is used by any non-React code to connect to Ion + * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the + * component + * @returns {number} an ID to use when calling disconnect + */ +function connect(mapping) { + const connectionID = lastConnectionID++; + callbackToStateMapping[connectionID] = mapping; + + if (mapping.initWithStoredValues === false) { + return connectionID; + } + + // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list + if (mapping.withIonInstance && !isCollectionKey(mapping.key) && isSafeEvictionKey(mapping.key)) { + // All React components subscribing to a key flagged as a safe eviction + // key must implement the canEvict property. + if (_.isUndefined(mapping.canEvict)) { + // eslint-disable-next-line max-len + throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`); + } + addLastAccessedKey(mapping.key); + } + + AsyncStorage.getAllKeys() + .then((keys) => { + // Find all the keys matched by the config key + const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key)); + + // If the key being connected to does not exist, initialize the value with null + if (matchingKeys.length === 0) { + sendDataToConnection(mapping, null); + return; + } + + // When using a callback subscriber we will trigger the callback + // for each key we find. It's up to the subscriber to know whether + // to expect a single key or multiple keys in the case of a collection. + // React components are an exception since we'll want to send their + // initial data as a single object when using collection keys. + if (mapping.withIonInstance && isCollectionKey(mapping.key)) { + Promise.all(_.map(matchingKeys, key => get(key))) + .then(values => _.reduce(values, (finalObject, value, i) => ({ + ...finalObject, + [matchingKeys[i]]: value, + }), {})) + .then(val => sendDataToConnection(mapping, val)); + } else { + _.each(matchingKeys, (key) => { + get(key).then(val => sendDataToConnection(mapping, val)); + }); + } + }); + + return connectionID; +} + +/** + * Remove the listener for a react component + * + * @param {string} connectionID + */ +function disconnect(connectionID) { + if (!callbackToStateMapping[connectionID]) { + return; + } + delete callbackToStateMapping[connectionID]; +} + +/** + * Remove a key from Ion and update the subscribers + * + * @param {String} key + * @return {Promise} + */ +function remove(key) { + return AsyncStorage.removeItem(key) + .then(() => keyChanged(key, null)); +} + +/** + * If we fail to set or merge we must handle this by + * evicting some data from Ion and then retrying to do + * whatever it is we attempted to do. + * + * @param {Error} error + * @param {Function} ionMethod + * @param {...any} args + * @return {Promise} + */ +function evictStorageAndRetry(error, ionMethod, ...args) { + // Find the first key that we can remove that has no subscribers in our blocklist + const keyForRemoval = _.find(recentlyAccessedKeys, (key) => { + const keyParts = key.split('_'); + const keyPrefix = `${keyParts.slice(0, keyParts.length - 1).join('_')}_`; + return !evictionBlocklist[keyPrefix]; + }); + + if (!keyForRemoval) { + logAlert('Out of storage. But found no acceptable keys to remove.'); + throw error; + } + + // We must immediately remove this so we do not try to remove the same key twice. + removeLastAccessedKey(keyForRemoval); + + // Remove the least recently viewed key that is not currently being accessed and retry. + logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); + return remove(keyForRemoval) + .then(() => ionMethod(...args)); +} + +/** + * Write a value to our store with the given key + * + * @param {string} key + * @param {mixed} val + * @returns {Promise} + */ +function set(key, val) { + // Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain + return AsyncStorage.setItem(key, JSON.stringify(val)) + .then(() => keyChanged(key, val)) + .catch(error => evictStorageAndRetry(error, set, key, val)); +} + +/** + * Sets multiple keys and values. Example + * Ion.multiSet({'key1': 'a', 'key2': 'b'}); + * + * @param {object} data + * @returns {Promise} + */ +function multiSet(data) { + // AsyncStorage expenses the data in an array like: + // [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]] + // This method will transform the params from a better JSON format like: + // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'} + const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([ + ...finalArray, + [key, JSON.stringify(val)], + ]), []); + + return AsyncStorage.multiSet(keyValuePairs) + .then(() => _.each(data, (val, key) => keyChanged(key, val))) + .catch(error => evictStorageAndRetry(error, multiSet, data)); +} + +/** + * Clear out all the data in the store + * + * @returns {Promise} + */ +function clear() { + let allKeys; + return AsyncStorage.getAllKeys() + .then(keys => allKeys = keys) + .then(() => AsyncStorage.clear()) + .then(() => { + _.each(allKeys, (key) => { + keyChanged(key, null); + }); + }); +} + +/** + * Merge a new value into an existing value at a key + * + * @param {string} key + * @param {*} val + */ +function merge(key, val) { + // Arrays need to be manually merged because the AsyncStorage behavior + // is not desired when merging arrays. `AsyncStorage.mergeItem('test', [1]); + // will result in `{0: 1}` being set in storage, when `[1]` is what is expected + if (_.isArray(val)) { + let newArray; + get(key) + .then((prevVal) => { + const previousValue = prevVal || []; + newArray = [...previousValue, ...val]; + return AsyncStorage.setItem(key, JSON.stringify(newArray)); + }) + .then(() => keyChanged(key, newArray)) + .catch(error => evictStorageAndRetry(error, merge, key, val)); + return; + } + + // Values that are objects are merged normally into storage + if (_.isObject(val)) { + AsyncStorage.mergeItem(key, JSON.stringify(val)) + .then(() => get(key)) + .then((newObject) => { + keyChanged(key, newObject); + }) + .catch(error => evictStorageAndRetry(error, merge, key, val)); + return; + } + + // Anything else (strings and numbers) need to be set into storage + set(key, val); +} + +/** + * Initialize the store with actions and listening for storage events + * + * @param {Object} [options] + * @param {String[]} [options.safeEvictionKeys] This is an array of IONKEYS + * (individual or collection patterns) that when provided to Ion are flagged + * as "safe" for removal. Any components subscribing to these keys must also + * implement a canEvict option. See the README for more info. + * @param {function} onStorageEvent a callback when a storage event happens. + * This applies to web platforms where the local storage emits storage events + * across all open tabs and allows Ion to stay in sync across all open tabs. + */ +function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { + // Let Ion know about which keys are safe to evict + evictionAllowList = safeEvictionKeys; + + // Initialize all of our keys with data provided + _.each(initialKeyStates, (state, key) => merge(key, state)); + + // Update any key whose value changes in storage + onStorageEvent((key, newValue) => keyChanged(key, newValue)); +} + +const Ion = { + connect, + disconnect, + set, + multiSet, + merge, + clear, + init, + registerLogger, + addToEvictionBlockList, + removeFromEvictionBlockList, + isSafeEvictionKey, +}; + +export default Ion; diff --git a/lib/Logger.js b/lib/Logger.js new file mode 100644 index 000000000..eda165918 --- /dev/null +++ b/lib/Logger.js @@ -0,0 +1,35 @@ +// Logging callback +let logger; + +/** + * Register the logging callback + * + * @param {Function} callback + */ +function registerLogger(callback) { + logger = callback; +} + +/** + * Send an alert message to the logger + * + * @param {String} message + */ +function logAlert(message) { + logger({message: `[Ion] ${message}`, level: 'alert'}); +} + +/** + * Send an info message to the logger + * + * @param {String} message + */ +function logInfo(message) { + logger({message: `[Ion] ${message}`, level: 'info'}); +} + +export { + registerLogger, + logInfo, + logAlert, +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..ced84a9c5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2250 @@ +{ + "name": "react-ion", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@eslint/eslintrc": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", + "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@react-native-community/async-storage": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@react-native-community/async-storage/-/async-storage-1.12.1.tgz", + "integrity": "sha512-70WGaH3PKYASi4BThuEEKMkyAgE9k7VytBqmgPRx3MzJx9/MkspwqJGmn3QLCgHLIFUgF1pit2mWICbRJ3T3lg==", + "requires": { + "deep-assign": "^3.0.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "classnames": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "clipboard": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", + "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "confusing-browser-globals": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz", + "integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.12.1.tgz", + "integrity": "sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-config-airbnb": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", + "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^14.0.0", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "eslint-config-airbnb-base": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", + "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.7", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "eslint-config-expensify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.11.tgz", + "integrity": "sha512-xd/ulRymbTAt+4B47fO2br7BRwEMbNQfGnKraGGGKBfYDGE/pzbapOaHebD7w8jBJwcWswxpD4ywZ+Z7j5Y8Qg==", + "dev": true, + "requires": { + "eslint": "6.8.0", + "eslint-config-airbnb": "18.0.1", + "eslint-config-airbnb-base": "14.0.0", + "eslint-plugin-import": "2.20.0", + "eslint-plugin-jsx-a11y": "6.2.3", + "eslint-plugin-react": "7.18.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz", + "integrity": "sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + } + }, + "eslint-plugin-react": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", + "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.14.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "requires": { + "delegate": "^3.1.2" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, + "js-libs": { + "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "requires": { + "classnames": "2.2.5", + "clipboard": "2.0.4", + "jquery": "3.3.1", + "lodash.get": "4.4.2", + "lodash.has": "4.5.2", + "moment": "2.20.1", + "prop-types": "15.7.2", + "react": "16.12.0", + "react-dom": "16.12.0", + "simply-deferred": "3.0.0", + "underscore": "1.9.1" + }, + "dependencies": { + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "jsx-ast-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", + "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", + "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "react": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", + "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.18.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "dev": true, + "requires": { + "is-core-module": "^2.0.0", + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simply-deferred": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/simply-deferred/-/simply-deferred-3.0.0.tgz", + "integrity": "sha1-bWagZMHysHERyATg7EMlp7UYYp0=" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "underscore": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..efac1ff05 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "react-ion", + "version": "1.0.0", + "author": "Expensify, Inc.", + "homepage": "https://expensify.com", + "description": "State management for React Native", + "private": true, + "scripts": { + "lint": "eslint ." + }, + "dependencies": { + "@react-native-community/async-storage": "^1.12.1", + "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "underscore": "^1.11.0" + }, + "devDependencies": { + "eslint": "^7.6.0", + "eslint-config-expensify": "^2.0.11" + } +} From 8c8d2688472641fc97079d240d143aa0ff4e9a7d Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 4 Nov 2020 14:00:02 -0700 Subject: [PATCH 02/18] Simplify the ignore file --- .gitignore | 59 ------------------------------------------------------ 1 file changed, 59 deletions(-) diff --git a/.gitignore b/.gitignore index 8efffec2c..6c55dbdcd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,66 +2,7 @@ # .DS_Store -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -*.cer -*.p12 -*.mobileprovision - -# Android/IntelliJ -# -build/ -.idea -.gradle -local.properties -*.iml - # node.js # node_modules/ npm-debug.log -yarn-error.log -dist/ - -# BUCK -buck-out/ -\.buckd/ -*.keystore -!debug.keystore -android/app/android-fastlane-json-key.json - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/ - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots - -# Bundle artifact -*.jsbundle - -# CocoaPods -/ios/Pods/ - -# Local DEV config -/.env From 6ca62ad07be3364e1ff0df5f56b0b656f9b57809 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 16:44:41 -0700 Subject: [PATCH 03/18] Remove sublime link --- .editorconfig | 3 --- 1 file changed, 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 212fef118..f2d42b06a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,5 @@ # EditorConfig is awesome: http://EditorConfig.org -# Howto with your editor: -# Sublime: https://github.com/sindresorhus/editorconfig-sublime - # top-most EditorConfig file root = true From 9241188226061801bfb18468121dc439607405ec Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 16:54:15 -0700 Subject: [PATCH 04/18] Rename to onyx --- README.md | 30 ++++++++++++++---------------- index.js | 42 +++++++++++++++++++++--------------------- lib/Logger.js | 4 ++-- package.json | 2 +- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ef0101df2..78ea4c85c 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,41 @@ -# React Ion +# React-Native-Onyx This is a persistent storage solution wrapped in a Pub/Sub library. In general that means: -- Ion stores and retrieves data from persistent storage +- Onyx stores and retrieves data from persistent storage - Data is stored as key/value pairs, where the value can be anything from a single piece of data to a complex object - Collections of data are usually not stored as a single key (eg. an array with multiple objects), but as individual keys+ID (eg. `report_1234`, `report_4567`, etc.). Store collections as individual keys when a component will bind directly to one of those keys. For example: reports are stored as individual keys because `SidebarLink.js` binds to the individual report keys for each link. However, report actions are stored as an array of objects because nothing binds directly to a single report action. -- Ion allows other code to subscribe to changes in data, and then publishes change events whenever data is changed -- Anything needing to read Ion data needs to: +- Onyx allows other code to subscribe to changes in data, and then publishes change events whenever data is changed +- Anything needing to read Onyx data needs to: 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage) - 2. Subscribe to changes of the data for a particular key or set of keys. React components use `withIon()` and non-React libs use `Ion.connect()`. - 3. Get initialized with the current value of that key from persistent storage (Ion does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process) -- Subscribing to Ion keys is done using a constant defined in `IONKEYS`. Each Ion key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (eg. display a list of them in the nav menu), then it would subscribe to the key `IONKEYS.COLLECTION.REPORT`. + 2. Subscribe to changes of the data for a particular key or set of keys. React components use `withOnyx()` and non-React libs use `Onyx.connect()`. + 3. Get initialized with the current value of that key from persistent storage (Onyx does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process) +- Subscribing to Onyx keys is done using a constant defined in `IONKEYS`. Each Onyx key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (eg. display a list of them in the nav menu), then it would subscribe to the key `IONKEYS.COLLECTION.REPORT`. ### Storage Eviction -Different platforms come with varying storage capacities and Ion has a way to gracefully fail when those storage limits are encountered. When Ion fails to set or modify a key the following steps are taken: -1. Ion looks at a list of recently accessed keys (access is defined as subscribed to or modified) and locates the key that was least recently accessed +Different platforms come with varying storage capacities and Onyx has a way to gracefully fail when those storage limits are encountered. When Onyx fails to set or modify a key the following steps are taken: +1. Onyx looks at a list of recently accessed keys (access is defined as subscribed to or modified) and locates the key that was least recently accessed 2. It then deletes this key and retries the original operation -By default, Ion will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise. +By default, Onyx will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise. **To flag a key as safe for removal:** -- Add the key to the `safeEvictionKeys` option in `Ion.init(options)` -- Implement `canEvict` in the Ion config for each component subscribing to a key +- Add the key to the `safeEvictionKeys` option in `Onyx.init(options)` +- Implement `canEvict` in the Onyx config for each component subscribing to a key - The key will only be deleted when all subscribers return `true` for `canEvict` e.g. ```js -Ion.init({ +Onyx.init({ safeEvictionKeys: [IONKEYS.COLLECTION.REPORT_ACTIONS], }); ``` ```js -export default withIon({ +export default withOnyx({ reportActions: { key: ({reportID}) => `${IONKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, canEvict: props => !props.isActiveReport, }, })(ReportActionsView); ``` - -# Deploying diff --git a/index.js b/index.js index aa3e180d7..bb465cb81 100644 --- a/index.js +++ b/index.js @@ -147,13 +147,13 @@ function keyChanged(key, data) { subscriber.callback(data, key); } - if (!subscriber.withIonInstance) { + if (!subscriber.withOnyxInstance) { return; } // Check if we are subscribing to a collection key and add this item as a collection if (isCollectionKey(subscriber.key)) { - subscriber.withIonInstance.setState((prevState) => { + subscriber.withOnyxInstance.setState((prevState) => { const collection = prevState[subscriber.statePropertyName] || {}; // If we have removed the value for this key or it has been @@ -175,7 +175,7 @@ function keyChanged(key, data) { }; }); } else { - subscriber.withIonInstance.setState({ + subscriber.withOnyxInstance.setState({ [subscriber.statePropertyName]: data, }); } @@ -185,18 +185,18 @@ function keyChanged(key, data) { /** * Sends the data obtained from the keys to the connection. It either: - * - sets state on the withIonInstances + * - sets state on the withOnyxInstances * - triggers the callback function * * @param {object} config - * @param {object} [config.withIonInstance] + * @param {object} [config.withOnyxInstance] * @param {string} [config.statePropertyName] * @param {function} [config.callback] * @param {*|null} val */ function sendDataToConnection(config, val) { - if (config.withIonInstance) { - config.withIonInstance.setState({ + if (config.withOnyxInstance) { + config.withOnyxInstance.setState({ [config.statePropertyName]: val, }); } else if (_.isFunction(config.callback)) { @@ -207,13 +207,13 @@ function sendDataToConnection(config, val) { /** * Subscribes a react component's state directly to a store key * - * @param {object} mapping the mapping information to connect Ion to the components state + * @param {object} mapping the mapping information to connect Onyx to the components state * @param {string} mapping.key * @param {string} mapping.statePropertyName the name of the property in the state to connect the data to - * @param {object} [mapping.withIonInstance] whose setState() method will be called with any changed data - * This is used by React components to connect to Ion + * @param {object} [mapping.withOnyxInstance] whose setState() method will be called with any changed data + * This is used by React components to connect to Onyx * @param {object} [mapping.callback] a method that will be called with changed data - * This is used by any non-React code to connect to Ion + * This is used by any non-React code to connect to Onyx * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the * component * @returns {number} an ID to use when calling disconnect @@ -227,7 +227,7 @@ function connect(mapping) { } // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list - if (mapping.withIonInstance && !isCollectionKey(mapping.key) && isSafeEvictionKey(mapping.key)) { + if (mapping.withOnyxInstance && !isCollectionKey(mapping.key) && isSafeEvictionKey(mapping.key)) { // All React components subscribing to a key flagged as a safe eviction // key must implement the canEvict property. if (_.isUndefined(mapping.canEvict)) { @@ -253,7 +253,7 @@ function connect(mapping) { // to expect a single key or multiple keys in the case of a collection. // React components are an exception since we'll want to send their // initial data as a single object when using collection keys. - if (mapping.withIonInstance && isCollectionKey(mapping.key)) { + if (mapping.withOnyxInstance && isCollectionKey(mapping.key)) { Promise.all(_.map(matchingKeys, key => get(key))) .then(values => _.reduce(values, (finalObject, value, i) => ({ ...finalObject, @@ -283,7 +283,7 @@ function disconnect(connectionID) { } /** - * Remove a key from Ion and update the subscribers + * Remove a key from Onyx and update the subscribers * * @param {String} key * @return {Promise} @@ -295,7 +295,7 @@ function remove(key) { /** * If we fail to set or merge we must handle this by - * evicting some data from Ion and then retrying to do + * evicting some data from Onyx and then retrying to do * whatever it is we attempted to do. * * @param {Error} error @@ -341,7 +341,7 @@ function set(key, val) { /** * Sets multiple keys and values. Example - * Ion.multiSet({'key1': 'a', 'key2': 'b'}); + * Onyx.multiSet({'key1': 'a', 'key2': 'b'}); * * @param {object} data * @returns {Promise} @@ -421,15 +421,15 @@ function merge(key, val) { * * @param {Object} [options] * @param {String[]} [options.safeEvictionKeys] This is an array of IONKEYS - * (individual or collection patterns) that when provided to Ion are flagged + * (individual or collection patterns) that when provided to Onyx are flagged * as "safe" for removal. Any components subscribing to these keys must also * implement a canEvict option. See the README for more info. * @param {function} onStorageEvent a callback when a storage event happens. * This applies to web platforms where the local storage emits storage events - * across all open tabs and allows Ion to stay in sync across all open tabs. + * across all open tabs and allows Onyx to stay in sync across all open tabs. */ function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { - // Let Ion know about which keys are safe to evict + // Let Onyx know about which keys are safe to evict evictionAllowList = safeEvictionKeys; // Initialize all of our keys with data provided @@ -439,7 +439,7 @@ function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { onStorageEvent((key, newValue) => keyChanged(key, newValue)); } -const Ion = { +const Onyx = { connect, disconnect, set, @@ -453,4 +453,4 @@ const Ion = { isSafeEvictionKey, }; -export default Ion; +export default Onyx; diff --git a/lib/Logger.js b/lib/Logger.js index eda165918..4ee3f925e 100644 --- a/lib/Logger.js +++ b/lib/Logger.js @@ -16,7 +16,7 @@ function registerLogger(callback) { * @param {String} message */ function logAlert(message) { - logger({message: `[Ion] ${message}`, level: 'alert'}); + logger({message: `[Onyx] ${message}`, level: 'alert'}); } /** @@ -25,7 +25,7 @@ function logAlert(message) { * @param {String} message */ function logInfo(message) { - logger({message: `[Ion] ${message}`, level: 'info'}); + logger({message: `[Onyx] ${message}`, level: 'info'}); } export { diff --git a/package.json b/package.json index efac1ff05..98d810607 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "react-ion", + "name": "react-native-onyx", "version": "1.0.0", "author": "Expensify, Inc.", "homepage": "https://expensify.com", From b4467e863d449ecc45efdd5b3bab0fd36688340a Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 16:59:27 -0700 Subject: [PATCH 05/18] Move onyx to the lib folder --- index.js | 457 +--------------------------------------------------- lib/Onyx.js | 456 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 459 insertions(+), 454 deletions(-) create mode 100644 lib/Onyx.js diff --git a/index.js b/index.js index bb465cb81..0eb59f408 100644 --- a/index.js +++ b/index.js @@ -1,456 +1,5 @@ -import _ from 'underscore'; -import AsyncStorage from '@react-native-community/async-storage'; -import Str from 'js-libs/lib/str'; -import {registerLogger, logInfo, logAlert} from './lib/Logger'; - -// Keeps track of the last connectionID that was used so we can keep incrementing it -let lastConnectionID = 0; - -// Holds a mapping of all the react components that want their state subscribed to a store key -const callbackToStateMapping = {}; - -// Holds a list of keys that have been directly subscribed to or recently modified from least to most recent -let recentlyAccessedKeys = []; - -// Holds a list of keys that are safe to remove when we reach max storage -let evictionAllowList = []; - -// Holds a list of keys that we should never remove -const evictionBlocklist = {}; - -/** - * When a key change happens, search for any callbacks matching the regex pattern and trigger those callbacks - * Get some data from the store - * - * @param {string} key - * @returns {Promise<*>} - */ -function get(key) { - return AsyncStorage.getItem(key) - .then(val => JSON.parse(val)) - .catch(err => logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); -} - -/** - * Checks to see if the a subscriber's supplied key - * is associated with a collection of keys. - * - * @param {String} key - * @returns {Boolean} - */ -function isCollectionKey(key) { - // Any key that ends with an underscore is a collection - return Str.endsWith(key, '_'); -} - -/** - * Checks to see if a given key matches with the - * configured key of our connected subscriber - * - * @param {String} configKey - * @param {String} key - * @return {Boolean} - */ -function isKeyMatch(configKey, key) { - return isCollectionKey(configKey) - ? Str.startsWith(key, configKey) - : configKey === key; -} - -/** - * Checks to see if this key has been flagged as - * safe for removal. - * - * @param {String} testKey - * @returns {Boolean} - */ -function isSafeEvictionKey(testKey) { - return _.some(evictionAllowList, key => isKeyMatch(key, testKey)); -} - -/** - * Remove a key from the recently accessed key list. - * - * @param {String} key - */ -function removeLastAccessedKey(key) { - recentlyAccessedKeys = _.without(recentlyAccessedKeys, key); -} - -/** - * Add a key to the list of recently accessed keys. The least - * recently accessed key should be at the head and the most - * recently accessed key at the tail. - * - * @param {String} key - */ -function addLastAccessedKey(key) { - // Only specific keys belong in this list since we cannot remove an entire collection. - if (isCollectionKey(key) || !isSafeEvictionKey(key)) { - return; - } - - removeLastAccessedKey(key); - recentlyAccessedKeys.push(key); -} - -/** - * Removes a key previously added to this list - * which will enable it to be deleted again. - * - * @param {String} key - * @param {Number} connectionID - */ -function removeFromEvictionBlockList(key, connectionID) { - evictionBlocklist[key] = _.without(evictionBlocklist[key] || [], connectionID); - - // Remove the key if there are no more subscribers - if (evictionBlocklist[key].length === 0) { - delete evictionBlocklist[key]; - } -} - -/** - * Keys added to this list can never be deleted. - * - * @param {String} key - * @param {Number} connectionID - */ -function addToEvictionBlockList(key, connectionID) { - removeFromEvictionBlockList(key, connectionID); - - if (!evictionBlocklist[key]) { - evictionBlocklist[key] = []; - } - - evictionBlocklist[key].push(connectionID); -} - -/** - * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks - * - * @param {string} key - * @param {mixed} data - */ -function keyChanged(key, data) { - // Add or remove this key from the recentlyAccessedKeys lists - if (!_.isNull(data)) { - addLastAccessedKey(key); - } else { - removeLastAccessedKey(key); - } - - // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data - _.each(callbackToStateMapping, (subscriber) => { - if (subscriber && isKeyMatch(subscriber.key, key)) { - if (_.isFunction(subscriber.callback)) { - subscriber.callback(data, key); - } - - if (!subscriber.withOnyxInstance) { - return; - } - - // Check if we are subscribing to a collection key and add this item as a collection - if (isCollectionKey(subscriber.key)) { - subscriber.withOnyxInstance.setState((prevState) => { - const collection = prevState[subscriber.statePropertyName] || {}; - - // If we have removed the value for this key or it has been - // deleted then remove it from the collection and update - if (_.isNull(data)) { - // We do not have this key in the collection so don't - // bother to update the component state here - if (!collection[key]) { - return; - } - - delete collection[key]; - } else { - collection[key] = data; - } - - return { - [subscriber.statePropertyName]: collection, - }; - }); - } else { - subscriber.withOnyxInstance.setState({ - [subscriber.statePropertyName]: data, - }); - } - } - }); -} - -/** - * Sends the data obtained from the keys to the connection. It either: - * - sets state on the withOnyxInstances - * - triggers the callback function - * - * @param {object} config - * @param {object} [config.withOnyxInstance] - * @param {string} [config.statePropertyName] - * @param {function} [config.callback] - * @param {*|null} val - */ -function sendDataToConnection(config, val) { - if (config.withOnyxInstance) { - config.withOnyxInstance.setState({ - [config.statePropertyName]: val, - }); - } else if (_.isFunction(config.callback)) { - config.callback(val); - } -} - -/** - * Subscribes a react component's state directly to a store key - * - * @param {object} mapping the mapping information to connect Onyx to the components state - * @param {string} mapping.key - * @param {string} mapping.statePropertyName the name of the property in the state to connect the data to - * @param {object} [mapping.withOnyxInstance] whose setState() method will be called with any changed data - * This is used by React components to connect to Onyx - * @param {object} [mapping.callback] a method that will be called with changed data - * This is used by any non-React code to connect to Onyx - * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the - * component - * @returns {number} an ID to use when calling disconnect - */ -function connect(mapping) { - const connectionID = lastConnectionID++; - callbackToStateMapping[connectionID] = mapping; - - if (mapping.initWithStoredValues === false) { - return connectionID; - } - - // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list - if (mapping.withOnyxInstance && !isCollectionKey(mapping.key) && isSafeEvictionKey(mapping.key)) { - // All React components subscribing to a key flagged as a safe eviction - // key must implement the canEvict property. - if (_.isUndefined(mapping.canEvict)) { - // eslint-disable-next-line max-len - throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`); - } - addLastAccessedKey(mapping.key); - } - - AsyncStorage.getAllKeys() - .then((keys) => { - // Find all the keys matched by the config key - const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key)); - - // If the key being connected to does not exist, initialize the value with null - if (matchingKeys.length === 0) { - sendDataToConnection(mapping, null); - return; - } - - // When using a callback subscriber we will trigger the callback - // for each key we find. It's up to the subscriber to know whether - // to expect a single key or multiple keys in the case of a collection. - // React components are an exception since we'll want to send their - // initial data as a single object when using collection keys. - if (mapping.withOnyxInstance && isCollectionKey(mapping.key)) { - Promise.all(_.map(matchingKeys, key => get(key))) - .then(values => _.reduce(values, (finalObject, value, i) => ({ - ...finalObject, - [matchingKeys[i]]: value, - }), {})) - .then(val => sendDataToConnection(mapping, val)); - } else { - _.each(matchingKeys, (key) => { - get(key).then(val => sendDataToConnection(mapping, val)); - }); - } - }); - - return connectionID; -} - -/** - * Remove the listener for a react component - * - * @param {string} connectionID - */ -function disconnect(connectionID) { - if (!callbackToStateMapping[connectionID]) { - return; - } - delete callbackToStateMapping[connectionID]; -} - -/** - * Remove a key from Onyx and update the subscribers - * - * @param {String} key - * @return {Promise} - */ -function remove(key) { - return AsyncStorage.removeItem(key) - .then(() => keyChanged(key, null)); -} - -/** - * If we fail to set or merge we must handle this by - * evicting some data from Onyx and then retrying to do - * whatever it is we attempted to do. - * - * @param {Error} error - * @param {Function} ionMethod - * @param {...any} args - * @return {Promise} - */ -function evictStorageAndRetry(error, ionMethod, ...args) { - // Find the first key that we can remove that has no subscribers in our blocklist - const keyForRemoval = _.find(recentlyAccessedKeys, (key) => { - const keyParts = key.split('_'); - const keyPrefix = `${keyParts.slice(0, keyParts.length - 1).join('_')}_`; - return !evictionBlocklist[keyPrefix]; - }); - - if (!keyForRemoval) { - logAlert('Out of storage. But found no acceptable keys to remove.'); - throw error; - } - - // We must immediately remove this so we do not try to remove the same key twice. - removeLastAccessedKey(keyForRemoval); - - // Remove the least recently viewed key that is not currently being accessed and retry. - logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); - return remove(keyForRemoval) - .then(() => ionMethod(...args)); -} - -/** - * Write a value to our store with the given key - * - * @param {string} key - * @param {mixed} val - * @returns {Promise} - */ -function set(key, val) { - // Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain - return AsyncStorage.setItem(key, JSON.stringify(val)) - .then(() => keyChanged(key, val)) - .catch(error => evictStorageAndRetry(error, set, key, val)); -} - -/** - * Sets multiple keys and values. Example - * Onyx.multiSet({'key1': 'a', 'key2': 'b'}); - * - * @param {object} data - * @returns {Promise} - */ -function multiSet(data) { - // AsyncStorage expenses the data in an array like: - // [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]] - // This method will transform the params from a better JSON format like: - // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'} - const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([ - ...finalArray, - [key, JSON.stringify(val)], - ]), []); - - return AsyncStorage.multiSet(keyValuePairs) - .then(() => _.each(data, (val, key) => keyChanged(key, val))) - .catch(error => evictStorageAndRetry(error, multiSet, data)); -} - -/** - * Clear out all the data in the store - * - * @returns {Promise} - */ -function clear() { - let allKeys; - return AsyncStorage.getAllKeys() - .then(keys => allKeys = keys) - .then(() => AsyncStorage.clear()) - .then(() => { - _.each(allKeys, (key) => { - keyChanged(key, null); - }); - }); -} - -/** - * Merge a new value into an existing value at a key - * - * @param {string} key - * @param {*} val - */ -function merge(key, val) { - // Arrays need to be manually merged because the AsyncStorage behavior - // is not desired when merging arrays. `AsyncStorage.mergeItem('test', [1]); - // will result in `{0: 1}` being set in storage, when `[1]` is what is expected - if (_.isArray(val)) { - let newArray; - get(key) - .then((prevVal) => { - const previousValue = prevVal || []; - newArray = [...previousValue, ...val]; - return AsyncStorage.setItem(key, JSON.stringify(newArray)); - }) - .then(() => keyChanged(key, newArray)) - .catch(error => evictStorageAndRetry(error, merge, key, val)); - return; - } - - // Values that are objects are merged normally into storage - if (_.isObject(val)) { - AsyncStorage.mergeItem(key, JSON.stringify(val)) - .then(() => get(key)) - .then((newObject) => { - keyChanged(key, newObject); - }) - .catch(error => evictStorageAndRetry(error, merge, key, val)); - return; - } - - // Anything else (strings and numbers) need to be set into storage - set(key, val); -} - -/** - * Initialize the store with actions and listening for storage events - * - * @param {Object} [options] - * @param {String[]} [options.safeEvictionKeys] This is an array of IONKEYS - * (individual or collection patterns) that when provided to Onyx are flagged - * as "safe" for removal. Any components subscribing to these keys must also - * implement a canEvict option. See the README for more info. - * @param {function} onStorageEvent a callback when a storage event happens. - * This applies to web platforms where the local storage emits storage events - * across all open tabs and allows Onyx to stay in sync across all open tabs. - */ -function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { - // Let Onyx know about which keys are safe to evict - evictionAllowList = safeEvictionKeys; - - // Initialize all of our keys with data provided - _.each(initialKeyStates, (state, key) => merge(key, state)); - - // Update any key whose value changes in storage - onStorageEvent((key, newValue) => keyChanged(key, newValue)); -} - -const Onyx = { - connect, - disconnect, - set, - multiSet, - merge, - clear, - init, - registerLogger, - addToEvictionBlockList, - removeFromEvictionBlockList, - isSafeEvictionKey, -}; +import Onyx from './lib/Onyx'; +import withOnyx from './lib/withOnyx'; export default Onyx; +export {withOnyx}; diff --git a/lib/Onyx.js b/lib/Onyx.js new file mode 100644 index 000000000..11c295dd6 --- /dev/null +++ b/lib/Onyx.js @@ -0,0 +1,456 @@ +import _ from 'underscore'; +import AsyncStorage from '@react-native-community/async-storage'; +import Str from 'js-libs/lib/str'; +import {registerLogger, logInfo, logAlert} from './Logger'; + +// Keeps track of the last connectionID that was used so we can keep incrementing it +let lastConnectionID = 0; + +// Holds a mapping of all the react components that want their state subscribed to a store key +const callbackToStateMapping = {}; + +// Holds a list of keys that have been directly subscribed to or recently modified from least to most recent +let recentlyAccessedKeys = []; + +// Holds a list of keys that are safe to remove when we reach max storage +let evictionAllowList = []; + +// Holds a list of keys that we should never remove +const evictionBlocklist = {}; + +/** + * When a key change happens, search for any callbacks matching the regex pattern and trigger those callbacks + * Get some data from the store + * + * @param {string} key + * @returns {Promise<*>} + */ +function get(key) { + return AsyncStorage.getItem(key) + .then(val => JSON.parse(val)) + .catch(err => logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); +} + +/** + * Checks to see if the a subscriber's supplied key + * is associated with a collection of keys. + * + * @param {String} key + * @returns {Boolean} + */ +function isCollectionKey(key) { + // Any key that ends with an underscore is a collection + return Str.endsWith(key, '_'); +} + +/** + * Checks to see if a given key matches with the + * configured key of our connected subscriber + * + * @param {String} configKey + * @param {String} key + * @return {Boolean} + */ +function isKeyMatch(configKey, key) { + return isCollectionKey(configKey) + ? Str.startsWith(key, configKey) + : configKey === key; +} + +/** + * Checks to see if this key has been flagged as + * safe for removal. + * + * @param {String} testKey + * @returns {Boolean} + */ +function isSafeEvictionKey(testKey) { + return _.some(evictionAllowList, key => isKeyMatch(key, testKey)); +} + +/** + * Remove a key from the recently accessed key list. + * + * @param {String} key + */ +function removeLastAccessedKey(key) { + recentlyAccessedKeys = _.without(recentlyAccessedKeys, key); +} + +/** + * Add a key to the list of recently accessed keys. The least + * recently accessed key should be at the head and the most + * recently accessed key at the tail. + * + * @param {String} key + */ +function addLastAccessedKey(key) { + // Only specific keys belong in this list since we cannot remove an entire collection. + if (isCollectionKey(key) || !isSafeEvictionKey(key)) { + return; + } + + removeLastAccessedKey(key); + recentlyAccessedKeys.push(key); +} + +/** + * Removes a key previously added to this list + * which will enable it to be deleted again. + * + * @param {String} key + * @param {Number} connectionID + */ +function removeFromEvictionBlockList(key, connectionID) { + evictionBlocklist[key] = _.without(evictionBlocklist[key] || [], connectionID); + + // Remove the key if there are no more subscribers + if (evictionBlocklist[key].length === 0) { + delete evictionBlocklist[key]; + } +} + +/** + * Keys added to this list can never be deleted. + * + * @param {String} key + * @param {Number} connectionID + */ +function addToEvictionBlockList(key, connectionID) { + removeFromEvictionBlockList(key, connectionID); + + if (!evictionBlocklist[key]) { + evictionBlocklist[key] = []; + } + + evictionBlocklist[key].push(connectionID); +} + +/** + * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks + * + * @param {string} key + * @param {mixed} data + */ +function keyChanged(key, data) { + // Add or remove this key from the recentlyAccessedKeys lists + if (!_.isNull(data)) { + addLastAccessedKey(key); + } else { + removeLastAccessedKey(key); + } + + // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data + _.each(callbackToStateMapping, (subscriber) => { + if (subscriber && isKeyMatch(subscriber.key, key)) { + if (_.isFunction(subscriber.callback)) { + subscriber.callback(data, key); + } + + if (!subscriber.withOnyxInstance) { + return; + } + + // Check if we are subscribing to a collection key and add this item as a collection + if (isCollectionKey(subscriber.key)) { + subscriber.withOnyxInstance.setState((prevState) => { + const collection = prevState[subscriber.statePropertyName] || {}; + + // If we have removed the value for this key or it has been + // deleted then remove it from the collection and update + if (_.isNull(data)) { + // We do not have this key in the collection so don't + // bother to update the component state here + if (!collection[key]) { + return; + } + + delete collection[key]; + } else { + collection[key] = data; + } + + return { + [subscriber.statePropertyName]: collection, + }; + }); + } else { + subscriber.withOnyxInstance.setState({ + [subscriber.statePropertyName]: data, + }); + } + } + }); +} + +/** + * Sends the data obtained from the keys to the connection. It either: + * - sets state on the withOnyxInstances + * - triggers the callback function + * + * @param {object} config + * @param {object} [config.withOnyxInstance] + * @param {string} [config.statePropertyName] + * @param {function} [config.callback] + * @param {*|null} val + */ +function sendDataToConnection(config, val) { + if (config.withOnyxInstance) { + config.withOnyxInstance.setState({ + [config.statePropertyName]: val, + }); + } else if (_.isFunction(config.callback)) { + config.callback(val); + } +} + +/** + * Subscribes a react component's state directly to a store key + * + * @param {object} mapping the mapping information to connect Onyx to the components state + * @param {string} mapping.key + * @param {string} mapping.statePropertyName the name of the property in the state to connect the data to + * @param {object} [mapping.withOnyxInstance] whose setState() method will be called with any changed data + * This is used by React components to connect to Onyx + * @param {object} [mapping.callback] a method that will be called with changed data + * This is used by any non-React code to connect to Onyx + * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the + * component + * @returns {number} an ID to use when calling disconnect + */ +function connect(mapping) { + const connectionID = lastConnectionID++; + callbackToStateMapping[connectionID] = mapping; + + if (mapping.initWithStoredValues === false) { + return connectionID; + } + + // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list + if (mapping.withOnyxInstance && !isCollectionKey(mapping.key) && isSafeEvictionKey(mapping.key)) { + // All React components subscribing to a key flagged as a safe eviction + // key must implement the canEvict property. + if (_.isUndefined(mapping.canEvict)) { + // eslint-disable-next-line max-len + throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`); + } + addLastAccessedKey(mapping.key); + } + + AsyncStorage.getAllKeys() + .then((keys) => { + // Find all the keys matched by the config key + const matchingKeys = _.filter(keys, key => isKeyMatch(mapping.key, key)); + + // If the key being connected to does not exist, initialize the value with null + if (matchingKeys.length === 0) { + sendDataToConnection(mapping, null); + return; + } + + // When using a callback subscriber we will trigger the callback + // for each key we find. It's up to the subscriber to know whether + // to expect a single key or multiple keys in the case of a collection. + // React components are an exception since we'll want to send their + // initial data as a single object when using collection keys. + if (mapping.withOnyxInstance && isCollectionKey(mapping.key)) { + Promise.all(_.map(matchingKeys, key => get(key))) + .then(values => _.reduce(values, (finalObject, value, i) => ({ + ...finalObject, + [matchingKeys[i]]: value, + }), {})) + .then(val => sendDataToConnection(mapping, val)); + } else { + _.each(matchingKeys, (key) => { + get(key).then(val => sendDataToConnection(mapping, val)); + }); + } + }); + + return connectionID; +} + +/** + * Remove the listener for a react component + * + * @param {string} connectionID + */ +function disconnect(connectionID) { + if (!callbackToStateMapping[connectionID]) { + return; + } + delete callbackToStateMapping[connectionID]; +} + +/** + * Remove a key from Onyx and update the subscribers + * + * @param {String} key + * @return {Promise} + */ +function remove(key) { + return AsyncStorage.removeItem(key) + .then(() => keyChanged(key, null)); +} + +/** + * If we fail to set or merge we must handle this by + * evicting some data from Onyx and then retrying to do + * whatever it is we attempted to do. + * + * @param {Error} error + * @param {Function} ionMethod + * @param {...any} args + * @return {Promise} + */ +function evictStorageAndRetry(error, ionMethod, ...args) { + // Find the first key that we can remove that has no subscribers in our blocklist + const keyForRemoval = _.find(recentlyAccessedKeys, (key) => { + const keyParts = key.split('_'); + const keyPrefix = `${keyParts.slice(0, keyParts.length - 1).join('_')}_`; + return !evictionBlocklist[keyPrefix]; + }); + + if (!keyForRemoval) { + logAlert('Out of storage. But found no acceptable keys to remove.'); + throw error; + } + + // We must immediately remove this so we do not try to remove the same key twice. + removeLastAccessedKey(keyForRemoval); + + // Remove the least recently viewed key that is not currently being accessed and retry. + logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); + return remove(keyForRemoval) + .then(() => ionMethod(...args)); +} + +/** + * Write a value to our store with the given key + * + * @param {string} key + * @param {mixed} val + * @returns {Promise} + */ +function set(key, val) { + // Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain + return AsyncStorage.setItem(key, JSON.stringify(val)) + .then(() => keyChanged(key, val)) + .catch(error => evictStorageAndRetry(error, set, key, val)); +} + +/** + * Sets multiple keys and values. Example + * Onyx.multiSet({'key1': 'a', 'key2': 'b'}); + * + * @param {object} data + * @returns {Promise} + */ +function multiSet(data) { + // AsyncStorage expenses the data in an array like: + // [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]] + // This method will transform the params from a better JSON format like: + // {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'} + const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([ + ...finalArray, + [key, JSON.stringify(val)], + ]), []); + + return AsyncStorage.multiSet(keyValuePairs) + .then(() => _.each(data, (val, key) => keyChanged(key, val))) + .catch(error => evictStorageAndRetry(error, multiSet, data)); +} + +/** + * Clear out all the data in the store + * + * @returns {Promise} + */ +function clear() { + let allKeys; + return AsyncStorage.getAllKeys() + .then(keys => allKeys = keys) + .then(() => AsyncStorage.clear()) + .then(() => { + _.each(allKeys, (key) => { + keyChanged(key, null); + }); + }); +} + +/** + * Merge a new value into an existing value at a key + * + * @param {string} key + * @param {*} val + */ +function merge(key, val) { + // Arrays need to be manually merged because the AsyncStorage behavior + // is not desired when merging arrays. `AsyncStorage.mergeItem('test', [1]); + // will result in `{0: 1}` being set in storage, when `[1]` is what is expected + if (_.isArray(val)) { + let newArray; + get(key) + .then((prevVal) => { + const previousValue = prevVal || []; + newArray = [...previousValue, ...val]; + return AsyncStorage.setItem(key, JSON.stringify(newArray)); + }) + .then(() => keyChanged(key, newArray)) + .catch(error => evictStorageAndRetry(error, merge, key, val)); + return; + } + + // Values that are objects are merged normally into storage + if (_.isObject(val)) { + AsyncStorage.mergeItem(key, JSON.stringify(val)) + .then(() => get(key)) + .then((newObject) => { + keyChanged(key, newObject); + }) + .catch(error => evictStorageAndRetry(error, merge, key, val)); + return; + } + + // Anything else (strings and numbers) need to be set into storage + set(key, val); +} + +/** + * Initialize the store with actions and listening for storage events + * + * @param {Object} [options] + * @param {String[]} [options.safeEvictionKeys] This is an array of IONKEYS + * (individual or collection patterns) that when provided to Onyx are flagged + * as "safe" for removal. Any components subscribing to these keys must also + * implement a canEvict option. See the README for more info. + * @param {function} onStorageEvent a callback when a storage event happens. + * This applies to web platforms where the local storage emits storage events + * across all open tabs and allows Onyx to stay in sync across all open tabs. + */ +function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { + // Let Onyx know about which keys are safe to evict + evictionAllowList = safeEvictionKeys; + + // Initialize all of our keys with data provided + _.each(initialKeyStates, (state, key) => merge(key, state)); + + // Update any key whose value changes in storage + onStorageEvent((key, newValue) => keyChanged(key, newValue)); +} + +const Onyx = { + connect, + disconnect, + set, + multiSet, + merge, + clear, + init, + registerLogger, + addToEvictionBlockList, + removeFromEvictionBlockList, + isSafeEvictionKey, +}; + +export default Onyx; From 6dae3a5eb101bbbe10a7ebea0052ec67650a33b8 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 16:59:36 -0700 Subject: [PATCH 06/18] Add the HOC --- lib/withOnyx.js | 163 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 lib/withOnyx.js diff --git a/lib/withOnyx.js b/lib/withOnyx.js new file mode 100644 index 000000000..31dd5461b --- /dev/null +++ b/lib/withOnyx.js @@ -0,0 +1,163 @@ +/** + * This is a higher order component that provides the ability to map a state property directly to + * something in Ion (a key/value store). That way, as soon as data in Ion changes, the state will be set and the view + * will automatically change to reflect the new data. + */ +import React from 'react'; +import _ from 'underscore'; +import Str from 'js-libs/lib/str'; +import Onyx from './Onyx'; + +/** + * Returns the display name of a component + * + * @param {object} component + * @returns {string} + */ +function getDisplayName(component) { + return component.displayName || component.name || 'Component'; +} + +export default function (mapIonToState) { + return (WrappedComponent) => { + class withOnyx extends React.Component { + constructor(props) { + super(props); + + // This stores all the Onyx connection IDs to be used when the component unmounts so everything can be + // disconnected. It is a key value store with the format {[mapping.key]: connectionID}. + this.activeConnectionIDs = {}; + + this.state = { + loading: true, + }; + } + + componentDidMount() { + // Subscribe each of the state properties to the proper Ion key + _.each(mapIonToState, (mapping, propertyName) => { + this.connectMappingToOnyx(mapping, propertyName); + }); + this.checkAndUpdateLoading(); + } + + componentDidUpdate(prevProps) { + // If any of the mappings use data from the props, then when the props change, all the + // connections need to be reconnected with the new props + _.each(mapIonToState, (mapping, propertyName) => { + const previousKey = Str.result(mapping.key, prevProps); + const newKey = Str.result(mapping.key, this.props); + + if (previousKey !== newKey) { + Onyx.disconnect(this.activeConnectionIDs[previousKey], previousKey); + delete this.activeConnectionIDs[previousKey]; + this.connectMappingToOnyx(mapping, propertyName); + } + }); + this.checkAndUpdateLoading(); + } + + componentWillUnmount() { + // Disconnect everything from Ion + _.each(mapIonToState, (mapping) => { + const key = Str.result(mapping.key, this.props); + const connectionID = this.activeConnectionIDs[key]; + Onyx.disconnect(connectionID, key); + }); + } + + /** + * Makes sure each Ion key we requested has been set to state with a value of some kind. + * We are doing this so that the wrapped component will only render when all the data + * it needs is available to it. + */ + checkAndUpdateLoading() { + // We will add this key to our list of recently accessed keys + // if the canEvict function returns true. This is necessary criteria + // we MUST use to specify if a key can be removed or not. + _.each(mapIonToState, (mapping) => { + if (_.isUndefined(mapping.canEvict)) { + return; + } + + const canEvict = Str.result(mapping.canEvict, this.props); + const key = Str.result(mapping.key, this.props); + + if (!Onyx.isSafeEvictionKey(key)) { + // eslint-disable-next-line max-len + throw new Error(`canEvict cannot be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Ion.init({safeEvictionKeys: []}).`); + } + + if (canEvict) { + Onyx.removeFromEvictionBlockList(key, mapping.connectionID); + } else { + Onyx.addToEvictionBlockList(key, mapping.connectionID); + } + }); + + if (!this.state.loading) { + return; + } + + // Filter all keys by those which we do want to init with stored values + // since keys that are configured to not init with stored values will + // never appear on state when the component mounts - only after they update + // organically. + const requiredKeysForInit = _.chain(mapIonToState) + .omit(config => config.initWithStoredValues === false) + .keys() + .value(); + + // All state keys should exist and at least have a value of null + if (_.every(requiredKeysForInit, key => !_.isUndefined(this.state[key]))) { + this.setState({loading: false}); + } + } + + /** + * Takes a single mapping and binds the state of the component to the store + * + * @param {object} mapping + * @param {string|function} mapping.key key to connect to. can be a string or a + * function that takes this.props as an argument and returns a string + * @param {string} statePropertyName the name of the state property that Ion will add the data to + * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the + * component + */ + connectMappingToOnyx(mapping, statePropertyName) { + const key = Str.result(mapping.key, this.props); + const connectionID = Onyx.connect({ + ...mapping, + key, + statePropertyName, + withOnyxInstance: this, + }); + + this.activeConnectionIDs[key] = connectionID; + } + + render() { + if (this.state.loading) { + return null; + } + + // Remove any internal state properties used by withOnyx + // that should not be passed to a wrapped component + const stateToPass = _.omit(this.state, 'loading'); + + // Spreading props and state is necessary in an HOC where the data cannot be predicted + return ( + + ); + } + } + + withOnyx.displayName = `withOnyx(${getDisplayName(WrappedComponent)})`; + return withOnyx; + }; +} From ecc516356f9297ba0fb113a42ec11203edddad90 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 16:59:45 -0700 Subject: [PATCH 07/18] Include react in the dependencies --- package-lock.json | 21 +++++++++++++++------ package.json | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ced84a9c5..a2a4b0852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "react-ion", + "name": "react-native-onyx", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -1396,6 +1396,16 @@ "underscore": "1.9.1" }, "dependencies": { + "react": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, "underscore": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", @@ -1796,13 +1806,12 @@ "dev": true }, "react": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", - "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", + "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" } }, "react-dom": { diff --git a/package.json b/package.json index 98d810607..e185c4de9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@react-native-community/async-storage": "^1.12.1", "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "react": "^17.0.1", "underscore": "^1.11.0" }, "devDependencies": { From 38ac6c9fec72ad63bbe4b41316446f6642b51ff8 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 17:01:31 -0700 Subject: [PATCH 08/18] More renaming --- lib/withOnyx.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/withOnyx.js b/lib/withOnyx.js index 31dd5461b..23d61ed82 100644 --- a/lib/withOnyx.js +++ b/lib/withOnyx.js @@ -1,6 +1,6 @@ /** * This is a higher order component that provides the ability to map a state property directly to - * something in Ion (a key/value store). That way, as soon as data in Ion changes, the state will be set and the view + * something in Onyx (a key/value store). That way, as soon as data in Onyx changes, the state will be set and the view * will automatically change to reflect the new data. */ import React from 'react'; @@ -18,7 +18,7 @@ function getDisplayName(component) { return component.displayName || component.name || 'Component'; } -export default function (mapIonToState) { +export default function (mapOnyxToState) { return (WrappedComponent) => { class withOnyx extends React.Component { constructor(props) { @@ -34,8 +34,8 @@ export default function (mapIonToState) { } componentDidMount() { - // Subscribe each of the state properties to the proper Ion key - _.each(mapIonToState, (mapping, propertyName) => { + // Subscribe each of the state properties to the proper Onyx key + _.each(mapOnyxToState, (mapping, propertyName) => { this.connectMappingToOnyx(mapping, propertyName); }); this.checkAndUpdateLoading(); @@ -44,7 +44,7 @@ export default function (mapIonToState) { componentDidUpdate(prevProps) { // If any of the mappings use data from the props, then when the props change, all the // connections need to be reconnected with the new props - _.each(mapIonToState, (mapping, propertyName) => { + _.each(mapOnyxToState, (mapping, propertyName) => { const previousKey = Str.result(mapping.key, prevProps); const newKey = Str.result(mapping.key, this.props); @@ -58,8 +58,8 @@ export default function (mapIonToState) { } componentWillUnmount() { - // Disconnect everything from Ion - _.each(mapIonToState, (mapping) => { + // Disconnect everything from Onyx + _.each(mapOnyxToState, (mapping) => { const key = Str.result(mapping.key, this.props); const connectionID = this.activeConnectionIDs[key]; Onyx.disconnect(connectionID, key); @@ -67,7 +67,7 @@ export default function (mapIonToState) { } /** - * Makes sure each Ion key we requested has been set to state with a value of some kind. + * Makes sure each Onyx key we requested has been set to state with a value of some kind. * We are doing this so that the wrapped component will only render when all the data * it needs is available to it. */ @@ -75,7 +75,7 @@ export default function (mapIonToState) { // We will add this key to our list of recently accessed keys // if the canEvict function returns true. This is necessary criteria // we MUST use to specify if a key can be removed or not. - _.each(mapIonToState, (mapping) => { + _.each(mapOnyxToState, (mapping) => { if (_.isUndefined(mapping.canEvict)) { return; } @@ -85,7 +85,7 @@ export default function (mapIonToState) { if (!Onyx.isSafeEvictionKey(key)) { // eslint-disable-next-line max-len - throw new Error(`canEvict cannot be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Ion.init({safeEvictionKeys: []}).`); + throw new Error(`canEvict cannot be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`); } if (canEvict) { @@ -103,7 +103,7 @@ export default function (mapIonToState) { // since keys that are configured to not init with stored values will // never appear on state when the component mounts - only after they update // organically. - const requiredKeysForInit = _.chain(mapIonToState) + const requiredKeysForInit = _.chain(mapOnyxToState) .omit(config => config.initWithStoredValues === false) .keys() .value(); @@ -120,7 +120,7 @@ export default function (mapIonToState) { * @param {object} mapping * @param {string|function} mapping.key key to connect to. can be a string or a * function that takes this.props as an argument and returns a string - * @param {string} statePropertyName the name of the state property that Ion will add the data to + * @param {string} statePropertyName the name of the state property that Onyx will add the data to * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the * component */ From 093897adbe7a69360640712a08f5458721d4ef78 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 10 Nov 2020 17:17:31 -0700 Subject: [PATCH 09/18] Update code from master --- lib/Onyx.js | 155 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 99 insertions(+), 56 deletions(-) diff --git a/lib/Onyx.js b/lib/Onyx.js index 11c295dd6..7573dd126 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -9,13 +9,18 @@ let lastConnectionID = 0; // Holds a mapping of all the react components that want their state subscribed to a store key const callbackToStateMapping = {}; +// Stores all of the keys that Onyx can use. Must be defined in init(). +let onyxKeys; + // Holds a list of keys that have been directly subscribed to or recently modified from least to most recent let recentlyAccessedKeys = []; -// Holds a list of keys that are safe to remove when we reach max storage +// Holds a list of keys that are safe to remove when we reach max storage. If a key does not match with +// whatever appears in this list it will NEVER be a candidate for eviction. let evictionAllowList = []; -// Holds a list of keys that we should never remove +// Holds a map of keys and connectionID arrays whose keys will never be automatically evicted as +// long as we have at least one subscriber that returns false for the canEvict property. const evictionBlocklist = {}; /** @@ -39,8 +44,7 @@ function get(key) { * @returns {Boolean} */ function isCollectionKey(key) { - // Any key that ends with an underscore is a collection - return Str.endsWith(key, '_'); + return _.contains(_.values(onyxKeys.COLLECTION), key); } /** @@ -126,6 +130,25 @@ function addToEvictionBlockList(key, connectionID) { evictionBlocklist[key].push(connectionID); } +/** + * Take all the keys that are safe to evict and add them to + * the recently accessed list when initializing the app. This + * enables keys that have not recently been accessed to be + * removed. + */ +function addAllSafeEvictionKeysToRecentlyAccessedList() { + AsyncStorage.getAllKeys() + .then((keys) => { + _.each(evictionAllowList, (safeEvictionKey) => { + _.each(keys, (key) => { + if (isKeyMatch(safeEvictionKey, key)) { + addLastAccessedKey(key); + } + }); + }); + }); +} + /** * When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks * @@ -155,21 +178,7 @@ function keyChanged(key, data) { if (isCollectionKey(subscriber.key)) { subscriber.withOnyxInstance.setState((prevState) => { const collection = prevState[subscriber.statePropertyName] || {}; - - // If we have removed the value for this key or it has been - // deleted then remove it from the collection and update - if (_.isNull(data)) { - // We do not have this key in the collection so don't - // bother to update the component state here - if (!collection[key]) { - return; - } - - delete collection[key]; - } else { - collection[key] = data; - } - + collection[key] = data; return { [subscriber.statePropertyName]: collection, }; @@ -273,12 +282,20 @@ function connect(mapping) { /** * Remove the listener for a react component * - * @param {string} connectionID + * @param {Number} connectionID + * @param {String} [keyToRemoveFromEvictionBlocklist] */ -function disconnect(connectionID) { +function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) { if (!callbackToStateMapping[connectionID]) { return; } + + // Remove this key from the eviction block list as we are no longer + // subscribing to it and it should be safe to delete again + if (keyToRemoveFromEvictionBlocklist) { + removeFromEvictionBlockList(keyToRemoveFromEvictionBlocklist, connectionID); + } + delete callbackToStateMapping[connectionID]; } @@ -305,20 +322,13 @@ function remove(key) { */ function evictStorageAndRetry(error, ionMethod, ...args) { // Find the first key that we can remove that has no subscribers in our blocklist - const keyForRemoval = _.find(recentlyAccessedKeys, (key) => { - const keyParts = key.split('_'); - const keyPrefix = `${keyParts.slice(0, keyParts.length - 1).join('_')}_`; - return !evictionBlocklist[keyPrefix]; - }); + const keyForRemoval = _.find(recentlyAccessedKeys, key => !evictionBlocklist[key]); if (!keyForRemoval) { logAlert('Out of storage. But found no acceptable keys to remove.'); throw error; } - // We must immediately remove this so we do not try to remove the same key twice. - removeLastAccessedKey(keyForRemoval); - // Remove the least recently viewed key that is not currently being accessed and retry. logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); return remove(keyForRemoval) @@ -378,6 +388,49 @@ function clear() { }); } +// Key/value store of Onyx key and arrays of values to merge +const mergeQueue = {}; + +/** + * Given an Onyx key and value this method will combine all queued + * value updates and return a single value. Merge attempts are + * batched. They must occur after a single call to get() so we + * can avoid race conditions. + * + * @param {String} key + * @param {*} data + * + * @returns {*} + */ +function applyMerge(key, data) { + const mergeValues = mergeQueue[key]; + + if (_.isArray(data)) { + // Array values will always just concatenate + // more items onto the end of the array + return _.reduce(mergeValues, (modifiedData, mergeValue) => [ + ...modifiedData, + ...mergeValue, + ], data); + } + + if (_.isObject(data)) { + // Object values are merged one after the other + return _.reduce(mergeValues, (modifiedData, mergeValue) => { + const newData = lodashMerge({}, modifiedData, mergeValue); + + // We will also delete any object keys that are undefined or null. + // Deleting keys is not supported by AsyncStorage so we do it this way. + // Remove all first level keys that are explicitly set to null. + return _.omit(newData, (value, finalObjectKey) => _.isNull(mergeValue[finalObjectKey])); + }, data); + } + + // If we have anything else we can't merge it so we'll + // simply return the last value that was queued + return _.last(mergeValues); +} + /** * Merge a new value into an existing value at a key * @@ -385,42 +438,28 @@ function clear() { * @param {*} val */ function merge(key, val) { - // Arrays need to be manually merged because the AsyncStorage behavior - // is not desired when merging arrays. `AsyncStorage.mergeItem('test', [1]); - // will result in `{0: 1}` being set in storage, when `[1]` is what is expected - if (_.isArray(val)) { - let newArray; - get(key) - .then((prevVal) => { - const previousValue = prevVal || []; - newArray = [...previousValue, ...val]; - return AsyncStorage.setItem(key, JSON.stringify(newArray)); - }) - .then(() => keyChanged(key, newArray)) - .catch(error => evictStorageAndRetry(error, merge, key, val)); + if (mergeQueue[key]) { + mergeQueue[key].push(val); return; } - // Values that are objects are merged normally into storage - if (_.isObject(val)) { - AsyncStorage.mergeItem(key, JSON.stringify(val)) - .then(() => get(key)) - .then((newObject) => { - keyChanged(key, newObject); - }) - .catch(error => evictStorageAndRetry(error, merge, key, val)); - return; - } + mergeQueue[key] = [val]; + get(key) + .then((data) => { + const modifiedData = applyMerge(key, data); - // Anything else (strings and numbers) need to be set into storage - set(key, val); + // Clean up the write queue so we + // don't apply these changes again + delete mergeQueue[key]; + set(key, modifiedData); + }); } /** * Initialize the store with actions and listening for storage events * * @param {Object} [options] - * @param {String[]} [options.safeEvictionKeys] This is an array of IONKEYS + * @param {String[]} [options.safeEvictionKeys] This is an array of keys * (individual or collection patterns) that when provided to Onyx are flagged * as "safe" for removal. Any components subscribing to these keys must also * implement a canEvict option. See the README for more info. @@ -428,9 +467,13 @@ function merge(key, val) { * This applies to web platforms where the local storage emits storage events * across all open tabs and allows Onyx to stay in sync across all open tabs. */ -function init({initialKeyStates, safeEvictionKeys, onStorageEvent}) { +function init({keys, initialKeyStates, safeEvictionKeys, onStorageEvent}) { + // Let Onyx know about all of our keys + onyxKeys = keys; + // Let Onyx know about which keys are safe to evict evictionAllowList = safeEvictionKeys; + addAllSafeEvictionKeysToRecentlyAccessedList(); // Initialize all of our keys with data provided _.each(initialKeyStates, (state, key) => merge(key, state)); From 1c87015e05daf25b304121bb4cb4f4d79ac593e5 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 10:33:40 -0700 Subject: [PATCH 10/18] Refactor storage event --- lib/Onyx.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Onyx.js b/lib/Onyx.js index 7573dd126..b385637a7 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -463,11 +463,11 @@ function merge(key, val) { * (individual or collection patterns) that when provided to Onyx are flagged * as "safe" for removal. Any components subscribing to these keys must also * implement a canEvict option. See the README for more info. - * @param {function} onStorageEvent a callback when a storage event happens. + * @param {function} registerStorageEventListener a callback when a storage event happens. * This applies to web platforms where the local storage emits storage events * across all open tabs and allows Onyx to stay in sync across all open tabs. */ -function init({keys, initialKeyStates, safeEvictionKeys, onStorageEvent}) { +function init({keys, initialKeyStates, safeEvictionKeys, registerStorageEventListener}) { // Let Onyx know about all of our keys onyxKeys = keys; @@ -479,7 +479,7 @@ function init({keys, initialKeyStates, safeEvictionKeys, onStorageEvent}) { _.each(initialKeyStates, (state, key) => merge(key, state)); // Update any key whose value changes in storage - onStorageEvent((key, newValue) => keyChanged(key, newValue)); + registerStorageEventListener((key, newValue) => keyChanged(key, newValue)); } const Onyx = { From bf228c32d6433f6bbf0e285aa48f9662088e2aea Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:22:12 -0700 Subject: [PATCH 11/18] Update js libs --- package-lock.json | 10 ++++++++-- package.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2a4b0852..cf7536e14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1186,6 +1186,11 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1380,11 +1385,12 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-libs": { - "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", - "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", + "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", + "html-entities": "^1.3.1", "jquery": "3.3.1", "lodash.get": "4.4.2", "lodash.has": "4.5.2", diff --git a/package.json b/package.json index e185c4de9..d9173b56a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@react-native-community/async-storage": "^1.12.1", - "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#7b13151f15f631bbf2ef10a7ef57e36f89261b0e", + "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", "react": "^17.0.1", "underscore": "^1.11.0" }, From 2ec059e61b5c2993aed2c54b88be86457c2804fc Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:25:44 -0700 Subject: [PATCH 12/18] Add lodash libs --- package-lock.json | 5 +++++ package.json | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cf7536e14..8c54d449b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1504,6 +1504,11 @@ "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", diff --git a/package.json b/package.json index d9173b56a..fb50a7855 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "@react-native-community/async-storage": "^1.12.1", "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", "react": "^17.0.1", - "underscore": "^1.11.0" + "underscore": "^1.11.0", + "lodash.get": "^4.4.2", + "lodash.has": "^4.5.2", + "lodash.merge": "^4.6.2" }, "devDependencies": { "eslint": "^7.6.0", From d5560e56191226fa61ff0a887490ada2be72fe69 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:28:04 -0700 Subject: [PATCH 13/18] Import lodash and remove unused lodash modules --- lib/Onyx.js | 1 + package.json | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Onyx.js b/lib/Onyx.js index b385637a7..4e6b47a99 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import AsyncStorage from '@react-native-community/async-storage'; import Str from 'js-libs/lib/str'; +import lodashMerge from 'lodash.merge'; import {registerLogger, logInfo, logAlert} from './Logger'; // Keeps track of the last connectionID that was used so we can keep incrementing it diff --git a/package.json b/package.json index fb50a7855..b73518f97 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,6 @@ "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", "react": "^17.0.1", "underscore": "^1.11.0", - "lodash.get": "^4.4.2", - "lodash.has": "^4.5.2", "lodash.merge": "^4.6.2" }, "devDependencies": { From e0ea42bb88a244357caa731afbb8d7f1f84a1293 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:29:38 -0700 Subject: [PATCH 14/18] Update JS-Libs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b73518f97..42c1fa70d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@react-native-community/async-storage": "^1.12.1", - "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", + "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", "react": "^17.0.1", "underscore": "^1.11.0", "lodash.merge": "^4.6.2" From b200cb39bae6c6eb6aac3738a9e42fe7c66aa49c Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:30:08 -0700 Subject: [PATCH 15/18] update lock file --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c54d449b..2dffca93e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1385,8 +1385,8 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-libs": { - "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", - "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#8b209e01091a1dffa92c59b4b9b50192ddd99e26", + "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", + "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", From 705f12b478d02659de2d6938fd48ce1e49bbcd34 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 11:33:19 -0700 Subject: [PATCH 16/18] Update js-libs --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dffca93e..e3e80e65c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1385,8 +1385,8 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-libs": { - "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", - "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", + "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", + "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", diff --git a/package.json b/package.json index 42c1fa70d..b8a81a927 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@react-native-community/async-storage": "^1.12.1", - "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#b957feafb037a18e409ab3e82b46e1c0812ca799", + "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", "react": "^17.0.1", "underscore": "^1.11.0", "lodash.merge": "^4.6.2" From 100062ce5cfaaada8eca10dce33bb50d7555b75c Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 12:36:15 -0700 Subject: [PATCH 17/18] Update to final JS libs hash --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3e80e65c..1b62ffbf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1385,8 +1385,8 @@ "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, "js-libs": { - "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", - "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", + "version": "git+ssh://git@github.com/Expensify/JS-Libs.git#92b874eed3640e7635f7342f8169ddf8f28ca7e4", + "from": "git+ssh://git@github.com/Expensify/JS-Libs.git#92b874eed3640e7635f7342f8169ddf8f28ca7e4", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", diff --git a/package.json b/package.json index b8a81a927..4282970e7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@react-native-community/async-storage": "^1.12.1", - "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#e5397f71ff3d3e69ca3da2eac132b0b61558b811", + "js-libs": "git+https://git@github.com:Expensify/JS-Libs.git#92b874eed3640e7635f7342f8169ddf8f28ca7e4", "react": "^17.0.1", "underscore": "^1.11.0", "lodash.merge": "^4.6.2" From e01c24337da0b46579049fd3b567da4e1d6321fe Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 11 Nov 2020 12:40:15 -0700 Subject: [PATCH 18/18] Fix lint errors --- .eslintrc.js | 4 ++++ lib/Onyx.js | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index da9bdd0fa..5f1cf34e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,7 @@ module.exports = { extends: 'expensify', + rules: { + // Overwriting this for now because web-e will conflict with this + 'react/jsx-filename-extension': [1, {extensions: ['.js']}], + }, }; diff --git a/lib/Onyx.js b/lib/Onyx.js index 4e6b47a99..a06f6a64c 100644 --- a/lib/Onyx.js +++ b/lib/Onyx.js @@ -468,7 +468,12 @@ function merge(key, val) { * This applies to web platforms where the local storage emits storage events * across all open tabs and allows Onyx to stay in sync across all open tabs. */ -function init({keys, initialKeyStates, safeEvictionKeys, registerStorageEventListener}) { +function init({ + keys, + initialKeyStates, + safeEvictionKeys, + registerStorageEventListener +}) { // Let Onyx know about all of our keys onyxKeys = keys;