From 3d2a862d9dab48814ef52128d2ee3df90e043da4 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Sat, 13 Oct 2018 22:14:46 +0200 Subject: [PATCH 001/322] Added design of the API key page --- client/modules/User/components/APIKeyForm.jsx | 71 ++++++++++++ client/modules/User/pages/AccountView.jsx | 13 ++- .../User/pages/AdvancedSettingsView.jsx | 104 ++++++++++++++++++ client/routes.jsx | 2 + client/styles/components/_form-container.scss | 14 +++ client/styles/components/_forms.scss | 17 +++ server/routes/server.routes.js | 8 ++ 7 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 client/modules/User/components/APIKeyForm.jsx create mode 100644 client/modules/User/pages/AdvancedSettingsView.jsx diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx new file mode 100644 index 00000000..feb81135 --- /dev/null +++ b/client/modules/User/components/APIKeyForm.jsx @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +class APIKeyForm extends React.Component { + constructor(props) { + super(props); + this.state = { keyLabel: '' }; + + this.addKey = this.addKey.bind(this); + } + + addKey(event) { + // TODO + console.log('addKey'); + this.props.updateSettings(); + event.preventDefault(); + return false; + } + + removeKey(k) { + // TODO + console.log(k); + } + + render() { + return ( +
+

Key label

+
+ { this.setState({ keyLabel: event.target.value }); }} + />
+ +
+ + + {[{ + id: 1, + label: 'MyFirstAPI', + createdAt: new Date(), + lastUsedAt: new Date() + }, { + id: 2, + label: 'MyOtherAPI', + createdAt: new Date(), + lastUsedAt: new Date() + }].map(v => ( + + + + + ))} + +
{v.label}
Created on: {v.createdAt.toLocaleDateString()} {v.createdAt.toLocaleTimeString()}
Last used on:
{v.lastUsedAt.toLocaleDateString()} {v.lastUsedAt.toLocaleTimeString()}
+
+ ); + } +} + +APIKeyForm.propTypes = { + updateSettings: PropTypes.func.isRequired, +}; + +export default APIKeyForm; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 2fb03b1f..d1bd6ba9 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; -import { browserHistory } from 'react-router'; +import { Link, browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; import axios from 'axios'; import { Helmet } from 'react-helmet'; @@ -22,8 +22,12 @@ class AccountView extends React.Component { this.gotoHomePage = this.gotoHomePage.bind(this); } + componentDidMount() { + document.body.className = this.props.theme; + } + closeAccountPage() { - browserHistory.push(this.props.previousPath); + browserHistory.goBack(); } gotoHomePage() { @@ -47,6 +51,7 @@ class AccountView extends React.Component {

My Account

+ Advanced Settings

Or

@@ -59,7 +64,7 @@ function mapStateToProps(state) { return { initialValues: state.user, // <- initialValues for reduxForm user: state.user, - previousPath: state.ide.previousPath + theme: state.preferences.theme }; } @@ -86,7 +91,7 @@ function asyncValidate(formProps, dispatch, props) { } AccountView.propTypes = { - previousPath: PropTypes.string.isRequired, + theme: PropTypes.string.isRequired }; export default reduxForm({ diff --git a/client/modules/User/pages/AdvancedSettingsView.jsx b/client/modules/User/pages/AdvancedSettingsView.jsx new file mode 100644 index 00000000..e963e5a4 --- /dev/null +++ b/client/modules/User/pages/AdvancedSettingsView.jsx @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { reduxForm } from 'redux-form'; +import { bindActionCreators } from 'redux'; +import { browserHistory } from 'react-router'; +import InlineSVG from 'react-inlinesvg'; +import axios from 'axios'; +import { Helmet } from 'react-helmet'; +import { updateSettings, initiateVerification } from '../actions'; +import { validateSettings } from '../../../utils/reduxFormUtils'; +import APIKeyForm from '../components/APIKeyForm'; + +const exitUrl = require('../../../images/exit.svg'); +const logoUrl = require('../../../images/p5js-logo.svg'); + +// TODO tmp +const ident = () => {}; + +class AccountView extends React.Component { + constructor(props) { + super(props); + this.closeAccountPage = this.closeAccountPage.bind(this); + this.gotoHomePage = this.gotoHomePage.bind(this); + } + + componentDidMount() { + document.body.className = this.props.theme; + } + + closeAccountPage() { + browserHistory.goBack(); + } + + gotoHomePage() { + browserHistory.push('/'); + } + + render() { + return ( +
+ + p5.js Web Editor | Advanced Settings + +
+ + +
+
+

Advanced Settings

+ +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + initialValues: state.user, // <- initialValues for reduxForm + user: state.user, + previousPath: state.ide.previousPath, + theme: state.preferences.theme + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ updateSettings, initiateVerification }, dispatch); +} + +function asyncValidate(formProps, dispatch, props) { + const fieldToValidate = props.form._active; + if (fieldToValidate) { + const queryParams = {}; + queryParams[fieldToValidate] = formProps[fieldToValidate]; + queryParams.check_type = fieldToValidate; + return axios.get('/api/signup/duplicate_check', { params: queryParams }) + .then((response) => { + if (response.data.exists) { + const error = {}; + error[fieldToValidate] = response.data.message; + throw error; + } + }); + } + return Promise.resolve(true).then(() => {}); +} + +AccountView.propTypes = { + theme: PropTypes.string.isRequired +}; + +export default reduxForm({ + form: 'updateAllSettings', + fields: ['username', 'email', 'currentPassword', 'newPassword'], + validate: validateSettings, + asyncValidate, + asyncBlurFields: ['username', 'email', 'currentPassword'] +}, mapStateToProps, mapDispatchToProps)(AccountView); diff --git a/client/routes.jsx b/client/routes.jsx index baa89884..9389d226 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -9,6 +9,7 @@ import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import EmailVerificationView from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; +import AdvancedSettingsView from './modules/User/pages/AdvancedSettingsView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; import { getUser } from './modules/User/actions'; import { stopSketch } from './modules/IDE/actions/ide'; @@ -38,6 +39,7 @@ const routes = store => ( + diff --git a/client/styles/components/_form-container.scss b/client/styles/components/_form-container.scss index 225dc155..4b371547 100644 --- a/client/styles/components/_form-container.scss +++ b/client/styles/components/_form-container.scss @@ -37,3 +37,17 @@ .form-container__exit-button { @extend %none-themify-icon-with-hover; } + +.form__table { + display: block; + max-width: 900px; + border-collapse: collapse; + & td { + max-width: 300px; + border: 1px solid #ddd; + padding: 0 10px 0 10px; + } + & tr:nth-child(even) { + background-color: #f2f2f2; + } +} diff --git a/client/styles/components/_forms.scss b/client/styles/components/_forms.scss index 342a8c80..8fea3f3d 100644 --- a/client/styles/components/_forms.scss +++ b/client/styles/components/_forms.scss @@ -51,3 +51,20 @@ .form input[type="submit"]:disabled { cursor: not-allowed; } + +.form__button-remove { + @extend %forms-button; + margin: 1rem 0 1rem 0; + @include themify() { + color: getThemifyVariable('console-error-background-color'); + border-color: getThemifyVariable('console-error-background-color'); + &:enabled:hover { + border-color: getThemifyVariable('console-error-background-color'); + background-color: getThemifyVariable('console-error-background-color'); + } + &:enabled:active { + border-color: getThemifyVariable('console-error-background-color'); + background-color: getThemifyVariable('console-error-background-color'); + } + } +} diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index adade10d..2f39e924 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -87,6 +87,14 @@ router.get('/account', (req, res) => { } }); +router.get('/account/advanced', (req, res) => { + if (req.user) { + res.send(renderIndex()); + } else { + res.redirect('/login'); + } +}); + router.get('/about', (req, res) => { res.send(renderIndex()); }); From db71a2b7c05d4ef65996924c280eee028a9fd323 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Sun, 14 Oct 2018 21:08:36 +0200 Subject: [PATCH 002/322] Added DB schema and backend logic for API keys creation and deletion --- server/controllers/session.controller.js | 2 ++ server/controllers/user.controller.js | 39 ++++++++++++++++++++++++ server/models/user.js | 15 +++++++++ server/routes/user.routes.js | 4 +++ 4 files changed, 60 insertions(+) diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index 211b8a46..4b4e3025 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -13,6 +13,7 @@ export function createSession(req, res, next) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, + apiKeys: req.user.apiKeys, verified: req.user.verified, id: req.user._id }); @@ -26,6 +27,7 @@ export function getSession(req, res) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, + apiKeys: req.user.apiKeys, verified: req.user.verified, id: req.user._id }); diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 00f3f022..d22b19ae 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -352,3 +352,42 @@ export function updateSettings(req, res) { } }); } + +export function addApiKey(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + if (!req.body.label || !req.body.hashedKey) { + res.status(400).json({ error: 'Expected field \'label\' or \'hashedKey\' was not present in request body' }); + return; + } + user.apiKeys.push(req.body); + saveUser(res, user); + }); +} + +export function removeApiKey(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + if (!keyToDelete) { + res.status(404).json({ error: 'Key does not exist for user' }); + return; + } + user.apiKeys.pull({ _id: req.params.keyId }); + saveUser(res, user); + }); +} diff --git a/server/models/user.js b/server/models/user.js index 5cbf12be..d15a8930 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -10,6 +10,20 @@ const EmailConfirmationStates = { const { Schema } = mongoose; +const apiKeySchema = new Schema({ + label: { type: String, default: 'API Key' }, + lastUsedAt: { type: Date, required: true, default: Date.now }, + hashedKey: { type: String, required: true }, +}, { timestamps: true, _id: true }); + +apiKeySchema.virtual('id').get(function getApiKeyId() { + return this._id.toHexString(); +}); + +apiKeySchema.set('toJSON', { + virtuals: true +}); + const userSchema = new Schema({ name: { type: String, default: '' }, username: { type: String, required: true, unique: true }, @@ -22,6 +36,7 @@ const userSchema = new Schema({ github: { type: String }, email: { type: String, unique: true }, tokens: Array, + apiKeys: { type: [apiKeySchema] }, preferences: { fontSize: { type: Number, default: 18 }, indentationAmount: { type: Number, default: 2 }, diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index 427e0705..49b09a94 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -18,6 +18,10 @@ router.post('/reset-password/:token', UserController.updatePassword); router.put('/account', isAuthenticated, UserController.updateSettings); +router.put('/account/api-keys', isAuthenticated, UserController.addApiKey); + +router.delete('/account/api-keys/:keyId', isAuthenticated, UserController.removeApiKey); + router.post('/verify/send', UserController.emailVerificationInitiate); router.get('/verify', UserController.verifyEmail); From 4d4f636623448338b79edf9549e582b0e9ecc352 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Tue, 16 Oct 2018 00:22:56 +0200 Subject: [PATCH 003/322] You can now generate keys from the advanced settings interface --- client/constants.js | 3 ++ client/modules/User/actions.js | 31 +++++++++++++ client/modules/User/components/APIKeyForm.jsx | 40 ++++++++-------- .../User/pages/AdvancedSettingsView.jsx | 46 ++++--------------- client/modules/User/reducers.js | 4 ++ 5 files changed, 64 insertions(+), 60 deletions(-) diff --git a/client/constants.js b/client/constants.js index 9cb3daa4..ffe8ba61 100644 --- a/client/constants.js +++ b/client/constants.js @@ -19,6 +19,9 @@ export const AUTH_ERROR = 'AUTH_ERROR'; export const SETTINGS_UPDATED = 'SETTINGS_UPDATED'; +export const ADDED_API_KEY = 'ADDED_API_KEY'; +export const REMOVED_API_KEY = 'REMOVED_API_KEY'; + export const SET_PROJECT_NAME = 'SET_PROJECT_NAME'; export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS'; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 91e5f72b..53fbb68e 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,5 +1,6 @@ import { browserHistory } from 'react-router'; import axios from 'axios'; +import crypto from 'crypto'; import * as ActionTypes from '../../constants'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showToast, setToastText } from '../IDE/actions/toast'; @@ -218,3 +219,33 @@ export function updateSettings(formValues) { }) .catch(response => Promise.reject(new Error(response.data.error))); } + +export function addApiKey(label) { + return ((dispatch) => { + crypto.randomBytes(20, (err, buf) => { + const key = buf.toString('hex'); + const hashedKey = Buffer.from(key).toString('base64'); + axios.put(`${ROOT_URL}/account/api-keys`, { label, hashedKey }, { withCredentials: true }) + .then((response) => { + window.alert(`Here is your key :\n${key}\nNote it somewhere, you won't be able to see it later !`); + dispatch({ + type: ActionTypes.ADDED_API_KEY, + user: response.data + }); + }) + .catch(response => Promise.reject(new Error(response.data.error))); + }); + }); +} + +export function removeApiKey(keyId) { + return dispatch => + axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.REMOVED_API_KEY, + user: response.data + }); + }) + .catch(response => Promise.reject(new Error(response.data.error))); +} diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index feb81135..2e85dce1 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -10,26 +10,25 @@ class APIKeyForm extends React.Component { } addKey(event) { - // TODO - console.log('addKey'); - this.props.updateSettings(); event.preventDefault(); + document.getElementById('addKeyForm').reset(); + this.props.addApiKey(this.state.keyLabel); return false; } - removeKey(k) { - // TODO - console.log(k); + removeKey(keyId) { + this.props.removeApiKey(keyId); } render() { return (

Key label

-
+ { this.setState({ keyLabel: event.target.value }); }} />
@@ -41,21 +40,11 @@ class APIKeyForm extends React.Component {
- {[{ - id: 1, - label: 'MyFirstAPI', - createdAt: new Date(), - lastUsedAt: new Date() - }, { - id: 2, - label: 'MyOtherAPI', - createdAt: new Date(), - lastUsedAt: new Date() - }].map(v => ( + {this.props.apiKeys && this.props.apiKeys.map(v => ( - - - + + + ))}
{v.label}
Created on: {v.createdAt.toLocaleDateString()} {v.createdAt.toLocaleTimeString()}
Last used on:
{v.lastUsedAt.toLocaleDateString()} {v.lastUsedAt.toLocaleTimeString()}
{v.label}
Created on: {v.createdAt}
Last used on:
{v.lastUsedAt}
@@ -65,7 +54,14 @@ class APIKeyForm extends React.Component { } APIKeyForm.propTypes = { - updateSettings: PropTypes.func.isRequired, + addApiKey: PropTypes.func.isRequired, + removeApiKey: PropTypes.func.isRequired, + apiKeys: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + createdAt: PropTypes.object.isRequired, + lastUsedAt: PropTypes.object.isRequired, + })).isRequired }; export default APIKeyForm; diff --git a/client/modules/User/pages/AdvancedSettingsView.jsx b/client/modules/User/pages/AdvancedSettingsView.jsx index e963e5a4..a8866bc1 100644 --- a/client/modules/User/pages/AdvancedSettingsView.jsx +++ b/client/modules/User/pages/AdvancedSettingsView.jsx @@ -1,22 +1,17 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { reduxForm } from 'redux-form'; +import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; -import axios from 'axios'; import { Helmet } from 'react-helmet'; -import { updateSettings, initiateVerification } from '../actions'; -import { validateSettings } from '../../../utils/reduxFormUtils'; +import { addApiKey, removeApiKey } from '../actions'; import APIKeyForm from '../components/APIKeyForm'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); -// TODO tmp -const ident = () => {}; - -class AccountView extends React.Component { +class AdvancedSettingsView extends React.Component { constructor(props) { super(props); this.closeAccountPage = this.closeAccountPage.bind(this); @@ -51,9 +46,7 @@ class AccountView extends React.Component {

Advanced Settings

- +
); @@ -64,41 +57,18 @@ function mapStateToProps(state) { return { initialValues: state.user, // <- initialValues for reduxForm user: state.user, + apiKeys: state.user.apiKeys, previousPath: state.ide.previousPath, theme: state.preferences.theme }; } function mapDispatchToProps(dispatch) { - return bindActionCreators({ updateSettings, initiateVerification }, dispatch); + return bindActionCreators({ addApiKey, removeApiKey }, dispatch); } -function asyncValidate(formProps, dispatch, props) { - const fieldToValidate = props.form._active; - if (fieldToValidate) { - const queryParams = {}; - queryParams[fieldToValidate] = formProps[fieldToValidate]; - queryParams.check_type = fieldToValidate; - return axios.get('/api/signup/duplicate_check', { params: queryParams }) - .then((response) => { - if (response.data.exists) { - const error = {}; - error[fieldToValidate] = response.data.message; - throw error; - } - }); - } - return Promise.resolve(true).then(() => {}); -} - -AccountView.propTypes = { +AdvancedSettingsView.propTypes = { theme: PropTypes.string.isRequired }; -export default reduxForm({ - form: 'updateAllSettings', - fields: ['username', 'email', 'currentPassword', 'newPassword'], - validate: validateSettings, - asyncValidate, - asyncBlurFields: ['username', 'email', 'currentPassword'] -}, mapStateToProps, mapDispatchToProps)(AccountView); +export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 73636f2b..718e967f 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -31,6 +31,10 @@ const user = (state = { authenticated: false }, action) => { return Object.assign({}, state, { emailVerificationTokenState: 'invalid' }); case ActionTypes.SETTINGS_UPDATED: return { ...state, ...action.user }; + case ActionTypes.REMOVED_API_KEY: + return { ...state, ...action.user }; + case ActionTypes.ADDED_API_KEY: + return { ...state, ...action.user }; default: return state; } From 78695d3983c39348d80f0142a59e196e562d6552 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Mon, 5 Nov 2018 22:28:52 +0100 Subject: [PATCH 004/322] Improved interface for copying key after creation --- client/modules/User/actions.js | 28 ++++++++++++++++++- client/modules/User/components/APIKeyForm.jsx | 4 ++- client/styles/components/_form-container.scss | 3 ++ client/styles/components/_forms.scss | 7 ++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 53fbb68e..90545b4c 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,4 +1,6 @@ +import React from 'react'; import { browserHistory } from 'react-router'; +import ReactDom from 'react-dom'; import axios from 'axios'; import crypto from 'crypto'; import * as ActionTypes from '../../constants'; @@ -227,7 +229,31 @@ export function addApiKey(label) { const hashedKey = Buffer.from(key).toString('base64'); axios.put(`${ROOT_URL}/account/api-keys`, { label, hashedKey }, { withCredentials: true }) .then((response) => { - window.alert(`Here is your key :\n${key}\nNote it somewhere, you won't be able to see it later !`); + // window.alert(`Here is your key :\n${key}\nNote it somewhere, you won't be able to see it later !`); + const elt = React.createElement( + 'tr', { className: 'new-key' }, + React.createElement('td', {}, 'Here is your new key ;\ncopy it somewhere, you won\'t be able to see it later !'), + React.createElement( + 'td', {}, + React.createElement('input', { + id: 'key-to-copy', type: 'text', value: key, readOnly: true + }) + ), + React.createElement( + 'td', {}, + React.createElement('input', { + type: 'submit', + value: 'Copy to clipboard', + className: 'form__table-button-copy', + onClick: () => { + const inputKey = document.getElementById('key-to-copy'); + inputKey.select(); + document.execCommand('copy'); + } + }) + ) + ); + ReactDom.render(elt, document.getElementById('form__table_new_key')); dispatch({ type: ActionTypes.ADDED_API_KEY, user: response.data diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 2e85dce1..d99005ac 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -13,6 +13,7 @@ class APIKeyForm extends React.Component { event.preventDefault(); document.getElementById('addKeyForm').reset(); this.props.addApiKey(this.state.keyLabel); + this.state.keyLabel = ''; return false; } @@ -39,12 +40,13 @@ class APIKeyForm extends React.Component { /> + {this.props.apiKeys && this.props.apiKeys.map(v => ( - + ))}
{v.label}
Created on: {v.createdAt}
Last used on:
{v.lastUsedAt}
diff --git a/client/styles/components/_form-container.scss b/client/styles/components/_form-container.scss index 4b371547..669edf8e 100644 --- a/client/styles/components/_form-container.scss +++ b/client/styles/components/_form-container.scss @@ -50,4 +50,7 @@ & tr:nth-child(even) { background-color: #f2f2f2; } + & tr.new-key { + background-color: #f2f2f2; + } } diff --git a/client/styles/components/_forms.scss b/client/styles/components/_forms.scss index 8fea3f3d..8fe66361 100644 --- a/client/styles/components/_forms.scss +++ b/client/styles/components/_forms.scss @@ -52,7 +52,7 @@ cursor: not-allowed; } -.form__button-remove { +.form__table-button-remove { @extend %forms-button; margin: 1rem 0 1rem 0; @include themify() { @@ -68,3 +68,8 @@ } } } + +.form__table-button-copy{ + @extend %forms-button; + margin: 1rem 0 1rem 0; +} From 3b55ff81d2d0276a947424e492604b105b53f408 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Tue, 6 Nov 2018 13:36:19 +0100 Subject: [PATCH 005/322] Hashing keys before storing them --- client/modules/User/actions.js | 4 ++-- server/controllers/user.controller.js | 6 +++--- server/models/user.js | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 90545b4c..0ba02f0d 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -226,8 +226,8 @@ export function addApiKey(label) { return ((dispatch) => { crypto.randomBytes(20, (err, buf) => { const key = buf.toString('hex'); - const hashedKey = Buffer.from(key).toString('base64'); - axios.put(`${ROOT_URL}/account/api-keys`, { label, hashedKey }, { withCredentials: true }) + const encodedKey = Buffer.from(key).toString('base64'); + axios.put(`${ROOT_URL}/account/api-keys`, { label, encodedKey }, { withCredentials: true }) .then((response) => { // window.alert(`Here is your key :\n${key}\nNote it somewhere, you won't be able to see it later !`); const elt = React.createElement( diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index d22b19ae..8243fc8c 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -363,11 +363,11 @@ export function addApiKey(req, res) { res.status(404).json({ error: 'User not found' }); return; } - if (!req.body.label || !req.body.hashedKey) { - res.status(400).json({ error: 'Expected field \'label\' or \'hashedKey\' was not present in request body' }); + if (!req.body.label || !req.body.encodedKey) { + res.status(400).json({ error: 'Expected field \'label\' or \'encodedKey\' was not present in request body' }); return; } - user.apiKeys.push(req.body); + user.apiKeys.push({ label: req.body.label, hashedKey: req.body.encodedKey }); saveUser(res, user); }); } diff --git a/server/models/user.js b/server/models/user.js index d15a8930..29e9934f 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -68,6 +68,29 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con }); }); +/** + * API keys hash middleware + */ +userSchema.pre('save', function checkApiKey(next) { + const user = this; + if (!user.isModified('apiKeys')) { return next(); } + let hasNew = false; + user.apiKeys.forEach((k) => { + if (k.isNew) { + hasNew = true; + bcrypt.genSalt(10, (err, salt) => { + if (err) { return next(err); } + bcrypt.hash(k.hashedKey, salt, null, (innerErr, hash) => { + if (innerErr) { return next(innerErr); } + k.hashedKey = hash; + return next(); + }); + }); + } + }); + if (!hasNew) return next(); +}); + userSchema.virtual('id').get(function idToString() { return this._id.toHexString(); }); From f0b669d5af3e2c1dade7e31986e3043c710af6f3 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Tue, 6 Nov 2018 17:28:17 +0100 Subject: [PATCH 006/322] Added Basic Auth using passport-http --- package.json | 1 + server/config/passport.js | 35 ++++++++++++++++++++++++++++------- server/models/user.js | 18 ++++++++++++++++-- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 1c0a5d4f..189c3c3f 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "passport": "^0.3.2", "passport-github": "^1.1.0", "passport-google-oauth20": "^1.0.0", + "passport-http": "^0.3.0", "passport-local": "^1.0.0", "pretty-bytes": "^3.0.1", "primer-tooltips": "^1.5.11", diff --git a/server/config/passport.js b/server/config/passport.js index 17975c02..1fb0013e 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -1,14 +1,15 @@ import slugify from 'slugify'; import friendlyWords from 'friendly-words'; +import lodash from 'lodash'; + +import passport from 'passport'; +import GitHubStrategy from 'passport-github'; +import LocalStrategy from 'passport-local'; +import GoogleStrategy from 'passport-google-oauth20'; +import { BasicStrategy } from 'passport-http'; + import User from '../models/user'; -const lodash = require('lodash'); -const passport = require('passport'); -const GitHubStrategy = require('passport-github').Strategy; -const LocalStrategy = require('passport-local').Strategy; -const GoogleStrategy = require('passport-google-oauth20').Strategy; - - passport.serializeUser((user, done) => { done(null, user.id); }); @@ -38,6 +39,26 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don .catch(err => done(null, false, { msg: err })); })); +/** + * Authentificate using Basic Auth (Username + Api Key) + */ +passport.use(new BasicStrategy((userid, key, done) => { + User.findOne({ username: userid }, (err, user) => { // eslint-disable-line consistent-return + if (err) { return done(err); } + if (!user) { return done(null, false); } + user.findMatchingKey(key, (innerErr, isMatch, keyID) => { + if (isMatch) { + User.update( + { 'apiKeys._id': keyID }, + { '$set': { 'apiKeys.$.lastUsedAt': Date.now() } } + ); + return done(null, user); + } + return done(null, false, { msg: 'Invalid username or API key' }); + }); + }); +})); + /* Input: [ diff --git a/server/models/user.js b/server/models/user.js index 29e9934f..7e3dab45 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -71,14 +71,14 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con /** * API keys hash middleware */ -userSchema.pre('save', function checkApiKey(next) { +userSchema.pre('save', function checkApiKey(next) { // eslint-disable-line consistent-return const user = this; if (!user.isModified('apiKeys')) { return next(); } let hasNew = false; user.apiKeys.forEach((k) => { if (k.isNew) { hasNew = true; - bcrypt.genSalt(10, (err, salt) => { + bcrypt.genSalt(10, (err, salt) => { // eslint-disable-line consistent-return if (err) { return next(err); } bcrypt.hash(k.hashedKey, salt, null, (innerErr, hash) => { if (innerErr) { return next(innerErr); } @@ -110,6 +110,20 @@ userSchema.methods.comparePassword = function comparePassword(candidatePassword, }); }; +/** + * Helper method for validating a user's api key + */ +userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb) { + let foundOne = false; + this.apiKeys.forEach((k) => { + if (bcrypt.compareSync(candidateKey, k.hashedKey)) { + foundOne = true; + cb(null, true, k._id); + } + }); + if (!foundOne) cb('Matching API key not found !', false, null); +}; + userSchema.statics.findByMailOrName = function findByMailOrName(email) { const query = { $or: [{ From 89babdc8c072ad8a5e8d8c8aaf0ddeda3b4a93a5 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 13 May 2019 18:22:06 +0200 Subject: [PATCH 007/322] Update test snapshots --- .../__test__/__snapshots__/Nav.test.jsx.snap | 88 ++----------------- 1 file changed, 5 insertions(+), 83 deletions(-) diff --git a/client/components/__test__/__snapshots__/Nav.test.jsx.snap b/client/components/__test__/__snapshots__/Nav.test.jsx.snap index 9e8143bf..19ecb479 100644 --- a/client/components/__test__/__snapshots__/Nav.test.jsx.snap +++ b/client/components/__test__/__snapshots__/Nav.test.jsx.snap @@ -13,7 +13,7 @@ exports[`Nav renders correctly 1`] = ` className="nav__item-logo" >
  • -
  • @@ -112,6 +102,7 @@ exports[`Nav renders correctly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onMouseOver={[Function]} > -
  • @@ -215,6 +195,7 @@ exports[`Nav renders correctly 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onMouseOver={[Function]} > -
  • @@ -297,44 +267,6 @@ exports[`Nav renders correctly 1`] = `
  • -
  • - -
  • -
  • - -
  • -
  • From a860762683b30751598c31bde66538700984bb08 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 13 May 2019 18:22:28 +0200 Subject: [PATCH 008/322] Adds passport-http to package-lock.json --- package-lock.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 71312297..abe38a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11480,6 +11480,14 @@ "passport-oauth2": "1.x.x" } }, + "passport-http": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz", + "integrity": "sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=", + "requires": { + "passport-strategy": "1.x.x" + } + }, "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", From 403234ae81876a2393fc8331bf3d5ee76848cd54 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 13 May 2019 21:29:00 +0200 Subject: [PATCH 009/322] Moves API key creation to server --- server/controllers/user.controller.js | 40 +---- .../user.controller/__tests__/apiKey.test.js | 143 ++++++++++++++++++ server/controllers/user.controller/apiKey.js | 66 ++++++++ server/models/__mocks__/user.js | 12 ++ server/routes/user.routes.js | 2 +- 5 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 server/controllers/user.controller/__tests__/apiKey.test.js create mode 100644 server/controllers/user.controller/apiKey.js create mode 100644 server/models/__mocks__/user.js diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 8243fc8c..3b5b2705 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -8,6 +8,8 @@ import { renderResetPassword, } from '../views/mail'; +export * from './user.controller/apiKey'; + const random = (done) => { crypto.randomBytes(20, (err, buf) => { const token = buf.toString('hex'); @@ -353,41 +355,3 @@ export function updateSettings(req, res) { }); } -export function addApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - if (!req.body.label || !req.body.encodedKey) { - res.status(400).json({ error: 'Expected field \'label\' or \'encodedKey\' was not present in request body' }); - return; - } - user.apiKeys.push({ label: req.body.label, hashedKey: req.body.encodedKey }); - saveUser(res, user); - }); -} - -export function removeApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); - if (!keyToDelete) { - res.status(404).json({ error: 'Key does not exist for user' }); - return; - } - user.apiKeys.pull({ _id: req.params.keyId }); - saveUser(res, user); - }); -} diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js new file mode 100644 index 00000000..01fe67e8 --- /dev/null +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -0,0 +1,143 @@ +/* @jest-environment node */ + +import { createApiKey, removeApiKey } from '../../user.controller/apiKey'; + +jest.mock('../../../models/user'); + +const createResponseMock = function (done) { + const json = jest.fn(() => { + if (done) { done(); } + }); + const status = jest.fn(() => ({ json })); + + return { + status, + json + }; +}; + +const User = require('../../../models/user').default; + +describe('user.controller', () => { + beforeEach(() => { + User.__setFindById(null, null); + }); + + describe('createApiKey', () => { + it('returns an error if user doesn\'t exist', () => { + const request = { user: { id: '1234' } }; + const response = createResponseMock(); + + createApiKey(request, response); + + expect(User.findById.mock.calls[0][0]).toBe('1234'); + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + + it('returns an error if label not provided', () => { + User.__setFindById(undefined, { + apiKeys: [] + }); + const request = { user: { id: '1234' }, body: {} }; + const response = createResponseMock(); + + createApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(400); + expect(response.json).toHaveBeenCalledWith({ + error: 'Expected field \'label\' was not present in request body' + }); + }); + + it('returns generated API key to the user', (done) => { + let response; + + const request = { + user: { id: '1234' }, + body: { label: 'my key' } + }; + + const foundUser = { + apiKeys: [], + save: jest.fn(callback => callback()) + }; + + const checkExpecations = () => { + expect(foundUser.apiKeys[0].label).toBe('my key'); + expect(typeof foundUser.apiKeys[0].hashedKey).toBe('string'); + + expect(response.json).toHaveBeenCalledWith({ + token: foundUser.apiKeys[0].hashedKey + }); + + done(); + }; + + response = createResponseMock(checkExpecations); + + User.__setFindById(undefined, foundUser); + + createApiKey(request, response); + }); + }); + + describe('removeApiKey', () => { + it('returns an error if user doesn\'t exist', () => { + const request = { user: { id: '1234' } }; + const response = createResponseMock(); + + removeApiKey(request, response); + + expect(User.findById.mock.calls[0][0]).toBe('1234'); + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'User not found' + }); + }); + + it('returns an error if specified key doesn\'t exist', () => { + const request = { + user: { id: '1234' }, + params: { keyId: 'not-a-real-key' } + }; + const response = createResponseMock(); + + const foundUser = { + apiKeys: [], + save: jest.fn(callback => callback()) + }; + User.__setFindById(undefined, foundUser); + + removeApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'Key does not exist for user' + }); + }); + + it.skip('removes key if it exists', () => { + const request = { + user: { id: '1234' }, + params: { keyId: 'the-key' } + }; + const response = createResponseMock(); + + const foundUser = { + apiKeys: [{ label: 'the-label', id: 'the-key' }], + save: jest.fn(callback => callback()) + }; + User.__setFindById(undefined, foundUser); + + removeApiKey(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + error: 'Key does not exist for user' + }); + }); + }); +}); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js new file mode 100644 index 00000000..35a0ba36 --- /dev/null +++ b/server/controllers/user.controller/apiKey.js @@ -0,0 +1,66 @@ +import crypto from 'crypto'; + +import User from '../../models/user'; + +/** + * Generates a unique token to be used as a Personal Access Token + * @returns Promise A promise that resolves to the token, or an Error + */ +function generateApiKey() { + return new Promise((resolve, reject) => { + crypto.randomBytes(20, (err, buf) => { + if (err) { + reject(err); + } + const key = buf.toString('hex'); + resolve(Buffer.from(key).toString('base64')); + }); + }); +} + +export function createApiKey(req, res) { + User.findById(req.user.id, async (err, user) => { + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + + if (!req.body.label) { + res.status(400).json({ error: 'Expected field \'label\' was not present in request body' }); + return; + } + + const hashedKey = await generateApiKey(); + + user.apiKeys.push({ label: req.body.label, hashedKey }); + + user.save((saveErr) => { + if (saveErr) { + res.status(500).json({ error: saveErr }); + return; + } + + res.json({ token: hashedKey }); + }); + }); +} + +export function removeApiKey(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + res.status(500).json({ error: err }); + return; + } + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + if (!keyToDelete) { + res.status(404).json({ error: 'Key does not exist for user' }); + return; + } + user.apiKeys.pull({ _id: req.params.keyId }); + saveUser(res, user); + }); +} diff --git a/server/models/__mocks__/user.js b/server/models/__mocks__/user.js new file mode 100644 index 00000000..585d8b67 --- /dev/null +++ b/server/models/__mocks__/user.js @@ -0,0 +1,12 @@ +let __err = null; +let __user = null; + +export default { + __setFindById(err, user) { + __err = err; + __user = user; + }, + findById: jest.fn(async (id, callback) => { + callback(__err, __user); + }) +}; diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index 49b09a94..3273025d 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -18,7 +18,7 @@ router.post('/reset-password/:token', UserController.updatePassword); router.put('/account', isAuthenticated, UserController.updateSettings); -router.put('/account/api-keys', isAuthenticated, UserController.addApiKey); +router.post('/account/api-keys', isAuthenticated, UserController.createApiKey); router.delete('/account/api-keys/:keyId', isAuthenticated, UserController.removeApiKey); From 90f34d7a5ab400f2b343a003e9af804f3b000bba Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 11:25:14 +0200 Subject: [PATCH 010/322] Updates client UI to request token generation from server --- client/constants.js | 2 +- client/modules/User/actions.js | 56 ++++++------------- client/modules/User/components/APIKeyForm.jsx | 47 ++++++++++------ .../User/pages/AdvancedSettingsView.jsx | 4 +- client/modules/User/reducers.js | 2 +- 5 files changed, 50 insertions(+), 61 deletions(-) diff --git a/client/constants.js b/client/constants.js index ffe8ba61..08b3f81b 100644 --- a/client/constants.js +++ b/client/constants.js @@ -19,7 +19,7 @@ export const AUTH_ERROR = 'AUTH_ERROR'; export const SETTINGS_UPDATED = 'SETTINGS_UPDATED'; -export const ADDED_API_KEY = 'ADDED_API_KEY'; +export const API_KEY_CREATED = 'API_KEY_CREATED'; export const REMOVED_API_KEY = 'REMOVED_API_KEY'; export const SET_PROJECT_NAME = 'SET_PROJECT_NAME'; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 0ba02f0d..cfd2664a 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -222,46 +222,22 @@ export function updateSettings(formValues) { .catch(response => Promise.reject(new Error(response.data.error))); } -export function addApiKey(label) { - return ((dispatch) => { - crypto.randomBytes(20, (err, buf) => { - const key = buf.toString('hex'); - const encodedKey = Buffer.from(key).toString('base64'); - axios.put(`${ROOT_URL}/account/api-keys`, { label, encodedKey }, { withCredentials: true }) - .then((response) => { - // window.alert(`Here is your key :\n${key}\nNote it somewhere, you won't be able to see it later !`); - const elt = React.createElement( - 'tr', { className: 'new-key' }, - React.createElement('td', {}, 'Here is your new key ;\ncopy it somewhere, you won\'t be able to see it later !'), - React.createElement( - 'td', {}, - React.createElement('input', { - id: 'key-to-copy', type: 'text', value: key, readOnly: true - }) - ), - React.createElement( - 'td', {}, - React.createElement('input', { - type: 'submit', - value: 'Copy to clipboard', - className: 'form__table-button-copy', - onClick: () => { - const inputKey = document.getElementById('key-to-copy'); - inputKey.select(); - document.execCommand('copy'); - } - }) - ) - ); - ReactDom.render(elt, document.getElementById('form__table_new_key')); - dispatch({ - type: ActionTypes.ADDED_API_KEY, - user: response.data - }); - }) - .catch(response => Promise.reject(new Error(response.data.error))); - }); - }); +export function createApiKeySuccess(token) { + return { + type: ActionTypes.API_KEY_CREATED, + token + }; +} + +export function createApiKey(label) { + return dispatch => + axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true }) + .then((response) => { + const { token } = response.data; + dispatch(createApiKeySuccess(token)); + return token; + }) + .catch(response => Promise.reject(new Error(response.data.error))); } export function removeApiKey(keyId) { diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index d99005ac..3d8dcef6 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -12,7 +12,8 @@ class APIKeyForm extends React.Component { addKey(event) { event.preventDefault(); document.getElementById('addKeyForm').reset(); - this.props.addApiKey(this.state.keyLabel); + this.props.createApiKey(this.state.keyLabel) + .then(newToken => this.setState({ newToken })); this.state.keyLabel = ''; return false; } @@ -22,23 +23,35 @@ class APIKeyForm extends React.Component { } render() { + const { newToken } = this.state; + + const content = newToken ? + ( +
    +

    Here is your new key. Copy it somewhere, you won't be able to see it later !

    + + +
    ) : + (
    +

    Key label

    + { this.setState({ keyLabel: event.target.value }); }} + />
    + +
    + ); + return (
    -

    Key label

    -
    - { this.setState({ keyLabel: event.target.value }); }} - />
    - -
    + {content} @@ -56,7 +69,7 @@ class APIKeyForm extends React.Component { } APIKeyForm.propTypes = { - addApiKey: PropTypes.func.isRequired, + createApiKey: PropTypes.func.isRequired, removeApiKey: PropTypes.func.isRequired, apiKeys: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.object.isRequired, diff --git a/client/modules/User/pages/AdvancedSettingsView.jsx b/client/modules/User/pages/AdvancedSettingsView.jsx index a8866bc1..83c7f5e2 100644 --- a/client/modules/User/pages/AdvancedSettingsView.jsx +++ b/client/modules/User/pages/AdvancedSettingsView.jsx @@ -5,7 +5,7 @@ import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; import { Helmet } from 'react-helmet'; -import { addApiKey, removeApiKey } from '../actions'; +import { createApiKey, removeApiKey } from '../actions'; import APIKeyForm from '../components/APIKeyForm'; const exitUrl = require('../../../images/exit.svg'); @@ -64,7 +64,7 @@ function mapStateToProps(state) { } function mapDispatchToProps(dispatch) { - return bindActionCreators({ addApiKey, removeApiKey }, dispatch); + return bindActionCreators({ createApiKey, removeApiKey }, dispatch); } AdvancedSettingsView.propTypes = { diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 718e967f..ea535b8f 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -33,7 +33,7 @@ const user = (state = { authenticated: false }, action) => { return { ...state, ...action.user }; case ActionTypes.REMOVED_API_KEY: return { ...state, ...action.user }; - case ActionTypes.ADDED_API_KEY: + case ActionTypes.API_KEY_CREATED: return { ...state, ...action.user }; default: return state; From de5e1a9e8ff729c02c6cd4602b8e4d58ab83c987 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 11:58:04 +0200 Subject: [PATCH 011/322] Removes AdvancedSettingsView as functionality now in AccountView --- client/modules/User/pages/AccountView.jsx | 33 +++++++-- .../User/pages/AdvancedSettingsView.jsx | 74 ------------------- client/routes.jsx | 2 - server/routes/server.routes.js | 8 -- 4 files changed, 25 insertions(+), 92 deletions(-) delete mode 100644 client/modules/User/pages/AdvancedSettingsView.jsx diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index d1bd6ba9..bac59dbd 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -3,13 +3,15 @@ import React from 'react'; import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; import { Link, browserHistory } from 'react-router'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import InlineSVG from 'react-inlinesvg'; import axios from 'axios'; import { Helmet } from 'react-helmet'; -import { updateSettings, initiateVerification } from '../actions'; +import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; import { validateSettings } from '../../../utils/reduxFormUtils'; import GithubButton from '../components/GithubButton'; +import APIKeyForm from '../components/APIKeyForm'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -36,7 +38,7 @@ class AccountView extends React.Component { render() { return ( -
    +
    p5.js Web Editor | Account @@ -50,12 +52,24 @@ class AccountView extends React.Component {

    My Account

    - - Advanced Settings -

    Or

    - + + +
    +

    Account

    +

    Access Tokens

    +
    +
    + + +

    Or

    + +
    + + + +
    - + ); } } @@ -64,12 +78,15 @@ function mapStateToProps(state) { return { initialValues: state.user, // <- initialValues for reduxForm user: state.user, + apiKeys: state.user.apiKeys, theme: state.preferences.theme }; } function mapDispatchToProps(dispatch) { - return bindActionCreators({ updateSettings, initiateVerification }, dispatch); + return bindActionCreators({ + updateSettings, initiateVerification, createApiKey, removeApiKey + }, dispatch); } function asyncValidate(formProps, dispatch, props) { diff --git a/client/modules/User/pages/AdvancedSettingsView.jsx b/client/modules/User/pages/AdvancedSettingsView.jsx deleted file mode 100644 index 83c7f5e2..00000000 --- a/client/modules/User/pages/AdvancedSettingsView.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { browserHistory } from 'react-router'; -import InlineSVG from 'react-inlinesvg'; -import { Helmet } from 'react-helmet'; -import { createApiKey, removeApiKey } from '../actions'; -import APIKeyForm from '../components/APIKeyForm'; - -const exitUrl = require('../../../images/exit.svg'); -const logoUrl = require('../../../images/p5js-logo.svg'); - -class AdvancedSettingsView extends React.Component { - constructor(props) { - super(props); - this.closeAccountPage = this.closeAccountPage.bind(this); - this.gotoHomePage = this.gotoHomePage.bind(this); - } - - componentDidMount() { - document.body.className = this.props.theme; - } - - closeAccountPage() { - browserHistory.goBack(); - } - - gotoHomePage() { - browserHistory.push('/'); - } - - render() { - return ( -
    - - p5.js Web Editor | Advanced Settings - -
    - - -
    -
    -

    Advanced Settings

    - -
    -
    - ); - } -} - -function mapStateToProps(state) { - return { - initialValues: state.user, // <- initialValues for reduxForm - user: state.user, - apiKeys: state.user.apiKeys, - previousPath: state.ide.previousPath, - theme: state.preferences.theme - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators({ createApiKey, removeApiKey }, dispatch); -} - -AdvancedSettingsView.propTypes = { - theme: PropTypes.string.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); diff --git a/client/routes.jsx b/client/routes.jsx index 9389d226..baa89884 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -9,7 +9,6 @@ import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import EmailVerificationView from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; -import AdvancedSettingsView from './modules/User/pages/AdvancedSettingsView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; import { getUser } from './modules/User/actions'; import { stopSketch } from './modules/IDE/actions/ide'; @@ -39,7 +38,6 @@ const routes = store => ( - diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 2f39e924..adade10d 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -87,14 +87,6 @@ router.get('/account', (req, res) => { } }); -router.get('/account/advanced', (req, res) => { - if (req.user) { - res.send(renderIndex()); - } else { - res.redirect('/login'); - } -}); - router.get('/about', (req, res) => { res.send(renderIndex()); }); From 7bfacf08d07ca13c4ab93bfc40c931d01efe6f69 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 12:26:25 +0200 Subject: [PATCH 012/322] Do not return any keys in API --- server/controllers/session.controller.js | 4 ++-- server/models/user.js | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index 4b4e3025..c096bb85 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -13,7 +13,7 @@ export function createSession(req, res, next) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, - apiKeys: req.user.apiKeys, + apiKeys: req.user.publicApiKeys, verified: req.user.verified, id: req.user._id }); @@ -27,7 +27,7 @@ export function getSession(req, res) { email: req.user.email, username: req.user.username, preferences: req.user.preferences, - apiKeys: req.user.apiKeys, + apiKeys: req.user.publicApiKeys, verified: req.user.verified, id: req.user._id }); diff --git a/server/models/user.js b/server/models/user.js index 7e3dab45..ef0ded8f 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -16,6 +16,10 @@ const apiKeySchema = new Schema({ hashedKey: { type: String, required: true }, }, { timestamps: true, _id: true }); +apiKeySchema.virtual('publicFields').get(function publicFields() { + return { id: this.id, label: this.label, lastUsedAt: this.lastUsedAt }; +}); + apiKeySchema.virtual('id').get(function getApiKeyId() { return this._id.toHexString(); }); @@ -95,6 +99,11 @@ userSchema.virtual('id').get(function idToString() { return this._id.toHexString(); }); +userSchema.virtual('publicApiKeys').get(function publicApiKeys() { + return this.apiKeys.map(apiKey => apiKey.publicFields); +}); + + userSchema.set('toJSON', { virtuals: true }); From 69d5a87861f199afe1a52b00606d0d9ac2917f1d Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 16:49:57 +0200 Subject: [PATCH 013/322] Fixes API controller tests The tests mock the mogoose User model and the express Response model which isn't good. We should find a solution that makes use of the actual model object. --- .../user.controller/__tests__/apiKey.test.js | 92 +++++++++++++------ server/controllers/user.controller/apiKey.js | 27 +++++- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js index 01fe67e8..8d1396c2 100644 --- a/server/controllers/user.controller/__tests__/apiKey.test.js +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -1,13 +1,18 @@ /* @jest-environment node */ +import last from 'lodash/last'; import { createApiKey, removeApiKey } from '../../user.controller/apiKey'; jest.mock('../../../models/user'); -const createResponseMock = function (done) { +/* + Create a mock object representing an express Response +*/ +const createResponseMock = function createResponseMock(done) { const json = jest.fn(() => { if (done) { done(); } }); + const status = jest.fn(() => ({ json })); return { @@ -16,6 +21,41 @@ const createResponseMock = function (done) { }; }; +/* + Create a mock of the mongoose User model +*/ +const createUserMock = function createUserMock() { + const apiKeys = []; + let nextId = 0; + + apiKeys.push = ({ label, hashedKey }) => { + const id = nextId; + nextId += 1; + const publicFields = { id, label }; + const allFields = { ...publicFields, hashedKey }; + + Object.defineProperty(allFields, 'publicFields', { + value: publicFields, + enumerable: false + }); + + return Array.prototype.push.call(apiKeys, allFields); + }; + + apiKeys.pull = ({ _id }) => { + const index = apiKeys.findIndex(({ id }) => id === _id); + return apiKeys.splice(index, 1); + }; + + return { + apiKeys, + get publicApiKeys() { + return apiKeys.map(k => k.publicFields) + }, + save: jest.fn(callback => callback()) + }; +}; + const User = require('../../../models/user').default; describe('user.controller', () => { @@ -38,9 +78,8 @@ describe('user.controller', () => { }); it('returns an error if label not provided', () => { - User.__setFindById(undefined, { - apiKeys: [] - }); + User.__setFindById(undefined, createUserMock()); + const request = { user: { id: '1234' }, body: {} }; const response = createResponseMock(); @@ -60,17 +99,18 @@ describe('user.controller', () => { body: { label: 'my key' } }; - const foundUser = { - apiKeys: [], - save: jest.fn(callback => callback()) - }; + const user = createUserMock(); const checkExpecations = () => { - expect(foundUser.apiKeys[0].label).toBe('my key'); - expect(typeof foundUser.apiKeys[0].hashedKey).toBe('string'); + const lastKey = last(user.apiKeys); + + expect(lastKey.label).toBe('my key'); + expect(typeof lastKey.hashedKey).toBe('string'); expect(response.json).toHaveBeenCalledWith({ - token: foundUser.apiKeys[0].hashedKey + apiKeys: [ + { id: 0, label: 'my key', token: lastKey.hashedKey } + ] }); done(); @@ -78,7 +118,7 @@ describe('user.controller', () => { response = createResponseMock(checkExpecations); - User.__setFindById(undefined, foundUser); + User.__setFindById(undefined, user); createApiKey(request, response); }); @@ -105,11 +145,8 @@ describe('user.controller', () => { }; const response = createResponseMock(); - const foundUser = { - apiKeys: [], - save: jest.fn(callback => callback()) - }; - User.__setFindById(undefined, foundUser); + const user = createUserMock(); + User.__setFindById(undefined, user); removeApiKey(request, response); @@ -119,24 +156,27 @@ describe('user.controller', () => { }); }); - it.skip('removes key if it exists', () => { + it('removes key if it exists', () => { const request = { user: { id: '1234' }, - params: { keyId: 'the-key' } + params: { keyId: 0 } }; const response = createResponseMock(); - const foundUser = { - apiKeys: [{ label: 'the-label', id: 'the-key' }], - save: jest.fn(callback => callback()) - }; - User.__setFindById(undefined, foundUser); + const user = createUserMock(); + + user.apiKeys.push({ label: 'first key' }); // id 0 + user.apiKeys.push({ label: 'second key' }); // id 1 + + User.__setFindById(undefined, user); removeApiKey(request, response); - expect(response.status).toHaveBeenCalledWith(404); + expect(response.status).toHaveBeenCalledWith(200); expect(response.json).toHaveBeenCalledWith({ - error: 'Key does not exist for user' + apiKeys: [ + { id: 1, label: 'second key' } + ] }); }); }); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js index 35a0ba36..80f508f0 100644 --- a/server/controllers/user.controller/apiKey.js +++ b/server/controllers/user.controller/apiKey.js @@ -30,9 +30,9 @@ export function createApiKey(req, res) { return; } - const hashedKey = await generateApiKey(); + const keyToBeHashed = await generateApiKey(); - user.apiKeys.push({ label: req.body.label, hashedKey }); + const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); user.save((saveErr) => { if (saveErr) { @@ -40,7 +40,17 @@ export function createApiKey(req, res) { return; } - res.json({ token: hashedKey }); + const apiKeys = user.apiKeys + .map((apiKey, index) => { + const fields = apiKey.publicFields; + const shouldIncludeToken = index === addedApiKeyIndex - 1; + + return shouldIncludeToken ? + { ...fields, token: keyToBeHashed } : + fields; + }); + + res.json({ apiKeys }); }); }); } @@ -60,7 +70,16 @@ export function removeApiKey(req, res) { res.status(404).json({ error: 'Key does not exist for user' }); return; } + user.apiKeys.pull({ _id: req.params.keyId }); - saveUser(res, user); + + user.save((saveErr) => { + if (saveErr) { + res.status(500).json({ error: saveErr }); + return; + } + + res.status(200).json({ apiKeys: user.publicApiKeys }); + }); }); } From 504eacaf640a3af2e379284c9aaa2cd5c73ee95b Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 19:50:33 +0200 Subject: [PATCH 014/322] Displays all API keys in a table, including new token information --- client/modules/User/actions.js | 8 +- client/modules/User/components/APIKeyForm.jsx | 144 ++++++++++++------ server/models/user.js | 4 +- 3 files changed, 106 insertions(+), 50 deletions(-) diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index cfd2664a..f0708c3c 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -222,10 +222,10 @@ export function updateSettings(formValues) { .catch(response => Promise.reject(new Error(response.data.error))); } -export function createApiKeySuccess(token) { +export function createApiKeySuccess(user) { return { type: ActionTypes.API_KEY_CREATED, - token + user }; } @@ -233,9 +233,7 @@ export function createApiKey(label) { return dispatch => axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true }) .then((response) => { - const { token } = response.data; - dispatch(createApiKeySuccess(token)); - return token; + dispatch(createApiKeySuccess(response.data)); }) .catch(response => Promise.reject(new Error(response.data.error))); } diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 3d8dcef6..17ea1542 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -1,5 +1,62 @@ import PropTypes from 'prop-types'; import React from 'react'; +import InlineSVG from 'react-inlinesvg'; +import format from 'date-fns/format'; +import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; +import orderBy from 'lodash/orderBy'; + +const trashCan = require('../../../images/trash-can.svg'); + +function NewTokenDisplay({ token }) { + return ( + +

    Make sure to copy your new personal access token now. You won’t be able to see it again!

    +

    + +
    + ); +} + +function TokenMetadataList({ tokens, onRemove }) { + return ( +
    + + + + + + + + + + {orderBy(tokens, ['createdAt'], ['desc']).map((v) => { + const keyRow = ( + + + + + + + ); + + const newKeyValue = v.token && ( + + + + ); + + return [keyRow, newKeyValue]; + })} + +
    NameCreated onLast usedActions
    {v.label}{format(new Date(v.createdAt), 'MMM D, YYYY h:mm A')}{distanceInWordsToNow(new Date(v.lastUsedAt), { addSuffix: true })} + +
    + +
    + ); +} class APIKeyForm extends React.Component { constructor(props) { @@ -7,62 +64,61 @@ class APIKeyForm extends React.Component { this.state = { keyLabel: '' }; this.addKey = this.addKey.bind(this); + this.removeKey = this.removeKey.bind(this); + this.renderApiKeys = this.renderApiKeys.bind(this); } addKey(event) { event.preventDefault(); - document.getElementById('addKeyForm').reset(); - this.props.createApiKey(this.state.keyLabel) - .then(newToken => this.setState({ newToken })); - this.state.keyLabel = ''; + const { keyLabel } = this.state; + + this.setState({ + keyLabel: '' + }); + + this.props.createApiKey(keyLabel); + return false; } - removeKey(keyId) { - this.props.removeApiKey(keyId); + removeKey(key) { + const message = `Are you sure you want to delete "${key.label}"?`; + + if (window.confirm(message)) { + this.props.removeApiKey(key.id); + } + } + + renderApiKeys() { + const hasApiKeys = this.props.apiKeys && this.props.apiKeys.length > 0; + + if (hasApiKeys) { + return ( + + ); + } + return

    You have no API keys

    ; } render() { - const { newToken } = this.state; - - const content = newToken ? - ( -
    -

    Here is your new key. Copy it somewhere, you won't be able to see it later !

    - - -
    ) : - (
    -

    Key label

    - { this.setState({ keyLabel: event.target.value }); }} - />
    - -
    - ); - return (
    - {content} - - - - {this.props.apiKeys && this.props.apiKeys.map(v => ( - - - - - ))} - -
    {v.label}
    Created on: {v.createdAt}
    Last used on:
    {v.lastUsedAt}
    +
    + + { this.setState({ keyLabel: event.target.value }); }} + /> + +
    + {this.renderApiKeys()}
    ); } diff --git a/server/models/user.js b/server/models/user.js index ef0ded8f..8bd33782 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -17,7 +17,9 @@ const apiKeySchema = new Schema({ }, { timestamps: true, _id: true }); apiKeySchema.virtual('publicFields').get(function publicFields() { - return { id: this.id, label: this.label, lastUsedAt: this.lastUsedAt }; + return { + id: this.id, label: this.label, lastUsedAt: this.lastUsedAt, createdAt: this.createdAt + }; }); apiKeySchema.virtual('id').get(function getApiKeyId() { From ed87d4cd26dc2c45b6ca641f7c92fde00528c463 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 19:50:54 +0200 Subject: [PATCH 015/322] Aligns AccountView to top of page --- client/modules/User/components/APIKeyForm.jsx | 2 +- client/modules/User/pages/AccountView.jsx | 2 +- client/styles/components/_form-container.scss | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 17ea1542..bffd0ffc 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -44,7 +44,7 @@ function TokenMetadataList({ tokens, onRemove }) { ); const newKeyValue = v.token && ( - + diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index bac59dbd..9cf18eb8 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -38,7 +38,7 @@ class AccountView extends React.Component { render() { return ( -
    +
    p5.js Web Editor | Account diff --git a/client/styles/components/_form-container.scss b/client/styles/components/_form-container.scss index 669edf8e..743aad21 100644 --- a/client/styles/components/_form-container.scss +++ b/client/styles/components/_form-container.scss @@ -6,6 +6,10 @@ align-items: center; } +.form-container--align-top { + height: unset; +} + .form-container__header { width: 100%; padding: #{15 / $base-font-size}rem #{34 / $base-font-size}rem; From a03eed16030c00cce3b94625832eefccb36f0516 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 14 May 2019 20:12:52 +0200 Subject: [PATCH 016/322] Copy button copies token to clipboard --- client/modules/User/components/APIKeyForm.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index bffd0ffc..a0da81b6 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -7,12 +7,17 @@ import orderBy from 'lodash/orderBy'; const trashCan = require('../../../images/trash-can.svg'); + function NewTokenDisplay({ token }) { + function handleCopyClick() { + navigator.clipboard.writeText(token); + } + return (

    Make sure to copy your new personal access token now. You won’t be able to see it again!

    - +
    ); } From 3e760ca0b8c48bbaf426841536c1e55428b5cc42 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 15 May 2019 12:11:58 +0200 Subject: [PATCH 017/322] Styles Account and APIKeys components --- client/modules/User/components/APIKeyForm.jsx | 135 +++++++----------- client/modules/User/components/APIKeyList.jsx | 50 +++++++ client/modules/User/pages/AccountView.jsx | 10 +- client/styles/components/_account.scss | 4 + client/styles/components/_api-key.scss | 102 +++++++++++++ client/styles/components/_form-container.scss | 31 ++-- client/styles/components/_forms.scss | 36 ++--- client/styles/main.scss | 2 + 8 files changed, 244 insertions(+), 126 deletions(-) create mode 100644 client/modules/User/components/APIKeyList.jsx create mode 100644 client/styles/components/_account.scss create mode 100644 client/styles/components/_api-key.scss diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index a0da81b6..17ec61e7 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -1,67 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineSVG from 'react-inlinesvg'; -import format from 'date-fns/format'; -import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; -import orderBy from 'lodash/orderBy'; -const trashCan = require('../../../images/trash-can.svg'); +import CopyableInput from '../../IDE/components/CopyableInput'; +import APIKeyList from './APIKeyList'; -function NewTokenDisplay({ token }) { - function handleCopyClick() { - navigator.clipboard.writeText(token); - } +const plusIcon = require('../../../images/plus-icon.svg'); - return ( - -

    Make sure to copy your new personal access token now. You won’t be able to see it again!

    -

    - -
    - ); -} - -function TokenMetadataList({ tokens, onRemove }) { - return ( - - - - - - - - - - - {orderBy(tokens, ['createdAt'], ['desc']).map((v) => { - const keyRow = ( - - - - - - - ); - - const newKeyValue = v.token && ( - - - - ); - - return [keyRow, newKeyValue]; - })} - -
    NameCreated onLast usedActions
    {v.label}{format(new Date(v.createdAt), 'MMM D, YYYY h:mm A')}{distanceInWordsToNow(new Date(v.lastUsedAt), { addSuffix: true })} - -
    - -
    - ); -} +export const APIKeyPropType = PropTypes.shape({ + id: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + createdAt: PropTypes.object.isRequired, + lastUsedAt: PropTypes.object.isRequired, +}); class APIKeyForm extends React.Component { constructor(props) { @@ -99,45 +51,64 @@ class APIKeyForm extends React.Component { if (hasApiKeys) { return ( - + ); } - return

    You have no API keys

    ; + return

    You have no exsiting tokens.

    ; } render() { + const keyWithToken = this.props.apiKeys.find(k => !!k.token); + return ( -
    -
    - - { this.setState({ keyLabel: event.target.value }); }} - /> - -
    - {this.renderApiKeys()} +
    +

    Personal Access Tokens act like your password to allow automated scripts to access the Editor API. Create a token for each script that needs access.

    + +
    +

    Create new token

    +
    + + { this.setState({ keyLabel: event.target.value }); }} + placeholder="What is this token for? e.g. Example import script" + type="text" + value={this.state.keyLabel} + /> + +
    + + { + keyWithToken && ( +
    +

    Your new access token

    +

    Make sure to copy your new personal access token now. You won’t be able to see it again!

    + +
    + ) + } +
    + +
    +

    Existing tokens

    + {this.renderApiKeys()} +
    ); } } APIKeyForm.propTypes = { + apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, createApiKey: PropTypes.func.isRequired, removeApiKey: PropTypes.func.isRequired, - apiKeys: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.object.isRequired, - label: PropTypes.string.isRequired, - createdAt: PropTypes.object.isRequired, - lastUsedAt: PropTypes.object.isRequired, - })).isRequired }; export default APIKeyForm; diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.jsx new file mode 100644 index 00000000..b3df65dd --- /dev/null +++ b/client/modules/User/components/APIKeyList.jsx @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import InlineSVG from 'react-inlinesvg'; +import format from 'date-fns/format'; +import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; +import orderBy from 'lodash/orderBy'; + +import { APIKeyPropType } from './APIKeyForm'; + +const trashCan = require('../../../images/trash-can.svg'); + +function APIKeyList({ apiKeys, onRemove }) { + return ( + + + + + + + + + + + {orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => { + const hasNewToken = !!key.token; + + return ( + + + + + + + ); + })} + +
    NameCreated onLast usedActions
    {key.label}{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}{distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true })} + +
    + ); +} + +APIKeyList.propTypes = { + apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, + onRemove: PropTypes.func.isRequired, +}; + +export default APIKeyList; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 9cf18eb8..0da5a37d 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -16,7 +16,6 @@ import APIKeyForm from '../components/APIKeyForm'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); - class AccountView extends React.Component { constructor(props) { super(props); @@ -38,7 +37,7 @@ class AccountView extends React.Component { render() { return ( -
    +
    p5.js Web Editor | Account @@ -52,7 +51,7 @@ class AccountView extends React.Component {

    My Account

    - +

    Account

    @@ -61,8 +60,9 @@ class AccountView extends React.Component { -

    Or

    - +

    Social Login

    +

    Link this account with your GitHub account to allow login from both.

    +
    diff --git a/client/styles/components/_account.scss b/client/styles/components/_account.scss new file mode 100644 index 00000000..2b8b601b --- /dev/null +++ b/client/styles/components/_account.scss @@ -0,0 +1,4 @@ +.account__tabs { + width: 500px; + padding-top: #{20 / $base-font-size}rem; +} diff --git a/client/styles/components/_api-key.scss b/client/styles/components/_api-key.scss new file mode 100644 index 00000000..16d2bbfe --- /dev/null +++ b/client/styles/components/_api-key.scss @@ -0,0 +1,102 @@ +.api-key-form__summary { + padding-top: #{25 / $base-font-size}rem; + @include themify() { + color: getThemifyVariable('heading-text-color'); + } +} + +.api-key-form__section { + padding-bottom: #{15 / $base-font-size}rem; +} + +.api-key-form__title { + padding: #{15 / $base-font-size}rem 0; + font-size: #{21 / $base-font-size}rem; + font-weight: bold; + @include themify() { + color: getThemifyVariable('heading-text-color'); + } +} + +.api-key-form__create-button { + display: flex; + justify-content: center; +} + +.api-key-form__create-button .isvg { + padding-right: 10px; +} + +.api-key-list { + display: block; + max-width: 900px; + border-collapse: collapse; + table-layout: fixed; + + thead tr th { + width: 30%; + } + + thead tr th:last-child { + width: 10%; + } + + th { + padding: #{5 / $base-font-size}rem; + } + + td { + padding: #{15 / $base-font-size}rem #{5 / $base-font-size}rem; + } + + tbody tr:nth-child(odd) { + background-color: #f2f2f2; + } +} + +.api-key-list__action { + text-align: center; +} + +.api-key-list__delete-button { + width:#{20 / $base-font-size}rem; + height:#{20 / $base-font-size}rem; + + text-align: center; + + @include themify() { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; + position: initial; + left: 0; + top: 0; + & g { + opacity: 1; + fill: getThemifyVariable('icon-color'); + } + } +} + +.api-key-list__delete-button:hover { + @include themify() { + & g { + opacity: 1; + fill: getThemifyVariable('icon-hover-color'); + } + } +} + +.api-key-form__new-token__title { + margin-bottom: #{10 / $base-font-size}rem; + font-size: #{18 / $base-font-size}rem; + font-weight: bold; + @include themify() { + color: getThemifyVariable('heading-text-color'); + } +} + +.api-key-form__new-token__info { + padding: #{10 / $base-font-size}rem 0; +} diff --git a/client/styles/components/_form-container.scss b/client/styles/components/_form-container.scss index 743aad21..3514d4f6 100644 --- a/client/styles/components/_form-container.scss +++ b/client/styles/components/_form-container.scss @@ -6,6 +6,10 @@ align-items: center; } +.form-container--align-left { + text-align: left; +} + .form-container--align-top { height: unset; } @@ -25,11 +29,21 @@ align-items: center; } +.form-container--align-left .form-container__content { + align-items: left; +} + .form-container__title { font-weight: normal; color: $form-title-color; } +.form-container__context { + @include themify() { + color: getThemifyVariable('secondary-text-color') + } +} + .form-container__divider { padding: #{20 / $base-font-size}rem 0; } @@ -41,20 +55,3 @@ .form-container__exit-button { @extend %none-themify-icon-with-hover; } - -.form__table { - display: block; - max-width: 900px; - border-collapse: collapse; - & td { - max-width: 300px; - border: 1px solid #ddd; - padding: 0 10px 0 10px; - } - & tr:nth-child(even) { - background-color: #f2f2f2; - } - & tr.new-key { - background-color: #f2f2f2; - } -} diff --git a/client/styles/components/_forms.scss b/client/styles/components/_forms.scss index 8fe66361..94ba4501 100644 --- a/client/styles/components/_forms.scss +++ b/client/styles/components/_forms.scss @@ -9,6 +9,11 @@ } } +.form--inline { + display: flex; + align-items: center; +} + .form__cancel-button { margin-top: #{10 / $base-font-size}rem; font-size: #{12 / $base-font-size}rem; @@ -20,6 +25,11 @@ color: $form-navigation-options-color; } +.form__legend{ + font-size: #{21 / $base-font-size}rem; + font-weight: bold; +} + .form__label { color: $secondary-form-title-color; font-size: #{12 / $base-font-size}rem; @@ -28,6 +38,10 @@ display: block; } +.form__label--hidden { + @extend %hidden-element; +} + .form__input { width: #{360 / $base-font-size}rem; height: #{40 / $base-font-size}rem; @@ -51,25 +65,3 @@ .form input[type="submit"]:disabled { cursor: not-allowed; } - -.form__table-button-remove { - @extend %forms-button; - margin: 1rem 0 1rem 0; - @include themify() { - color: getThemifyVariable('console-error-background-color'); - border-color: getThemifyVariable('console-error-background-color'); - &:enabled:hover { - border-color: getThemifyVariable('console-error-background-color'); - background-color: getThemifyVariable('console-error-background-color'); - } - &:enabled:active { - border-color: getThemifyVariable('console-error-background-color'); - background-color: getThemifyVariable('console-error-background-color'); - } - } -} - -.form__table-button-copy{ - @extend %forms-button; - margin: 1rem 0 1rem 0; -} diff --git a/client/styles/main.scss b/client/styles/main.scss index 3d9bf977..cff7ff67 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -14,6 +14,8 @@ @import 'components/p5-light-codemirror-theme'; @import 'components/p5-dark-codemirror-theme'; @import 'components/p5-contrast-codemirror-theme'; +@import 'components/account'; +@import 'components/api-key'; @import 'components/editor'; @import 'components/nav'; @import 'components/preview-nav'; From 89dd41d81f16f9d6c0bc1e67652127f8a667e466 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 15 May 2019 12:28:18 +0200 Subject: [PATCH 018/322] lastUserAt should be null if the key has never been used --- client/modules/User/components/APIKeyForm.jsx | 5 +++-- client/modules/User/components/APIKeyList.jsx | 4 ++-- server/models/user.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 17ec61e7..94850099 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -10,9 +10,10 @@ const plusIcon = require('../../../images/plus-icon.svg'); export const APIKeyPropType = PropTypes.shape({ id: PropTypes.object.isRequired, + token: PropTypes.object, label: PropTypes.string.isRequired, - createdAt: PropTypes.object.isRequired, - lastUsedAt: PropTypes.object.isRequired, + createdAt: PropTypes.string.isRequired, + lastUsedAt: PropTypes.string, }); class APIKeyForm extends React.Component { diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.jsx index b3df65dd..3f036318 100644 --- a/client/modules/User/components/APIKeyList.jsx +++ b/client/modules/User/components/APIKeyList.jsx @@ -22,13 +22,13 @@ function APIKeyList({ apiKeys, onRemove }) { {orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => { - const hasNewToken = !!key.token; + const lastUsed = key.lastUsedAt ? distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) : 'Never'; return ( {key.label} {format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')} - {distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true })} + {lastUsed} - -
    -
    -

    My Account

    - - -
    -

    Account

    -

    Access Tokens

    -
    -
    - - -
    + + + +
    +
    +
    +

    My Account

    + +
    + + +
    +

    Account

    +

    Access Tokens

    +
    +
    + +

    Social Login

    -

    +

    Link this account with your GitHub account to allow login from both.

    -
    - - - - - -
    -
    + + + + + +
    + + ); } } diff --git a/client/styles/components/_account.scss b/client/styles/components/_account.scss index 44d8939d..8a6cb4fc 100644 --- a/client/styles/components/_account.scss +++ b/client/styles/components/_account.scss @@ -2,6 +2,6 @@ padding-top: #{20 / $base-font-size}rem; } -.account__social__context { +.account__social-text { padding-bottom: #{15 / $base-font-size}rem; } From 1f95718f5811762b19a0daf4e0c07ce3f1ce70d5 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 4 Jun 2019 14:30:10 +0200 Subject: [PATCH 040/322] Page background matches selected theme for user pages --- client/modules/User/pages/AccountView.jsx | 6 +++--- client/styles/layout/_user.scss | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index e2ce9c90..f4d25fce 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -37,14 +37,14 @@ class AccountView extends React.Component { render() { return ( - +
    p5.js Web Editor | Account -
    +

    My Account

    @@ -73,7 +73,7 @@ class AccountView extends React.Component {
    - +
    ); } } diff --git a/client/styles/layout/_user.scss b/client/styles/layout/_user.scss index ac9436f1..5f07a771 100644 --- a/client/styles/layout/_user.scss +++ b/client/styles/layout/_user.scss @@ -1,4 +1,8 @@ .user { + display: flex; + flex-direction: column; + height: 100%; + flex-wrap: wrap; @include themify() { color: getThemifyVariable('primary-text-color'); background-color: getThemifyVariable('background-color'); From eb4846c3c28e02dc6b6d1e9f9b662efcda5c53b8 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 4 Jun 2019 14:57:48 +0200 Subject: [PATCH 041/322] Fix layout on user pages --- .../User/pages/EmailVerificationView.jsx | 32 +++++------ client/modules/User/pages/LoginView.jsx | 54 ++++++++++--------- client/modules/User/pages/NewPasswordView.jsx | 38 ++++++------- .../modules/User/pages/ResetPasswordView.jsx | 50 ++++++++--------- client/modules/User/pages/SignupView.jsx | 40 +++++++------- 5 files changed, 112 insertions(+), 102 deletions(-) diff --git a/client/modules/User/pages/EmailVerificationView.jsx b/client/modules/User/pages/EmailVerificationView.jsx index 49d3b7fa..55b5a1f3 100644 --- a/client/modules/User/pages/EmailVerificationView.jsx +++ b/client/modules/User/pages/EmailVerificationView.jsx @@ -65,21 +65,23 @@ class EmailVerificationView extends React.Component { } return ( -
    - - p5.js Web Editor | Email Verification - -
    - - -
    -
    -

    Verify your email

    - {status} +
    +
    + + p5.js Web Editor | Email Verification + +
    + + +
    +
    +

    Verify your email

    + {status} +
    ); diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index 9e0b3656..4ce4ab8d 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -34,32 +34,34 @@ class LoginView extends React.Component { return null; } return ( -
    - - p5.js Web Editor | Login - -
    - - -
    -
    -

    Log In

    - -

    Or

    - - -

    - Don't have an account?  - Sign Up -

    -

    - Forgot your password?  - Reset your password -

    +
    +
    + + p5.js Web Editor | Login + +
    + + +
    +
    +

    Log In

    + +

    Or

    + + +

    + Don't have an account?  + Sign Up +

    +

    + Forgot your password?  + Reset your password +

    +
    ); diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index 6fcd1bde..32b9ae91 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -35,24 +35,26 @@ class NewPasswordView extends React.Component { 'user': true }); return ( -
    - - p5.js Web Editor | New Password - -
    - - -
    -
    -

    Set a New Password

    - -

    - The password reset token is invalid or has expired. -

    +
    +
    + + p5.js Web Editor | New Password + +
    + + +
    +
    +

    Set a New Password

    + +

    + The password reset token is invalid or has expired. +

    +
    ); diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx index 2dfae858..c6db189a 100644 --- a/client/modules/User/pages/ResetPasswordView.jsx +++ b/client/modules/User/pages/ResetPasswordView.jsx @@ -34,30 +34,32 @@ class ResetPasswordView extends React.Component { 'user': true }); return ( -
    - - p5.js Web Editor | Reset Password - -
    - - -
    -
    -

    Reset Your Password

    - -

    - Your password reset email should arrive shortly. If you don't see it, check - in your spam folder as sometimes it can end up there. -

    -

    - Log In -  or  - Sign Up -

    +
    +
    + + p5.js Web Editor | Reset Password + +
    + + +
    +
    +

    Reset Your Password

    + +

    + Your password reset email should arrive shortly. If you don't see it, check + in your spam folder as sometimes it can end up there. +

    +

    + Log In +  or  + Sign Up +

    +
    ); diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index c8d168f5..bcd98a3a 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -34,25 +34,27 @@ class SignupView extends React.Component { return null; } return ( -
    - - p5.js Web Editor | Signup - -
    - - -
    -
    -

    Sign Up

    - -

    - Already have an account?  - Log In -

    +
    +
    + + p5.js Web Editor | Signup + +
    + + +
    +
    +

    Sign Up

    + +

    + Already have an account?  + Log In +

    +
    ); From 4679d6a0bddd22b012e7f379bd069af07e63e1e2 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 4 Jun 2019 14:58:13 +0200 Subject: [PATCH 042/322] Show "Back to the editor" in nav on Account view page --- client/components/NavBasic.jsx | 21 +++++++++++++++++++-- client/images/triangle-arrow-left.svg | 14 ++++++++++++++ client/modules/User/pages/AccountView.jsx | 5 +---- 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 client/images/triangle-arrow-left.svg diff --git a/client/components/NavBasic.jsx b/client/components/NavBasic.jsx index 1cb89b71..92913add 100644 --- a/client/components/NavBasic.jsx +++ b/client/components/NavBasic.jsx @@ -1,10 +1,15 @@ +import PropTypes from 'prop-types'; import React from 'react'; import InlineSVG from 'react-inlinesvg'; const logoUrl = require('../images/p5js-logo-small.svg'); +const arrowUrl = require('../images/triangle-arrow-left.svg'); class NavBasic extends React.PureComponent { - + static defaultProps = { + onBack: null + } + render() { return ( ); } } -NavBasic.propTypes = {}; +NavBasic.propTypes = { + onBack: PropTypes.func, +}; export default NavBasic; diff --git a/client/images/triangle-arrow-left.svg b/client/images/triangle-arrow-left.svg new file mode 100644 index 00000000..dcc159df --- /dev/null +++ b/client/images/triangle-arrow-left.svg @@ -0,0 +1,14 @@ + + Left Arrow + Created with Sketch. + + + + + + + + + diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index f4d25fce..7382656e 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -42,15 +42,12 @@ class AccountView extends React.Component { p5.js Web Editor | Account - +

    My Account

    -
    From 6f1b6fd51cbba96d4034fbcaec3d4382871176ab Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 5 Jun 2019 12:05:31 -0400 Subject: [PATCH 043/322] for #950, update babel to v7 (#1077) * for #950, upgrade babel to v7 * fix linting errors * for #950, remove @babel/core from devDependencies (so it's only in dependencies) and change babel-loader config to use .babelrc * for #950, changes to .babelrc to make work * for #950, include core-js modules in webpack config for IE support with babel/plugin-syntax-dynamic-import * for #950, update babel and associated packages to LTS --- .babelrc | 82 +- client/modules/IDE/components/Editor.jsx | 2 - client/modules/IDE/components/ErrorModal.jsx | 3 +- client/modules/IDE/components/SketchList.jsx | 4 +- client/modules/IDE/reducers/project.js | 2 +- index.js | 14 +- package-lock.json | 5905 +++++++++--------- package.json | 51 +- server/migrations/db_reformat_start.js | 4 +- server/migrations/start.js | 4 +- server/scripts/fetch-examples-gg.js | 4 +- server/scripts/fetch-examples-ml5.js | 4 +- server/scripts/fetch-examples.js | 4 +- webpack/config.babel.js | 43 - webpack/config.dev.js | 20 +- webpack/config.examples.js | 54 +- webpack/config.prod.js | 9 +- webpack/config.server.js | 18 +- 18 files changed, 3115 insertions(+), 3112 deletions(-) delete mode 100644 webpack/config.babel.js diff --git a/.babelrc b/.babelrc index d86e5154..aae12b97 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,83 @@ { - "presets": ["react", "env", "stage-0"], + "presets": [ + "@babel/preset-react", + "@babel/preset-env" + ], "env": { "production": { "plugins": [ "transform-react-remove-prop-types", - "transform-react-constant-elements", - "transform-react-inline-elements" + "@babel/plugin-transform-react-constant-elements", + "@babel/plugin-transform-react-inline-elements", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-syntax-import-meta", + [ + "@babel/plugin-proposal-decorators", + { + "legacy": true + } + ], + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } + ], + "@babel/plugin-proposal-json-strings", + "@babel/plugin-proposal-function-sent", + "@babel/plugin-proposal-export-namespace-from", + "@babel/plugin-proposal-numeric-separator", + "@babel/plugin-proposal-throw-expressions", + "@babel/plugin-proposal-export-default-from", + "@babel/plugin-proposal-logical-assignment-operators", + "@babel/plugin-proposal-optional-chaining", + [ + "@babel/plugin-proposal-pipeline-operator", + { + "proposal": "minimal" + } + ], + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-proposal-do-expressions", + "@babel/plugin-proposal-function-bind" ], - "presets": ["env", "react", "react-optimize", "stage-0"] + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ] } - } -} \ No newline at end of file + }, + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-syntax-import-meta", + [ + "@babel/plugin-proposal-decorators", + { + "legacy": true + } + ], + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } + ], + "@babel/plugin-proposal-json-strings", + "@babel/plugin-proposal-function-sent", + "@babel/plugin-proposal-export-namespace-from", + "@babel/plugin-proposal-numeric-separator", + "@babel/plugin-proposal-throw-expressions", + "@babel/plugin-proposal-export-default-from", + "@babel/plugin-proposal-logical-assignment-operators", + "@babel/plugin-proposal-optional-chaining", + [ + "@babel/plugin-proposal-pipeline-operator", + { + "proposal": "minimal" + } + ], + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-proposal-do-expressions", + "@babel/plugin-proposal-function-bind" + ] +} diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 3f749955..2b970660 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -283,8 +283,6 @@ class Editor extends React.Component { } } - _cm: CodeMirror.Editor - render() { const editorSectionClass = classNames({ 'editor': true, diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index 04ea3598..b1ff6b10 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -26,7 +26,8 @@ class ErrorModal extends React.Component { staleProject() { return (

    - The project you have attempted to save has been saved from another window. Please refresh the page to see the latest version. + The project you have attempted to save has been saved from another window. + Please refresh the page to see the latest version.

    ); } diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index c0086ab5..79d5394f 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -36,7 +36,9 @@ class SketchList extends React.Component { } renderEmptyTable() { - if (!this.props.loading && this.props.sketches.length === 0) return (

    No sketches.

    ); + if (!this.props.loading && this.props.sketches.length === 0) { + return (

    No sketches.

    ); + } return null; } diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 6109474a..0779c0f5 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -50,7 +50,7 @@ const project = (state, action) => { return Object.assign({}, state, { updatedAt: action.value }); case ActionTypes.START_SAVING_PROJECT: return Object.assign({}, state, { isSaving: true }); - case ActionTypes.START_STOP_PROJECT: + case ActionTypes.END_SAVING_PROJECT: return Object.assign({}, state, { isSaving: false }); default: return state; diff --git a/index.js b/index.js index b13b7378..55c35ac7 100644 --- a/index.js +++ b/index.js @@ -4,18 +4,10 @@ if (process.env.NODE_ENV === 'production') { require('./dist/server.bundle.js'); } else { let parsed = require('dotenv').config(); - require('babel-register')({ - "plugins": [ - [ - "babel-plugin-webpack-loaders", - { - "config": "./webpack/config.babel.js", - "verbose": false - } - ] - ] + require('@babel/register')({ + presets: ["@babel/preset-env"] }); - require('babel-polyfill'); + require('@babel/polyfill'); //// in development, let .env values override those in the environment already (i.e. in docker-compose.yml) // so commenting this out makes the docker container work. // if (process.env.NODE_ENV === 'development') { diff --git a/package-lock.json b/package-lock.json index abe38a18..ae4a04e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,154 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } }, + "@babel/core": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.5.tgz", + "integrity": "sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helpers": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.4.5", + "@babel/types": "^7.4.4", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", + "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "requires": { + "@babel/types": "^7.4.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", + "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", + "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz", + "integrity": "sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.4.4", + "@babel/helper-split-export-declaration": "^7.4.4" + } + }, + "@babel/helper-define-map": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", + "integrity": "sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", + "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", + "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, "@babel/helper-module-imports": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", @@ -21,99 +164,984 @@ "@babel/types": "^7.0.0" } }, + "@babel/helper-module-transforms": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz", + "integrity": "sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/template": "^7.4.4", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.4.4.tgz", + "integrity": "sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", + "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz", + "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==", + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", + "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==" + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz", + "integrity": "sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz", + "integrity": "sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-decorators": "^7.2.0" + } + }, + "@babel/plugin-proposal-do-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.2.0.tgz", + "integrity": "sha512-2bWN48zQHf/W5T8XvemGQJSi8hzhIo7y4kv/RiA08UcMLQ73lkTknhlaFGf1HjCJzG8FGopgsq6pSe1C+10fPg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-do-expressions": "^7.2.0" + } + }, + "@babel/plugin-proposal-export-default-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.2.0.tgz", + "integrity": "sha512-NVfNe7F6nsasG1FnvcFxh2FN0l04ZNe75qTOAVOILWPam0tw9a63RtT/Dab8dPjedZa4fTQaQ83yMMywF9OSug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-export-default-from": "^7.2.0" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.2.0.tgz", + "integrity": "sha512-DZUxbHYxQ5fUFIkMEnh75ogEdBLPfL+mQUqrO2hNY2LGm+tqFnxE924+mhAcCOh/8za8AaZsWHbq6bBoS3TAzA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-export-namespace-from": "^7.2.0" + } + }, + "@babel/plugin-proposal-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz", + "integrity": "sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-function-bind": "^7.2.0" + } + }, + "@babel/plugin-proposal-function-sent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.2.0.tgz", + "integrity": "sha512-qQBDKRSCu1wGJi3jbngs18vrujVQA4F+OkSuIQYRhE6y19jcPzeEIGOc683mCQXDUR3BQCz8JyCupIwv+IRFmA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-wrap-function": "^7.2.0", + "@babel/plugin-syntax-function-sent": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz", + "integrity": "sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.2.0" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.4.4.tgz", + "integrity": "sha512-Amph7Epui1Dh/xxUxS2+K22/MUi6+6JVTvy3P58tja3B6yKTSjwwx0/d83rF7551D6PVSSoplQb8GCwqec7HRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.2.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz", + "integrity": "sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-numeric-separator": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz", + "integrity": "sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.2.0.tgz", + "integrity": "sha512-ea3Q6edZC/55wEBVZAEz42v528VulyO0eir+7uky/sT4XRcdkWJcFi1aPtitTlwUzGnECWJNExWww1SStt+yWw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.2.0" + } + }, + "@babel/plugin-proposal-pipeline-operator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.3.2.tgz", + "integrity": "sha512-wuzx8U/KZLJYoqU6joiaKY0PixHuYZ3Vxys+wPahNAZEEm+EDb1eTc19DuJob3BdxYSD9PWPbwyoRbhkdoYErg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-pipeline-operator": "^7.3.0" + } + }, + "@babel/plugin-proposal-throw-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz", + "integrity": "sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-throw-expressions": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", + "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz", + "integrity": "sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-do-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.2.0.tgz", + "integrity": "sha512-/u4rJ+XEmZkIhspVuKRS+7WLvm7Dky9j9TvGK5IgId8B3FKir9MG+nQxDZ9xLn10QMBvW58dZ6ABe2juSmARjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-export-default-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.2.0.tgz", + "integrity": "sha512-c7nqUnNST97BWPtoe+Ssi+fJukc9P9/JMZ71IOMNQWza2E+Psrd46N6AEvtw6pqK+gt7ChjXyrw4SPDO79f3Lw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz", + "integrity": "sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-function-bind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz", + "integrity": "sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-function-sent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.2.0.tgz", + "integrity": "sha512-2MOVuJ6IMAifp2cf0RFkHQaOvHpbBYyWCvgtF/WVqXhTd7Bgtov8iXVCadLXp2FN1BrI2EFl+JXuwXy0qr3KoQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz", + "integrity": "sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", + "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.2.0.tgz", + "integrity": "sha512-l/NKSlrnvd73/EL540t9hZhcSo4TULBrIPs9Palju8Oc/A8DXDO+xQf04whfeuZLpi8AuIvCAdpKmmubLN4EfQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.2.0.tgz", + "integrity": "sha512-lRCEaKE+LTxDQtgbYajI04ddt6WW0WJq57xqkAZ+s11h4YgfRHhVA/Y2VhfPzzFD4qeLHWg32DMp9HooY4Kqlg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.2.0.tgz", + "integrity": "sha512-DroeVNkO/BnGpL2R7+ZNZqW+E24aR/4YWxP3Qb15d6lPU8KDzF8HlIUIRCOJRn4X77/oyW4mJY+7FHfY82NLtQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz", + "integrity": "sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-pipeline-operator": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.3.0.tgz", + "integrity": "sha512-LAa3ZcOAyfPOUDTp0W5EiXGSAFh1vz9sD8yY7sZzWzEkZdIC404pqBP60Yfu9GJDj0ggh+UTQY6EYlIDXVr0/Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-throw-expressions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz", + "integrity": "sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz", + "integrity": "sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz", + "integrity": "sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.11" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz", + "integrity": "sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.4.4", + "@babel/helper-split-export-declaration": "^7.4.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz", + "integrity": "sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", + "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz", + "integrity": "sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", + "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz", + "integrity": "sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz", + "integrity": "sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz", + "integrity": "sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", + "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", + "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.6" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", + "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-constant-elements": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.2.0.tgz", + "integrity": "sha512-YYQFg6giRFMsZPKUM9v+VcHOdfSQdz9jHCx3akAi3UYgyjndmdYGSXylQ/V+HswQt4fL8IklchD9HTsaOCrWQQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", + "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-inline-elements": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-inline-elements/-/plugin-transform-react-inline-elements-7.2.0.tgz", + "integrity": "sha512-OAflI+josEl8xoAzZYpFnN+C4e9wvxDecExTtvDsteAcChIZtsH/D2kMNcJnrrzbFzCroGajCTr9tAB7K0KsiQ==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.3.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", + "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.2.0.tgz", + "integrity": "sha512-A32OkKTp4i5U6aE88GwwcuV4HAprUgHcTq0sSafLxjr6AW0QahrCRCjxogkbbcdtpbXkuTOlgpjophCxb6sh5g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", + "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", + "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", + "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/polyfill": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz", + "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/preset-env": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.4.5.tgz", + "integrity": "sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.4.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.4.4", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.4.4", + "@babel/plugin-transform-classes": "^7.4.4", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/plugin-transform-duplicate-keys": "^7.2.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.4.4", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.4.4", + "@babel/plugin-transform-modules-systemjs": "^7.4.4", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.2.0", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/preset-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", + "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0" + } + }, + "@babel/register": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.4.4.tgz", + "integrity": "sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==", + "requires": { + "core-js": "^3.0.0", + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.11", + "mkdirp": "^0.5.1", + "pirates": "^4.0.0", + "source-map-support": "^0.5.9" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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" - } + "core-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.2.tgz", + "integrity": "sha512-3poRGjbu56leCtZCZCzCgQ7GcKOflDFnjWIepaPFUsM0IXUBrne10sl3aa2Bkcz3+FjRdIxBe9dAMhIJmEnQNA==" } } }, "@babel/runtime": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz", - "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", + "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", "requires": { - "regenerator-runtime": "^0.12.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" - } + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", + "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/types": "^7.4.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" } }, "@babel/types": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", - "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", + "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - } } }, "@emotion/babel-utils": { @@ -181,14 +1209,9 @@ "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==" }, "@types/node": { - "version": "10.12.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.27.tgz", - "integrity": "sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg==" - }, - "@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" + "version": "8.10.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.48.tgz", + "integrity": "sha512-c35YEBTkL4rzXY2ucpSKy+UYHjUBIIkuJbWYbsGIrKLEWU5dgJMmLkkIb3qeC3O3Tpb1ZQCwecscvJTDjDjkRw==" }, "abab": { "version": "1.0.4", @@ -201,12 +1224,12 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" } }, "acorn": { @@ -266,10 +1289,28 @@ "es6-promisify": "^5.0.0" } }, + "airbnb-prop-types": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz", + "integrity": "sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==", + "dev": true, + "requires": { + "array.prototype.find": "^2.0.4", + "function.prototype.name": "^1.1.0", + "has": "^1.0.3", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.8.6" + } + }, "ajv": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.2.tgz", - "integrity": "sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -336,9 +1377,12 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "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==", + "requires": { + "color-convert": "^1.9.0" + } }, "any-promise": { "version": "1.3.0", @@ -480,6 +1524,16 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" }, + "array.prototype.find": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz", + "integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.13.0" + } + }, "array.prototype.flat": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", @@ -521,10 +1575,11 @@ } }, "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "requires": { + "object-assign": "^4.1.1", "util": "0.10.3" }, "dependencies": { @@ -554,9 +1609,9 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "ast-types": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.12.2.tgz", - "integrity": "sha512-8c83xDLJM/dLDyXNLiR6afRRm4dPKN6KAnKqytRK3DBJul9lA+atxdQkNDkSVPdTqea5HiRq3lnnOIZ0MBpvdg==" + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.1.tgz", + "integrity": "sha512-b+EeK0WlzrSmpMw5jktWvQGxblpWnvMrV+vOp69RLjzGiHwWV0vgq75DPKtUjppKni3yWwSW8WLGV3Ch/XIWcQ==" }, "ast-types-flow": { "version": "0.0.7", @@ -579,9 +1634,9 @@ } }, "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, "async-foreach": { "version": "0.1.3", @@ -676,54 +1731,71 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, "requires": { "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true }, "babel-eslint": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", - "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-9.0.0.tgz", + "integrity": "sha512-itv1MwE3TMbY0QtNfeL7wzak1mV47Uy+n6HtSOO4Xd7rvmO+tsGQSgyOEEgo6Y2vHZKZphaoelNeSVj4vkLA1g==", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", - "babel-traverse": "^6.23.1", - "babel-types": "^6.23.0", - "babylon": "^6.17.0" + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" } }, "babel-generator": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, "requires": { "babel-messages": "^6.23.0", "babel-runtime": "^6.26.0", @@ -733,179 +1805,21 @@ "lodash": "^4.17.4", "source-map": "^0.5.7", "trim-right": "^1.0.1" - } - }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "dev": true, - "requires": { - "babel-helper-bindify-decorators": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-is-react-class": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-helper-is-react-class/-/babel-helper-is-react-class-1.0.0.tgz", - "integrity": "sha1-7282eLBcdtve7a3q16+YwnJNhDE=", - "dev": true - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } } }, "babel-helpers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, "requires": { "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" @@ -922,59 +1836,21 @@ } }, "babel-loader": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.5.tgz", - "integrity": "sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", + "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", "dev": true, "requires": { - "find-cache-dir": "^1.0.0", + "find-cache-dir": "^2.0.0", "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1" - }, - "dependencies": { - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" - } - }, - "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" - } - }, - "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" - } - } + "mkdirp": "^0.5.1", + "pify": "^4.0.1" } }, "babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", "dev": true, "requires": { "babel-runtime": "^6.22.0" @@ -1019,6 +1895,40 @@ "requires": { "locate-path": "^2.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" + } + }, + "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 } } }, @@ -1029,80 +1939,15 @@ "dev": true }, "babel-plugin-macros": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.5.0.tgz", - "integrity": "sha512-BWw0lD0kVZAXRD3Od1kMrdmfudqzDzYv2qrN3l2ISR1HVp1EgLKfbOrYV9xmY5k3qx3RIu5uPAUZZZHpo0o5Iw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.5.1.tgz", + "integrity": "sha512-xN3KhAxPzsJ6OQTktCanNpIFnnMsCV+t8OloKxIL72D6+SUZYFn9qfklPgef5HyyDtzYZqqb+fs1S12+gQY82Q==", "requires": { - "cosmiconfig": "^5.0.5", - "resolve": "^1.8.1" + "@babel/runtime": "^7.4.2", + "cosmiconfig": "^5.2.0", + "resolve": "^1.10.0" } }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-constructor-call": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", - "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-do-expressions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", - "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-export-extensions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", - "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", - "dev": true - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", - "dev": true - }, - "babel-plugin-syntax-function-bind": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", - "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", - "dev": true - }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -1114,595 +1959,12 @@ "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", "dev": true }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-generators": "^6.5.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-class-constructor-call": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", - "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", - "dev": true, - "requires": { - "babel-plugin-syntax-class-constructor-call": "^6.18.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "dev": true, - "requires": { - "babel-helper-explode-class": "^6.24.1", - "babel-plugin-syntax-decorators": "^6.13.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-do-expressions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", - "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", - "dev": true, - "requires": { - "babel-plugin-syntax-do-expressions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "dev": true, - "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "dev": true, - "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "dev": true, - "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-export-extensions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", - "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", - "dev": true, - "requires": { - "babel-plugin-syntax-export-extensions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "dev": true, - "requires": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-function-bind": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", - "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", - "dev": true, - "requires": { - "babel-plugin-syntax-function-bind": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.26.0" - } - }, - "babel-plugin-transform-react-constant-elements": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-constant-elements/-/babel-plugin-transform-react-constant-elements-6.23.0.tgz", - "integrity": "sha1-LxGb9NLN1F65uqrldAU8YE9hR90=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-inline-elements": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-inline-elements/-/babel-plugin-transform-react-inline-elements-6.22.0.tgz", - "integrity": "sha1-ZochGjK0mlLyLFc6K1UEol7xfFM=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "dev": true, - "requires": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-self": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-pure-class-to-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-pure-class-to-function/-/babel-plugin-transform-react-pure-class-to-function-1.0.1.tgz", - "integrity": "sha1-MqZJyX1lMlC0Gc/RSJMxsCkNnuQ=", - "dev": true, - "requires": { - "babel-helper-is-react-class": "^1.0.0" - } - }, "babel-plugin-transform-react-remove-prop-types": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.2.12.tgz", "integrity": "sha1-NAZpbfC4tFYIn51ybSfn4SPS+Sk=", "dev": true }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "dev": true, - "requires": { - "regenerator-transform": "^0.10.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-webpack-loaders": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-webpack-loaders/-/babel-plugin-webpack-loaders-0.9.0.tgz", - "integrity": "sha1-aG7ByrnbNIlY+ZCpX1cJD34Wt0M=", - "dev": true, - "requires": { - "babel-preset-es2015": "^6.3.13", - "babel-preset-stage-0": "^6.5.0", - "babel-register": "^6.4.3", - "babel-traverse": "^6.3.26", - "babel-types": "^6.3.24", - "babylon": "^6.3.26", - "colors": "^1.1.2", - "enhanced-resolve": "^2.2.2", - "lodash": "^4.6.1", - "rimraf": "^2.5.0" - }, - "dependencies": { - "enhanced-resolve": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz", - "integrity": "sha1-oRXDJQS2MC6Fp2Jp16V8zdli41k=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.3.0", - "object-assign": "^4.0.1", - "tapable": "^0.2.3" - } - }, - "memory-fs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz", - "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - } - } - }, - "babel-preset-env": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", - "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.23.0", - "babel-plugin-transform-es2015-classes": "^6.23.0", - "babel-plugin-transform-es2015-computed-properties": "^6.22.0", - "babel-plugin-transform-es2015-destructuring": "^6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", - "babel-plugin-transform-es2015-for-of": "^6.23.0", - "babel-plugin-transform-es2015-function-name": "^6.22.0", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", - "babel-plugin-transform-es2015-modules-umd": "^6.23.0", - "babel-plugin-transform-es2015-object-super": "^6.22.0", - "babel-plugin-transform-es2015-parameters": "^6.23.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", - "babel-plugin-transform-exponentiation-operator": "^6.22.0", - "babel-plugin-transform-regenerator": "^6.22.0", - "browserslist": "^3.2.6", - "invariant": "^2.2.2", - "semver": "^5.3.0" - } - }, - "babel-preset-es2015": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", - "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.24.1", - "babel-plugin-transform-es2015-classes": "^6.24.1", - "babel-plugin-transform-es2015-computed-properties": "^6.24.1", - "babel-plugin-transform-es2015-destructuring": "^6.22.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", - "babel-plugin-transform-es2015-for-of": "^6.22.0", - "babel-plugin-transform-es2015-function-name": "^6.24.1", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-umd": "^6.24.1", - "babel-plugin-transform-es2015-object-super": "^6.24.1", - "babel-plugin-transform-es2015-parameters": "^6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", - "babel-plugin-transform-regenerator": "^6.24.1" - } - }, - "babel-preset-flow": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", - "dev": true, - "requires": { - "babel-plugin-transform-flow-strip-types": "^6.22.0" - } - }, "babel-preset-jest": { "version": "23.2.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", @@ -1713,83 +1975,11 @@ "babel-plugin-syntax-object-rest-spread": "^6.13.0" } }, - "babel-preset-react": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", - "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-plugin-transform-react-jsx-self": "^6.22.0", - "babel-plugin-transform-react-jsx-source": "^6.22.0", - "babel-preset-flow": "^6.23.0" - } - }, - "babel-preset-react-optimize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-optimize/-/babel-preset-react-optimize-1.0.1.tgz", - "integrity": "sha1-wjUJ+6fLx2195wUOfSa80ivDBOg=", - "dev": true, - "requires": { - "babel-plugin-transform-react-constant-elements": "^6.5.0", - "babel-plugin-transform-react-inline-elements": "^6.6.5", - "babel-plugin-transform-react-pure-class-to-function": "^1.0.1", - "babel-plugin-transform-react-remove-prop-types": "^0.2.5" - } - }, - "babel-preset-stage-0": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", - "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", - "dev": true, - "requires": { - "babel-plugin-transform-do-expressions": "^6.22.0", - "babel-plugin-transform-function-bind": "^6.22.0", - "babel-preset-stage-1": "^6.24.1" - } - }, - "babel-preset-stage-1": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", - "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", - "dev": true, - "requires": { - "babel-plugin-transform-class-constructor-call": "^6.24.1", - "babel-plugin-transform-export-extensions": "^6.22.0", - "babel-preset-stage-2": "^6.24.1" - } - }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "dev": true, - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-preset-stage-3": "^6.24.1" - } - }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "dev": true, - "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-exponentiation-operator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0" - } - }, "babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, "requires": { "babel-core": "^6.26.0", "babel-runtime": "^6.26.0", @@ -1798,6 +1988,65 @@ "lodash": "^4.17.4", "mkdirp": "^0.5.1", "source-map-support": "^0.4.15" + }, + "dependencies": { + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + } + }, + "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" + } + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + } } }, "babel-runtime": { @@ -1807,12 +2056,20 @@ "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } } }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, "requires": { "babel-runtime": "^6.26.0", "babel-traverse": "^6.26.0", @@ -1825,6 +2082,7 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, "requires": { "babel-code-frame": "^6.26.0", "babel-messages": "^6.23.0", @@ -1835,23 +2093,56 @@ "globals": "^9.18.0", "invariant": "^2.2.2", "lodash": "^4.17.4" + }, + "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" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, "requires": { "babel-runtime": "^6.26.0", "esutils": "^2.0.2", "lodash": "^4.17.4", "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + } } }, "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true }, "balanced-match": { "version": "1.0.0", @@ -1928,6 +2219,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1955,9 +2251,9 @@ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "binary-extensions": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz", - "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, "bl": { "version": "1.2.2", @@ -1992,9 +2288,9 @@ } }, "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", + "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" }, "bn.js": { "version": "4.11.8", @@ -2002,20 +2298,35 @@ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, "boolbase": { @@ -2038,55 +2349,11 @@ "widest-line": "^2.0.0" }, "dependencies": { - "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" - } - }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } } } }, @@ -2203,13 +2470,14 @@ } }, "browserslist": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", - "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.0.tgz", + "integrity": "sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000844", - "electron-to-chromium": "^1.3.47" + "caniuse-lite": "^1.0.30000967", + "electron-to-chromium": "^1.3.133", + "node-releases": "^1.1.19" } }, "bser": { @@ -2227,9 +2495,9 @@ "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" }, "bson-objectid": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/bson-objectid/-/bson-objectid-1.2.4.tgz", - "integrity": "sha512-z511nO3HgvLpB0Ax2+kfM2HRfgGKB0VbMnRKh5yNM9Eoh2o1/DRFXBrsoTpvjR09vNIHUMUmcwFFYhSU+0PBGQ==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/bson-objectid/-/bson-objectid-1.2.5.tgz", + "integrity": "sha512-tKLZNr1eyK/Dd48k/5FHXmpN69dUHMioQCvNv1kxy8qJINkoyQkjf7+u933EZ77bMXC3lmN1HmyFm2WS6mRYxg==" }, "buffer": { "version": "5.2.1", @@ -2267,8 +2535,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-shims": { "version": "1.0.0", @@ -2307,9 +2574,9 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "cache-base": { "version": "1.0.1", @@ -2412,15 +2679,15 @@ } }, "caniuse-db": { - "version": "1.0.30000939", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000939.tgz", - "integrity": "sha512-nB5tLf3hOs+biXl1lhKjHRgNC0J1I7H52h/t1FP7qxARKKwpB0z+P/JewJLYAlxCBP/q7rxJzQzHHrQMl0viKg==", + "version": "1.0.30000971", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000971.tgz", + "integrity": "sha512-ubSZfYXO2KMYtCVmDez82mjodeZa+mBYWAnBMAmFBPAn4C2PY4SD0eC/diYQD4Rj1K+WNdp0vr0JDtm0SQ6GNg==", "dev": true }, "caniuse-lite": { - "version": "1.0.30000939", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000939.tgz", - "integrity": "sha512-oXB23ImDJOgQpGjRv1tCtzAvJr4/OvrHi5SO2vUgB0g0xpdZZoA/BxfImiWfdwoYdUTtQrPsXsvYU/dmCSM8gg==", + "version": "1.0.30000971", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000971.tgz", + "integrity": "sha512-TQFYFhRS0O5rdsmSbF1Wn+16latXYsQJat66f7S7lizXW1PVpWJeZw9wqqVLIjuxDRz7s7xRUj13QCfd8hKn6g==", "dev": true }, "capture-exit": { @@ -2453,15 +2720,13 @@ } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "chardet": { @@ -2512,9 +2777,9 @@ } }, "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2575,6 +2840,33 @@ "dev": true, "requires": { "chalk": "^1.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "class-utils": { @@ -2735,9 +3027,9 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codemirror": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.44.0.tgz", - "integrity": "sha512-3l42syTNakCdCQuYeZJXTyxina6Y9i4V0ighSJXNCQtRbaCN76smKKLu1ZHPHQon3rnzC7l4i/0r4gp809K1wg==" + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.47.0.tgz", + "integrity": "sha512-kV49Fr+NGFHFc/Imsx6g180hSlkGhuHxTSDDmDHOuyln0MQYFLixDY4+bFkBVeCEiepYfDimAF/e++9jPJk4QA==" }, "collection-visit": { "version": "1.0.0", @@ -2764,22 +3056,16 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true - }, - "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-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } }, "color-name": { "version": "1.1.3", @@ -2812,9 +3098,9 @@ "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==" }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -2830,9 +3116,9 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "compress-commons": { "version": "1.2.2", @@ -2883,6 +3169,23 @@ "unique-string": "^1.0.0", "write-file-atomic": "^2.0.0", "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "connect-mongo": { @@ -2909,9 +3212,9 @@ "dev": true }, "console-feed": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/console-feed/-/console-feed-2.8.5.tgz", - "integrity": "sha512-cUClwZUScfP/7OwOxu/7f+AuVr8mCNquG5s0UPiLq1VHI+TBoIeYOM7sjsHthJfmBNbOWf/B5NNOqP43OrHMBw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/console-feed/-/console-feed-2.8.8.tgz", + "integrity": "sha512-tqONbPYrolH9Gn8yMc+7sBZG9F9FkNJM68ziG5dInznBOHbHNua+Mq0S70EgvkhEpE9AifvV9d7EX2Jm5yDw2w==", "requires": { "emotion": "^9.1.1", "emotion-theming": "^9.0.0", @@ -2940,9 +3243,12 @@ "dev": true }, "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } }, "content-type": { "version": "1.0.4", @@ -2987,9 +3293,34 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.8.tgz", + "integrity": "sha512-RWlREFU74TEkdXzyl1bka66O3kYp8jeTXrvJZDzVVMH8AiHUSOFpL1yfhQJ+wHocAm1m+4971W1PPzfLuCv1vg==" + }, + "core-js-compat": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.2.tgz", + "integrity": "sha512-X0Ch5f6itrHxhg5HSJucX6nNLNAGr+jq+biBh6nPGc3YAWz2a8p/ZIZY8cUkDzSRNG54omAuu3hoEF8qZbu/6Q==", + "dev": true, + "requires": { + "browserslist": "^4.6.0", + "core-js-pure": "3.1.2", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.0.tgz", + "integrity": "sha512-kCqEOOHoBcFs/2Ccuk4Xarm/KiWRSLEX9CAZF8xkJ6ZPlIoTZ8V5f7J16vYLJqDbR7KrxTJpR2lqjIEm2Qx9cQ==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.2.tgz", + "integrity": "sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w==", + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3006,14 +3337,13 @@ } }, "cosmiconfig": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", - "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", "requires": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "lodash.get": "^4.4.2", + "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, @@ -3180,12 +3510,6 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true } } }, @@ -3220,6 +3544,12 @@ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", "dev": true }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, "loader-utils": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", @@ -3344,9 +3674,9 @@ } }, "csstype": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.2.tgz", - "integrity": "sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow==" + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz", + "integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==" }, "currently-unhandled": { "version": "0.4.1", @@ -3365,9 +3695,9 @@ } }, "damerau-levenshtein": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", - "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", + "integrity": "sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA==", "dev": true }, "dashdash": { @@ -3379,18 +3709,11 @@ } }, "data-uri-to-buffer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.0.tgz", - "integrity": "sha512-YbKCNLPPP4inc0E5If4OaalBc7gpaM2MRv77Pv2VThVComLKfbGYtJcdDCViDyp1Wd4SebhHLz94vp91zbK6bw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.1.tgz", + "integrity": "sha512-OkVVLrerfAKZlW2ZZ3Ve2y65jgiWqBKsTfUIAFbn8nVbPcCZg6l6gikKlEYv0kXcmzqGm6mFq/Jf2vriuEkv8A==", "requires": { "@types/node": "^8.0.7" - }, - "dependencies": { - "@types/node": { - "version": "8.10.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.40.tgz", - "integrity": "sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==" - } } }, "data-urls": { @@ -3433,13 +3756,12 @@ } }, "datauri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/datauri/-/datauri-1.1.0.tgz", - "integrity": "sha512-0q+cTTKx7q8eDteZRIQLTFJuiIsVing17UbWTPssY4JLSMaYsk/VKpNulBDo9NSgQWcvlPrkEHW8kUO67T/7mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/datauri/-/datauri-2.0.0.tgz", + "integrity": "sha512-zS2HSf9pI5XPlNZgIqJg/wCJpecgU/HA6E/uv2EfaWnW1EiTGLfy/EexTIsC9c99yoCOTXlqeeWk4FkCSuO3/g==", "requires": { - "image-size": "^0.6.2", - "mimer": "^0.3.2", - "semver": "^5.5.0" + "image-size": "^0.7.3", + "mimer": "^1.0.0" } }, "date-fns": { @@ -3453,11 +3775,11 @@ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "decamelize": { @@ -3630,6 +3952,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, "requires": { "repeating": "^2.0.0" } @@ -3763,22 +4086,20 @@ } }, "editorconfig": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", - "integrity": "sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", "requires": { - "@types/node": "^10.11.7", - "@types/semver": "^5.5.0", "commander": "^2.19.0", - "lru-cache": "^4.1.3", + "lru-cache": "^4.1.5", "semver": "^5.6.0", "sigmund": "^1.0.1" }, "dependencies": { "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" } } }, @@ -3788,9 +4109,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.113", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz", - "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==", + "version": "1.3.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.137.tgz", + "integrity": "sha512-kGi32g42a8vS/WnYE7ELJyejRT7hbr3UeOOu0WeuYuQ29gCpg9Lrf6RdcTQVXSt/v0bjCfnlb/EWOOsiKpTmkw==", "dev": true }, "elliptic": { @@ -3902,13 +4223,13 @@ }, "dependencies": { "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", "dev": true, "requires": { "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", + "dom-serializer": "~0.1.1", "entities": "~1.1.1", "htmlparser2": "^3.9.1", "lodash": "^4.15.0", @@ -3945,9 +4266,9 @@ } }, "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -3958,30 +4279,32 @@ } }, "enzyme-adapter-react-16": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.1.tgz", - "integrity": "sha512-Egzogv1y77DUxdnq/CyHxLHaNxmSSKDDSDNNB/EiAXCZVFXdFibaNy2uUuRQ1n24T2m6KH/1Rw16XDRq+1yVEg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.13.1.tgz", + "integrity": "sha512-DCKbkiVlfLTbn4SXO8mXDQx1SmmwON5oKXn2QfQSMCt8eTYGwUXy/OBGSuss6KKwY5w5QfK1sQFxhgFOkMCjrw==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.10.0", - "function.prototype.name": "^1.1.0", + "enzyme-adapter-utils": "^1.12.0", + "has": "^1.0.3", "object.assign": "^4.1.0", "object.values": "^1.1.0", - "prop-types": "^15.6.2", - "react-is": "^16.7.0", - "react-test-renderer": "^16.0.0-0" + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.6.0" } }, "enzyme-adapter-utils": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz", - "integrity": "sha512-VnIXJDYVTzKGbdW+lgK8MQmYHJquTQZiGzu/AseCZ7eHtOMAj4Rtvk8ZRopodkfPves0EXaHkXBDkVhPa3t0jA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz", + "integrity": "sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==", "dev": true, "requires": { + "airbnb-prop-types": "^2.13.2", "function.prototype.name": "^1.1.0", "object.assign": "^4.1.0", "object.fromentries": "^2.0.0", - "prop-types": "^15.6.2", + "prop-types": "^15.7.2", "semver": "^5.6.0" } }, @@ -4025,13 +4348,13 @@ } }, "es5-ext": { - "version": "0.10.48", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.48.tgz", - "integrity": "sha512-CdRvPlX/24Mj5L4NVxTs4804sxiS2CjVprgCmrgoDkdmjdY4D+ySHa7K3jJf8R40dFg0tIm3z/dk326LrnuSGw==", + "version": "0.10.50", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", + "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", "requires": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.1", - "next-tick": "1" + "next-tick": "^1.0.0" } }, "es6-iterator": { @@ -4219,35 +4542,6 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "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" - } - }, - "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" - } - }, - "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" - } - }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -4274,30 +4568,12 @@ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, - "globals": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", - "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -4306,15 +4582,6 @@ "requires": { "ansi-regex": "^3.0.0" } - }, - "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" - } } } }, @@ -4344,12 +4611,29 @@ "requires": { "debug": "^2.6.9", "resolve": "^1.5.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-loader": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-1.9.0.tgz", - "integrity": "sha512-40aN976qSNPyb9ejTqjEthZITpls1SVKtwguahmH1dzGCwQU/vySE+xX33VZmD8csU0ahVNCtFlsPgKqRBiqgg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.1.2.tgz", + "integrity": "sha512-rA9XiXEOilLYPOIInvVH5S/hYfyTPyxag6DZhoQOduM+3TkghAEQ3VcFO8VnX4J4qg/UIBzp72aOf/xvYmpmsg==", "requires": { "loader-fs-cache": "^1.0.0", "loader-utils": "^1.0.2", @@ -4359,15 +4643,24 @@ } }, "eslint-module-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz", - "integrity": "sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", + "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", "dev": true, "requires": { "debug": "^2.6.8", "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" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -4377,6 +4670,46 @@ "locate-path": "^2.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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "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 + }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -4389,23 +4722,33 @@ } }, "eslint-plugin-import": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz", - "integrity": "sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==", + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz", + "integrity": "sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g==", "dev": true, "requires": { + "array-includes": "^3.0.3", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.3.0", + "eslint-module-utils": "^2.4.0", "has": "^1.0.3", "lodash": "^4.17.11", "minimatch": "^3.0.4", "read-pkg-up": "^2.0.0", - "resolve": "^1.9.0" + "resolve": "^1.10.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", @@ -4415,6 +4758,12 @@ "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 } } }, @@ -4435,18 +4784,18 @@ } }, "eslint-plugin-react": { - "version": "7.12.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz", - "integrity": "sha512-1puHJkXJY+oS1t467MjbqjvX53uQ05HXwjqDgdbGBqf5j9eeydI54G3KwiJmWciQ0HTBacIKw2jgwSBSH3yfgQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.13.0.tgz", + "integrity": "sha512-uA5LrHylu8lW/eAH3bEQe9YdzpPaFd9yAJTwTi/i/BKTD7j6aQMKVAdGM/ML72zD6womuSK7EiGtMKuK06lWjQ==", "dev": true, "requires": { "array-includes": "^3.0.3", "doctrine": "^2.1.0", "has": "^1.0.3", - "jsx-ast-utils": "^2.0.1", + "jsx-ast-utils": "^2.1.0", "object.fromentries": "^2.0.0", - "prop-types": "^15.6.2", - "resolve": "^1.9.0" + "prop-types": "^15.7.2", + "resolve": "^1.10.1" } }, "eslint-restricted-globals": { @@ -4456,9 +4805,9 @@ "dev": true }, "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -4622,100 +4971,105 @@ "jest-matcher-utils": "^23.6.0", "jest-message-util": "^23.4.0", "jest-regex-util": "^23.3.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - } } }, "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.0.tgz", + "integrity": "sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==", "requires": { - "accepts": "~1.3.5", + "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", "content-type": "~1.0.4", - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.1.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, "express-basic-auth": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.1.6.tgz", - "integrity": "sha512-fRh/UU2q/YhvY0/Pkzi3VcLyjIExveW2NOOnOGgO6yO0jKXt6zcKPVPWSrL8nlhlh+YEH5LOjz+CGFML5dJQNw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.0.tgz", + "integrity": "sha512-iJ0h1Gk6fZRrFmO7tP9nIbxwNgCUJASfNj5fb0Hy15lGtbqqsxpt7609+wq+0XlByZjXmC/rslWQtnuSTVRIcg==", "requires": { "basic-auth": "^2.0.1" } }, "express-session": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", - "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz", + "integrity": "sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==", "requires": { "cookie": "0.3.1", "cookie-signature": "1.0.6", - "crc": "3.4.4", "debug": "2.6.9", - "depd": "~1.1.1", - "on-headers": "~1.0.1", + "depd": "~2.0.0", + "on-headers": "~1.0.2", "parseurl": "~1.3.2", - "uid-safe": "~2.1.5", - "utils-merge": "1.0.1" + "safe-buffer": "5.1.2", + "uid-safe": "~2.1.5" }, "dependencies": { - "crc": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", - "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -4913,34 +5267,42 @@ } }, "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" }, "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, "find-cache-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", - "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "requires": { "commondir": "^1.0.1", - "mkdirp": "^0.5.1", - "pkg-dir": "^1.0.0" + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" } }, "find-root": { @@ -4949,12 +5311,11 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "locate-path": "^3.0.0" } }, "findit2": { @@ -4987,6 +5348,21 @@ "requires": { "debug": "^2.2.0", "stream-consume": "^0.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, "for-each": { @@ -5044,9 +5420,9 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "friendly-words": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/friendly-words/-/friendly-words-1.1.3.tgz", - "integrity": "sha512-o4wUonKIZlPiVL3Q/qah1SBKrTlGAk9t8Tv1VUk24plhPib0I5utFszQlzHIuFpOIhYX0+GQ0VNQ7KSmr4Y3CQ==" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/friendly-words/-/friendly-words-1.1.6.tgz", + "integrity": "sha512-DViztJlOCvB4PnpZkp8fkAiXDh/C9koJ3DRBHy8m+f8QrhodFCtptzStxIapHCBVGuk5WizXJQ3tWE7kiP69mQ==" }, "fs-constants": { "version": "1.0.0", @@ -5070,13 +5446,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" }, "dependencies": { "abbrev": { @@ -5137,11 +5513,11 @@ "optional": true }, "debug": { - "version": "2.6.9", + "version": "4.1.1", "bundled": true, "optional": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "deep-extend": { @@ -5286,22 +5662,22 @@ } }, "ms": { - "version": "2.0.0", + "version": "2.1.1", "bundled": true, "optional": true }, "needle": { - "version": "2.2.4", + "version": "2.3.0", "bundled": true, "optional": true, "requires": { - "debug": "^2.1.2", + "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.10.3", + "version": "0.12.0", "bundled": true, "optional": true, "requires": { @@ -5327,12 +5703,12 @@ } }, "npm-bundled": { - "version": "1.0.5", + "version": "1.0.6", "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.2.0", + "version": "1.4.1", "bundled": true, "optional": true, "requires": { @@ -5451,7 +5827,7 @@ "optional": true }, "semver": { - "version": "5.6.0", + "version": "5.7.0", "bundled": true, "optional": true }, @@ -5532,9 +5908,9 @@ } }, "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -5664,23 +6040,10 @@ "readable-stream": "3" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5703,9 +6066,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5751,9 +6114,9 @@ } }, "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "globule": { "version": "1.2.1", @@ -5767,9 +6130,9 @@ } }, "gonzales-pe": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.3.tgz", - "integrity": "sha512-Kjhohco0esHQnOiqqdJeNz/5fyPkOMD/d6XVjwTAoPGUFh0mCollPUTUTa2OZy4dYNAqlPIQdTiNzJTWdd9Htw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz", + "integrity": "sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ==", "requires": { "minimist": "1.1.x" }, @@ -5820,12 +6183,12 @@ "dev": true }, "handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "dev": true, "requires": { - "async": "^2.5.0", + "neo-async": "^2.6.0", "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" @@ -5870,9 +6233,9 @@ } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -5992,6 +6355,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.1" @@ -6014,9 +6378,9 @@ "dev": true }, "html-element-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.0.0.tgz", - "integrity": "sha512-/SP6aOiM5Ai9zALvCxDubIeez0LvG3qP7R9GcRDnJEP/HBmv0A8A9K0o8+HFudcFt46+i921ANjzKsjPjb7Enw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.0.1.tgz", + "integrity": "sha512-BZSfdEm6n706/lBfXKWa4frZRZcT5k1cOusw95ijZsHlI+GdgY0v95h6IzO3iIDf2ROwq570YTwqNPqHcNMozw==", "dev": true, "requires": { "array-filter": "^1.0.0" @@ -6085,6 +6449,19 @@ "parserlib": "~1.1.1" } }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "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" + } + }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -6129,14 +6506,15 @@ } }, "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, "http-proxy-agent": { @@ -6155,6 +6533,11 @@ "requires": { "ms": "2.0.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -6220,18 +6603,13 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -6243,9 +6621,9 @@ "dev": true }, "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "3.3.10", @@ -6260,9 +6638,9 @@ "dev": true }, "image-size": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", - "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==" + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.4.tgz", + "integrity": "sha512-GqPgxs+VkOr12aWwjSkyRzf5atzObWpFtiRuDgxCl2I/SDpZOKZFRD3iIAeAN6/usmn8SeLWRt7a8JRYK0Whbw==" }, "immediate": { "version": "3.0.6", @@ -6315,6 +6693,40 @@ "locate-path": "^2.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" + } + }, + "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 + }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -6410,41 +6822,6 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -6453,15 +6830,6 @@ "requires": { "ansi-regex": "^3.0.0" } - }, - "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" - } } } }, @@ -6489,9 +6857,9 @@ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, "is-absolute-url": { "version": "2.1.0", @@ -6612,6 +6980,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6948,12 +7317,6 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true } } }, @@ -6982,32 +7345,12 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "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" - } - }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "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" - } - }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -7019,15 +7362,6 @@ "wrap-ansi": "^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" - } - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -7037,12 +7371,6 @@ "locate-path": "^2.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "jest-cli": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", @@ -7087,6 +7415,40 @@ "yargs": "^11.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" + } + }, + "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 + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -7096,15 +7458,6 @@ "ansi-regex": "^3.0.0" } }, - "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" - } - }, "yargs": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", @@ -7167,49 +7520,53 @@ "pretty-format": "^23.6.0" }, "dependencies": { - "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==", + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ms": "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "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" - } + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -7223,52 +7580,6 @@ "diff": "^3.2.0", "jest-get-type": "^22.1.0", "pretty-format": "^23.6.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-docblock": { @@ -7288,52 +7599,6 @@ "requires": { "chalk": "^2.0.1", "pretty-format": "^23.6.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-environment-jsdom": { @@ -7360,9 +7625,9 @@ "dev": true }, "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", + "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", "dev": true, "requires": { "acorn": "^6.0.1", @@ -7370,17 +7635,17 @@ }, "dependencies": { "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", "dev": true } } }, "cssstyle": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", - "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", + "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", "dev": true, "requires": { "cssom": "0.3.x" @@ -7504,52 +7769,6 @@ "jest-snapshot": "^23.6.0", "jest-util": "^23.4.0", "pretty-format": "^23.6.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-leak-detector": { @@ -7570,52 +7789,6 @@ "chalk": "^2.0.1", "jest-get-type": "^22.1.0", "pretty-format": "^23.6.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-message-util": { @@ -7629,52 +7802,6 @@ "micromatch": "^2.3.11", "slash": "^1.0.0", "stack-utils": "^1.0.1" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-mock": { @@ -7698,52 +7825,6 @@ "browser-resolve": "^1.11.3", "chalk": "^2.0.1", "realpath-native": "^1.0.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-resolve-dependencies": { @@ -7775,24 +7856,6 @@ "jest-worker": "^23.2.0", "source-map-support": "^0.5.6", "throat": "^4.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } } }, "jest-runtime": { @@ -7830,13 +7893,31 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "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==", + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" } }, "camelcase": { @@ -7845,17 +7926,6 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "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" - } - }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -7867,13 +7937,13 @@ "wrap-ansi": "^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==", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "color-name": "1.1.3" + "ms": "2.0.0" } }, "find-up": { @@ -7885,10 +7955,50 @@ "locate-path": "^2.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "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 }, "strip-ansi": { @@ -7900,15 +8010,6 @@ "ansi-regex": "^3.0.0" } }, - "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" - } - }, "yargs": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", @@ -7962,52 +8063,6 @@ "natural-compare": "^1.4.0", "pretty-format": "^23.6.0", "semver": "^5.5.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-util": { @@ -8026,55 +8081,11 @@ "source-map": "^0.6.0" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -8088,52 +8099,6 @@ "jest-get-type": "^22.1.0", "leven": "^2.1.0", "pretty-format": "^23.6.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-watcher": { @@ -8145,52 +8110,6 @@ "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", "string-length": "^2.0.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "jest-worker": { @@ -8203,9 +8122,9 @@ } }, "jquery": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" }, "js-base64": { "version": "2.5.1", @@ -8214,14 +8133,14 @@ "dev": true }, "js-beautify": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.8.9.tgz", - "integrity": "sha512-MwPmLywK9RSX0SPsUJjN7i+RQY9w/yC17Lbrq9ViEefpLRgqAR2BgrMN2AbifkUuhDV8tRauLhLda/9+bE0YQA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.0.tgz", + "integrity": "sha512-OMwf/tPDpE/BLlYKqZOhqWsd3/z2N3KOlyn1wsCRGFwViE8LOQTcDtathQvHvZc+q+zWmcNAbwKSC+iJoMaH2Q==", "requires": { "config-chain": "^1.1.12", - "editorconfig": "^0.15.2", + "editorconfig": "^0.15.3", "glob": "^7.1.3", - "mkdirp": "~0.5.0", + "mkdirp": "~0.5.1", "nopt": "~4.0.1" }, "dependencies": { @@ -8236,15 +8155,21 @@ } } }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + "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.12.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -8282,20 +8207,20 @@ } }, "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "jshint": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.1.tgz", - "integrity": "sha512-9GpPfKeffYBl7oBDX2lHPG16j0AM7D2bn3aLy9DaWTr6CWa0i/7UGhX8WLZ7V14QQnnr4hXbjauTLYg06F+HYw==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.2.tgz", + "integrity": "sha512-e7KZgCSXMJxznE/4WULzybCMNXNAd/bf5TSrvVEq78Q/K8ZwFpmBqQeDtNiHc3l49nV4E/+YeHU/JZjSUIrLAA==", "requires": { "cli": "~1.0.0", "console-browserify": "1.1.x", "exit": "0.1.x", "htmlparser2": "3.8.x", - "lodash": "~4.17.10", + "lodash": "~4.17.11", "minimatch": "~3.0.2", "shelljs": "0.3.x", "strip-json-comments": "1.0.x" @@ -8340,9 +8265,12 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "^1.2.0" + } }, "jsonfile": { "version": "4.0.0", @@ -8365,9 +8293,9 @@ } }, "jsx-ast-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", - "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", + "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", "dev": true, "requires": { "array-includes": "^3.0.3" @@ -8532,16 +8460,58 @@ "requires": { "error-ex": "^1.2.0" } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } }, "loader-fs-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz", - "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz", + "integrity": "sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw==", "requires": { "find-cache-dir": "^0.1.1", "mkdirp": "0.5.1" + }, + "dependencies": { + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "requires": { + "find-up": "^1.0.0" + } + } } }, "loader-runner": { @@ -8566,28 +8536,16 @@ "requires": { "minimist": "^1.2.0" } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { - "p-locate": "^2.0.0", + "p-locate": "^3.0.0", "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } } }, "lodash": { @@ -8627,12 +8585,6 @@ "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", "dev": true }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - }, "lodash.assignin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", @@ -8652,12 +8604,6 @@ "lodash._createcompounder": "^3.0.0" } }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", @@ -8744,12 +8690,6 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" }, - "lodash.mergewith": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", - "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", - "dev": true - }, "lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", @@ -8837,47 +8777,6 @@ "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "requires": { "chalk": "^2.0.1" - }, - "dependencies": { - "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==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.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==", - "requires": { - "color-name": "1.1.3" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "loglevelnext": { @@ -8967,24 +8866,21 @@ "requires": { "ms": "2.0.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } + "pify": "^4.0.1", + "semver": "^5.6.0" } }, "makeerror": { @@ -9081,6 +8977,16 @@ "trim-newlines": "^1.0.0" }, "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -9094,12 +9000,6 @@ "strip-bom": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -9109,6 +9009,15 @@ "error-ex": "^1.2.0" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -9120,6 +9029,12 @@ "pinkie-promise": "^2.0.0" } }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -9207,27 +9122,27 @@ } }, "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.38.0" + "mime-db": "1.40.0" } }, "mimer": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/mimer/-/mimer-0.3.2.tgz", - "integrity": "sha512-N6NcgDQAevhP/02DQ/epK6daLy4NKrIHyTlJcO6qBiYn98q+Y4a/knNsAATCe1xLS2F0nEmJp+QYli2s8vKwyQ==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimer/-/mimer-1.0.0.tgz", + "integrity": "sha512-4ZJvCzfcwsBgPbkKXUzGoVZMWjv8IDIygkGzVc7uUYhgnK0t2LmGxxjdgH1i+pn0/KQfB5F/VKUJlfyTSOFQjg==" }, "mimic-fn": { "version": "1.2.0", @@ -9261,9 +9176,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { "version": "1.3.1", @@ -9495,11 +9410,24 @@ "warning": "^3.0.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "hoist-non-react-statics": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -9962,6 +9890,13 @@ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } } }, "mongodb": { @@ -10059,6 +9994,11 @@ "require_optional": "~1.0.0" } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -10120,6 +10060,19 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, "sliced": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", @@ -10128,9 +10081,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "muri": { "version": "1.3.0", @@ -10154,14 +10107,14 @@ } }, "nan": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanoid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz", - "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.3.tgz", + "integrity": "sha512-NbaoqdhIYmY6FXDRB4eYtDVC9Z9eCbn8TyaiC16LNKtpPv/aqa0tOPD8y6gNE4yUNnaZ7LLhYtXOev/6+cBtfw==" }, "nanomatch": { "version": "1.2.13", @@ -10231,22 +10184,22 @@ }, "dependencies": { "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "dev": true } } }, "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, "netmask": { "version": "1.0.6", @@ -10375,6 +10328,11 @@ } } }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" + }, "node-notifier": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", @@ -10388,10 +10346,19 @@ "which": "^1.3.0" } }, + "node-releases": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.21.tgz", + "integrity": "sha512-TwnURTCjc8a+ElJUjmDqU6+12jhli1Q61xOQmdZ7ECZVBZuQpN/1UnembiIHDM1wCcfLvh5wrWXUF5H6ufX64Q==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, "node-sass": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", - "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -10401,12 +10368,10 @@ "get-stdin": "^4.0.1", "glob": "^7.0.3", "in-publish": "^2.0.0", - "lodash.assign": "^4.2.0", - "lodash.clonedeep": "^4.3.2", - "lodash.mergewith": "^4.6.0", + "lodash": "^4.17.11", "meow": "^3.7.0", "mkdirp": "^0.5.1", - "nan": "^2.10.0", + "nan": "^2.13.2", "node-gyp": "^3.8.0", "npmlog": "^4.0.0", "request": "^2.88.0", @@ -10415,6 +10380,25 @@ "true-case-path": "^1.0.2" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -10424,6 +10408,12 @@ "lru-cache": "^4.0.1", "which": "^1.2.9" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true } } }, @@ -10507,12 +10497,12 @@ "integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U=" }, "nodemon": { - "version": "1.18.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.10.tgz", - "integrity": "sha512-we51yBb1TfEvZamFchRgcfLbVYgg0xlGbyXmOtbBzDwxwgewYS/YbZ5tnlnsH51+AoSTTsT3A2E/FloUbtH8cQ==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.0.tgz", + "integrity": "sha512-NHKpb/Je0Urmwi3QPDHlYuFY9m1vaVfTsRZG5X73rY46xPj0JpNe8WhUGQdkDXQDOxrBNIU3JrcflE9Y44EcuA==", "dev": true, "requires": { - "chokidar": "^2.1.0", + "chokidar": "^2.1.5", "debug": "^3.1.0", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", @@ -10587,9 +10577,9 @@ } }, "chokidar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", - "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -10603,7 +10593,7 @@ "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", - "upath": "^1.1.0" + "upath": "^1.1.1" } }, "debug": { @@ -10804,12 +10794,6 @@ } } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -10846,9 +10830,9 @@ "dev": true }, "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "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" @@ -10907,21 +10891,18 @@ "to-regex": "^3.0.2" } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "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" - } - }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -11028,9 +11009,9 @@ "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==" }, "nwsapi": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.1.tgz", - "integrity": "sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", "dev": true }, "oauth": { @@ -11086,9 +11067,9 @@ "dev": true }, "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -11232,6 +11213,12 @@ "wordwrap": "~0.0.2" }, "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -11293,25 +11280,25 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.0.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=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pac-proxy-agent": { "version": "3.0.0", @@ -11335,11 +11322,6 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, @@ -11368,9 +11350,9 @@ } }, "pako": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "param-case": { "version": "2.1.1", @@ -11399,6 +11381,13 @@ "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", "requires": { "color-convert": "~0.5.0" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } } }, "parse-glob": { @@ -11446,9 +11435,9 @@ "integrity": "sha1-hZB92GBaoGq7PdKV1QuyuPpN0Rc=" }, "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "pascalcase": { "version": "0.1.1", @@ -11497,10 +11486,11 @@ } }, "passport-oauth2": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", - "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", "requires": { + "base64url": "3.x.x", "oauth": "0.9.x", "passport-strategy": "1.x.x", "uid2": "0.0.x", @@ -11523,12 +11513,9 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" }, "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", @@ -11577,6 +11564,13 @@ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", "requires": { "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } } }, "pause": { @@ -11607,9 +11601,9 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, "pinkie": { "version": "2.0.4", @@ -11624,6 +11618,14 @@ "pinkie": "^2.0.0" } }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, "pixrem": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pixrem/-/pixrem-3.0.2.tgz", @@ -11648,11 +11650,11 @@ } }, "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "requires": { - "find-up": "^1.0.0" + "find-up": "^3.0.0" } }, "pleeease-filters": { @@ -11694,6 +11696,33 @@ "supports-color": "^3.2.3" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "has-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", @@ -11794,6 +11823,12 @@ "color-convert": "^0.5.3", "color-string": "^0.3.0" } + }, + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", + "dev": true } } }, @@ -11909,6 +11944,33 @@ "postcss-replace-overflow-wrap": "^1.0.0", "postcss-selector-matches": "^2.0.0", "postcss-selector-not": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "postcss-custom-media": { @@ -12051,41 +12113,6 @@ "postcss-media-query-parser": "^0.2.3" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "postcss": { "version": "6.0.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", @@ -12102,15 +12129,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -12140,6 +12158,12 @@ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", "dev": true }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, "loader-utils": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", @@ -12274,41 +12298,6 @@ "postcss": "^6.0.1" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "postcss": { "version": "6.0.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", @@ -12325,15 +12314,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -12347,35 +12327,6 @@ "postcss": "^6.0.1" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, "css-selector-tokenizer": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", @@ -12387,10 +12338,10 @@ "regexpu-core": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true }, "postcss": { @@ -12415,20 +12366,26 @@ "regjsparser": "^0.1.4" } }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -12442,35 +12399,6 @@ "postcss": "^6.0.1" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, "css-selector-tokenizer": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", @@ -12482,10 +12410,10 @@ "regexpu-core": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true }, "postcss": { @@ -12510,20 +12438,26 @@ "regjsparser": "^0.1.4" } }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -12537,41 +12471,6 @@ "postcss": "^6.0.1" }, "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "postcss": { "version": "6.0.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", @@ -12588,15 +12487,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "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" - } } } }, @@ -12723,6 +12613,25 @@ "postcss": "^5.0.0" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, "log-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", @@ -12731,6 +12640,12 @@ "requires": { "chalk": "^1.0.0" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true } } }, @@ -12860,24 +12775,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "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" - } - }, - "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" - } } } }, @@ -12897,7 +12794,8 @@ "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true }, "process": { "version": "0.5.2", @@ -12951,18 +12849,29 @@ "react-is": "^16.8.1" } }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" }, "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "ipaddr.js": "1.9.0" } }, "proxy-agent": { @@ -12987,11 +12896,6 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, @@ -13050,9 +12954,9 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "query-ast": { "version": "1.0.2", @@ -13162,18 +13066,18 @@ } }, "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, @@ -13194,24 +13098,18 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true } } }, "react": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz", - "integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==", + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.3" + "scheduler": "^0.13.6" } }, "react-base16-styling": { @@ -13235,14 +13133,14 @@ } }, "react-dom": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz", - "integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==", + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", + "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.3" + "scheduler": "^0.13.6" } }, "react-emotion": { @@ -13254,33 +13152,46 @@ "create-emotion-styled": "^9.2.8" } }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-helmet": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.0.tgz", - "integrity": "sha1-qBgR3yExOm1VxfBYxK66XW89l6c=", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz", + "integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==", "requires": { - "deep-equal": "^1.0.1", "object-assign": "^4.1.1", "prop-types": "^15.5.4", + "react-fast-compare": "^2.0.2", "react-side-effect": "^1.1.0" } }, "react-hot-loader": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.7.1.tgz", - "integrity": "sha512-OVq9tBndJ+KJWyWbj6kAkJbRVFx3Ykx+XOlndT3zyxAQMBFFygV+AS9RQi6Z2axkPIcEkuE5K6nkpcjlzR8I9w==", + "version": "4.8.8", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.8.8.tgz", + "integrity": "sha512-58bgeS7So8V93MhhnKogbraor8xdrTncil+b6IoIXkTIr3blJNAE7bU4tn/iJvy2J7rjxQmKFRaxKrWdKUZpqg==", "requires": { "fast-levenshtein": "^2.0.6", "global": "^4.3.0", - "hoist-non-react-statics": "^2.5.0", + "hoist-non-react-statics": "^3.3.0", "loader-utils": "^1.1.0", - "lodash.merge": "^4.6.1", + "lodash": "^4.17.11", "prop-types": "^15.6.1", "react-lifecycles-compat": "^3.0.4", "shallowequal": "^1.0.2", "source-map": "^0.7.3" }, "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "requires": { + "react-is": "^16.7.0" + } + }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", @@ -13308,9 +13219,9 @@ } }, "react-is": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", - "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "react-json-tree": { "version": "0.11.2", @@ -13388,13 +13299,11 @@ } }, "react-split-pane": { - "version": "0.1.85", - "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.85.tgz", - "integrity": "sha512-3GhaYs6+eVNrewgN4eQKJoNMQ4pcegNMTMhR5bO/NFO91K6/98qdD1sCuWPpsefCjzxNTjkvVYWQC0bMaC45mA==", + "version": "0.1.87", + "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.87.tgz", + "integrity": "sha512-F22jqWyKB1WximT0U5HKdSuB9tmJGjjP+WUyveHxJJys3ANsljj163kCdsI6M3gdfyCVC+B2rq8sc5m2Ko02RA==", "requires": { "prop-types": "^15.5.10", - "react": "^16.6.3", - "react-dom": "^16.6.3", "react-lifecycles-compat": "^3.0.4", "react-style-proptype": "^3.0.0" } @@ -13417,15 +13326,15 @@ } }, "react-test-renderer": { - "version": "16.8.3", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.3.tgz", - "integrity": "sha512-rjJGYebduKNZH0k1bUivVrRLX04JfIQ0FKJLPK10TAb06XWhfi4gTobooF9K/DEFNW98iGac3OSxkfIJUN9Mdg==", + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", + "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", "dev": true, "requires": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "react-is": "^16.8.3", - "scheduler": "^0.13.3" + "react-is": "^16.8.6", + "scheduler": "^0.13.6" } }, "read-pkg": { @@ -13454,6 +13363,36 @@ "requires": { "locate-path": "^2.0.0" } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "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=", + "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=" } } }, @@ -13518,6 +13457,14 @@ } } }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -13728,6 +13675,11 @@ "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -13856,23 +13808,16 @@ } }, "redux-form": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-5.3.6.tgz", - "integrity": "sha1-93qB2/ONRNJupBEQCiPxninNGUY=", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-5.4.0.tgz", + "integrity": "sha512-Ssftdf3Or6nwEBl1SqgPbZdxhmZC8AejHTTLnt1R6SfCqyoHgPnPyxu8NuewS2KvDN925nwRT9dGxeIYDgtAWQ==", "requires": { "deep-equal": "^1.0.1", - "hoist-non-react-statics": "^1.0.5", + "hoist-non-react-statics": "^2.3.1", "invariant": "^2.0.0", "is-promise": "^2.1.0", "prop-types": "^15.5.8", "react-lazy-cache": "^3.0.1" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - } } }, "redux-thunk": { @@ -13880,25 +13825,38 @@ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", + "dev": true + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", "dev": true }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + }, + "regenerator-transform": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", + "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", "dev": true, "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", "private": "^0.1.6" } }, @@ -13924,6 +13882,12 @@ "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" }, + "regexp-tree": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.10.tgz", + "integrity": "sha512-K1qVSbcedffwuIslMwpe6vGlj+ZXRnGkvjAtFHfDZZZuEdA/h0dxljAPu9vhUo6Rrx2U2AwJ+nSQ6hK+lrP5MQ==", + "dev": true + }, "regexpp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", @@ -13931,20 +13895,23 @@ "dev": true }, "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", + "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", "dev": true, "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.0.2", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" } }, "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", "dev": true, "requires": { "rc": "^1.1.6", @@ -13961,15 +13928,15 @@ } }, "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", "dev": true }, "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -14007,6 +13974,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, "requires": { "is-finite": "^1.0.0" } @@ -14038,6 +14006,11 @@ "uuid": "^3.3.2" }, "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -14128,9 +14101,9 @@ } }, "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", + "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", "requires": { "path-parse": "^1.0.6" } @@ -14381,6 +14354,15 @@ } } }, + "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" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -14614,10 +14596,10 @@ "to-regex": "^3.0.2" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } } @@ -14678,6 +14660,16 @@ "wrap-ansi": "^2.0.0" } }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -14709,6 +14701,15 @@ "error-ex": "^1.2.0" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -14720,6 +14721,12 @@ "pinkie-promise": "^2.0.0" } }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -14826,9 +14833,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "scheduler": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz", - "integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==", + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -14896,9 +14903,9 @@ "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, "semver-diff": { "version": "2.1.0", @@ -14910,9 +14917,9 @@ } }, "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -14921,30 +14928,40 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } } } }, "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.17.1" } }, "set-blocking": { @@ -14979,9 +14996,9 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "sha.js": { "version": "2.4.11", @@ -15067,7 +15084,8 @@ "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true }, "slice-ansi": { "version": "1.0.0", @@ -15130,6 +15148,14 @@ "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -15145,6 +15171,11 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -15222,12 +15253,12 @@ } }, "socks-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", - "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", "requires": { - "agent-base": "~4.2.0", - "socks": "~2.2.0" + "agent-base": "~4.2.1", + "socks": "~2.3.2" }, "dependencies": { "smart-buffer": { @@ -15236,9 +15267,9 @@ "integrity": "sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==" }, "socks": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.3.tgz", - "integrity": "sha512-+2r83WaRT3PXYoO/1z+RDEBE7Z2f9YcdQnJ0K/ncXXbV5gJ6wYfNAebYFYiiUjM6E4JyXnPY8cimwyvFYHVUUA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.2.tgz", + "integrity": "sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ==", "requires": { "ip": "^1.1.5", "smart-buffer": "4.0.2" @@ -15278,11 +15309,19 @@ } }, "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", "requires": { - "source-map": "^0.5.6" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "source-map-url": { @@ -15314,9 +15353,9 @@ } }, "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" }, "split-string": { "version": "3.1.0", @@ -15558,9 +15597,12 @@ "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } }, "svgo": { "version": "0.7.2", @@ -15643,61 +15685,17 @@ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "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" - } - }, - "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" - } - }, - "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" - } - }, "fast-deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "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" - } } } }, @@ -15707,13 +15705,13 @@ "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==" }, "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "dev": true, "requires": { "block-stream": "*", - "fstream": "^1.0.2", + "fstream": "^1.0.12", "inherits": "2" } }, @@ -15753,6 +15751,16 @@ "require-main-filename": "^1.0.1" }, "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -15775,6 +15783,15 @@ "error-ex": "^1.2.0" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -15786,6 +15803,12 @@ "pinkie-promise": "^2.0.0" } }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -15902,9 +15925,9 @@ "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" }, "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -15944,6 +15967,11 @@ } } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "touch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz", @@ -16025,12 +16053,12 @@ } }, "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" } }, "typedarray": { @@ -16045,14 +16073,19 @@ "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" }, "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", "requires": { - "commander": "~2.17.1", + "commander": "~2.19.0", "source-map": "~0.6.1" }, "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16119,6 +16152,23 @@ "dev": true, "requires": { "debug": "^2.2.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 + } } }, "underscore": { @@ -16126,6 +16176,34 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", @@ -16248,9 +16326,9 @@ "dev": true }, "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" }, "update-notifier": { "version": "2.5.0", @@ -16268,52 +16346,6 @@ "latest-version": "^3.0.0", "semver-diff": "^2.0.0", "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } } }, "upper-case": { @@ -16425,9 +16457,9 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "vendors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", - "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz", + "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==", "dev": true }, "verror": { @@ -16493,14 +16525,6 @@ "requires": { "exec-sh": "^0.2.0", "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "watchpack": { @@ -16570,9 +16594,9 @@ } }, "chokidar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", - "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.1", @@ -16585,7 +16609,15 @@ "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", - "upath": "^1.1.0" + "upath": "^1.1.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" } }, "expand-brackets": { @@ -16776,9 +16808,9 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "requires": { "is-extglob": "^2.1.1" } @@ -16831,6 +16863,11 @@ "to-regex": "^3.0.2" } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -16839,20 +16876,38 @@ } }, "web-resource-inliner": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.2.1.tgz", - "integrity": "sha512-fOWnBQHVX8zHvEbECDTxtYL0FXIIZZ5H3LWoez8mGopYJK7inEru1kVMDzM1lVdeJBNEqUnNP5FBGxvzuMcwwQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.3.2.tgz", + "integrity": "sha512-eVnNqwG20sbAgqv2JONwyr57UNZFJP4oauioeUjpCMY83AM11956eIhxlCGGXfSMi7bRBjR9Vao05bXFzslh7w==", "requires": { "async": "^2.1.2", "chalk": "^1.1.3", - "datauri": "^1.0.4", + "datauri": "^2.0.0", "htmlparser2": "^3.9.2", "lodash.unescape": "^4.0.1", "request": "^2.78.0", + "safer-buffer": "^2.1.2", "valid-data-url": "^0.1.4", "xtend": "^4.0.0" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -16872,14 +16927,19 @@ } }, "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" } } }, @@ -16922,6 +16982,16 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", @@ -16974,16 +17044,16 @@ }, "dependencies": { "mime": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", - "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.3.tgz", + "integrity": "sha512-QgrPRJfE+riq5TPZMcHZOtm8c6K/yYrMbKIoRfapfiGLxS8OTeIfRhUGW5LU7MlRa52KOAGCfUNruqLrIBvWZw==" } } }, "webpack-hot-middleware": { - "version": "2.24.3", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz", - "integrity": "sha512-pPlmcdoR2Fn6UhYjAhp1g/IJy1Yc9hD+T6O9mjRcWV2pFbBjIFoJXhP0CoD0xPOhWJuWXuZXGBga9ybbOdzXpg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.0.tgz", + "integrity": "sha512-xs5dPOrGPCzuRXNi8F6rwhawWvQQkeli5Ro48PRuQh8pYPCPmNnltP9itiUPT4xI8oW+y0m59lyyeQk54s5VgA==", "requires": { "ansi-html": "0.0.7", "html-entities": "^1.2.0", @@ -17002,45 +17072,6 @@ "uuid": "^3.1.0" }, "dependencies": { - "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==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.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==", - "requires": { - "color-name": "1.1.3" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -17060,9 +17091,9 @@ }, "dependencies": { "tapable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", - "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true } } @@ -17095,16 +17126,6 @@ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", "requires": { "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } } }, "whatwg-fetch": { diff --git a/package.json b/package.json index 70d60ca2..59af6f6a 100644 --- a/package.json +++ b/package.json @@ -38,40 +38,55 @@ "url": "git+https://github.com/catarak/p5.js-web-editor.git" }, "devDependencies": { - "babel-eslint": "^7.1.1", - "babel-loader": "^7.1.5", - "babel-plugin-transform-react-constant-elements": "^6.23.0", - "babel-plugin-transform-react-inline-elements": "^6.22.0", + "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/plugin-proposal-decorators": "^7.4.4", + "@babel/plugin-proposal-do-expressions": "^7.2.0", + "@babel/plugin-proposal-export-default-from": "^7.2.0", + "@babel/plugin-proposal-export-namespace-from": "^7.2.0", + "@babel/plugin-proposal-function-bind": "^7.2.0", + "@babel/plugin-proposal-function-sent": "^7.2.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", + "@babel/plugin-proposal-numeric-separator": "^7.2.0", + "@babel/plugin-proposal-optional-chaining": "^7.2.0", + "@babel/plugin-proposal-pipeline-operator": "^7.3.2", + "@babel/plugin-proposal-throw-expressions": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-import-meta": "^7.2.0", + "@babel/plugin-transform-react-constant-elements": "^7.2.0", + "@babel/plugin-transform-react-inline-elements": "^7.2.0", + "@babel/preset-env": "^7.4.5", + "@babel/preset-react": "^7.0.0", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^9.0.0", + "babel-jest": "^23.4.2", + "babel-loader": "^8.0.0", "babel-plugin-transform-react-remove-prop-types": "^0.2.12", - "babel-plugin-webpack-loaders": "^0.9.0", - "babel-preset-env": "^1.7.0", - "babel-preset-react": "^6.24.1", - "babel-preset-react-optimize": "^1.0.1", - "babel-preset-stage-0": "^6.24.1", "chunk-manifest-webpack-plugin": "github:catarak/chunk-manifest-webpack-plugin", "css-loader": "^0.23.1", "cssnano": "^3.10.0", - "eslint": "^4.19.1", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", + "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-react": "^7.12.3", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^2.0.0", + "jest": "^23.6.0", "node-sass": "^4.11.0", "nodemon": "^1.18.9", "postcss-cssnext": "^2.11.0", - "jest": "^23.6.0", "postcss-focus": "^1.0.0", "postcss-loader": "^0.9.1", "postcss-reporter": "^1.4.1", + "react-test-renderer": "^16.6.0", "rimraf": "^2.6.3", "sass-loader": "^6.0.7", "style-loader": "^0.13.2", "webpack-manifest-plugin": "^2.0.4", - "react-test-renderer": "^16.6.0", "webpack-node-externals": "^1.7.2" }, "engines": { @@ -79,12 +94,12 @@ "npm": "6.4.1" }, "dependencies": { + "@babel/core": "^7.4.5", + "@babel/polyfill": "^7.4.4", + "@babel/register": "^7.4.4", "archiver": "^1.1.0", "async": "^2.6.1", "axios": "^0.12.0", - "babel-core": "^6.26.3", - "babel-polyfill": "^6.26.0", - "babel-register": "^6.26.0", "bcrypt-nodejs": "0.0.3", "blob-util": "^1.2.1", "body-parser": "^1.18.3", @@ -93,7 +108,7 @@ "clipboard": "^1.7.1", "codemirror": "^5.42.2", "connect-mongo": "^1.3.2", - "console-feed": "^2.8.5", + "console-feed": "^2.8.8", "cookie-parser": "^1.4.3", "cors": "^2.8.5", "cross-env": "^5.2.0", @@ -103,9 +118,9 @@ "dotenv": "^2.0.0", "dropzone": "^4.3.0", "escape-string-regexp": "^1.0.5", - "eslint-loader": "^1.3.0", + "eslint-loader": "^2.1.2", "express": "^4.16.4", - "express-basic-auth": "^1.1.6", + "express-basic-auth": "^1.2.0", "express-session": "^1.15.6", "friendly-words": "^1.1.3", "htmlhint": "^0.10.1", diff --git a/server/migrations/db_reformat_start.js b/server/migrations/db_reformat_start.js index aea58a29..75efb0f6 100644 --- a/server/migrations/db_reformat_start.js +++ b/server/migrations/db_reformat_start.js @@ -1,3 +1,3 @@ -require('babel-register'); -require('babel-polyfill'); +require('@babel/register'); +require('@babel/polyfill'); require('./db_reformat'); diff --git a/server/migrations/start.js b/server/migrations/start.js index 3f01413f..40ac1fd1 100644 --- a/server/migrations/start.js +++ b/server/migrations/start.js @@ -1,3 +1,3 @@ -require('babel-register'); -require('babel-polyfill'); +require('@babel/register'); +require('@babel/polyfill'); require('./moveBucket'); diff --git a/server/scripts/fetch-examples-gg.js b/server/scripts/fetch-examples-gg.js index 6aa5ac64..52507dcd 100644 --- a/server/scripts/fetch-examples-gg.js +++ b/server/scripts/fetch-examples-gg.js @@ -1,5 +1,5 @@ -require('babel-register'); -require('babel-polyfill'); +require('@babel/register'); +require('@babel/polyfill'); const dotenv = require('dotenv'); if (process.env.NODE_ENV === 'development') { diff --git a/server/scripts/fetch-examples-ml5.js b/server/scripts/fetch-examples-ml5.js index a1ac750a..2ff03a14 100644 --- a/server/scripts/fetch-examples-ml5.js +++ b/server/scripts/fetch-examples-ml5.js @@ -1,5 +1,5 @@ -require('babel-register'); -require('babel-polyfill'); +require('@babel/register'); +require('@babel/polyfill'); const dotenv = require('dotenv'); if (process.env.NODE_ENV === 'development') { diff --git a/server/scripts/fetch-examples.js b/server/scripts/fetch-examples.js index a4b05da4..c3c0a814 100644 --- a/server/scripts/fetch-examples.js +++ b/server/scripts/fetch-examples.js @@ -1,5 +1,5 @@ -require('babel-register'); -require('babel-polyfill'); +require('@babel/register'); +require('@babel/polyfill'); const dotenv = require('dotenv'); if (process.env.NODE_ENV === 'development') { diff --git a/webpack/config.babel.js b/webpack/config.babel.js deleted file mode 100644 index 85726fff..00000000 --- a/webpack/config.babel.js +++ /dev/null @@ -1,43 +0,0 @@ -const ExtractTextPlugin = require('extract-text-webpack-plugin'); -const cssnext = require('postcss-cssnext'); -const postcssFocus = require('postcss-focus'); -const postcssReporter = require('postcss-reporter'); - -module.exports = { - output: { - publicPath: '/', - libraryTarget: 'commonjs2', - }, - resolve: { - extensions: ['', '.js', '.jsx'], - modules: [ - 'client', - 'node_modules', - ], - }, - module: { - loaders: [ - { - test: /main\.scss$/, - exclude: /node_modules/, - loader: ExtractTextPlugin.extract({ - fallback: 'style-loader', - use: 'css-loader!sass-loader!postcss-loader' - }) - }, - { - test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.mp3$|\.eot$|\.ttf$|\.woff$|\.woff2$/i, - loader: 'url-loader?limit=10000', - }, - ], - }, - postcss: () => [ - postcssFocus(), - cssnext({ - browsers: ['last 2 versions', 'IE > 10'], - }), - postcssReporter({ - clearMessages: true, - }), - ], -}; \ No newline at end of file diff --git a/webpack/config.dev.js b/webpack/config.dev.js index e528a41b..95f5b1e8 100644 --- a/webpack/config.dev.js +++ b/webpack/config.dev.js @@ -9,6 +9,8 @@ module.exports = [{ devtool: 'cheap-module-eval-source-map', entry: { app: [ + 'core-js/modules/es6.promise', + 'core-js/modules/es6.array.iterator', 'webpack-hot-middleware/client', 'react-hot-loader/patch', './client/index.jsx', @@ -120,21 +122,9 @@ module.exports = [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: [ - 'react', - 'env', - 'stage-0', - ], - plugins: [ - [ - 'babel-plugin-webpack-loaders', { - 'config': path.resolve(__dirname, './config.babel.js'), - 'verbose': false - } - ] - ] - }, + options: { + babelrc: true + } } ], }, diff --git a/webpack/config.examples.js b/webpack/config.examples.js index 5c3aa214..d3c345b7 100644 --- a/webpack/config.examples.js +++ b/webpack/config.examples.js @@ -27,21 +27,9 @@ module.exports = [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: [ - 'react', - 'env', - 'stage-0', - ], - plugins: [ - [ - 'babel-plugin-webpack-loaders', { - 'config': path.resolve(__dirname, './config.babel.js'), - "verbose": false - } - ] - ] - }, + options: { + babelrc: true + } } ], }, @@ -72,21 +60,9 @@ module.exports = [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: [ - 'react', - 'env', - 'stage-0', - ], - plugins: [ - [ - 'babel-plugin-webpack-loaders', { - 'config': path.resolve(__dirname, './config.babel.js'), - "verbose": false - } - ] - ] - }, + options: { + babelrc: true + } } ], }, @@ -117,21 +93,9 @@ module.exports = [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: [ - 'react', - 'env', - 'stage-0', - ], - plugins: [ - [ - 'babel-plugin-webpack-loaders', { - 'config': path.resolve(__dirname, './config.babel.js'), - "verbose": false - } - ] - ] - }, + options: { + babelrc: true + } } ], }, diff --git a/webpack/config.prod.js b/webpack/config.prod.js index 1b6b01e6..4004c3a5 100644 --- a/webpack/config.prod.js +++ b/webpack/config.prod.js @@ -16,7 +16,9 @@ module.exports = [{ entry: { app: [ - 'babel-polyfill', + '@babel/polyfill', + 'core-js/modules/es6.promise', + 'core-js/modules/es6.array.iterator', path.resolve(__dirname, '../client/index.jsx') ], vendor: [ @@ -167,7 +169,10 @@ module.exports = [{ { test: /\.jsx?$/, exclude: /node_modules/, - loader: 'babel-loader' + loader: 'babel-loader', + options: { + babelrc: true + } } ] }, diff --git a/webpack/config.server.js b/webpack/config.server.js index ddeee689..8e22087e 100644 --- a/webpack/config.server.js +++ b/webpack/config.server.js @@ -33,21 +33,9 @@ module.exports = { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: [ - 'react', - 'env', - 'stage-0', - ], - plugins: [ - [ - 'babel-plugin-webpack-loaders', { - 'config': path.resolve(__dirname, './config.babel.js'), - "verbose": false - } - ] - ] - }, + options: { + babelrc: true + } }, { test: /\.json$/, loader: 'json-loader', From 8caeb0d439ad2cf4fec34c89af112645c7917270 Mon Sep 17 00:00:00 2001 From: siddhant <30566406+siddhant1@users.noreply.github.com> Date: Fri, 7 Jun 2019 02:47:33 +0530 Subject: [PATCH 044/322] Add sorting to sketches #789 (#910) * reselect added * Added Reselect Sorting * Refactor App * added svgs * Refactor * Fixed Issues * re: #789, update sorting styling, create sorting actions and reducers, add sort by sketch name * re #789, change names of svg icons * re: #789, use orderBy instead of sortBy, fix styling jumps --- client/constants.js | 3 ++ client/images/sort-arrow-down.svg | 9 ++++ client/images/sort-arrow-up.svg | 9 ++++ client/index.jsx | 1 + client/modules/IDE/actions/sorting.js | 27 ++++++++++ client/modules/IDE/components/SketchList.jsx | 57 ++++++++++++++++---- client/modules/IDE/reducers/sorting.js | 29 ++++++++++ client/modules/IDE/selectors/projects.js | 32 +++++++++++ client/reducers.js | 2 + client/styles/components/_sketch-list.scss | 25 +++++++++ package-lock.json | 46 ++++++++++++---- package.json | 1 + 12 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 client/images/sort-arrow-down.svg create mode 100644 client/images/sort-arrow-up.svg create mode 100644 client/modules/IDE/actions/sorting.js create mode 100644 client/modules/IDE/reducers/sorting.js create mode 100644 client/modules/IDE/selectors/projects.js diff --git a/client/constants.js b/client/constants.js index 5facb1f3..c4dc80ae 100644 --- a/client/constants.js +++ b/client/constants.js @@ -121,6 +121,9 @@ export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING'; export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING'; export const SET_ASSETS = 'SET_ASSETS'; +export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION'; +export const SET_SORTING = 'SET_SORTING'; + export const START_LOADING = 'START_LOADING'; export const STOP_LOADING = 'STOP_LOADING'; diff --git a/client/images/sort-arrow-down.svg b/client/images/sort-arrow-down.svg new file mode 100644 index 00000000..f9a1fc8e --- /dev/null +++ b/client/images/sort-arrow-down.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/client/images/sort-arrow-up.svg b/client/images/sort-arrow-up.svg new file mode 100644 index 00000000..4fc7ca5d --- /dev/null +++ b/client/images/sort-arrow-up.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/client/index.jsx b/client/index.jsx index 0cecf42b..980b4ec1 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -13,6 +13,7 @@ require('./images/p5js-square-logo.png'); const history = browserHistory; const initialState = window.__INITIAL_STATE__; + const store = configureStore(initialState); const App = () => ( diff --git a/client/modules/IDE/actions/sorting.js b/client/modules/IDE/actions/sorting.js new file mode 100644 index 00000000..0e306b46 --- /dev/null +++ b/client/modules/IDE/actions/sorting.js @@ -0,0 +1,27 @@ +import * as ActionTypes from '../../../constants'; + +export const DIRECTION = { + ASC: 'ASCENDING', + DESC: 'DESCENDING' +}; + +export function setSorting(field, direction) { + return { + type: ActionTypes.SET_SORTING, + payload: { + field, + direction + } + }; +} + +export function resetSorting() { + return setSorting('createdAt', DIRECTION.DESC); +} + +export function toggleDirectionForField(field) { + return { + type: ActionTypes.TOGGLE_DIRECTION, + field + }; +} diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 79d5394f..f51a815a 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -6,17 +6,24 @@ import InlineSVG from 'react-inlinesvg'; import { connect } from 'react-redux'; import { browserHistory, Link } from 'react-router'; import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; import * as ProjectActions from '../actions/project'; import * as SketchActions from '../actions/projects'; import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; const trashCan = require('../../../images/trash-can.svg'); +const arrowUp = require('../../../images/sort-arrow-up.svg'); +const arrowDown = require('../../../images/sort-arrow-down.svg'); class SketchList extends React.Component { constructor(props) { super(props); this.props.getProjects(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); } getSketchesTitle() { @@ -30,18 +37,39 @@ class SketchList extends React.Component { return !this.props.loading && this.props.sketches.length > 0; } - renderLoader() { + _renderLoader() { if (this.props.loading) return ; return null; } - renderEmptyTable() { + _renderEmptyTable() { if (!this.props.loading && this.props.sketches.length === 0) { return (

    No sketches.

    ); } return null; } + _renderFieldHeader(fieldName, displayName) { + const { field, direction } = this.props.sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + return ( + + + + ); + } + render() { const username = this.props.username !== undefined ? this.props.username : this.props.user.username; return ( @@ -49,16 +77,16 @@ class SketchList extends React.Component { {this.getSketchesTitle()} - {this.renderLoader()} - {this.renderEmptyTable()} + {this._renderLoader()} + {this._renderEmptyTable()} {this.hasSketches() && - - - + {this._renderFieldHeader('name', 'Sketch')} + {this._renderFieldHeader('createdAt', 'Date Created')} + {this._renderFieldHeader('updatedAt', 'Date Updated')} @@ -112,7 +140,13 @@ SketchList.propTypes = { })).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, - deleteProject: PropTypes.func.isRequired + deleteProject: PropTypes.func.isRequired, + toggleDirectionForField: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired, }; SketchList.defaultProps = { @@ -122,13 +156,14 @@ SketchList.defaultProps = { function mapStateToProps(state) { return { user: state.user, - sketches: state.sketches, - loading: state.loading, + sketches: getSortedSketches(state), + sorting: state.sorting, + loading: state.loading }; } function mapDispatchToProps(dispatch) { - return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions), dispatch); + return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions, SortingActions), dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(SketchList); diff --git a/client/modules/IDE/reducers/sorting.js b/client/modules/IDE/reducers/sorting.js new file mode 100644 index 00000000..91c7addd --- /dev/null +++ b/client/modules/IDE/reducers/sorting.js @@ -0,0 +1,29 @@ +import * as ActionTypes from '../../../constants'; +import { DIRECTION } from '../actions/sorting'; + +const initialState = { + field: 'createdAt', + direction: DIRECTION.DESC +}; + +const sorting = (state = initialState, action) => { + switch (action.type) { + case ActionTypes.TOGGLE_DIRECTION: + if (action.field && action.field !== state.field) { + if (action.field === 'name') { + return { ...state, field: action.field, direction: DIRECTION.ASC }; + } + return { ...state, field: action.field, direction: DIRECTION.DESC }; + } + if (state.direction === DIRECTION.ASC) { + return { ...state, direction: DIRECTION.DESC }; + } + return { ...state, direction: DIRECTION.ASC }; + case ActionTypes.SET_SORTING: + return { ...state, field: action.payload.field, direction: action.payload.direction }; + default: + return state; + } +}; + +export default sorting; diff --git a/client/modules/IDE/selectors/projects.js b/client/modules/IDE/selectors/projects.js new file mode 100644 index 00000000..9ff655d3 --- /dev/null +++ b/client/modules/IDE/selectors/projects.js @@ -0,0 +1,32 @@ +import { createSelector } from 'reselect'; +import differenceInMilliseconds from 'date-fns/difference_in_milliseconds'; +import orderBy from 'lodash/orderBy'; +import { DIRECTION } from '../actions/sorting'; + +const getSketches = state => state.sketches; +const getField = state => state.sorting.field; +const getDirection = state => state.sorting.direction; + +const getSortedSketches = createSelector( + getSketches, + getField, + getDirection, + (sketches, field, direction) => { + if (field === 'name') { + if (direction === DIRECTION.DESC) { + return orderBy(sketches, 'name', 'desc'); + } + return orderBy(sketches, 'name', 'asc'); + } + const sortedSketches = [...sketches].sort((a, b) => { + const result = + direction === DIRECTION.ASC + ? differenceInMilliseconds(new Date(a[field]), new Date(b[field])) + : differenceInMilliseconds(new Date(b[field]), new Date(a[field])); + return result; + }); + return sortedSketches; + } +); + +export default getSortedSketches; diff --git a/client/reducers.js b/client/reducers.js index ef4d39c3..057dbd62 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -10,6 +10,7 @@ import sketches from './modules/IDE/reducers/projects'; import toast from './modules/IDE/reducers/toast'; import console from './modules/IDE/reducers/console'; import assets from './modules/IDE/reducers/assets'; +import sorting from './modules/IDE/reducers/sorting'; import loading from './modules/IDE/reducers/loading'; const rootReducer = combineReducers({ @@ -20,6 +21,7 @@ const rootReducer = combineReducers({ user, project, sketches, + sorting, editorAccessibility, toast, console, diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 2735bb25..328b07b2 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -19,6 +19,31 @@ height: #{32 / $base-font-size}rem; } +.sketch-list__sort-button { + display: flex; + align-items: center; + height: #{35 / $base-font-size}rem; + & svg { + @include themify() { + fill: getThemifyVariable('inactive-text-color') + } + } +} + +.sketches-table__header { + border-bottom: 2px dashed transparent; + padding: #{3 / $base-font-size}rem 0; + @include themify() { + color: getThemifyVariable('inactive-text-color') + } +} + +.sketches-table__header--selected { + @include themify() { + border-color: getThemifyVariable('logo-color'); + } +} + .sketches-table__row { margin: #{10 / $base-font-size}rem; height: #{72 / $base-font-size}rem; diff --git a/package-lock.json b/package-lock.json index ae4a04e1..6e776b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5462,7 +5462,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5480,11 +5481,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5497,15 +5500,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5608,7 +5614,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5618,6 +5625,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5630,17 +5638,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5657,6 +5668,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5729,7 +5741,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5739,6 +5752,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5814,7 +5828,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5844,6 +5859,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5861,6 +5877,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5899,11 +5916,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -14100,6 +14119,11 @@ "semver": "^5.1.0" } }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", diff --git a/package.json b/package.json index 59af6f6a..f7c3275f 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "redux-thunk": "^2.3.0", "request": "^2.88.0", "request-promise": "^4.1.1", + "reselect": "^4.0.0", "s3": "^4.4.0", "s3-policy": "^0.2.0", "sass-extract": "^2.1.0", From 528f57ad0c8d41745651fd55e52a1edcb9bd9e87 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 11 Jun 2019 23:29:42 +0200 Subject: [PATCH 045/322] Removes the createProject IDE action as it's not used (#1097) --- client/modules/IDE/actions/project.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 355faecf..0fa2d344 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -215,32 +215,6 @@ export function autosaveProject() { }; } -export function createProject() { - return (dispatch, getState) => { - const state = getState(); - if (state.project.isSaving) { - Promise.resolve(); - return; - } - dispatch(startSavingProject()); - axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true }) - .then((response) => { - dispatch(endSavingProject()); - dispatch(setUnsavedChanges(false)); - browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); - const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); - if (hasChanges) { - dispatch(setUnsavedChanges(true)); - } - dispatch(setNewProject(synchedProject)); - }) - .catch((response) => { - dispatch(endSavingProject()); - dispatch(projectSaveFail(response.data)); - }); - }; -} - export function exportProjectAsZip(projectId) { const win = window.open(`${ROOT_URL}/projects/${projectId}/zip`, '_blank'); win.focus(); From 1ef07ed7a6388fb0753e80383feaa1d030b8ca62 Mon Sep 17 00:00:00 2001 From: Shan Rauf <40217167+shanrauf@users.noreply.github.com> Date: Tue, 11 Jun 2019 14:46:37 -0700 Subject: [PATCH 046/322] Fix hover effect on Log in and Sign up nav items (#1085) * Fix hover effect on Log in and Sign up nav items * Fix Login and Signup unequal spacing * Fix HTML syntax and right nav__item-header hover --- client/components/Nav.jsx | 16 ++++++++++------ client/styles/components/_nav.scss | 14 ++++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 987ba11d..253146b0 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -530,12 +530,16 @@ class Nav extends React.PureComponent { { __process.env.LOGIN_ENABLED && !this.props.user.authenticated &&
      -
    • -

      - Log in - or - Sign up -

      +
    • + + Log in + +
    • + or +
    • + + Sign up +
    } { __process.env.LOGIN_ENABLED && this.props.user.authenticated && diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 707636bd..30496400 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -51,10 +51,6 @@ padding-right: #{15 / $base-font-size}rem; } -.nav__item-header { - margin-right: #{5 / $base-font-size}rem; -} - .nav__item:hover { .nav__item-header { @include themify() { @@ -69,6 +65,16 @@ } } +.nav__item-header:hover { + @include themify() { + color: getThemifyVariable('nav-hover-color'); + } +} + +.nav__item-header-triangle { + margin-left: #{5 / $base-font-size}rem; +} + .nav__dropdown { @include themify() { background-color: map-get($theme-map, 'modal-background-color'); From f859cfbd0ae3c2b7308a85ea10460fc780ebc084 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 12 Jun 2019 12:37:47 +0200 Subject: [PATCH 047/322] Include @babel/polyfill for jest tests --- jest.setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.setup.js b/jest.setup.js index 3d6cd1d5..7c9d2b77 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,5 @@ import { configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-16' +import '@babel/polyfill' configure({ adapter: new Adapter() }) From 5534f6536a61423ed34048195e5810f27834db5e Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 10 Jul 2019 10:39:58 +0200 Subject: [PATCH 048/322] Public API for Sketch management documentation (#1076) Adds public API documentation and proposed API --- developer_docs/public_api.md | 219 ++++++++++++++++++++++++ developer_docs/public_api_proposed.md | 238 ++++++++++++++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 developer_docs/public_api.md create mode 100644 developer_docs/public_api_proposed.md diff --git a/developer_docs/public_api.md b/developer_docs/public_api.md new file mode 100644 index 00000000..07a1fafa --- /dev/null +++ b/developer_docs/public_api.md @@ -0,0 +1,219 @@ +# Public API + +This API provides a way to programmatically import data into the p5.js Web Editor. + +# Authentication + +Access to the API is available via a Personal Access Token, linked to an existing editor user account. Tokens can be created and deleted via logged-in user’s Settings page. + +When contacting the API, the username and token must be sent with every request using basic auth. + +This involved sending the base64 encoded `${username}:${personalAccessToken}` in the `Authorization` header. For example: + `Authorization: Basic cDU6YWJjMTIzYWJj` + +# API Access + +- All requests send and receive `Content-Type: application/json` unless otherwise stated + +# Versioning + +The API is versioned and this version is indicated in the root URL path e.g. version 1 of the API can be found at `http://editor.p5js.org/api/v1`. + +You must provide the version number when accessing the API. + +| Version | Release date | +| ------- | ------------ | +| v1 | Unreleased | + +# Models + +The API accepts and returns the following model objects, as JSON. + +## Sketch + +| Name | Type | Description | +| ----- | ----------------- | ------------------------------------------------------------------------------------ | +| name | String | The sketch’s title | +| files | DirectoryContents | The files and directories in this sketch. See `DirectoryContents` for the structure. | +| slug | String | A path that can be used to access the sketch | + + + { + "id": String, // opaque ID + "name: String, + "files": DirectoryContents, + "slug": String // optional + } + +### Validations + +- `files` must have exactly one top-level file with the `.html` extension. If none is provided, then a default `index.html` and associated `style.css` will be automatically created. +- `slug` must be an URL-safe string +- `slug` must be unique across all user's sketches + +## DirectoryContents + +A map of filenames to `File` or `Directory`. The key of each item is used as the filename. Using a map ensures that filenames are unique in the directory. + + + { + [String]: File | Directory + } + + + { + "sketch.js": { "content": "var answer = 42;" }, + "index.html" { "content": "..." } + } + +## DirectFile + +This file is editable in the Editor UI and stored in the Editor's database. + +| Name | Type | Description | +| ------- | ------------ | ------------------------------------------ | +| content | UTF-8 String | The contents of the file as a UTF-8 string | + + { + "content": String + } + +## ReferencedFile + +This file is hosted elsewhere on the Internet. It appears in the Editor's listing and can be referenced using a proxy URL in the Editor. + + +| Name | Type | Description | +| ---- | ---- | ----------------------------------------------- | +| url | URL | A valid URL pointing to a file hosted elsewhere | + + { + "url": URL + } + +## File + +A `File` is either a `DirectFile` or `ReferencedFile`. The API supports both everywhere. + +## Directory + +| Name | Type | Description | +| ----- | ----------------- | ------------------------------- | +| files | DirectoryContents | A map of the directory contents | + + { + "files": DirectoryContents + } + +# API endpoints + +## Sketches + +## `GET /:user/sketches` + +List a user’s sketches. + +This will not return the files within the sketch, just the sketch metadata. + +### Request format +No body. + +### Response format + { + "sketches": Array + } + +### Example + + GET /p5/sketches + + { + "sketches": [ + { "id": "H1PLJg8_", "name": "My Lovely Sketch" }, + { "id": "Bkhf0APpg", "name": "My Lovely Sketch 2" } + ] + } + + +## `POST /:user/sketches` + +Create a new sketch. + +A sketch must contain at least one file with the `.html` extension. If none if provided in the payload, a default `index.html` and linked `style.css` file will be created automatically. + +### Request format +See `Sketch` in Models above. + +### Response format + { + "id": String + } + +### Example + + POST /p5/sketches + + { + "name": "My Lovely Sketch", + "files": { + "index.html": { "content": "Hello!" }, + "sketch.js": { "content": "var useless = true;" } + } + } + +`files` can be nested to represent a folder structure. For example, this will create an empty “data” directory in the sketch: + + + POST /p5/sketches + + { + "name": "My Lovely Sketch 2", + "files": [ + { + "name": "assets", + "type": "", + "files": { + "index.html": { "content": "Hello!" }, + "data": { + "files": {} + } + } + } + } + +### Responses + +| HTTP code | Body | +| ------------------------ | ----------------------------------------------------------------- | +| 201 Created | id of sketch | +| 422 Unprocessable Entity | file validation failed, unsupported filetype, slug already exists | + + +### Examples + + 201 CREATED + + { + "id": "Ckhf0APpg" + } + +## `DELETE /:user/sketches/:id` + +Delete a sketch and all it’s associated files. + +### Request format +No body + +### Response format +No body + +### Example + + DELETE /p5/sketches/Ckhf0APpg + +### Responses + +| HTTP code | Description | +| ------------- | ----------------------- | +| 200 OK | Sketch has been deleted | +| 404 Not Found | Sketch does not exist | diff --git a/developer_docs/public_api_proposed.md b/developer_docs/public_api_proposed.md new file mode 100644 index 00000000..b5531364 --- /dev/null +++ b/developer_docs/public_api_proposed.md @@ -0,0 +1,238 @@ +# Proposed Public API extensions + +This describes proposed extensions to the Public API. None of these extensions are confirmed, but are recorded here for reference and discussion. + +Refer to [Public API](./public_api.md) for the current version of the API. + +# Authentication + +- Support for sending tokens can via the `Authorization: Bearer {your_access_token}` HTTP header instead of just Basic Auth + +# API Access + +- API writes are rate-limited to X per second, per access token +- Operations are transactional, e.g. if a file is somehow invalid, the sketch won’t be left partially uploaded + +# Proposed API endpoints + +## Sketches + +## `GET /:user/sketches/:id` + +Fetch a sketch. + +### Request format +No body. + +### Response format +Returns `Sketch`. + +### Example + + GET /p5/sketches/Ckhf0APpg + + { + "name": "Another title", + "slug": "example-1", + "files": { + "index.html": { "Hallo!" }, + "something.js": { "var uselessness = 12;" } + } + } + +### Responses + +| HTTP code | Description | +| ------------- | ---------------------------- | +| 200 OK | Returns ID of created sketch | +| 404 Not Found | Sketch does not exist | + + +## `PUT /:user/sketches/:id` + +Replace the sketch with an entirely new one, maintaining the same ID. Any existing files will be deleted before the new ones are created. + +### Request format +See `Sketch` in Models above. + +### Response format +No body. + +### Example + + PUT /p5/sketches/Ckhf0APpg + + { + "name": "Another title", + "files": { + "index.html": { "content": "Hallo!" }, + "something.js": { "content": "var uselessness = 12;" + } + } + +### Responses + +| HTTP code | Description | +| ------------------------ | -------------------------------------------- | +| 200 OK | | +| 404 Not Found | Sketch does not exist | +| 422 Unprocessable Entity | file validation failed, unsupported filetype | + + +## `PATCH /:user/sketches/:id` + +Update the sketch whilst maintaining existing data: + +- Change the name +- Update file’s contents or add new files + +### Request format +See `Sketch` in Models above. + +### Response format +No body. + +### Example +Change the name of the sketch + + PATCH /p5/sketches/Ckhf0APpg + + { + "name": "My Very Lovely Sketch" + } + +### Example +Add a file to a sketch, or replace an existing file. + + PATCH /p5/sketches/Ckhf0APpg + + { + "files": { + "index.html": { "content": "My new content" }, // contents will be replaced + "new-file.js": { "content": "some new stuff" } // new file will be added + } + } + +### Responses + +| HTTP code | Description | +| ------------------------ | ------------------------- | +| 200 OK | Change were made | +| 404 Not Found | Sketch does not exist | +| 422 Unprocessable Entity | Validation error of files | + + +## Operating on files within a sketch + +Files within a sketch can be individually accessed via their `path` e.g. `data/something.json`. + +## `GET /:user/sketches/:id/files/:path` + +Fetch the contents of a file. + +### Request format +No body. + +### Response format +Returns file contents. + +### Example + + GET /p5/sketches/Ckhf0APpg/files/assets/something.js + + Content-Type: application/javascript + + var uselessness = 12; + +### Responses + +| HTTP code | Description | +| ------------- | ------------------------------------------------------------------------ | +| 200 OK | Returns body of the file with the content-type set by the file extension | +| 404 Not Found | File does not exist | + + +## `PATCH /:user/sketches/:id/files/:path` + +Update the name or contents a file or directory. + +### Request format +See `File` and `Directory` above. + +### Response format +No body. + +### Example: Change file name + + PATCH /p5/sketches/Ckhf0APpg/files/assets/something.js + + { + "name": "new-name.js" + } + +File `assets/something.js` → `assets/new-name.js`. + +### Example: Change file contents + + PATCH /p5/sketches/Ckhf0APpg/files/assets/something.js + + { + "content": "var answer = 24;" + } + +Content of `assets/something.js` will be replaced with `var answer = 24;`. + +### Example: Create directory + + PATCH /p5/sketches/Ckhf0APpg/files/assets + { + "files": { + "info.csv": { "content": "some,good,data" } + } + } + +`assets/data/info.csv` will now exist with the content `some,good,data` + +Files are added to the directory, in addition to what is there. + +### Responses + +| HTTP code | Description | +| ------------------------ | -------------------------- | +| 200 OK | The changes have been made | +| 404 Not Found | Path does not exist | +| 422 Unprocessable Entity | Validation error of files | + + +## `DELETE /:user/:sketches/files/:path` + +Delete a file/directory, and it’s contents. + +### Request format +No body. + +### Response format +No body. + +### Example: Delete file + + DELETE /p5/sketches/Ckhf0APpg/files/assets/something.js + +`assets/something.js` will be removed from the sketch. + +### Example: Delete directory + + + DELETE /p5/sketches/Ckhf0APpg/files/assets + +The `assets` directory and everything within it, will be removed. + +### Responses + +| HTTP code | Description | +| ------------- | ------------------------- | +| 200 OK | The item has been deleted | +| 404 Not Found | Path does not exist | + + + From e81cce1925ea2cf08fbbd250033f70dabee7a856 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Thu, 11 Jul 2019 16:42:03 -0400 Subject: [PATCH 049/322] fix server crash in which error 'regeneratorRuntime is not defined' was being thrown --- webpack/config.server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/config.server.js b/webpack/config.server.js index 8e22087e..9c83cda8 100644 --- a/webpack/config.server.js +++ b/webpack/config.server.js @@ -4,7 +4,7 @@ const nodeExternals = require('webpack-node-externals'); module.exports = { - entry: path.resolve(__dirname, '../server/server.js'), + entry: ['@babel/polyfill', path.resolve(__dirname, '../server/server.js')], output: { path: path.resolve(__dirname, '../dist/'), From f13e80639823372f531ea451f36127ee9f0357f7 Mon Sep 17 00:00:00 2001 From: Apoorv Taneja Date: Thu, 13 Jun 2019 01:28:02 +0530 Subject: [PATCH 050/322] Added hover effect on sidebar elements (#887) * fixes * changes * fixes #886, adjusts sidebar styling to match navigation dropdown, move nav dropdown and sidebar dropdowns into common placeholder --- client/styles/abstracts/_placeholders.scss | 63 ++++++++++++ client/styles/components/_nav.scss | 110 +-------------------- client/styles/components/_sidebar.scss | 67 +++---------- 3 files changed, 80 insertions(+), 160 deletions(-) diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index b7af3212..80fbbb45 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -176,3 +176,66 @@ } } } + +%dropdown-open { + @include themify() { + background-color: map-get($theme-map, 'modal-background-color'); + border: 1px solid map-get($theme-map, 'modal-border-color'); + box-shadow: 0 0 18px 0 getThemifyVariable('shadow-color'); + color: getThemifyVariable('dropdown-color'); + } + text-align: left; + width: #{180 / $base-font-size}rem; + display: flex; + position: absolute; + flex-direction: column; + top: 100%; + height: auto; + z-index: 9999; + border-radius: #{6 / $base-font-size}rem; + & li:last-child { + border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem; + } + & li { + & button, + & a { + @include themify() { + color: getThemifyVariable('dropdown-color'); + } + } + padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; + height: #{35 / $base-font-size}rem; + cursor: pointer; + display: flex; + align-items: center; + } + & li:hover { + @include themify() { + background-color: getThemifyVariable('button-background-hover-color'); + color: getThemifyVariable('button-hover-color') + } + & button, & a { + @include themify() { + color: getThemifyVariable('button-hover-color'); + } + } + } +} + +%dropdown-open-left { + @extend %dropdown-open; + left: 0; + border-top-left-radius: 0px; + & li:first-child { + border-radius: 0 #{5 / $base-font-size}rem 0 0; + } +} + +%dropdown-open-right { + @extend %dropdown-open; + right: 0; + border-top-right-radius: 0px; + & li:first-child { + border-radius: #{5 / $base-font-size}rem 0 0 0; + } +} diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 30496400..f7335f35 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -76,29 +76,11 @@ } .nav__dropdown { - @include themify() { - background-color: map-get($theme-map, 'modal-background-color'); - border: 1px solid map-get($theme-map, 'modal-border-color'); - box-shadow: 0 0 18px 0 getThemifyVariable('shadow-color'); - color: getThemifyVariable('dropdown-color'); - } - + @extend %dropdown-open-left; display: none; - text-align: left; - width: #{180 / $base-font-size}rem; - .nav__item--open & { display: flex; - position: absolute; - flex-direction: column; - top: #{40 / $base-font-size}rem; - left: 0; - height: auto; } - - z-index: 9999; - border-radius: #{6 / $base-font-size}rem; - border-top-left-radius: 0px; } .nav__items-right { @@ -116,94 +98,11 @@ } } -.nav__dropdown a, -button { - @include themify() { - color: getThemifyVariable('secondary-text-color'); - } -} - .nav__dropdown button { padding: 0; } -.nav__dropdown a:hover { - @include themify() { - color: getThemifyVariable('primary-text-color'); - } -} - -.nav__dropdown-heading { - @include themify() { - border-bottom: 1px dashed map-get($theme-map, 'inactive-text-color'); - } - - height: #{(42 - 5) / $base-font-size}rem; - display: flex; - align-items: center; - justify-content: space-between; - margin: 0 #{16 / $base-font-size}rem; - cursor: pointer; - - &:hover { - span { - @include themify() { - color: getThemifyVariable('nav-hover-color'); - } - } - - polygon { - @include themify() { - fill: getThemifyVariable('nav-hover-color'); - } - } - } -} - -.nav__dropdown-heading a, -.nav__dropdown-heading a:hover { - @include themify() { - color: getThemifyVariable('inactive-text-color'); - } - - cursor: default; - width: 100%; -} - -.nav__dropdown-heading svg { - transform-origin: 50% 50%; - transform: rotate(180deg); -} - .nav__dropdown-item { - height: #{32 / $base-font-size}rem; - width: 100%; - display: flex; - align-items: center; - padding: 0 #{16 / $base-font-size}rem; - cursor: pointer; - - & button, - & a { - @include themify() { - color: getThemifyVariable('dropdown-color'); - } - } - - &:hover { - @include themify() { - background-color: getThemifyVariable('button-background-hover-color'); - color: getThemifyVariable('button-hover-color') - } - - & button, - & a { - @include themify() { - color: getThemifyVariable('button-hover-color'); - } - } - } - & button, & a { width: 100%; @@ -214,13 +113,6 @@ button { } } -.nav__dropdown-item:last-child { - border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem; -} -.nav__dropdown-item:first-child { - border-radius: 0 #{5 / $base-font-size}rem 0 0; -} - .nav__announce { position: absolute; top: #{40 / $base-font-size}rem; diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index 2c25d4fa..45fe2739 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -1,7 +1,6 @@ .sidebar { display: flex; flex-flow: column; - } .sidebar__header { @@ -12,6 +11,7 @@ align-items: center; height: #{29 / $base-font-size}rem; min-height: #{29 / $base-font-size}rem; + position: relative; } .sidebar__title { @@ -51,7 +51,6 @@ } .sidebar__file-item { - // height: #{20 / $base-font-size}rem; font-size: #{12 / $base-font-size}rem; cursor: pointer; position: relative; @@ -151,30 +150,12 @@ } .sidebar__file-item-options { - @extend %modal; - @include themify() { - background-color: getThemifyVariable('modal-background-color'); - box-shadow: 0 0 18px getThemifyVariable('shadow-color'); - } - position: absolute; - top: 95%; - left: #{15 / $base-font-size}rem; - right: #{0 / $base-font-size}rem; + @extend %dropdown-open-right; display: none; - z-index: 100; - padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; + width: 100%; + max-width: #{180 / $base-font-size}rem; .sidebar__file-item--open > .file-item__content & { - display: block; - } -} - -.sidebar__project-options li, .sidebar__file-item-options li { - padding: #{4 / $base-font-size}rem 0; -} - -.sidebar__project-options a, .sidebar__file-item-options a { - @include themify() { - color: getThemifyVariable('secondary-text-color'); + display: flex; } } @@ -220,7 +201,7 @@ .sidebar__icons { display: flex; align-items: center; - position: relative; + height: 100%; .sidebar--cant-edit & { display: none; } @@ -270,32 +251,16 @@ } } -.sidebar__project-options { - @extend %modal; - @include themify() { - background-color: getThemifyVariable('modal-background-color'); - box-shadow: 0 0 18px getThemifyVariable('shadow-color'); - } - display: none; - position: absolute; - .sidebar--project-options & { - display: block; - } - top: #{22 / $base-font-size}rem; - right: #{-6 / $base-font-size}rem; - padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; - width: #{145 / $base-font-size}rem; -} - -.sidebar__file-item-option { - @include themify() { - @extend %link; - } - background-color: transparent; - border: none; -} - - .sidebar__file-item--closed .file-item__children { display: none; } + +.sidebar__project-options { + @extend %dropdown-open-right; + display: none; + width: 100%; + max-width: #{180 / $base-font-size}rem; + .sidebar--project-options & { + display: flex; + } +} From edcd225b9d396f3eb2b93b7f2464708caa23c7d3 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 12 Jun 2019 16:12:19 -0400 Subject: [PATCH 051/322] fixes #943 (#1101) --- client/styles/abstracts/_placeholders.scss | 4 +++- client/styles/components/_nav.scss | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index 80fbbb45..10899bb2 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -202,8 +202,10 @@ @include themify() { color: getThemifyVariable('dropdown-color'); } + width: 100%; + text-align: left; + padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; } - padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; height: #{35 / $base-font-size}rem; cursor: pointer; display: flex; diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index f7335f35..205f817d 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -98,9 +98,9 @@ } } -.nav__dropdown button { - padding: 0; -} +// .nav__dropdown button { +// padding: 0; +// } .nav__dropdown-item { & button, From 38e50226610f78414af9dc246baaa71bbb1312cc Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 12 Jun 2019 17:11:35 -0400 Subject: [PATCH 052/322] fixes #945 (#1102) --- client/modules/IDE/components/Editor.jsx | 11 +++++++++-- client/styles/components/_editor.scss | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 2b970660..892028c4 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -290,6 +290,11 @@ class Editor extends React.Component { 'editor--options': this.props.editorOptionsVisible }); + const editorHolderClass = classNames({ + 'editor-holder': true, + 'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url + }); + return (
    -
    { this.codemirrorContainer = element; }} className="editor-holder" > +
    { this.codemirrorContainer = element; }} className={editorHolderClass} >
    Date: Fri, 14 Jun 2019 13:30:13 -0400 Subject: [PATCH 053/322] re #389, update styling in sidebar and editor for icon consistency (#1104) * re #389, update styling in sidebar and editor for icon consistency * re #389, remove link to unused icon --- client/images/triangle-arrow-down-white.svg | 12 ++++++++++++ client/images/triangle-arrow-right-white.svg | 12 ++++++++++++ client/modules/IDE/components/FileNode.jsx | 2 +- client/modules/IDE/components/Sidebar.jsx | 8 ++------ client/styles/abstracts/_placeholders.scss | 2 +- client/styles/abstracts/_variables.scss | 12 ++++++------ client/styles/components/_sidebar.scss | 10 +++++----- 7 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 client/images/triangle-arrow-down-white.svg create mode 100644 client/images/triangle-arrow-right-white.svg diff --git a/client/images/triangle-arrow-down-white.svg b/client/images/triangle-arrow-down-white.svg new file mode 100644 index 00000000..a66e9ff6 --- /dev/null +++ b/client/images/triangle-arrow-down-white.svg @@ -0,0 +1,12 @@ + + Down Arrow + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/client/images/triangle-arrow-right-white.svg b/client/images/triangle-arrow-right-white.svg new file mode 100644 index 00000000..ff4a6b67 --- /dev/null +++ b/client/images/triangle-arrow-right-white.svg @@ -0,0 +1,12 @@ + + Right Arrow + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index a749eb9c..0131651d 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -7,7 +7,7 @@ import classNames from 'classnames'; import * as IDEActions from '../actions/ide'; import * as FileActions from '../actions/files'; -const downArrowUrl = require('../../../images/down-arrow.svg'); +const downArrowUrl = require('../../../images/down-filled-triangle.svg'); const folderRightUrl = require('../../../images/triangle-arrow-right.svg'); const folderDownUrl = require('../../../images/triangle-arrow-down.svg'); const fileUrl = require('../../../images/file.svg'); diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index c7f825dc..00145a0e 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -4,8 +4,7 @@ import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; import ConnectedFileNode from './FileNode'; -const folderUrl = require('../../../images/folder.svg'); -const downArrowUrl = require('../../../images/down-arrow.svg'); +const downArrowUrl = require('../../../images/down-filled-triangle.svg'); class Sidebar extends React.Component { constructor(props) { @@ -72,10 +71,7 @@ class Sidebar extends React.Component {
    - + ))} diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index f51a815a..bcfdf15f 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -4,19 +4,255 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import InlineSVG from 'react-inlinesvg'; import { connect } from 'react-redux'; -import { browserHistory, Link } from 'react-router'; +import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import * as ProjectActions from '../actions/project'; -import * as SketchActions from '../actions/projects'; +import * as ProjectsActions from '../actions/projects'; import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; +import * as IdeActions from '../actions/ide'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; -const trashCan = require('../../../images/trash-can.svg'); const arrowUp = require('../../../images/sort-arrow-up.svg'); const arrowDown = require('../../../images/sort-arrow-down.svg'); +const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); + +class SketchListRowBase extends React.Component { + constructor(props) { + super(props); + this.state = { + optionsOpen: false, + renameOpen: false, + renameValue: props.sketch.name, + isFocused: false + }; + } + + onFocusComponent = () => { + this.setState({ isFocused: true }); + } + + onBlurComponent = () => { + this.setState({ isFocused: false }); + setTimeout(() => { + if (!this.state.isFocused) { + this.closeAll(); + } + }, 200); + } + + openOptions = () => { + this.setState({ + optionsOpen: true + }); + } + + closeOptions = () => { + this.setState({ + optionsOpen: false + }); + } + + toggleOptions = () => { + if (this.state.optionsOpen) { + this.closeOptions(); + } else { + this.openOptions(); + } + } + + openRename = () => { + this.setState({ + renameOpen: true + }); + } + + closeRename = () => { + this.setState({ + renameOpen: false + }); + } + + closeAll = () => { + this.setState({ + renameOpen: false, + optionsOpen: false + }); + } + + handleRenameChange = (e) => { + this.setState({ + renameValue: e.target.value + }); + } + + handleRenameEnter = (e) => { + if (e.key === 'Enter') { + // TODO pass this func + this.props.changeProjectName(this.props.sketch.id, this.state.renameValue); + this.closeAll(); + } + } + + resetSketchName = () => { + this.setState({ + renameValue: this.props.sketch.name + }); + } + + handleDropdownOpen = () => { + this.closeAll(); + this.openOptions(); + } + + handleRenameOpen = () => { + this.closeAll(); + this.openRename(); + } + + handleSketchDownload = () => { + this.props.exportProjectAsZip(this.props.sketch.id); + } + + handleSketchDuplicate = () => { + this.closeAll(); + this.props.cloneProject(this.props.sketch.id); + } + + handleSketchShare = () => { + this.closeAll(); + this.props.showShareModal(this.props.sketch.id, this.props.sketch.name, this.props.username); + } + + handleSketchDelete = () => { + this.closeAll(); + if (window.confirm(`Are you sure you want to delete "${this.props.sketch.name}"?`)) { + this.props.deleteProject(this.props.sketch.id); + } + } + + render() { + const { sketch, username } = this.props; + const { renameOpen, optionsOpen, renameValue } = this.state; + const userIsOwner = this.props.user.username === this.props.username; + return ( + + + + + + ); + } +} + +SketchListRowBase.propTypes = { + sketch: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteProject: PropTypes.func.isRequired, + showShareModal: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + changeProjectName: PropTypes.func.isRequired +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); +} + +const SketchListRow = connect(null, mapDispatchToPropsSketchListRow)(SketchListRowBase); class SketchList extends React.Component { constructor(props) { @@ -83,43 +319,20 @@ class SketchList extends React.Component {
    SketchDate createdDate updated
    {asset.name} {prettyBytes(asset.size)} View{asset.sketchName}{asset.sketchName}
    + + {renameOpen ? '' : sketch.name} + + {renameOpen + && + e.stopPropagation()} + /> + } + {format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')} + + {optionsOpen && +
      + {userIsOwner && +
    • + +
    • } +
    • + +
    • + {this.props.user.authenticated && +
    • + +
    • } + { /*
    • + +
    • */ } + {userIsOwner && +
    • + +
    • } +
    } +
    - {this._renderFieldHeader('name', 'Sketch')} {this._renderFieldHeader('createdAt', 'Date Created')} {this._renderFieldHeader('updatedAt', 'Date Updated')} + {this.props.sketches.map(sketch => - // eslint-disable-next-line - browserHistory.push(`/${username}/sketches/${sketch.id}`)} - > - - - - - )} + sketch={sketch} + user={this.props.user} + username={username} + />))}
    - {(() => { // eslint-disable-line - if (this.props.username === this.props.user.username || this.props.username === undefined) { - return ( - - ); - } - })()} - {sketch.name}{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}
    }
    @@ -129,7 +342,8 @@ class SketchList extends React.Component { SketchList.propTypes = { user: PropTypes.shape({ - username: PropTypes.string + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired }).isRequired, getProjects: PropTypes.func.isRequired, sketches: PropTypes.arrayOf(PropTypes.shape({ @@ -140,16 +354,25 @@ SketchList.propTypes = { })).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, - deleteProject: PropTypes.func.isRequired, toggleDirectionForField: PropTypes.func.isRequired, resetSorting: PropTypes.func.isRequired, sorting: PropTypes.shape({ field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired }).isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + owner: PropTypes.shape({ + id: PropTypes.string + }) + }) }; SketchList.defaultProps = { + project: { + id: undefined, + owner: undefined + }, username: undefined }; @@ -158,12 +381,13 @@ function mapStateToProps(state) { user: state.user, sketches: getSortedSketches(state), sorting: state.sorting, - loading: state.loading + loading: state.loading, + project: state.project }; } function mapDispatchToProps(dispatch) { - return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions, SortingActions), dispatch); + return bindActionCreators(Object.assign({}, ProjectsActions, ToastActions, SortingActions), dispatch); } export default connect(mapStateToProps, mapDispatchToProps)(SketchList); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index ed19bb8b..b2ae1ebb 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -414,9 +414,9 @@ class IDEView extends React.Component { closeOverlay={this.props.closeShareModal} > } @@ -481,6 +481,9 @@ IDEView.propTypes = { projectOptionsVisible: PropTypes.bool.isRequired, newFolderModalVisible: PropTypes.bool.isRequired, shareModalVisible: PropTypes.bool.isRequired, + shareModalProjectId: PropTypes.string.isRequired, + shareModalProjectName: PropTypes.string.isRequired, + shareModalProjectUsername: PropTypes.string.isRequired, editorOptionsVisible: PropTypes.bool.isRequired, keyboardShortcutVisible: PropTypes.bool.isRequired, unsavedChanges: PropTypes.bool.isRequired, diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 379237bc..f9559234 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -10,6 +10,9 @@ const initialState = { projectOptionsVisible: false, newFolderModalVisible: false, shareModalVisible: false, + shareModalProjectId: null, + shareModalProjectName: null, + shareModalProjectUsername: null, editorOptionsVisible: false, keyboardShortcutVisible: false, unsavedChanges: false, @@ -61,7 +64,12 @@ const ide = (state = initialState, action) => { case ActionTypes.CLOSE_NEW_FOLDER_MODAL: return Object.assign({}, state, { newFolderModalVisible: false }); case ActionTypes.SHOW_SHARE_MODAL: - return Object.assign({}, state, { shareModalVisible: true }); + return Object.assign({}, state, { + shareModalVisible: true, + shareModalProjectId: action.payload.shareModalProjectId, + shareModalProjectName: action.payload.shareModalProjectName, + shareModalProjectUsername: action.payload.shareModalProjectUsername, + }); case ActionTypes.CLOSE_SHARE_MODAL: return Object.assign({}, state, { shareModalVisible: false }); case ActionTypes.SHOW_EDITOR_OPTIONS: diff --git a/client/modules/IDE/reducers/projects.js b/client/modules/IDE/reducers/projects.js index ba3bb4c9..ff06c5c7 100644 --- a/client/modules/IDE/reducers/projects.js +++ b/client/modules/IDE/reducers/projects.js @@ -7,6 +7,14 @@ const sketches = (state = [], action) => { case ActionTypes.DELETE_PROJECT: return state.filter(sketch => sketch.id !== action.id); + case ActionTypes.RENAME_PROJECT: { + return state.map((sketch) => { + if (sketch.id === action.payload.id) { + return { ...sketch, name: action.payload.name }; + } + return { ...sketch }; + }); + } default: return state; } diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index d6bc73f4..23b03f0e 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -193,6 +193,9 @@ height: auto; z-index: 9999; border-radius: #{6 / $base-font-size}rem; + & li:first-child { + border-radius: #{5 / $base-font-size}rem #{5 / $base-font-size}rem 0 0; + } & li:last-child { border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem; } @@ -227,17 +230,9 @@ %dropdown-open-left { @extend %dropdown-open; left: 0; - border-top-left-radius: 0px; - & li:first-child { - border-radius: 0 #{5 / $base-font-size}rem 0 0; - } } %dropdown-open-right { @extend %dropdown-open; right: 0; - border-top-right-radius: 0px; - & li:first-child { - border-radius: #{5 / $base-font-size}rem 0 0 0; - } } diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index 9c852e9d..345d0b40 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -25,6 +25,8 @@ } .sidebar__add { + width: #{20 / $base-font-size}rem; + height: #{20 / $base-font-size}rem; @include icon(); .sidebar--contracted & { display: none; @@ -121,10 +123,11 @@ } .sidebar__file-item-show-options { + width: #{20 / $base-font-size}rem; + height: #{20 / $base-font-size}rem; @include icon(); @include themify() { - padding: #{4 / $base-font-size}rem 0; - padding-right: #{6 / $base-font-size}rem; + margin-right: #{5 / $base-font-size}rem; } display: none; position: absolute; diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 328b07b2..dca1d759 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -10,8 +10,9 @@ padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem; max-height: 100%; border-spacing: 0; - & .sketch-list__trash-column { - width: #{23 / $base-font-size}rem; + & .sketch-list__dropdown-column { + width: #{60 / $base-font-size}rem; + position: relative; } } @@ -44,11 +45,14 @@ } } +.sketches-table thead th:nth-child(1){ + padding-left: #{12 / $base-font-size}rem; +} + .sketches-table__row { margin: #{10 / $base-font-size}rem; height: #{72 / $base-font-size}rem; font-size: #{16 / $base-font-size}rem; - cursor: pointer; } .sketches-table__row:nth-child(odd) { @@ -57,6 +61,10 @@ } } +.sketches-table__row > th:nth-child(1) { + padding-left: #{12 / $base-font-size}rem; +} + .sketches-table__row a { @include themify() { color: getThemifyVariable('primary-text-color'); @@ -75,28 +83,26 @@ font-weight: normal; } -.visibility-toggle .sketch-list__trash-button { - @extend %hidden-element; - width:#{20 / $base-font-size}rem; - height:#{20 / $base-font-size}rem; -} - -.visibility-toggle:hover .sketch-list__trash-button { - @include themify() { - background-color: transparent; - border: none; - cursor: pointer; - padding: 0; - position: initial; - left: 0; - top: 0; - & g { - opacity: 1; - fill: getThemifyVariable('icon-hover-color'); +.sketch-list__dropdown-button { + width:#{25 / $base-font-size}rem; + height:#{25 / $base-font-size}rem; + @include themify() { + & polygon { + fill: getThemifyVariable('dropdown-color'); } } } +.sketch-list__action-dialogue { + @extend %dropdown-open-right; + top: 63%; + right: calc(100% - 26px); +} + +.sketch-list__action-option { + +} + .sketches-table__empty { text-align: center; font-size: #{16 / $base-font-size}rem; diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index 527ac64a..ae029918 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -40,7 +40,7 @@ export function updateProject(req, res) { res.json({ success: false }); return; } - if (updatedProject.files.length !== req.body.files.length) { + if (req.body.files && updatedProject.files.length !== req.body.files.length) { const oldFileIds = updatedProject.files.map(file => file.id); const newFileIds = req.body.files.map(file => file.id); const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); From f6416738ae13f9674a6eb95b00cbadd88d757ddf Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 19 Jun 2019 17:03:15 -0400 Subject: [PATCH 057/322] Bug/orphaned assets (#1108) * fixes #498 * fix linting errors --- server/controllers/aws.controller.js | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index 09c1480e..4e94e24e 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -126,21 +126,31 @@ export function listObjectsInS3ForUser(req, res) { .on('end', () => { const projectAssets = []; getProjectsForUserId(userId).then((projects) => { - projects.forEach((project) => { - project.files.forEach((file) => { - if (!file.url) return; - - const foundAsset = assets.find(asset => file.url.includes(asset.key)); - if (!foundAsset) return; - projectAssets.push({ - name: file.name, - sketchName: project.name, - sketchId: project.id, - url: file.url, - key: foundAsset.key, - size: foundAsset.size + assets.forEach((asset) => { + const name = asset.key.split('/').pop(); + const foundAsset = { + key: asset.key, + name, + size: asset.size, + url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}` + }; + projects.some((project) => { + let found = false; + project.files.some((file) => { + if (!file.url) return false; + if (file.url.includes(asset.key)) { + found = true; + foundAsset.name = file.name; + foundAsset.sketchName = project.name; + foundAsset.sketchId = project.id; + foundAsset.url = file.url; + return true; + } + return false; }); + return found; }); + projectAssets.push(foundAsset); }); res.json({ assets: projectAssets }); }); From b94ca8a52de03bcee6123b3fda0820d7223ceb74 Mon Sep 17 00:00:00 2001 From: Rachel Lim Date: Sun, 30 Jun 2019 19:41:27 -0400 Subject: [PATCH 058/322] for #989, files cannot be renamed to empty string --- client/modules/IDE/components/FileNode.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index 0131651d..f554ae09 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -67,6 +67,7 @@ export class FileNode extends React.Component { validateFileName() { const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i); const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i); + const newFileName = this.props.name; if (oldFileExtension && !newFileExtension) { this.props.updateFileName(this.props.id, this.originalFileName); } @@ -77,6 +78,12 @@ export class FileNode extends React.Component { ) { this.props.updateFileName(this.props.id, this.originalFileName); } + if (newFileName === '') { + this.props.updateFileName(this.props.id, this.originalFileName); + } + if (newFileName === newFileExtension[0]) { + this.props.updateFileName(this.props.id, this.originalFileName); + } } toggleFileOptions(e) { From 1dd723cb9ff9c5d8e6046081b1512e1d9d5f32aa Mon Sep 17 00:00:00 2001 From: Rachel Lim Date: Thu, 11 Jul 2019 02:05:43 -0400 Subject: [PATCH 059/322] for 989, updated folder cannot be renamed to empty string --- client/modules/IDE/components/FileNode.jsx | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index f554ae09..a9c4f0a7 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -68,20 +68,12 @@ export class FileNode extends React.Component { const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i); const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i); const newFileName = this.props.name; - if (oldFileExtension && !newFileExtension) { - this.props.updateFileName(this.props.id, this.originalFileName); - } - if ( - oldFileExtension && - newFileExtension && - oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase() - ) { - this.props.updateFileName(this.props.id, this.originalFileName); - } - if (newFileName === '') { - this.props.updateFileName(this.props.id, this.originalFileName); - } - if (newFileName === newFileExtension[0]) { + const hasNoExtension = oldFileExtension && !newFileExtension; + const notSameExtension = oldFileExtension && newFileExtension && + oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); + const hasEmptyFilename = newFileName === ''; + const hasOnlyExtension = newFileExtension && newFileName === newFileExtension[0]; + if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension) { this.props.updateFileName(this.props.id, this.originalFileName); } } From d4914983a4890f2cddbf66557c8335c40a00b10b Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 17 Jul 2019 12:30:26 -0400 Subject: [PATCH 060/322] prevent users from adding extension to folder name --- client/modules/IDE/components/FileNode.jsx | 8 +++++--- client/modules/IDE/components/NewFolderModal.jsx | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index a9c4f0a7..1b28bd4e 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -67,13 +67,15 @@ export class FileNode extends React.Component { validateFileName() { const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i); const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i); + const hasPeriod = this.props.name.match(/\.+/); const newFileName = this.props.name; const hasNoExtension = oldFileExtension && !newFileExtension; - const notSameExtension = oldFileExtension && newFileExtension && - oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); + const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod; + const notSameExtension = oldFileExtension && newFileExtension + && oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); const hasEmptyFilename = newFileName === ''; const hasOnlyExtension = newFileExtension && newFileName === newFileExtension[0]; - if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension) { + if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) { this.props.updateFileName(this.props.id, this.originalFileName); } } diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index 4d1e714c..60483ce8 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -38,6 +38,8 @@ function validate(formProps) { errors.name = 'Please enter a name'; } else if (formProps.name.trim().length === 0) { errors.name = 'Folder name cannot contain only spaces'; + } else if (formProps.name.match(/\.+/i)) { + errors.name = 'Folder name cannot contain an extension'; } return errors; From cd21e9ae7260c96c5ae4a33195fa70e912e26d93 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sun, 21 Jul 2019 16:31:14 +0200 Subject: [PATCH 061/322] Fixes bug where requestsOfType() would fail if no body - Passes if request has no body - Returns a JSON object with an error message when request doesn't match type, the response body was "[object Object]" --- server/server.js | 11 +++++++++++ server/utils/requestsOfType.js | 9 ++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/server/server.js b/server/server.js index eadfcb06..4483b722 100644 --- a/server/server.js +++ b/server/server.js @@ -138,6 +138,17 @@ app.get('/', (req, res) => { res.sendFile(renderIndex()); }); +// Handle API errors +app.use('/api', (error, req, res, next) => { + if (error && error.code && !res.headersSent) { + res.status(error.code).json({ error: error.message }); + return; + } + + next(error); +}); + + // Handle missing routes. app.get('*', (req, res) => { res.status(404); diff --git a/server/utils/requestsOfType.js b/server/utils/requestsOfType.js index 20d806d6..5aac7b8c 100644 --- a/server/utils/requestsOfType.js +++ b/server/utils/requestsOfType.js @@ -4,11 +4,14 @@ header does not match `type` */ const requestsOfType = type => (req, res, next) => { - if (req.get('content-type') != null && !req.is(type)) { + const hasContentType = req.get('content-type') !== null; + const isCorrectType = req.is(type) === null || req.is(type) === type; + + if (hasContentType && !isCorrectType) { if (process.env.NODE_ENV === 'development') { - console.log('in requests of type error'); + console.error(`Requests with a body must be of Content-Type "${type}". Sending HTTP 406`); } - return next({ statusCode: 406 }); // 406 UNACCEPTABLE + return next({ code: 406, message: `Requests with a body must be of Content-Type "${type}"` }); // 406 UNACCEPTABLE } return next(); From 64caab0702602eb25182dc6173c47427ef25e5a2 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Tue, 16 Oct 2018 00:22:56 +0200 Subject: [PATCH 062/322] You can now generate keys from the advanced settings interface --- client/modules/User/actions.js | 1 + client/modules/User/pages/AccountView.jsx | 11 ++--------- client/modules/User/reducers.js | 6 ++++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 93a874fb..3c2f6d5b 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,5 +1,6 @@ import { browserHistory } from 'react-router'; import axios from 'axios'; +import crypto from 'crypto'; import * as ActionTypes from '../../constants'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showToast, setToastText } from '../IDE/actions/toast'; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 7382656e..35fa127f 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -1,11 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { reduxForm } from 'redux-form'; +import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import InlineSVG from 'react-inlinesvg'; -import axios from 'axios'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; @@ -114,10 +113,4 @@ AccountView.propTypes = { theme: PropTypes.string.isRequired }; -export default reduxForm({ - form: 'updateAllSettings', - fields: ['username', 'email', 'currentPassword', 'newPassword'], - validate: validateSettings, - asyncValidate, - asyncBlurFields: ['username', 'email', 'currentPassword'] -}, mapStateToProps, mapDispatchToProps)(AccountView); +export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 00454acd..b298724d 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -31,9 +31,15 @@ const user = (state = { authenticated: false }, action) => { return Object.assign({}, state, { emailVerificationTokenState: 'invalid' }); case ActionTypes.SETTINGS_UPDATED: return { ...state, ...action.user }; +<<<<<<< HEAD case ActionTypes.API_KEY_REMOVED: return { ...state, ...action.user }; case ActionTypes.API_KEY_CREATED: +======= + case ActionTypes.REMOVED_API_KEY: + return { ...state, ...action.user }; + case ActionTypes.ADDED_API_KEY: +>>>>>>> You can now generate keys from the advanced settings interface return { ...state, ...action.user }; default: return state; From e77cbf4fa3dc4bdd25b4babfbd239d5adf965cdf Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 15 May 2019 16:39:53 +0200 Subject: [PATCH 063/322] Changes API_KEY_REMOVED action constant to match API_KEY_CREATED --- client/modules/User/reducers.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index b298724d..00454acd 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -31,15 +31,9 @@ const user = (state = { authenticated: false }, action) => { return Object.assign({}, state, { emailVerificationTokenState: 'invalid' }); case ActionTypes.SETTINGS_UPDATED: return { ...state, ...action.user }; -<<<<<<< HEAD case ActionTypes.API_KEY_REMOVED: return { ...state, ...action.user }; case ActionTypes.API_KEY_CREATED: -======= - case ActionTypes.REMOVED_API_KEY: - return { ...state, ...action.user }; - case ActionTypes.ADDED_API_KEY: ->>>>>>> You can now generate keys from the advanced settings interface return { ...state, ...action.user }; default: return state; From f937e72131896250dc4fbb3268d1f6ab21b5a592 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Wed, 22 May 2019 15:38:24 +0200 Subject: [PATCH 064/322] Makes form CSS theme-aware --- client/styles/abstracts/_variables.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index 3ef7e420..5a983d27 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -135,7 +135,6 @@ $themes: ( table-row-stripe-color: #3f3f3f, codefold-icon-open: url(../images/triangle-arrow-down-white.svg), codefold-icon-closed: url(../images/triangle-arrow-right-white.svg), - form-title-color: $white, form-secondary-title-color: #b5b5b5, form-border-color: #b5b5b5, @@ -195,7 +194,6 @@ $themes: ( table-row-stripe-color: #3f3f3f, codefold-icon-open: url(../images/triangle-arrow-down-white.svg), codefold-icon-closed: url(../images/triangle-arrow-right-white.svg), - form-title-color: $white, form-secondary-title-color: #b5b5b5, form-border-color: #b5b5b5, From e2676ecda7baea88b3f452e6f26e2b9d35c63b21 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 5 Jun 2019 12:05:31 -0400 Subject: [PATCH 065/322] for #950, update babel to v7 (#1077) * for #950, upgrade babel to v7 * fix linting errors * for #950, remove @babel/core from devDependencies (so it's only in dependencies) and change babel-loader config to use .babelrc * for #950, changes to .babelrc to make work * for #950, include core-js modules in webpack config for IE support with babel/plugin-syntax-dynamic-import * for #950, update babel and associated packages to LTS --- package-lock.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3146e496..03db0dfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13054,14 +13054,6 @@ "passport-oauth2": "1.x.x" } }, - "passport-http": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz", - "integrity": "sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=", - "requires": { - "passport-strategy": "1.x.x" - } - }, "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", From d7837a62be4c74048ed102bab7c5cf007ea45a6b Mon Sep 17 00:00:00 2001 From: siddhant <30566406+siddhant1@users.noreply.github.com> Date: Fri, 7 Jun 2019 02:47:33 +0530 Subject: [PATCH 066/322] Add sorting to sketches #789 (#910) * reselect added * Added Reselect Sorting * Refactor App * added svgs * Refactor * Fixed Issues * re: #789, update sorting styling, create sorting actions and reducers, add sort by sketch name * re #789, change names of svg icons * re: #789, use orderBy instead of sortBy, fix styling jumps --- client/modules/IDE/components/SketchList.jsx | 15 +------ package-lock.json | 41 ++++++++++++++------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index bcfdf15f..3b9b958e 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -359,20 +359,10 @@ SketchList.propTypes = { sorting: PropTypes.shape({ field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired - }).isRequired, - project: PropTypes.shape({ - id: PropTypes.string, - owner: PropTypes.shape({ - id: PropTypes.string - }) - }) + }).isRequired }; SketchList.defaultProps = { - project: { - id: undefined, - owner: undefined - }, username: undefined }; @@ -381,8 +371,7 @@ function mapStateToProps(state) { user: state.user, sketches: getSortedSketches(state), sorting: state.sorting, - loading: state.loading, - project: state.project + loading: state.loading }; } diff --git a/package-lock.json b/package-lock.json index 03db0dfe..01170097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6104,7 +6104,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6122,11 +6123,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6139,15 +6142,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6250,7 +6256,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6260,6 +6267,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6272,17 +6280,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6299,6 +6310,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6371,7 +6383,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6381,6 +6394,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6456,7 +6470,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6486,6 +6501,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6503,6 +6519,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6541,11 +6558,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, From 4e0ee8938013f7cf22125f0cc91bb7d62bd729d1 Mon Sep 17 00:00:00 2001 From: Shan Rauf <40217167+shanrauf@users.noreply.github.com> Date: Tue, 11 Jun 2019 14:46:37 -0700 Subject: [PATCH 067/322] Fix hover effect on Log in and Sign up nav items (#1085) * Fix hover effect on Log in and Sign up nav items * Fix Login and Signup unequal spacing * Fix HTML syntax and right nav__item-header hover --- client/styles/components/_nav.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 205f817d..65aa5899 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -75,6 +75,16 @@ margin-left: #{5 / $base-font-size}rem; } +.nav__dropdown { + @include themify() { + color: getThemifyVariable('nav-hover-color'); + } +} + +.nav__item-header-triangle { + margin-left: #{5 / $base-font-size}rem; +} + .nav__dropdown { @extend %dropdown-open-left; display: none; From ab7ecfc012a6836bcf82dab6bf1c375aaa74a248 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 22 Jul 2019 17:22:36 -0400 Subject: [PATCH 068/322] fix lingering errors from rebase --- client/modules/User/pages/AccountView.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 35fa127f..7382656e 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; +import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import InlineSVG from 'react-inlinesvg'; +import axios from 'axios'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; @@ -113,4 +114,10 @@ AccountView.propTypes = { theme: PropTypes.string.isRequired }; -export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); +export default reduxForm({ + form: 'updateAllSettings', + fields: ['username', 'email', 'currentPassword', 'newPassword'], + validate: validateSettings, + asyncValidate, + asyncBlurFields: ['username', 'email', 'currentPassword'] +}, mapStateToProps, mapDispatchToProps)(AccountView); From bfef3e93dcbf090be7627d079f9ed79aedb17f39 Mon Sep 17 00:00:00 2001 From: Laksh Singla <30999375+LakshSingla@users.noreply.github.com> Date: Thu, 20 Jun 2019 01:51:25 +0530 Subject: [PATCH 069/322] Update sketch list styling (#819) * parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla 1549106083 +0530 committer Cassie Tarakajian 1560540243 -0400 parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla 1549106083 +0530 committer Cassie Tarakajian 1560540198 -0400 parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla 1549106083 +0530 committer Cassie Tarakajian 1560539667 -0400 Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Dropdown menu disappearing while clicking anywhere on the table Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Made changes in the dropdown: Arrow positioned slightly updwards, Removed blank space and added box-shadow in dropdown, themifyed dropdowns dashed border color Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box Added download functionality to Dialog box SketchList does not open a sketch if dialogue box is opened SketchList Rename initial UI completed Enter key handled for rename project option [WIP] Updating rename functionality Download option now working for all the sketches Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Final styling of ActionDialogueBox commplete Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Added Delete and Share functionality to Dialog box [WIP] Updating rename functionality Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Final styling of ActionDialogueBox commplete Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX * fix bugs in merge commit * move sketch list dialogue to ul/li * update sketch option dropdown to use dropdown placeholder, remove unused css * major refactor of sketchlist component, fix showShareModal action, minor updates ot icon sizing * fix broken links on asset list * remove unused image, fix options for different users in sketch list --- client/modules/IDE/components/SketchList.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 3b9b958e..9d324cb9 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -363,6 +363,10 @@ SketchList.propTypes = { }; SketchList.defaultProps = { + project: { + id: undefined, + owner: undefined + }, username: undefined }; @@ -371,7 +375,8 @@ function mapStateToProps(state) { user: state.user, sketches: getSortedSketches(state), sorting: state.sorting, - loading: state.loading + loading: state.loading, + project: state.project }; } From a10e0f03279a489d7a49841dd12f48ece696a0d6 Mon Sep 17 00:00:00 2001 From: Vertmo Date: Tue, 16 Oct 2018 00:22:56 +0200 Subject: [PATCH 070/322] You can now generate keys from the advanced settings interface --- client/modules/User/pages/AccountView.jsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 7382656e..35fa127f 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -1,11 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { reduxForm } from 'redux-form'; +import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import InlineSVG from 'react-inlinesvg'; -import axios from 'axios'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; @@ -114,10 +113,4 @@ AccountView.propTypes = { theme: PropTypes.string.isRequired }; -export default reduxForm({ - form: 'updateAllSettings', - fields: ['username', 'email', 'currentPassword', 'newPassword'], - validate: validateSettings, - asyncValidate, - asyncBlurFields: ['username', 'email', 'currentPassword'] -}, mapStateToProps, mapDispatchToProps)(AccountView); +export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); From a851eda254f38038b7f616427686f8d43080d809 Mon Sep 17 00:00:00 2001 From: siddhant <30566406+siddhant1@users.noreply.github.com> Date: Fri, 7 Jun 2019 02:47:33 +0530 Subject: [PATCH 071/322] Add sorting to sketches #789 (#910) * reselect added * Added Reselect Sorting * Refactor App * added svgs * Refactor * Fixed Issues * re: #789, update sorting styling, create sorting actions and reducers, add sort by sketch name * re #789, change names of svg icons * re: #789, use orderBy instead of sortBy, fix styling jumps --- client/modules/IDE/components/SketchList.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 9d324cb9..3b9b958e 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -363,10 +363,6 @@ SketchList.propTypes = { }; SketchList.defaultProps = { - project: { - id: undefined, - owner: undefined - }, username: undefined }; @@ -375,8 +371,7 @@ function mapStateToProps(state) { user: state.user, sketches: getSortedSketches(state), sorting: state.sorting, - loading: state.loading, - project: state.project + loading: state.loading }; } From 116675f866cab516cde653256f3b99ed182bcd5c Mon Sep 17 00:00:00 2001 From: Shan Rauf <40217167+shanrauf@users.noreply.github.com> Date: Tue, 11 Jun 2019 14:46:37 -0700 Subject: [PATCH 072/322] Fix hover effect on Log in and Sign up nav items (#1085) * Fix hover effect on Log in and Sign up nav items * Fix Login and Signup unequal spacing * Fix HTML syntax and right nav__item-header hover --- client/styles/components/_nav.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 65aa5899..74945c78 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -85,6 +85,16 @@ margin-left: #{5 / $base-font-size}rem; } +.nav__dropdown { + @include themify() { + color: getThemifyVariable('nav-hover-color'); + } +} + +.nav__item-header-triangle { + margin-left: #{5 / $base-font-size}rem; +} + .nav__dropdown { @extend %dropdown-open-left; display: none; From 443232380c6dacf0340ffcb594740d3e3cbbdbb1 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 22 Jul 2019 17:52:19 -0400 Subject: [PATCH 073/322] fix errors from rebase, again --- client/modules/User/pages/AccountView.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 35fa127f..7382656e 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; +import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import InlineSVG from 'react-inlinesvg'; +import axios from 'axios'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; @@ -113,4 +114,10 @@ AccountView.propTypes = { theme: PropTypes.string.isRequired }; -export default connect(mapStateToProps, mapDispatchToProps)(AdvancedSettingsView); +export default reduxForm({ + form: 'updateAllSettings', + fields: ['username', 'email', 'currentPassword', 'newPassword'], + validate: validateSettings, + asyncValidate, + asyncBlurFields: ['username', 'email', 'currentPassword'] +}, mapStateToProps, mapDispatchToProps)(AccountView); From 1b461d33cf588f20fd1a9760bd4f31a774cda9cd Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sat, 24 Aug 2019 13:28:21 +0200 Subject: [PATCH 074/322] Do not set prevPath if navigation passes through skipSavingPath flag --- client/modules/App/App.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 2678de7e..9a8e6bdb 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -18,7 +18,10 @@ class App extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.location !== this.props.location) { + const locationWillChange = nextProps.location !== this.props.location; + const shouldSkipRemembering = nextProps.location.state && nextProps.location.state.skipSavingPath === true; + + if (locationWillChange && !shouldSkipRemembering) { this.props.setPreviousPath(this.props.location.pathname); } } From 0193ee82979d689f63abf2d7e6ef42d0a5799f1f Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sun, 11 Aug 2019 11:08:17 +0200 Subject: [PATCH 075/322] Standalone sketch list and asset page --- client/modules/IDE/pages/IDEView.jsx | 26 ---- .../User/components/DashboardTabSwitcher.jsx | 47 +++++++ client/modules/User/pages/DashboardView.jsx | 123 ++++++++++++++++++ client/routes.jsx | 7 +- client/styles/components/_asset-list.scss | 3 +- .../styles/components/_dashboard-header.scss | 53 ++++++++ client/styles/components/_sketch-list.scss | 2 +- client/styles/layout/_dashboard.scss | 9 ++ client/styles/main.scss | 2 + 9 files changed, 241 insertions(+), 31 deletions(-) create mode 100644 client/modules/User/components/DashboardTabSwitcher.jsx create mode 100644 client/modules/User/pages/DashboardView.jsx create mode 100644 client/styles/components/_dashboard-header.scss create mode 100644 client/styles/layout/_dashboard.scss diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index b2ae1ebb..b0be087e 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -29,8 +29,6 @@ import * as ToastActions from '../actions/toast'; import * as ConsoleActions from '../actions/console'; import { getHTMLFile } from '../reducers/files'; import Overlay from '../../App/components/Overlay'; -import SketchList from '../components/SketchList'; -import AssetList from '../components/AssetList'; import About from '../components/About'; import Feedback from '../components/Feedback'; @@ -365,30 +363,6 @@ class IDEView extends React.Component { createFolder={this.props.createFolder} /> } - { this.props.location.pathname.match(/sketches$/) && - - - - } - { this.props.location.pathname.match(/assets$/) && - - - - } { this.props.location.pathname === '/about' && { + const selectedClassName = 'dashboard-header__tab--selected'; + + const location = { pathname: to, state: { skipSavingPath: true } }; + const content = isSelected ? children : {children}; + return ( +
  • +

    + {content} +

    +
  • + ); +}; + +Tab.propTypes = { + children: PropTypes.element.isRequired, + isSelected: PropTypes.bool.isRequired, + to: PropTypes.string.isRequired, +}; + +const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => { + return ( +
      +
      + Sketches + {isOwner && Assets} +
      +
    + ); +}; + +DashboardTabSwitcher.propTypes = { + currentTab: PropTypes.string.isRequired, + isOwner: PropTypes.bool.isRequired, + username: PropTypes.string.isRequired, +}; + +export { DashboardTabSwitcher as default, TabKey }; diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx new file mode 100644 index 00000000..a3172eee --- /dev/null +++ b/client/modules/User/pages/DashboardView.jsx @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { browserHistory, Link } from 'react-router'; +import { Helmet } from 'react-helmet'; +import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; +import NavBasic from '../../../components/NavBasic'; + +import AssetList from '../../IDE/components/AssetList'; +import SketchList from '../../IDE/components/SketchList'; + +import DashboardTabSwitcher, { TabKey } from '../components/DashboardTabSwitcher'; + +class DashboardView extends React.Component { + static defaultProps = { + user: null, + }; + + constructor(props) { + super(props); + this.closeAccountPage = this.closeAccountPage.bind(this); + this.gotoHomePage = this.gotoHomePage.bind(this); + } + + componentDidMount() { + document.body.className = this.props.theme; + } + + closeAccountPage() { + browserHistory.push(this.props.previousPath); + } + + gotoHomePage() { + browserHistory.push('/'); + } + + selectedTabName() { + const path = this.props.location.pathname; + + if (/assets/.test(path)) { + return TabKey.assets; + } + + return TabKey.sketches; + } + + ownerName() { + if (this.props.params.username) { + return this.props.params.username; + } + + return this.props.user.username; + } + + isOwner() { + return this.props.user.username === this.props.params.username; + } + + navigationItem() { + + } + + render() { + const currentTab = this.selectedTabName(); + const isOwner = this.isOwner(); + const { username } = this.props.params; + + return ( +
    + + p5.js Web Editor | Account + + + + +
    +
    +

    {this.ownerName()}

    + + +
    + +
    + { + currentTab === TabKey.sketches ? : + } +
    +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + previousPath: state.ide.previousPath, + user: state.user, + theme: state.preferences.theme + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ + updateSettings, initiateVerification, createApiKey, removeApiKey + }, dispatch); +} + +DashboardView.propTypes = { + location: PropTypes.shape({ + pathname: PropTypes.string.isRequired, + }).isRequired, + params: PropTypes.shape({ + username: PropTypes.string.isRequired, + }).isRequired, + previousPath: PropTypes.string.isRequired, + theme: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string.isRequired, + }), +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardView); diff --git a/client/routes.jsx b/client/routes.jsx index baa89884..0bcad82b 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -9,6 +9,7 @@ import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import EmailVerificationView from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; +import DashboardView from './modules/User/pages/DashboardView'; // import SketchListView from './modules/Sketch/pages/SketchListView'; import { getUser } from './modules/User/actions'; import { stopSketch } from './modules/IDE/actions/ide'; @@ -35,11 +36,11 @@ const routes = store => ( - - + + - + diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index a8e76c74..af1a173b 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -8,7 +8,7 @@ .asset-table { width: 100%; - padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem; + padding: #{10 / $base-font-size}rem 0; max-height: 100%; border-spacing: 0; & .asset-list__delete-column { @@ -53,4 +53,5 @@ .asset-table__empty { text-align: center; font-size: #{16 / $base-font-size}rem; + padding: #{42 / $base-font-size}rem 0; } diff --git a/client/styles/components/_dashboard-header.scss b/client/styles/components/_dashboard-header.scss new file mode 100644 index 00000000..96e4de22 --- /dev/null +++ b/client/styles/components/_dashboard-header.scss @@ -0,0 +1,53 @@ +.dashboard-header { + padding: 24px 66px; +} + +.dashboard-header__tabs { + display: flex; + padding-top: #{24 / $base-font-size}rem; + padding-bottom: #{24 / $base-font-size}rem; +} + +.dashboard-header__tab { + @include themify() { + color: getThemifyVariable('inactive-text-color'); + border-right: 2px solid getThemifyVariable('inactive-text-color'); + + padding: 0 13px; + + &:hover, &:focus { + color: getThemifyVariable('primary-text-color'); + cursor: pointer; + } + &:focus { + color: getThemifyVariable('primary-text-color'); + cursor: pointer; + } + } + + font-size: #{21 / $base-font-size}rem; +} + + +.dashboard-header__tab:first-child { + padding-left: 0; +} + +.dashboard-header__tab:last-child { + border-right: none; +} + +.dashboard-header__tab--selected { + @include themify() { + color: getThemifyVariable('primary-text-color'); + } + cursor: auto; +} + +.dashboard-header__tab a { + color: inherit; +} + +.dashboard-header__tab__title { + margin: 0; +} diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index dca1d759..34c962a4 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -7,7 +7,6 @@ .sketches-table { width: 100%; - padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem; max-height: 100%; border-spacing: 0; & .sketch-list__dropdown-column { @@ -106,4 +105,5 @@ .sketches-table__empty { text-align: center; font-size: #{16 / $base-font-size}rem; + padding: #{42 / $base-font-size}rem 0; } diff --git a/client/styles/layout/_dashboard.scss b/client/styles/layout/_dashboard.scss new file mode 100644 index 00000000..f928ffd3 --- /dev/null +++ b/client/styles/layout/_dashboard.scss @@ -0,0 +1,9 @@ +.dashboard { + display: flex; + flex-direction: column; + flex-wrap: wrap; + @include themify() { + color: getThemifyVariable('primary-text-color'); + background-color: getThemifyVariable('background-color'); + } +} diff --git a/client/styles/main.scss b/client/styles/main.scss index 62b8d4cc..decb6aba 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -46,7 +46,9 @@ @import 'components/loader'; @import 'components/uploader'; @import 'components/tabs'; +@import 'components/dashboard-header'; +@import 'layout/dashboard'; @import 'layout/ide'; @import 'layout/fullscreen'; @import 'layout/user'; From a0a13ab7fc5ccb1bea542993aa03f9ba763564b7 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 19 Aug 2019 21:12:09 +0200 Subject: [PATCH 076/322] Flexible table width --- client/styles/components/_asset-list.scss | 1 - client/styles/components/_sketch-list.scss | 1 - 2 files changed, 2 deletions(-) diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index af1a173b..560ad944 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -2,7 +2,6 @@ // flex: 1 1 0%; overflow-y: scroll; max-width: 100%; - width: #{1000 / $base-font-size}rem; min-height: #{400 / $base-font-size}rem; } diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 34c962a4..3dbde984 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -1,7 +1,6 @@ .sketches-table-container { overflow-y: scroll; max-width: 100%; - width: #{1000 / $base-font-size}rem; min-height: #{400 / $base-font-size}rem; } From b1bfb91f805fe1c53fdc0f4f9a016d88764112a4 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sat, 24 Aug 2019 12:38:42 +0200 Subject: [PATCH 077/322] Serve assets from /:username/assets, redirecting old path --- client/components/Nav.jsx | 2 +- .../components/createRedirectWithUsername.jsx | 28 +++++++++++++++++++ client/routes.jsx | 8 ++++-- server/routes/server.routes.js | 10 +++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 client/components/createRedirectWithUsername.jsx diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 253146b0..31b7e5b8 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -576,7 +576,7 @@ class Nav extends React.PureComponent {
  • { + React.useEffect(() => { + if (username == null) { + return; + } + + browserHistory.replace(url.replace(':username', username)); + }, [username]); + + return null; +}; + +function mapStateToProps(state) { + return { + username: state.user ? state.user.username : null, + }; +} + +const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); + +const createRedirectWithUsername = (url) => (props) => ; + +export default createRedirectWithUsername; diff --git a/client/routes.jsx b/client/routes.jsx index 0bcad82b..89db6977 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -10,7 +10,7 @@ import EmailVerificationView from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; import DashboardView from './modules/User/pages/DashboardView'; -// import SketchListView from './modules/Sketch/pages/SketchListView'; +import createRedirectWithUsername from './components/createRedirectWithUsername'; import { getUser } from './modules/User/actions'; import { stopSketch } from './modules/IDE/actions/ide'; @@ -36,11 +36,13 @@ const routes = store => ( - - + + + + diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index adade10d..41d7dc60 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -79,6 +79,16 @@ router.get('/assets', (req, res) => { } }); +router.get('/:username/assets', (req, res) => { + userExists(req.params.username, (exists) => { + const isLoggedInUser = req.user && req.user.username === req.params.username; + const canAccess = exists && isLoggedInUser; + return canAccess ? + res.send(renderIndex()) : + get404Sketch(html => res.send(html)); + }); +}); + router.get('/account', (req, res) => { if (req.user) { res.send(renderIndex()); From d44a058fd861b4f92ac5362e056be81ffd58405d Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 20:26:57 +0200 Subject: [PATCH 078/322] Public API: Create new project (fixes #1095) (#1106) * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour. --- jest.setup.js | 3 + .../controllers/__mocks__/aws.controller.js | 5 + .../__test__/project.controller.test.js | 155 ------- server/controllers/project.controller.js | 59 +-- .../__test__/createProject.test.js | 391 ++++++++++++++++++ .../__test__/deleteProject.test.js | 119 ++++++ .../__test__/getProjectsForUser.test.js | 151 +++++++ .../project.controller/createProject.js | 65 +++ .../project.controller/deleteProject.js | 57 +++ .../project.controller/getProjectsForUser.js | 72 ++++ .../user.controller/__tests__/apiKey.test.js | 182 ++++---- server/controllers/user.controller/apiKey.js | 106 +++-- server/domain-objects/Project.js | 133 ++++++ .../domain-objects/__test__/Project.test.js | 385 +++++++++++++++++ server/domain-objects/createDefaultFiles.js | 48 +++ server/models/__mocks__/project.js | 18 + server/models/__mocks__/user.js | 41 +- server/models/project.js | 37 +- server/routes/api.routes.js | 27 ++ server/scripts/examples-ml5.js | 342 +++++++-------- server/server.js | 4 +- server/utils/__mocks__/createId.js | 16 + server/utils/createApplicationErrorClass.js | 33 ++ server/utils/createId.js | 8 + 24 files changed, 1905 insertions(+), 552 deletions(-) create mode 100644 server/controllers/__mocks__/aws.controller.js delete mode 100644 server/controllers/__test__/project.controller.test.js create mode 100644 server/controllers/project.controller/__test__/createProject.test.js create mode 100644 server/controllers/project.controller/__test__/deleteProject.test.js create mode 100644 server/controllers/project.controller/__test__/getProjectsForUser.test.js create mode 100644 server/controllers/project.controller/deleteProject.js create mode 100644 server/controllers/project.controller/getProjectsForUser.js create mode 100644 server/domain-objects/Project.js create mode 100644 server/domain-objects/__test__/Project.test.js create mode 100644 server/domain-objects/createDefaultFiles.js create mode 100644 server/routes/api.routes.js create mode 100644 server/utils/__mocks__/createId.js create mode 100644 server/utils/createApplicationErrorClass.js create mode 100644 server/utils/createId.js diff --git a/jest.setup.js b/jest.setup.js index 7c9d2b77..da036785 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,5 +1,8 @@ import { configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-16' import '@babel/polyfill' +import mongoose from 'mongoose' + +mongoose.Promise = global.Promise; configure({ adapter: new Adapter() }) diff --git a/server/controllers/__mocks__/aws.controller.js b/server/controllers/__mocks__/aws.controller.js new file mode 100644 index 00000000..9e5709c4 --- /dev/null +++ b/server/controllers/__mocks__/aws.controller.js @@ -0,0 +1,5 @@ +export const getObjectKey = jest.mock(); +export const deleteObjectsFromS3 = jest.fn(); +export const signS3 = jest.fn(); +export const copyObjectInS3 = jest.fn(); +export const listObjectsInS3ForUser = jest.fn(); diff --git a/server/controllers/__test__/project.controller.test.js b/server/controllers/__test__/project.controller.test.js deleted file mode 100644 index e0852e61..00000000 --- a/server/controllers/__test__/project.controller.test.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @jest-environment node - */ -import { Response } from 'jest-express'; - -import { createMock } from '../../models/project'; -import createProject from '../project.controller/createProject'; - -jest.mock('../../models/project'); - -describe('project.controller', () => { - describe('createProject()', () => { - let ProjectMock; - - beforeEach(() => { - ProjectMock = createMock(); - }); - - afterEach(() => { - ProjectMock.restore(); - }); - - it('fails if create fails', (done) => { - const error = new Error('An error'); - - ProjectMock - .expects('create') - .rejects(error); - - const request = { user: {} }; - const response = new Response(); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalledWith({ success: false }); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - it('extracts parameters from request body', (done) => { - const request = { - user: { _id: 'abc123' }, - body: { - name: 'Wriggly worm', - files: [{ name: 'file.js', content: 'var hello = true;' }] - } - }; - const response = new Response(); - - - ProjectMock - .expects('create') - .withArgs({ - user: 'abc123', - name: 'Wriggly worm', - files: [{ name: 'file.js', content: 'var hello = true;' }] - }) - .resolves(); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalled(); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - // TODO: This should be extracted to a new model object - // so the controllers just have to call a single - // method for this operation - it('populates referenced user on project creation', (done) => { - const request = { user: { _id: 'abc123' } }; - const response = new Response(); - - const result = { - _id: 'abc123', - id: 'abc123', - name: 'Project name', - serveSecure: false, - files: [] - }; - - const resultWithUser = { - ...result, - user: {} - }; - - ProjectMock - .expects('create') - .withArgs({ user: 'abc123' }) - .resolves(result); - - ProjectMock - .expects('populate') - .withArgs(result) - .yields(null, resultWithUser) - .resolves(resultWithUser); - - const promise = createProject(request, response); - - function expectations() { - const doc = response.json.mock.calls[0][0]; - - expect(response.json).toHaveBeenCalled(); - - expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - it('fails if referenced user population fails', (done) => { - const request = { user: { _id: 'abc123' } }; - const response = new Response(); - - const result = { - _id: 'abc123', - id: 'abc123', - name: 'Project name', - serveSecure: false, - files: [] - }; - - const error = new Error('An error'); - - ProjectMock - .expects('create') - .resolves(result); - - ProjectMock - .expects('populate') - .yields(error) - .resolves(error); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalledWith({ success: false }); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - }); -}); diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index ae029918..bf59a865 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -2,7 +2,6 @@ import archiver from 'archiver'; import format from 'date-fns/format'; import isUrl from 'is-url'; import jsdom, { serializeDocument } from 'jsdom'; -import isBefore from 'date-fns/is_before'; import isAfter from 'date-fns/is_after'; import request from 'request'; import slugify from 'slugify'; @@ -10,9 +9,10 @@ import Project from '../models/project'; import User from '../models/user'; import { resolvePathToFile } from '../utils/filePath'; import generateFileSystemSafeName from '../utils/generateFileSystemSafeName'; -import { deleteObjectsFromS3, getObjectKey } from './aws.controller'; -export { default as createProject } from './project.controller/createProject'; +export { default as createProject, apiCreateProject } from './project.controller/createProject'; +export { default as deleteProject } from './project.controller/deleteProject'; +export { default as getProjectsForUser, apiGetProjectsForUser } from './project.controller/getProjectsForUser'; export function updateProject(req, res) { Project.findById(req.params.project_id, (findProjectErr, project) => { @@ -84,37 +84,6 @@ export function getProject(req, res) { }); } -function deleteFilesFromS3(files) { - deleteObjectsFromS3(files.filter((file) => { - if (file.url) { - if (!process.env.S3_DATE || ( - process.env.S3_DATE && - isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) { - return true; - } - } - return false; - }) - .map(file => getObjectKey(file.url))); -} - -export function deleteProject(req, res) { - Project.findById(req.params.project_id, (findProjectErr, project) => { - if (!project.user.equals(req.user._id)) { - res.status(403).json({ success: false, message: 'Session does not match owner of project.' }); - return; - } - deleteFilesFromS3(project.files); - Project.remove({ _id: req.params.project_id }, (removeProjectError) => { - if (removeProjectError) { - res.status(404).send({ message: 'Project with that id does not exist' }); - return; - } - res.json({ success: true }); - }); - }); -} - export function getProjectsForUserId(userId) { return new Promise((resolve, reject) => { Project.find({ user: userId }) @@ -157,10 +126,6 @@ export function getProjectAsset(req, res) { }); } -export function getProjectsForUserName(username) { - -} - export function getProjects(req, res) { if (req.user) { getProjectsForUserId(req.user._id) @@ -173,24 +138,6 @@ export function getProjects(req, res) { } } -export function getProjectsForUser(req, res) { - if (req.params.username) { - User.findOne({ username: req.params.username }, (err, user) => { - if (!user) { - res.status(404).json({ message: 'User with that username does not exist.' }); - return; - } - Project.find({ user: user._id }) - .sort('-createdAt') - .select('name files id createdAt updatedAt') - .exec((innerErr, projects) => res.json(projects)); - }); - } else { - // could just move this to client side - res.json([]); - } -} - export function projectExists(projectId, callback) { Project.findById(projectId, (err, project) => ( project ? callback(true) : callback(false) diff --git a/server/controllers/project.controller/__test__/createProject.test.js b/server/controllers/project.controller/__test__/createProject.test.js new file mode 100644 index 00000000..68025a0c --- /dev/null +++ b/server/controllers/project.controller/__test__/createProject.test.js @@ -0,0 +1,391 @@ +/** + * @jest-environment node + */ +import { Response } from 'jest-express'; + +import Project, { createMock, createInstanceMock } from '../../../models/project'; +import createProject, { apiCreateProject } from '../createProject'; + +jest.mock('../../../models/project'); + +describe('project.controller', () => { + describe('createProject()', () => { + let ProjectMock; + + beforeEach(() => { + ProjectMock = createMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + }); + + it('fails if create fails', (done) => { + const error = new Error('An error'); + + ProjectMock + .expects('create') + .rejects(error); + + const request = { user: {} }; + const response = new Response(); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalledWith({ success: false }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('extracts parameters from request body', (done) => { + const request = { + user: { _id: 'abc123' }, + body: { + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + } + }; + const response = new Response(); + + + ProjectMock + .expects('create') + .withArgs({ + user: 'abc123', + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + }) + .resolves(); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + // TODO: This should be extracted to a new model object + // so the controllers just have to call a single + // method for this operation + it('populates referenced user on project creation', (done) => { + const request = { user: { _id: 'abc123' } }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + const resultWithUser = { + ...result, + user: {} + }; + + ProjectMock + .expects('create') + .withArgs({ user: 'abc123' }) + .resolves(result); + + ProjectMock + .expects('populate') + .withArgs(result) + .yields(null, resultWithUser) + .resolves(resultWithUser); + + const promise = createProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if referenced user population fails', (done) => { + const request = { user: { _id: 'abc123' } }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + const error = new Error('An error'); + + ProjectMock + .expects('create') + .resolves(result); + + ProjectMock + .expects('populate') + .yields(error) + .resolves(error); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalledWith({ success: false }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); + + describe('apiCreateProject()', () => { + let ProjectMock; + let ProjectInstanceMock; + + beforeEach(() => { + ProjectMock = createMock(); + ProjectInstanceMock = createInstanceMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + ProjectInstanceMock.restore(); + }); + + it('returns 201 with id of created sketch', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: true, conflictingIds: [] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.status).toHaveBeenCalledWith(201); + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject({ + id: 'abc123' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if slug is not unique', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + slug: 'a-slug', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + // slug: 'a-slug', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: false, conflictingIds: ['cde123'] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.status).toHaveBeenCalledWith(409); + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject({ + message: 'Sketch Validation Failed', + detail: 'Slug "a-slug" is not unique. Check cde123' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if user does not have permission', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { + username: 'dana', + }, + body: { + name: 'My sketch', + slug: 'a-slug', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: true, conflictingIds: [] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(401); + expect(response.json).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns validation errors on files input', (done) => { + const request = { + user: { username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + files: { + 'index.html': { + // missing content or url + } + } + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).not.toBeNull(); + expect(responseBody.errors.length).toBe(1); + expect(responseBody.errors).toEqual([ + { name: 'index.html', message: 'missing \'url\' or \'content\'' } + ]); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('rejects file parameters not in object format', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).toBe('\'files\' must be an object'); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('rejects if files object in not provided', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'Wriggly worm', + // files: {} is missing + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).toBe('\'files\' must be an object'); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/__test__/deleteProject.test.js b/server/controllers/project.controller/__test__/deleteProject.test.js new file mode 100644 index 00000000..84a6824f --- /dev/null +++ b/server/controllers/project.controller/__test__/deleteProject.test.js @@ -0,0 +1,119 @@ +/** + * @jest-environment node + */ +import { Request, Response } from 'jest-express'; + +import Project, { createMock, createInstanceMock } from '../../../models/project'; +import User from '../../../models/user'; +import deleteProject from '../../project.controller/deleteProject'; +import { deleteObjectsFromS3 } from '../../aws.controller'; + + +jest.mock('../../../models/project'); +jest.mock('../../aws.controller'); + +describe('project.controller', () => { + describe('deleteProject()', () => { + let ProjectMock; + let ProjectInstanceMock; + + beforeEach(() => { + ProjectMock = createMock(); + ProjectInstanceMock = createInstanceMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + ProjectInstanceMock.restore(); + }); + + it('returns 403 if project is not owned by authenticated user', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: 'abc123' }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(project); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(403); + expect(response.json).toHaveBeenCalledWith({ + message: 'Authenticated user does not match owner of project' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 404 if project does not exist', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: 'abc123' }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(null); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + message: 'Project with that id does not exist' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('deletes project and dependent files from S3 ', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: user._id }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(project); + + ProjectInstanceMock.expects('remove') + .yields(); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).not.toHaveBeenCalled(); + expect(deleteObjectsFromS3).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/__test__/getProjectsForUser.test.js b/server/controllers/project.controller/__test__/getProjectsForUser.test.js new file mode 100644 index 00000000..05a0cf71 --- /dev/null +++ b/server/controllers/project.controller/__test__/getProjectsForUser.test.js @@ -0,0 +1,151 @@ +/** + * @jest-environment node + */ +import { Request, Response } from 'jest-express'; + +import { createMock } from '../../../models/user'; +import getProjectsForUser, { apiGetProjectsForUser } from '../../project.controller/getProjectsForUser'; + +jest.mock('../../../models/user'); +jest.mock('../../aws.controller'); + +describe('project.controller', () => { + let UserMock; + + beforeEach(() => { + UserMock = createMock(); + }); + + afterEach(() => { + UserMock.restore(); + }); + + describe('getProjectsForUser()', () => { + it('returns empty array user not supplied as parameter', (done) => { + const request = new Request(); + request.setParams({}); + const response = new Response(); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).toHaveBeenCalledWith([]); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 404 if user does not exist', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(null, null); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 500 on other errors', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(new Error(), null); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(500); + expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); + + describe('apiGetProjectsForUser()', () => { + it('returns validation error if user id not provided', (done) => { + const request = new Request(); + request.setParams({}); + const response = new Response(); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(422); + expect(response.json).toHaveBeenCalledWith({ + message: 'Username not provided' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + + it('returns 404 if user does not exist', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(null, null); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 500 on other errors', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(new Error(), null); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(500); + expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/createProject.js b/server/controllers/project.controller/createProject.js index 2cf34e32..e4040268 100644 --- a/server/controllers/project.controller/createProject.js +++ b/server/controllers/project.controller/createProject.js @@ -1,4 +1,5 @@ import Project from '../../models/project'; +import { toModel, FileValidationError, ProjectValidationError } from '../../domain-objects/Project'; export default function createProject(req, res) { let projectValues = { @@ -30,3 +31,67 @@ export default function createProject(req, res) { .then(populateUserData) .catch(sendFailure); } + +// TODO: What happens if you don't supply any files? +export function apiCreateProject(req, res) { + const params = Object.assign({ user: req.user._id }, req.body); + + function sendValidationErrors(err, type, code = 422) { + res.status(code).json({ + message: `${type} Validation Failed`, + detail: err.message, + errors: err.files, + }); + } + + // TODO: Error handling to match spec + function sendFailure(err) { + res.status(500).end(); + } + + function handleErrors(err) { + if (err instanceof FileValidationError) { + sendValidationErrors(err, 'File', err.code); + } else if (err instanceof ProjectValidationError) { + sendValidationErrors(err, 'Sketch', err.code); + } else { + sendFailure(); + } + } + + function checkUserHasPermission() { + if (req.user.username !== req.params.username) { + console.log('no permission'); + const error = new ProjectValidationError(`'${req.user.username}' does not have permission to create for '${req.params.username}'`); + error.code = 401; + + throw error; + } + } + + try { + checkUserHasPermission(); + + const model = toModel(params); + + return model.isSlugUnique() + .then(({ isUnique, conflictingIds }) => { + if (isUnique) { + return model.save() + .then((newProject) => { + res.status(201).json({ id: newProject.id }); + }); + } + + const error = new ProjectValidationError(`Slug "${model.slug}" is not unique. Check ${conflictingIds.join(', ')}`); + error.code = 409; + + throw error; + }) + .then(checkUserHasPermission) + .catch(handleErrors); + } catch (err) { + handleErrors(err); + return Promise.reject(err); + } +} diff --git a/server/controllers/project.controller/deleteProject.js b/server/controllers/project.controller/deleteProject.js new file mode 100644 index 00000000..dd980b23 --- /dev/null +++ b/server/controllers/project.controller/deleteProject.js @@ -0,0 +1,57 @@ +import isBefore from 'date-fns/is_before'; +import Project from '../../models/project'; +import { deleteObjectsFromS3, getObjectKey } from '../aws.controller'; +import createApplicationErrorClass from '../../utils/createApplicationErrorClass'; + +const ProjectDeletionError = createApplicationErrorClass('ProjectDeletionError'); + +function deleteFilesFromS3(files) { + deleteObjectsFromS3(files.filter((file) => { + if (file.url) { + if (!process.env.S3_DATE || ( + process.env.S3_DATE && + isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) { + return true; + } + } + return false; + }) + .map(file => getObjectKey(file.url))); +} + +export default function deleteProject(req, res) { + function sendFailure(error) { + res.status(error.code).json({ message: error.message }); + } + + function sendProjectNotFound() { + sendFailure(new ProjectDeletionError('Project with that id does not exist', { code: 404 })); + } + + function handleProjectDeletion(project) { + if (project == null) { + sendProjectNotFound(); + return; + } + + if (!project.user.equals(req.user._id)) { + sendFailure(new ProjectDeletionError('Authenticated user does not match owner of project', { code: 403 })); + return; + } + + deleteFilesFromS3(project.files); + + project.remove((removeProjectError) => { + if (removeProjectError) { + sendProjectNotFound(); + return; + } + + res.status(200).end(); + }); + } + + return Project.findById(req.params.project_id) + .then(handleProjectDeletion) + .catch(sendFailure); +} diff --git a/server/controllers/project.controller/getProjectsForUser.js b/server/controllers/project.controller/getProjectsForUser.js new file mode 100644 index 00000000..1d9a0e34 --- /dev/null +++ b/server/controllers/project.controller/getProjectsForUser.js @@ -0,0 +1,72 @@ +import Project from '../../models/project'; +import User from '../../models/user'; +import { toApi as toApiProjectObject } from '../../domain-objects/Project'; +import createApplicationErrorClass from '../../utils/createApplicationErrorClass'; + +const UserNotFoundError = createApplicationErrorClass('UserNotFoundError'); + +function getProjectsForUserName(username) { + return new Promise((resolve, reject) => { + User.findOne({ username }, (err, user) => { + if (err) { + reject(err); + return; + } + + if (!user) { + reject(new UserNotFoundError()); + return; + } + + Project.find({ user: user._id }) + .sort('-createdAt') + .select('name files id createdAt updatedAt') + .exec((innerErr, projects) => { + if (innerErr) { + reject(innerErr); + return; + } + + resolve(projects); + }); + }); + }); +} + +export default function getProjectsForUser(req, res) { + if (req.params.username) { + return getProjectsForUserName(req.params.username) + .then(projects => res.json(projects)) + .catch((err) => { + if (err instanceof UserNotFoundError) { + res.status(404).json({ message: 'User with that username does not exist.' }); + } else { + res.status(500).json({ message: 'Error fetching projects' }); + } + }); + } + + // could just move this to client side + res.status(200).json([]); + return Promise.resolve(); +} + +export function apiGetProjectsForUser(req, res) { + if (req.params.username) { + return getProjectsForUserName(req.params.username) + .then((projects) => { + const asApiObjects = projects.map(p => toApiProjectObject(p)); + res.json({ sketches: asApiObjects }); + }) + .catch((err) => { + if (err instanceof UserNotFoundError) { + res.status(404).json({ message: 'User with that username does not exist.' }); + } else { + res.status(500).json({ message: 'Error fetching projects' }); + } + }); + } + + res.status(422).json({ message: 'Username not provided' }); + return Promise.resolve(); +} diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js index a496373b..7e396288 100644 --- a/server/controllers/user.controller/__tests__/apiKey.test.js +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -1,73 +1,40 @@ /* @jest-environment node */ import last from 'lodash/last'; +import { Request, Response } from 'jest-express'; + +import User, { createMock, createInstanceMock } from '../../../models/user'; import { createApiKey, removeApiKey } from '../../user.controller/apiKey'; jest.mock('../../../models/user'); -/* - Create a mock object representing an express Response -*/ -const createResponseMock = function createResponseMock(done) { - const json = jest.fn(() => { - if (done) { done(); } - }); - - const status = jest.fn(() => ({ json })); - - return { - status, - json - }; -}; - -/* - Create a mock of the mongoose User model -*/ -const createUserMock = function createUserMock() { - const apiKeys = []; - let nextId = 0; - - apiKeys.push = ({ label, hashedKey }) => { - const id = nextId; - nextId += 1; - const publicFields = { id, label }; - const allFields = { ...publicFields, hashedKey }; - - Object.defineProperty(allFields, 'toObject', { - value: () => publicFields, - enumerable: false - }); - - return Array.prototype.push.call(apiKeys, allFields); - }; - - apiKeys.pull = ({ _id }) => { - const index = apiKeys.findIndex(({ id }) => id === _id); - return apiKeys.splice(index, 1); - }; - - return { - apiKeys, - save: jest.fn(callback => callback()) - }; -}; - -const User = require('../../../models/user').default; - describe('user.controller', () => { + let UserMock; + let UserInstanceMock; + beforeEach(() => { - User.__setFindById(null, null); + UserMock = createMock(); + UserInstanceMock = createInstanceMock(); }); + afterEach(() => { + UserMock.restore(); + UserInstanceMock.restore(); + }); + + describe('createApiKey', () => { it('returns an error if user doesn\'t exist', () => { const request = { user: { id: '1234' } }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, null); createApiKey(request, response); - expect(User.findById.mock.calls[0][0]).toBe('1234'); expect(response.status).toHaveBeenCalledWith(404); expect(response.json).toHaveBeenCalledWith({ error: 'User not found' @@ -75,10 +42,13 @@ describe('user.controller', () => { }); it('returns an error if label not provided', () => { - User.__setFindById(undefined, createUserMock()); - const request = { user: { id: '1234' }, body: {} }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, new User()); createApiKey(request, response); @@ -89,46 +59,59 @@ describe('user.controller', () => { }); it('returns generated API key to the user', (done) => { - let response; + const request = new Request(); + request.setBody({ label: 'my key' }); + request.user = { id: '1234' }; - const request = { - user: { id: '1234' }, - body: { label: 'my key' } - }; + const response = new Response(); - const user = createUserMock(); + const user = new User(); - const checkExpecations = () => { + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); + + UserInstanceMock.expects('save') + .yields(); + + function expectations() { const lastKey = last(user.apiKeys); expect(lastKey.label).toBe('my key'); expect(typeof lastKey.hashedKey).toBe('string'); - expect(response.json).toHaveBeenCalledWith({ - apiKeys: [ - { id: 0, label: 'my key', token: lastKey.hashedKey } - ] + const responseData = response.json.mock.calls[0][0]; + + expect(responseData.apiKeys.length).toBe(1); + expect(responseData.apiKeys[0]).toMatchObject({ + label: 'my key', + token: lastKey.hashedKey, + lastUsedAt: undefined, + createdAt: undefined }); done(); - }; + } - response = createResponseMock(checkExpecations); + const promise = createApiKey(request, response); - User.__setFindById(undefined, user); - - createApiKey(request, response); + promise.then(expectations, expectations).catch(expectations); }); }); describe('removeApiKey', () => { it('returns an error if user doesn\'t exist', () => { const request = { user: { id: '1234' } }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, null); removeApiKey(request, response); - expect(User.findById.mock.calls[0][0]).toBe('1234'); expect(response.status).toHaveBeenCalledWith(404); expect(response.json).toHaveBeenCalledWith({ error: 'User not found' @@ -140,10 +123,13 @@ describe('user.controller', () => { user: { id: '1234' }, params: { keyId: 'not-a-real-key' } }; - const response = createResponseMock(); + const response = new Response(); + const user = new User(); - const user = createUserMock(); - User.__setFindById(undefined, user); + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); removeApiKey(request, response); @@ -153,27 +139,41 @@ describe('user.controller', () => { }); }); - it.skip('removes key if it exists', () => { - const request = { - user: { id: '1234' }, - params: { keyId: 0 } - }; - const response = createResponseMock(); - - const user = createUserMock(); - + it('removes key if it exists', () => { + const user = new User(); user.apiKeys.push({ label: 'first key' }); // id 0 user.apiKeys.push({ label: 'second key' }); // id 1 - User.__setFindById(undefined, user); + const firstKeyId = user.apiKeys[0]._id.toString(); + const secondKeyId = user.apiKeys[1]._id.toString(); + + const request = { + user: { id: '1234' }, + params: { keyId: firstKeyId } + }; + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); + + UserInstanceMock + .expects('save') + .yields(); removeApiKey(request, response); expect(response.status).toHaveBeenCalledWith(200); - expect(response.json).toHaveBeenCalledWith({ - apiKeys: [ - { id: 1, label: 'second key' } - ] + + const responseData = response.json.mock.calls[0][0]; + + expect(responseData.apiKeys.length).toBe(1); + expect(responseData.apiKeys[0]).toMatchObject({ + id: secondKeyId, + label: 'second key', + lastUsedAt: undefined, + createdAt: undefined }); }); }); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js index eed22e6f..5d45b123 100644 --- a/server/controllers/user.controller/apiKey.js +++ b/server/controllers/user.controller/apiKey.js @@ -19,67 +19,85 @@ function generateApiKey() { } export function createApiKey(req, res) { - User.findById(req.user.id, async (err, user) => { - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; + return new Promise((resolve, reject) => { + function sendFailure(code, error) { + res.status(code).json({ error }); + resolve(); } - if (!req.body.label) { - res.status(400).json({ error: 'Expected field \'label\' was not present in request body' }); - return; - } - - const keyToBeHashed = await generateApiKey(); - - const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); - - user.save((saveErr) => { - if (saveErr) { - res.status(500).json({ error: saveErr }); + User.findById(req.user.id, async (err, user) => { + if (!user) { + sendFailure(404, 'User not found'); return; } - const apiKeys = user.apiKeys - .map((apiKey, index) => { - const fields = apiKey.toObject(); - const shouldIncludeToken = index === addedApiKeyIndex - 1; + if (!req.body.label) { + sendFailure(400, 'Expected field \'label\' was not present in request body'); + return; + } - return shouldIncludeToken ? - { ...fields, token: keyToBeHashed } : - fields; - }); + const keyToBeHashed = await generateApiKey(); - res.json({ apiKeys }); + const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); + + user.save((saveErr) => { + if (saveErr) { + sendFailure(500, saveErr); + return; + } + + const apiKeys = user.apiKeys + .map((apiKey, index) => { + const fields = apiKey.toObject(); + const shouldIncludeToken = index === addedApiKeyIndex - 1; + + return shouldIncludeToken ? + { ...fields, token: keyToBeHashed } : + fields; + }); + + res.json({ apiKeys }); + resolve(); + }); }); }); } export function removeApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); - if (!keyToDelete) { - res.status(404).json({ error: 'Key does not exist for user' }); - return; + return new Promise((resolve, reject) => { + function sendFailure(code, error) { + res.status(code).json({ error }); + resolve(); } - user.apiKeys.pull({ _id: req.params.keyId }); - - user.save((saveErr) => { - if (saveErr) { - res.status(500).json({ error: saveErr }); + User.findById(req.user.id, (err, user) => { + if (err) { + sendFailure(500, err); return; } - res.status(200).json({ apiKeys: user.apiKeys }); + if (!user) { + sendFailure(404, 'User not found'); + return; + } + + const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + if (!keyToDelete) { + sendFailure(404, 'Key does not exist for user'); + return; + } + + user.apiKeys.pull({ _id: req.params.keyId }); + + user.save((saveErr) => { + if (saveErr) { + sendFailure(500, saveErr); + return; + } + + res.status(200).json({ apiKeys: user.apiKeys }); + resolve(); + }); }); }); } diff --git a/server/domain-objects/Project.js b/server/domain-objects/Project.js new file mode 100644 index 00000000..f0de023f --- /dev/null +++ b/server/domain-objects/Project.js @@ -0,0 +1,133 @@ +import isPlainObject from 'lodash/isPlainObject'; +import pick from 'lodash/pick'; +import Project from '../models/project'; +import createId from '../utils/createId'; +import createApplicationErrorClass from '../utils/createApplicationErrorClass'; +import createDefaultFiles from './createDefaultFiles'; + +export const FileValidationError = createApplicationErrorClass('FileValidationError'); +export const ProjectValidationError = createApplicationErrorClass('ProjectValidationError'); + +/** + * This converts between a mongoose Project model + * and the public API Project object properties + * + */ +export function toApi(model) { + return { + id: model.id, + name: model.name, + }; +} + +/** + * Transforms a tree of files matching the APIs DirectoryContents + * format into the data structure stored in mongodb + * + * - flattens the tree into an array of file/folders + * - each file/folder gets a generated BSON-ID + * - each folder has a `children` array containing the IDs of it's children + */ +function transformFilesInner(tree = {}, parentNode) { + const files = []; + const errors = []; + + Object.entries(tree).forEach(([name, params]) => { + const id = createId(); + const isFolder = params.files != null; + + if (isFolder) { + const folder = { + _id: id, + name, + fileType: 'folder', + children: [] // Initialise an empty folder + }; + + files.push(folder); + + // The recursion will return a list of child files/folders + // It will also push the child's id into `folder.children` + const subFolder = transformFilesInner(params.files, folder); + files.push(...subFolder.files); + errors.push(...subFolder.errors); + } else { + const file = { + _id: id, + name, + fileType: 'file' + }; + + if (typeof params.url === 'string') { + file.url = params.url; + } else if (typeof params.content === 'string') { + file.content = params.content; + } else { + errors.push({ name, message: 'missing \'url\' or \'content\'' }); + } + + files.push(file); + } + + // Push this child's ID onto it's parent's list + // of children + if (parentNode != null) { + parentNode.children.push(id); + } + }); + + return { files, errors }; +} + +export function transformFiles(tree = {}) { + const withRoot = { + root: { + files: tree + } + }; + + const { files, errors } = transformFilesInner(withRoot); + + if (errors.length > 0) { + const message = `${errors.length} files failed validation. See error.files for individual errors. + + Errors: + ${errors.map(e => `* ${e.name}: ${e.message}`).join('\n')} +`; + const error = new FileValidationError(message); + error.files = errors; + + throw error; + } + + return files; +} + +export function containsRootHtmlFile(tree) { + return Object.keys(tree).find(name => /\.html$/.test(name)) != null; +} + +/** + * This converts between the public API's Project object + * properties and a mongoose Project model + * + */ +export function toModel(object) { + let files = []; + let tree = object.files; + + if (isPlainObject(tree)) { + if (!containsRootHtmlFile(tree)) { + tree = Object.assign(createDefaultFiles(), tree); + } + + files = transformFiles(tree); + } else { + throw new FileValidationError('\'files\' must be an object'); + } + + const projectValues = pick(object, ['user', 'name', 'slug']); + projectValues.files = files; + + return new Project(projectValues); +} diff --git a/server/domain-objects/__test__/Project.test.js b/server/domain-objects/__test__/Project.test.js new file mode 100644 index 00000000..0f3c9dd9 --- /dev/null +++ b/server/domain-objects/__test__/Project.test.js @@ -0,0 +1,385 @@ +import find from 'lodash/find'; + +import { containsRootHtmlFile, toModel, transformFiles, FileValidationError } from '../Project'; + +jest.mock('../../utils/createId'); + +// TODO: File name validation +// TODO: File extension validation +// +describe('domain-objects/Project', () => { + describe('containsRootHtmlFile', () => { + it('returns true for at least one root .html', () => { + expect(containsRootHtmlFile({ 'index.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'another-one.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe(true); + }); + + it('returns false anything else', () => { + expect(containsRootHtmlFile({ 'sketch.js': {} })).toBe(false); + }); + + it('ignores nested html', () => { + expect(containsRootHtmlFile({ + examples: { + files: { + 'index.html': {} + } + } + })).toBe(false); + }); + }); + + describe('toModel', () => { + it('filters extra properties', () => { + const params = { + name: 'My sketch', + extraThing: 'oopsie', + files: {} + }; + + const model = toModel(params); + + expect(model.name).toBe('My sketch'); + expect(model.extraThing).toBeUndefined(); + }); + + it('throws FileValidationError', () => { + const params = { + files: { + 'index.html': {} // missing content or url + } + }; + + expect(() => toModel(params)).toThrowError(FileValidationError); + }); + + it('throws if files is not an object', () => { + const params = { + files: [] + }; + + expect(() => toModel(params)).toThrowError(FileValidationError); + }); + + it('creates default index.html and dependent files if no root .html is provided', () => { + const params = { + files: {} + }; + + const { files } = toModel(params); + + expect(files.length).toBe(4); + expect(find(files, { name: 'index.html' })).not.toBeUndefined(); + expect(find(files, { name: 'sketch.js' })).not.toBeUndefined(); + expect(find(files, { name: 'style.css' })).not.toBeUndefined(); + }); + + it('does not create default files if any root .html is provided', () => { + const params = { + files: { + 'example.html': { + content: 'Hello!' + } + } + }; + + const { files } = toModel(params); + + expect(files.length).toBe(2); + expect(find(files, { name: 'example.html' })).not.toBeUndefined(); + expect(find(files, { name: 'index.html' })).toBeUndefined(); + expect(find(files, { name: 'sketch.js' })).toBeUndefined(); + expect(find(files, { name: 'style.css' })).toBeUndefined(); + }); + + it('does not overwrite default CSS and JS of the same name if provided', () => { + const params = { + files: { + 'sketch.js': { + content: 'const sketch = true;' + }, + 'style.css': { + content: 'body { outline: 10px solid red; }' + } + } + }; + + const { files } = toModel(params); + + expect(files.length).toBe(4); + expect(find(files, { name: 'index.html' })).not.toBeUndefined(); + + const sketchFile = find(files, { name: 'sketch.js' }); + expect(sketchFile.content).toBe('const sketch = true;'); + + const cssFile = find(files, { name: 'style.css' }); + expect(cssFile.content).toBe('body { outline: 10px solid red; }'); + }); + }); +}); + +describe('transformFiles', () => { + beforeEach(() => { + // eslint-disable-next-line global-require + const { resetMockCreateId } = require('../../utils/createId'); + + resetMockCreateId(); + }); + + it('creates an empty root with no data', () => { + const tree = {}; + + expect(transformFiles(tree)).toEqual([{ + _id: '0', + fileType: 'folder', + name: 'root', + children: [] + }]); + }); + + it('converts tree-shaped files into list', () => { + const tree = { + 'index.html': { + content: 'some contents', + } + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + content: 'some contents', + fileType: 'file', + name: 'index.html' + } + ]); + }); + + it('uses file url over content', () => { + const tree = { + 'script.js': { + url: 'http://example.net/something.js' + } + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + url: 'http://example.net/something.js', + fileType: 'file', + name: 'script.js' + } + ]); + }); + + it('creates folders', () => { + const tree = { + 'a-folder': { + files: {} + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + children: [], + fileType: 'folder', + name: 'a-folder' + } + ]); + }); + + it('walks the tree processing files', () => { + const tree = { + 'index.html': { + content: 'some contents', + }, + 'a-folder': { + files: { + 'data.csv': { + content: 'this,is,data' + }, + 'another.txt': { + content: 'blah blah' + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1', '2'] + }, + { + _id: '1', + name: 'index.html', + fileType: 'file', + content: 'some contents' + }, + { + _id: '2', + name: 'a-folder', + fileType: 'folder', + children: ['3', '4'] + }, + { + _id: '3', + name: 'data.csv', + fileType: 'file', + content: 'this,is,data' + }, + { + _id: '4', + name: 'another.txt', + fileType: 'file', + content: 'blah blah' + } + ]); + }); + + it('handles deep nesting', () => { + const tree = { + first: { + files: { + second: { + files: { + third: { + files: { + 'hello.js': { + content: 'world!' + } + } + } + } + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + name: 'first', + fileType: 'folder', + children: ['2'] + }, + { + _id: '2', + name: 'second', + fileType: 'folder', + children: ['3'] + }, + { + _id: '3', + name: 'third', + fileType: 'folder', + children: ['4'] + }, + { + _id: '4', + name: 'hello.js', + fileType: 'file', + content: 'world!' + } + ]); + }); + + + it('allows duplicate names in different folder', () => { + const tree = { + 'index.html': { + content: 'some contents', + }, + 'data': { + files: { + 'index.html': { + content: 'different file' + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1', '2'] + }, + { + _id: '1', + name: 'index.html', + fileType: 'file', + content: 'some contents' + }, + { + _id: '2', + name: 'data', + fileType: 'folder', + children: ['3'] + }, + { + _id: '3', + name: 'index.html', + fileType: 'file', + content: 'different file' + } + ]); + }); + + it('validates files', () => { + const tree = { + 'index.html': {} // missing `content` + }; + + expect(() => transformFiles(tree)).toThrowError(FileValidationError); + }); + + it('collects all file validation errors', () => { + const tree = { + 'index.html': {}, // missing `content` + 'something.js': {} // also missing `content` + }; + + try { + transformFiles(tree); + + // Should not get here + throw new Error('should have thrown before this point'); + } catch (err) { + expect(err).toBeInstanceOf(FileValidationError); + expect(err.files).toEqual([ + { name: 'index.html', message: 'missing \'url\' or \'content\'' }, + { name: 'something.js', message: 'missing \'url\' or \'content\'' } + ]); + } + }); +}); diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js new file mode 100644 index 00000000..9164c12a --- /dev/null +++ b/server/domain-objects/createDefaultFiles.js @@ -0,0 +1,48 @@ +const defaultSketch = `function setup() { + createCanvas(400, 400); +} + +function draw() { + background(220); +}`; + +const defaultHTML = + ` + + + + + + + + + + + + + +`; + +const defaultCSS = + `html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} +`; + +export default function createDefaultFiles() { + return { + 'index.html': { + content: defaultHTML + }, + 'style.css': { + content: defaultCSS + }, + 'sketch.js': { + content: defaultSketch + } + }; +} diff --git a/server/models/__mocks__/project.js b/server/models/__mocks__/project.js index 7260fcfd..2590046f 100644 --- a/server/models/__mocks__/project.js +++ b/server/models/__mocks__/project.js @@ -11,6 +11,24 @@ export function createMock() { return sinon.mock(Project); } +// Wraps the Project.prototype i.e. the +// instance methods in a mock so +// Project.save() can be mocked +export function createInstanceMock() { + // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro + Object.defineProperty(Project.prototype, 'save', { + value: Project.prototype.save, + configurable: true, + }); + + Object.defineProperty(Project.prototype, 'remove', { + value: Project.prototype.remove, + configurable: true, + }); + + return sinon.mock(Project.prototype); +} + // Re-export the model, it will be // altered by mockingoose whenever // we call methods on the MockConfig diff --git a/server/models/__mocks__/user.js b/server/models/__mocks__/user.js index 585d8b67..fcd73f32 100644 --- a/server/models/__mocks__/user.js +++ b/server/models/__mocks__/user.js @@ -1,12 +1,31 @@ -let __err = null; -let __user = null; +import sinon from 'sinon'; +import 'sinon-mongoose'; -export default { - __setFindById(err, user) { - __err = err; - __user = user; - }, - findById: jest.fn(async (id, callback) => { - callback(__err, __user); - }) -}; +// Import the actual model to be mocked +const User = jest.requireActual('../user').default; + +// Wrap User in a sinon mock +// The returned object is used to configure +// the mocked model's behaviour +export function createMock() { + return sinon.mock(User); +} + +// Wraps the User.prototype i.e. the +// instance methods in a mock so +// User.save() can be mocked +export function createInstanceMock() { + // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro + Object.defineProperty(User.prototype, 'save', { + value: User.prototype.save, + configurable: true, + }); + + return sinon.mock(User.prototype); +} + + +// Re-export the model, it will be +// altered by mockingoose whenever +// we call methods on the MockConfig +export default User; diff --git a/server/models/project.js b/server/models/project.js index 8642bc90..bf8c992e 100644 --- a/server/models/project.js +++ b/server/models/project.js @@ -49,8 +49,43 @@ projectSchema.set('toJSON', { projectSchema.pre('save', function generateSlug(next) { const project = this; - project.slug = slugify(project.name, '_'); + + if (!project.slug) { + project.slug = slugify(project.name, '_'); + } + return next(); }); +/** + * Check if slug is unique for this user's projects + */ +projectSchema.methods.isSlugUnique = async function isSlugUnique(cb) { + const project = this; + const hasCallback = typeof cb === 'function'; + + try { + const docsWithSlug = await project.model('Project') + .find({ user: project.user, slug: project.slug }, '_id') + .exec(); + + const result = { + isUnique: docsWithSlug.length === 0, + conflictingIds: docsWithSlug.map(d => d._id) || [] + }; + + if (hasCallback) { + cb(null, result); + } + + return result; + } catch (err) { + if (hasCallback) { + cb(err, null); + } + + throw err; + } +}; + export default mongoose.model('Project', projectSchema); diff --git a/server/routes/api.routes.js b/server/routes/api.routes.js new file mode 100644 index 00000000..60be3494 --- /dev/null +++ b/server/routes/api.routes.js @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import passport from 'passport'; +import * as ProjectController from '../controllers/project.controller'; + +const router = new Router(); + +router.get( + '/:username/sketches', + passport.authenticate('basic', { session: false }), + ProjectController.apiGetProjectsForUser +); + +router.post( + '/:username/sketches', + passport.authenticate('basic', { session: false }), + ProjectController.apiCreateProject +); + +// NOTE: Currently :username will not be checked for ownership +// only the project's owner in the database. +router.delete( + '/:username/sketches/:project_id', + passport.authenticate('basic', { session: false }), + ProjectController.deleteProject +); + +export default router; diff --git a/server/scripts/examples-ml5.js b/server/scripts/examples-ml5.js index 0721fe54..51e1fe5d 100644 --- a/server/scripts/examples-ml5.js +++ b/server/scripts/examples-ml5.js @@ -1,11 +1,7 @@ import fs from 'fs'; import rp from 'request-promise'; import Q from 'q'; -import mongoose from 'mongoose'; -import objectID from 'bson-objectid'; -import shortid from 'shortid'; -import User from '../models/user'; -import Project from '../models/project'; +import { ok } from 'assert'; // TODO: Change branchName if necessary const branchName = 'release'; @@ -14,11 +10,20 @@ const baseUrl = 'https://api.github.com/repos/ml5js/ml5-examples/contents'; const clientId = process.env.GITHUB_ID; const clientSecret = process.env.GITHUB_SECRET; const editorUsername = process.env.ML5_EXAMPLES_USERNAME; +const personalAccessToken = process.env.EDITOR_API_ACCESS_TOKEN; +const editorApiUrl = process.env.EDITOR_API_URL; const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; -const requestOptions = { +ok(clientId, 'GITHUB_ID is required'); +ok(clientSecret, 'GITHUB_SECRET is required'); +ok(editorUsername, 'ML5_EXAMPLES_USERNAME is required'); +ok(personalAccessToken, 'EDITOR_API_ACCESS_TOKEN is required'); +ok(editorApiUrl, 'EDITOR_API_URL is required'); + +// +const githubRequestOptions = { url: baseUrl, qs: { client_id: clientId, @@ -29,14 +34,15 @@ const requestOptions = { json: true }; -const mongoConnectionString = process.env.MONGO_URL; -mongoose.connect(mongoConnectionString, { - useMongoClient: true -}); -mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); - process.exit(1); -}); +const editorRequestOptions = { + url: `${editorApiUrl}/${editorUsername}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from(`${editorUsername}:${personalAccessToken}`).toString('base64')}` + }, + json: true +}; /** * --------------------------------------------------------- @@ -51,12 +57,52 @@ function flatten(list) { return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); } +/** + * Fetch data for a single HTML/JS file, or return + * an url to the file's CDN location + */ +async function fetchFileContent(item) { + const { name } = item; + const file = { url: item.url }; + + // if it is an html or js file + if ( + (file.url != null && name.endsWith('.html')) || + name.endsWith('.js') + ) { + const options = Object.assign({}, githubRequestOptions); + options.url = `${file.url}`; + + if ( + options.url !== undefined || + options.url !== null || + options.url !== '' + ) { + file.content = await rp(options); + // NOTE: remove the URL property if there's content + // Otherwise the p5 editor will try to pull from that url + if (file.content !== null) delete file.url; + } + + return file; + // if it is NOT an html or js file + } + + if (file.url) { + const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${file.url.split(branchName)[1]}`; + file.url = cdnRef; + } + + return file; +} + + /** * STEP 1: Get the top level cateogories */ async function getCategories() { try { - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}/p5js${branchRef}`; const results = await rp(options); @@ -76,13 +122,18 @@ async function getCategoryExamples(sketchRootList) { const output = []; const sketchRootCategories = sketchRootList.map(async (categories) => { // let options = Object.assign({url: `${requestOptions.url}/${categories.path}${branchRef}`}, requestOptions) - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}${categories.path}${branchRef}`; // console.log(options) const sketchDirs = await rp(options); - const result = flatten(sketchDirs); - return result; + try { + const result = flatten(sketchDirs); + + return result; + } catch (err) { + return []; + } }); const sketchList = await Q.all(sketchRootCategories); @@ -107,7 +158,7 @@ async function traverseSketchTree(parentObject) { return output; } // let options = `https://api.github.com/repos/ml5js/ml5-examples/contents/${sketches.path}${branchRef}` - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}${parentObject.path}${branchRef}`; output.tree = await rp(options); @@ -124,7 +175,6 @@ async function traverseSketchTree(parentObject) { * @param {*} categoryExamples - all of the categories in an array */ async function traverseSketchTreeAll(categoryExamples) { - // const sketches = categoryExamples.map(async sketch => await traverseSketchTree(sketch)); const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch)); const result = await Q.all(sketches); @@ -139,37 +189,24 @@ function traverseAndFormat(parentObject) { const parent = Object.assign({}, parentObject); if (!parentObject.tree) { - const newid = objectID().toHexString(); // returns the files return { name: parent.name, - url: parent.download_url, - content: null, - id: newid, - _id: newid, - fileType: 'file' + url: parent.download_url }; } const subdir = parentObject.tree.map((item) => { - const newid = objectID().toHexString(); if (!item.tree) { // returns the files return { name: item.name, - url: item.download_url, - content: null, - id: newid, - _id: newid, - fileType: 'file' + url: item.download_url }; } const feat = { name: item.name, - id: newid, - _id: newid, - fileType: 'folder', children: traverseAndFormat(item) }; return feat; @@ -178,69 +215,27 @@ function traverseAndFormat(parentObject) { } /** - * Traverse the tree and flatten for project.files[] + * Traverse the tree and download all the content, + * transforming into an object keyed by file/directory name * @param {*} projectFileTree */ -function traverseAndFlatten(projectFileTree) { - const r = objectID().toHexString(); +async function traverseAndDownload(projectFileTree) { + return projectFileTree.reduce( + async (previousPromise, item, idx) => { + const result = await previousPromise; - const projectRoot = { - name: 'root', - id: r, - _id: r, - children: [], - fileType: 'folder' - }; - - let currentParent; - - const output = projectFileTree.reduce( - (result, item, idx) => { - if (idx < projectFileTree.length) { - projectRoot.children.push(item.id); - } - - if (item.fileType === 'file') { - if (item.name === 'sketch.js') { - item.isSelectedFile = true; - } - result.push(item); - } - - // here's where the magic happens *twinkles* - if (item.fileType === 'folder') { - // recursively go down the tree of children - currentParent = traverseAndFlatten(item.children); - // the above will return an array of the children files - // concatenate that with the results - result = result.concat(currentParent); // eslint-disable-line no-param-reassign - // since we want to get the children ids, - // we can map the child ids to the current item - // then push that to our result array to get - // our flat files array. - item.children = item.children.map(child => child.id); - result.push(item); + if (Array.isArray(item.children)) { + result[item.name] = { + files: await traverseAndDownload(item.children) + }; + } else { + result[item.name] = await fetchFileContent(item); } return result; }, - [projectRoot] + {} ); - - // Kind of hacky way to remove all roots other than the starting one - let counter = 0; - output.forEach((item, idx) => { - if (item.name === 'root') { - if (counter === 0) { - counter += 1; - } else { - output.splice(idx, 1); - counter += 1; - } - } - }); - - return output; } /** @@ -249,16 +244,14 @@ function traverseAndFlatten(projectFileTree) { * @param {*} sketch * @param {*} user */ -function formatSketchForStorage(sketch, user) { - const newProject = new Project({ - _id: shortid.generate(), +async function formatSketchForStorage(sketch, user) { + const newProject = { name: sketch.name, - user: user._id, - files: [] // <== add files to this array as file objects and add _id reference to children of root - }); + files: {} // <== add files to this object + }; let projectFiles = traverseAndFormat(sketch); - projectFiles = traverseAndFlatten(projectFiles); + projectFiles = await traverseAndDownload(projectFiles); newProject.files = projectFiles; return newProject; } @@ -271,68 +264,50 @@ function formatSketchForStorageAll(sketchWithItems, user) { sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user)); - return sketchList; + return Promise.all(sketchList); } /** - * Get all the content for the relevant files in project.files[] - * @param {*} projectObject + * Fetch a list of all projects from the API */ -async function fetchSketchContent(projectObject) { - const output = Object.assign({}, JSON.parse(JSON.stringify(projectObject))); +async function getProjectsList() { + const options = Object.assign({}, editorRequestOptions); + options.url = `${options.url}/sketches`; - const newFiles = output.files.map(async (item, i) => { - // if it is an html or js file - if ( - (item.fileType === 'file' && item.name.endsWith('.html')) || - item.name.endsWith('.js') - ) { - const options = Object.assign({}, requestOptions); - options.url = `${item.url}`; + const results = await rp(options); - if ( - options.url !== undefined || - options.url !== null || - options.url !== '' - ) { - item.content = await rp(options); - // NOTE: remove the URL property if there's content - // Otherwise the p5 editor will try to pull from that url - if (item.content !== null) delete item.url; - } - - return item; - // if it is NOT an html or js file - } - - if (item.url) { - const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${ - item.url.split(branchName)[1] - }`; - item.content = cdnRef; - item.url = cdnRef; - } - - return item; - }); - - output.files = await Q.all(newFiles); - return output; + return results.sketches; } /** - * STEP 5 - * Get all the content for the relevant files in project.files[] for all sketches - * @param {*} formattedSketchList + * Delete a project */ -async function fetchSketchContentAll(formattedSketchList) { - let output = formattedSketchList.slice(0); +async function deleteProject(project) { + const options = Object.assign({}, editorRequestOptions); + options.method = 'DELETE'; + options.url = `${options.url}/sketches/${project.id}`; - output = output.map(async item => fetchSketchContent(item)); + const results = await rp(options); - output = await Q.all(output); + return results; +} - return output; +/** + * Create a new project + */ +async function createProject(project) { + try { + const options = Object.assign({}, editorRequestOptions); + options.method = 'POST'; + options.url = `${options.url}/sketches`; + options.body = project; + + const results = await rp(options); + + return results; + } catch (err) { + throw err; + } } /** @@ -342,43 +317,34 @@ async function fetchSketchContentAll(formattedSketchList) { * @param {*} user */ async function createProjectsInP5User(filledProjectList, user) { - const userProjects = await Project.find({ user: user._id }); - const removeProjects = userProjects.map(async project => Project.remove({ _id: project._id })); - await Q.all(removeProjects); - console.log('deleted old projects!'); + console.log('Finding existing projects...'); - const newProjects = filledProjectList.map(async (project) => { - const item = new Project(project); - console.log(`saving ${project.name}`); - await item.save(); - }); - await Q.all(newProjects); - console.log(`Projects saved to User: ${editorUsername}!`); -} + const existingProjects = await getProjectsList(); -/** - * STEP 0 - * CHECK if user exists, ifnot create one - * - */ -async function checkP5User() { - const user = await User.findOne({ username: editorUsername }); + console.log(`Will delete ${existingProjects.length} projects`); - if (!user) { - const ml5user = new User({ - username: editorUsername, - email: process.env.ML5_EXAMPLES_EMAIL, - password: process.env.ML5_EXAMPLES_PASS - }); - - await ml5user.save((saveErr) => { - if (saveErr) throw saveErr; - console.log(`Created a user p5${ml5user}`); + try { + await Q.all(existingProjects.map(deleteProject)); + console.log('deleted old projects!'); + } catch (error) { + console.log('Problem deleting projects'); + console.log(error); + process.exit(1); + } + + try { + const newProjects = filledProjectList.map(async (project) => { + console.log(`saving ${project.name}`); + await createProject(project); }); + await Q.all(newProjects); + console.log(`Projects saved to User: ${editorUsername}!`); + } catch (error) { + console.log('Error saving projects'); + console.log(error); } } - /** * --------------------------------------------------------- * --------------------- main ------------------------------ @@ -394,18 +360,14 @@ async function checkP5User() { * Delete existing and save */ async function make() { - await checkP5User(); - // Get the user - const user = await User.findOne({ - username: editorUsername - }); // Get the categories and their examples const categories = await getCategories(); const categoryExamples = await getCategoryExamples(categories); + const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples); - const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user); - const filledProjectList = await fetchSketchContentAll(formattedSketchList); - await createProjectsInP5User(filledProjectList, user); + const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); + + await createProjectsInP5User(formattedSketchList); console.log('done!'); process.exit(); } @@ -418,20 +380,14 @@ async function make() { * Format the sketch files to be save to the db * Delete existing and save */ +// eslint-disable-next-line no-unused-vars async function test() { - await checkP5User(); - // Get the user - const user = await User.findOne({ - username: editorUsername - }); - // read from file while testing const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json')); - const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user); + const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); - const filledProjectList = await fetchSketchContentAll(formattedSketchList); - await createProjectsInP5User(filledProjectList, user); + await createProjectsInP5User(formattedSketchList); console.log('done!'); process.exit(); } diff --git a/server/server.js b/server/server.js index 4483b722..af6ea25a 100644 --- a/server/server.js +++ b/server/server.js @@ -16,6 +16,7 @@ import webpackHotMiddleware from 'webpack-hot-middleware'; import config from '../webpack/config.dev'; // Import all required modules +import api from './routes/api.routes'; import users from './routes/user.routes'; import sessions from './routes/session.routes'; import projects from './routes/project.routes'; @@ -51,7 +52,7 @@ const corsOriginsWhitelist = [ // Run Webpack dev server in development mode if (process.env.NODE_ENV === 'development') { const compiler = webpack(config); - app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config[0].output.publicPath })); + app.use(webpackDevMiddleware(compiler, { lazy: false, noInfo: true, publicPath: config[0].output.publicPath })); app.use(webpackHotMiddleware(compiler)); corsOriginsWhitelist.push(/localhost/); @@ -95,6 +96,7 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); +app.use('/api/v1', requestsOfTypeJSON(), api); app.use('/api', requestsOfTypeJSON(), users); app.use('/api', requestsOfTypeJSON(), sessions); app.use('/api', requestsOfTypeJSON(), files); diff --git a/server/utils/__mocks__/createId.js b/server/utils/__mocks__/createId.js new file mode 100644 index 00000000..919ea252 --- /dev/null +++ b/server/utils/__mocks__/createId.js @@ -0,0 +1,16 @@ +/** + * Creates an increasing numeric ID + * for testing + */ +let nextId = 0; + +export default function mockCreateId() { + const id = nextId; + nextId += 1; + + return String(id); +} + +export function resetMockCreateId() { + nextId = 0; +} diff --git a/server/utils/createApplicationErrorClass.js b/server/utils/createApplicationErrorClass.js new file mode 100644 index 00000000..f949866c --- /dev/null +++ b/server/utils/createApplicationErrorClass.js @@ -0,0 +1,33 @@ +/** + * This is the base class for custom errors in + * the application. + */ +export class ApplicationError extends Error { + constructor(message, extra = {}) { + super(); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + this.name = 'ApplicationError'; + this.message = message; + this.code = extra.code; + } +} + +/** + * Create a new custom error class e.g. + * const UserNotFoundError = createApplicationErrorClass('UserNotFoundError'); + * + * // Later + * throw new UserNotFoundError(`user ${user.name} not found`); + * + */ +export default function createApplicationErrorClass(name) { + return class extends ApplicationError { + constructor(...params) { + super(...params); + + this.name = name; + } + }; +} diff --git a/server/utils/createId.js b/server/utils/createId.js new file mode 100644 index 00000000..65b8290f --- /dev/null +++ b/server/utils/createId.js @@ -0,0 +1,8 @@ +import objectID from 'bson-objectid'; + +/** + * Creates a mongo ID + */ +export default function createId() { + return objectID().toHexString(); +} From 1f4bd581a89b74da68b56788c30384ef9596b063 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 20:39:45 +0200 Subject: [PATCH 079/322] Public API: Namespace private and public APIs (#1148) * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour. * Splits private and public APIs The private API is under /editor and the public API under /api --- .env.example | 2 +- app.json | 2 +- server/server.js | 15 +++++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 7680fb90..0385cf26 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -API_URL=/api +API_URL=/editor AWS_ACCESS_KEY= AWS_REGION= AWS_SECRET_KEY= diff --git a/app.json b/app.json index a588258e..3eff82c9 100644 --- a/app.json +++ b/app.json @@ -16,7 +16,7 @@ ], "env": { "API_URL": { - "value": "/api" + "value": "/editor" }, "AWS_ACCESS_KEY": { "description": "AWS Access Key", diff --git a/server/server.js b/server/server.js index af6ea25a..a7acc387 100644 --- a/server/server.js +++ b/server/server.js @@ -97,16 +97,19 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); app.use('/api/v1', requestsOfTypeJSON(), api); -app.use('/api', requestsOfTypeJSON(), users); -app.use('/api', requestsOfTypeJSON(), sessions); -app.use('/api', requestsOfTypeJSON(), files); -app.use('/api', requestsOfTypeJSON(), projects); -app.use('/api', requestsOfTypeJSON(), aws); +app.use('/editor', requestsOfTypeJSON(), users); +app.use('/editor', requestsOfTypeJSON(), sessions); +app.use('/editor', requestsOfTypeJSON(), files); +app.use('/editor', requestsOfTypeJSON(), projects); +app.use('/editor', requestsOfTypeJSON(), aws); // This is a temporary way to test access via Personal Access Tokens // Sending a valid username: combination will // return the user's information. -app.get('/api/auth/access-check', passport.authenticate('basic', { session: false }), (req, res) => res.json(req.user)); +app.get( + '/api/v1/auth/access-check', + passport.authenticate('basic', { session: false }), (req, res) => res.json(req.user) +); app.use(assetRoutes); // this is supposed to be TEMPORARY -- until i figure out From 162d5276f17c262a4495a4b32f76c5cd9082d4c2 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 22:15:13 +0200 Subject: [PATCH 080/322] Display Access Token tab depending on UI_ACCESS_TOKEN_ENABLED feature flag (#1149) --- .env.example | 1 + client/modules/User/pages/AccountView.jsx | 4 +++- server/views/index.js | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0385cf26..17b6fe5a 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,4 @@ PORT=8000 S3_BUCKET= S3_BUCKET_URL_BASE= SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production +UI_ACCESS_TOKEN_ENABLED=false diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 7382656e..d529b5dd 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -36,6 +36,8 @@ class AccountView extends React.Component { } render() { + const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED; + return (
    @@ -53,7 +55,7 @@ class AccountView extends React.Component {

    Account

    -

    Access Tokens

    + {accessTokensUIEnabled &&

    Access Tokens

    }
    diff --git a/server/views/index.js b/server/views/index.js index d4062061..0539b927 100644 --- a/server/views/index.js +++ b/server/views/index.js @@ -30,6 +30,8 @@ export function renderIndex() { window.process.env.CLIENT = true; window.process.env.LOGIN_ENABLED = ${process.env.LOGIN_ENABLED === 'false' ? false : true}; window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true}; + window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true}; + window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true}; From 7ea4ae563714ac7f120349bc67ebedbea683c01f Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sun, 8 Sep 2019 16:43:16 +0200 Subject: [PATCH 081/322] Fix linting errors and warnings --- .../components/createRedirectWithUsername.jsx | 5 ++--- client/modules/App/App.jsx | 5 ++++- client/modules/User/actions.js | 1 - .../User/components/DashboardTabSwitcher.jsx | 18 ++++++++---------- client/modules/User/pages/AccountView.jsx | 3 --- client/modules/User/pages/DashboardView.jsx | 4 ++-- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/client/components/createRedirectWithUsername.jsx b/client/components/createRedirectWithUsername.jsx index a2bdd790..fe76b5cd 100644 --- a/client/components/createRedirectWithUsername.jsx +++ b/client/components/createRedirectWithUsername.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { browserHistory } from 'react-router'; @@ -8,7 +7,7 @@ const RedirectToUser = ({ username, url = '/:username/sketches' }) => { if (username == null) { return; } - + browserHistory.replace(url.replace(':username', username)); }, [username]); @@ -23,6 +22,6 @@ function mapStateToProps(state) { const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); -const createRedirectWithUsername = (url) => (props) => ; +const createRedirectWithUsername = url => props => ; export default createRedirectWithUsername; diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 9a8e6bdb..045074eb 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -39,7 +39,10 @@ class App extends React.Component { App.propTypes = { children: PropTypes.element, location: PropTypes.shape({ - pathname: PropTypes.string + pathname: PropTypes.string, + state: PropTypes.shape({ + skipSavingPath: PropTypes.bool, + }), }).isRequired, setPreviousPath: PropTypes.func.isRequired, }; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 3c2f6d5b..93a874fb 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,6 +1,5 @@ import { browserHistory } from 'react-router'; import axios from 'axios'; -import crypto from 'crypto'; import * as ActionTypes from '../../constants'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showToast, setToastText } from '../IDE/actions/toast'; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 3d333064..8143759f 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -27,16 +27,14 @@ Tab.propTypes = { to: PropTypes.string.isRequired, }; -const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => { - return ( -
      -
      - Sketches - {isOwner && Assets} -
      -
    - ); -}; +const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => ( +
      +
      + Sketches + {isOwner && Assets} +
      +
    +); DashboardTabSwitcher.propTypes = { currentTab: PropTypes.string.isRequired, diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 7382656e..5648cee2 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -4,7 +4,6 @@ import { reduxForm } from 'redux-form'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import InlineSVG from 'react-inlinesvg'; import axios from 'axios'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; @@ -14,8 +13,6 @@ import GithubButton from '../components/GithubButton'; import APIKeyForm from '../components/APIKeyForm'; import NavBasic from '../../../components/NavBasic'; -const exitUrl = require('../../../images/exit.svg'); - class AccountView extends React.Component { constructor(props) { super(props); diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index a3172eee..ba1b63f0 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { browserHistory, Link } from 'react-router'; +import { browserHistory } from 'react-router'; import { Helmet } from 'react-helmet'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import NavBasic from '../../../components/NavBasic'; @@ -80,7 +80,7 @@ class DashboardView extends React.Component {
    - +
    { currentTab === TabKey.sketches ? : From 37fcf46972ad69fe5a9546e793faafd948b7c28c Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 20:26:57 +0200 Subject: [PATCH 082/322] Public API: Create new project (fixes #1095) (#1106) * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour. --- jest.setup.js | 3 + .../controllers/__mocks__/aws.controller.js | 5 + .../__test__/project.controller.test.js | 155 ------- server/controllers/project.controller.js | 59 +-- .../__test__/createProject.test.js | 391 ++++++++++++++++++ .../__test__/deleteProject.test.js | 119 ++++++ .../__test__/getProjectsForUser.test.js | 151 +++++++ .../project.controller/createProject.js | 65 +++ .../project.controller/deleteProject.js | 57 +++ .../project.controller/getProjectsForUser.js | 72 ++++ .../user.controller/__tests__/apiKey.test.js | 182 ++++---- server/controllers/user.controller/apiKey.js | 106 +++-- server/domain-objects/Project.js | 133 ++++++ .../domain-objects/__test__/Project.test.js | 385 +++++++++++++++++ server/domain-objects/createDefaultFiles.js | 48 +++ server/models/__mocks__/project.js | 18 + server/models/__mocks__/user.js | 41 +- server/models/project.js | 37 +- server/routes/api.routes.js | 27 ++ server/scripts/examples-ml5.js | 342 +++++++-------- server/server.js | 4 +- server/utils/__mocks__/createId.js | 16 + server/utils/createApplicationErrorClass.js | 33 ++ server/utils/createId.js | 8 + 24 files changed, 1905 insertions(+), 552 deletions(-) create mode 100644 server/controllers/__mocks__/aws.controller.js delete mode 100644 server/controllers/__test__/project.controller.test.js create mode 100644 server/controllers/project.controller/__test__/createProject.test.js create mode 100644 server/controllers/project.controller/__test__/deleteProject.test.js create mode 100644 server/controllers/project.controller/__test__/getProjectsForUser.test.js create mode 100644 server/controllers/project.controller/deleteProject.js create mode 100644 server/controllers/project.controller/getProjectsForUser.js create mode 100644 server/domain-objects/Project.js create mode 100644 server/domain-objects/__test__/Project.test.js create mode 100644 server/domain-objects/createDefaultFiles.js create mode 100644 server/routes/api.routes.js create mode 100644 server/utils/__mocks__/createId.js create mode 100644 server/utils/createApplicationErrorClass.js create mode 100644 server/utils/createId.js diff --git a/jest.setup.js b/jest.setup.js index 7c9d2b77..da036785 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,5 +1,8 @@ import { configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-16' import '@babel/polyfill' +import mongoose from 'mongoose' + +mongoose.Promise = global.Promise; configure({ adapter: new Adapter() }) diff --git a/server/controllers/__mocks__/aws.controller.js b/server/controllers/__mocks__/aws.controller.js new file mode 100644 index 00000000..9e5709c4 --- /dev/null +++ b/server/controllers/__mocks__/aws.controller.js @@ -0,0 +1,5 @@ +export const getObjectKey = jest.mock(); +export const deleteObjectsFromS3 = jest.fn(); +export const signS3 = jest.fn(); +export const copyObjectInS3 = jest.fn(); +export const listObjectsInS3ForUser = jest.fn(); diff --git a/server/controllers/__test__/project.controller.test.js b/server/controllers/__test__/project.controller.test.js deleted file mode 100644 index e0852e61..00000000 --- a/server/controllers/__test__/project.controller.test.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @jest-environment node - */ -import { Response } from 'jest-express'; - -import { createMock } from '../../models/project'; -import createProject from '../project.controller/createProject'; - -jest.mock('../../models/project'); - -describe('project.controller', () => { - describe('createProject()', () => { - let ProjectMock; - - beforeEach(() => { - ProjectMock = createMock(); - }); - - afterEach(() => { - ProjectMock.restore(); - }); - - it('fails if create fails', (done) => { - const error = new Error('An error'); - - ProjectMock - .expects('create') - .rejects(error); - - const request = { user: {} }; - const response = new Response(); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalledWith({ success: false }); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - it('extracts parameters from request body', (done) => { - const request = { - user: { _id: 'abc123' }, - body: { - name: 'Wriggly worm', - files: [{ name: 'file.js', content: 'var hello = true;' }] - } - }; - const response = new Response(); - - - ProjectMock - .expects('create') - .withArgs({ - user: 'abc123', - name: 'Wriggly worm', - files: [{ name: 'file.js', content: 'var hello = true;' }] - }) - .resolves(); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalled(); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - // TODO: This should be extracted to a new model object - // so the controllers just have to call a single - // method for this operation - it('populates referenced user on project creation', (done) => { - const request = { user: { _id: 'abc123' } }; - const response = new Response(); - - const result = { - _id: 'abc123', - id: 'abc123', - name: 'Project name', - serveSecure: false, - files: [] - }; - - const resultWithUser = { - ...result, - user: {} - }; - - ProjectMock - .expects('create') - .withArgs({ user: 'abc123' }) - .resolves(result); - - ProjectMock - .expects('populate') - .withArgs(result) - .yields(null, resultWithUser) - .resolves(resultWithUser); - - const promise = createProject(request, response); - - function expectations() { - const doc = response.json.mock.calls[0][0]; - - expect(response.json).toHaveBeenCalled(); - - expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - - it('fails if referenced user population fails', (done) => { - const request = { user: { _id: 'abc123' } }; - const response = new Response(); - - const result = { - _id: 'abc123', - id: 'abc123', - name: 'Project name', - serveSecure: false, - files: [] - }; - - const error = new Error('An error'); - - ProjectMock - .expects('create') - .resolves(result); - - ProjectMock - .expects('populate') - .yields(error) - .resolves(error); - - const promise = createProject(request, response); - - function expectations() { - expect(response.json).toHaveBeenCalledWith({ success: false }); - - done(); - } - - promise.then(expectations, expectations).catch(expectations); - }); - }); -}); diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index ae029918..bf59a865 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -2,7 +2,6 @@ import archiver from 'archiver'; import format from 'date-fns/format'; import isUrl from 'is-url'; import jsdom, { serializeDocument } from 'jsdom'; -import isBefore from 'date-fns/is_before'; import isAfter from 'date-fns/is_after'; import request from 'request'; import slugify from 'slugify'; @@ -10,9 +9,10 @@ import Project from '../models/project'; import User from '../models/user'; import { resolvePathToFile } from '../utils/filePath'; import generateFileSystemSafeName from '../utils/generateFileSystemSafeName'; -import { deleteObjectsFromS3, getObjectKey } from './aws.controller'; -export { default as createProject } from './project.controller/createProject'; +export { default as createProject, apiCreateProject } from './project.controller/createProject'; +export { default as deleteProject } from './project.controller/deleteProject'; +export { default as getProjectsForUser, apiGetProjectsForUser } from './project.controller/getProjectsForUser'; export function updateProject(req, res) { Project.findById(req.params.project_id, (findProjectErr, project) => { @@ -84,37 +84,6 @@ export function getProject(req, res) { }); } -function deleteFilesFromS3(files) { - deleteObjectsFromS3(files.filter((file) => { - if (file.url) { - if (!process.env.S3_DATE || ( - process.env.S3_DATE && - isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) { - return true; - } - } - return false; - }) - .map(file => getObjectKey(file.url))); -} - -export function deleteProject(req, res) { - Project.findById(req.params.project_id, (findProjectErr, project) => { - if (!project.user.equals(req.user._id)) { - res.status(403).json({ success: false, message: 'Session does not match owner of project.' }); - return; - } - deleteFilesFromS3(project.files); - Project.remove({ _id: req.params.project_id }, (removeProjectError) => { - if (removeProjectError) { - res.status(404).send({ message: 'Project with that id does not exist' }); - return; - } - res.json({ success: true }); - }); - }); -} - export function getProjectsForUserId(userId) { return new Promise((resolve, reject) => { Project.find({ user: userId }) @@ -157,10 +126,6 @@ export function getProjectAsset(req, res) { }); } -export function getProjectsForUserName(username) { - -} - export function getProjects(req, res) { if (req.user) { getProjectsForUserId(req.user._id) @@ -173,24 +138,6 @@ export function getProjects(req, res) { } } -export function getProjectsForUser(req, res) { - if (req.params.username) { - User.findOne({ username: req.params.username }, (err, user) => { - if (!user) { - res.status(404).json({ message: 'User with that username does not exist.' }); - return; - } - Project.find({ user: user._id }) - .sort('-createdAt') - .select('name files id createdAt updatedAt') - .exec((innerErr, projects) => res.json(projects)); - }); - } else { - // could just move this to client side - res.json([]); - } -} - export function projectExists(projectId, callback) { Project.findById(projectId, (err, project) => ( project ? callback(true) : callback(false) diff --git a/server/controllers/project.controller/__test__/createProject.test.js b/server/controllers/project.controller/__test__/createProject.test.js new file mode 100644 index 00000000..68025a0c --- /dev/null +++ b/server/controllers/project.controller/__test__/createProject.test.js @@ -0,0 +1,391 @@ +/** + * @jest-environment node + */ +import { Response } from 'jest-express'; + +import Project, { createMock, createInstanceMock } from '../../../models/project'; +import createProject, { apiCreateProject } from '../createProject'; + +jest.mock('../../../models/project'); + +describe('project.controller', () => { + describe('createProject()', () => { + let ProjectMock; + + beforeEach(() => { + ProjectMock = createMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + }); + + it('fails if create fails', (done) => { + const error = new Error('An error'); + + ProjectMock + .expects('create') + .rejects(error); + + const request = { user: {} }; + const response = new Response(); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalledWith({ success: false }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('extracts parameters from request body', (done) => { + const request = { + user: { _id: 'abc123' }, + body: { + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + } + }; + const response = new Response(); + + + ProjectMock + .expects('create') + .withArgs({ + user: 'abc123', + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + }) + .resolves(); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + // TODO: This should be extracted to a new model object + // so the controllers just have to call a single + // method for this operation + it('populates referenced user on project creation', (done) => { + const request = { user: { _id: 'abc123' } }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + const resultWithUser = { + ...result, + user: {} + }; + + ProjectMock + .expects('create') + .withArgs({ user: 'abc123' }) + .resolves(result); + + ProjectMock + .expects('populate') + .withArgs(result) + .yields(null, resultWithUser) + .resolves(resultWithUser); + + const promise = createProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject(resultWithUser); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if referenced user population fails', (done) => { + const request = { user: { _id: 'abc123' } }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + const error = new Error('An error'); + + ProjectMock + .expects('create') + .resolves(result); + + ProjectMock + .expects('populate') + .yields(error) + .resolves(error); + + const promise = createProject(request, response); + + function expectations() { + expect(response.json).toHaveBeenCalledWith({ success: false }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); + + describe('apiCreateProject()', () => { + let ProjectMock; + let ProjectInstanceMock; + + beforeEach(() => { + ProjectMock = createMock(); + ProjectInstanceMock = createInstanceMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + ProjectInstanceMock.restore(); + }); + + it('returns 201 with id of created sketch', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: true, conflictingIds: [] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.status).toHaveBeenCalledWith(201); + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject({ + id: 'abc123' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if slug is not unique', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + slug: 'a-slug', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + // slug: 'a-slug', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: false, conflictingIds: ['cde123'] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + expect(response.status).toHaveBeenCalledWith(409); + expect(response.json).toHaveBeenCalled(); + + expect(JSON.parse(JSON.stringify(doc))).toMatchObject({ + message: 'Sketch Validation Failed', + detail: 'Slug "a-slug" is not unique. Check cde123' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('fails if user does not have permission', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { + username: 'dana', + }, + body: { + name: 'My sketch', + slug: 'a-slug', + files: {} + } + }; + const response = new Response(); + + const result = { + _id: 'abc123', + id: 'abc123', + name: 'Project name', + serveSecure: false, + files: [] + }; + + ProjectInstanceMock.expects('isSlugUnique') + .resolves({ isUnique: true, conflictingIds: [] }); + + ProjectInstanceMock.expects('save') + .resolves(new Project(result)); + + const promise = apiCreateProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(401); + expect(response.json).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns validation errors on files input', (done) => { + const request = { + user: { username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'My sketch', + files: { + 'index.html': { + // missing content or url + } + } + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).not.toBeNull(); + expect(responseBody.errors.length).toBe(1); + expect(responseBody.errors).toEqual([ + { name: 'index.html', message: 'missing \'url\' or \'content\'' } + ]); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('rejects file parameters not in object format', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'Wriggly worm', + files: [{ name: 'file.js', content: 'var hello = true;' }] + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).toBe('\'files\' must be an object'); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('rejects if files object in not provided', (done) => { + const request = { + user: { _id: 'abc123', username: 'alice' }, + params: { username: 'alice' }, + body: { + name: 'Wriggly worm', + // files: {} is missing + } + }; + const response = new Response(); + + const promise = apiCreateProject(request, response); + + function expectations() { + const doc = response.json.mock.calls[0][0]; + + const responseBody = JSON.parse(JSON.stringify(doc)); + + expect(response.status).toHaveBeenCalledWith(422); + expect(responseBody.message).toBe('File Validation Failed'); + expect(responseBody.detail).toBe('\'files\' must be an object'); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/__test__/deleteProject.test.js b/server/controllers/project.controller/__test__/deleteProject.test.js new file mode 100644 index 00000000..84a6824f --- /dev/null +++ b/server/controllers/project.controller/__test__/deleteProject.test.js @@ -0,0 +1,119 @@ +/** + * @jest-environment node + */ +import { Request, Response } from 'jest-express'; + +import Project, { createMock, createInstanceMock } from '../../../models/project'; +import User from '../../../models/user'; +import deleteProject from '../../project.controller/deleteProject'; +import { deleteObjectsFromS3 } from '../../aws.controller'; + + +jest.mock('../../../models/project'); +jest.mock('../../aws.controller'); + +describe('project.controller', () => { + describe('deleteProject()', () => { + let ProjectMock; + let ProjectInstanceMock; + + beforeEach(() => { + ProjectMock = createMock(); + ProjectInstanceMock = createInstanceMock(); + }); + + afterEach(() => { + ProjectMock.restore(); + ProjectInstanceMock.restore(); + }); + + it('returns 403 if project is not owned by authenticated user', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: 'abc123' }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(project); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(403); + expect(response.json).toHaveBeenCalledWith({ + message: 'Authenticated user does not match owner of project' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 404 if project does not exist', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: 'abc123' }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(null); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ + message: 'Project with that id does not exist' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('deletes project and dependent files from S3 ', (done) => { + const user = new User(); + const project = new Project(); + project.user = user; + + const request = new Request(); + request.setParams({ project_id: project._id }); + request.user = { _id: user._id }; + + const response = new Response(); + + ProjectMock + .expects('findById') + .resolves(project); + + ProjectInstanceMock.expects('remove') + .yields(); + + const promise = deleteProject(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).not.toHaveBeenCalled(); + expect(deleteObjectsFromS3).toHaveBeenCalled(); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/__test__/getProjectsForUser.test.js b/server/controllers/project.controller/__test__/getProjectsForUser.test.js new file mode 100644 index 00000000..05a0cf71 --- /dev/null +++ b/server/controllers/project.controller/__test__/getProjectsForUser.test.js @@ -0,0 +1,151 @@ +/** + * @jest-environment node + */ +import { Request, Response } from 'jest-express'; + +import { createMock } from '../../../models/user'; +import getProjectsForUser, { apiGetProjectsForUser } from '../../project.controller/getProjectsForUser'; + +jest.mock('../../../models/user'); +jest.mock('../../aws.controller'); + +describe('project.controller', () => { + let UserMock; + + beforeEach(() => { + UserMock = createMock(); + }); + + afterEach(() => { + UserMock.restore(); + }); + + describe('getProjectsForUser()', () => { + it('returns empty array user not supplied as parameter', (done) => { + const request = new Request(); + request.setParams({}); + const response = new Response(); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).toHaveBeenCalledWith([]); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 404 if user does not exist', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(null, null); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 500 on other errors', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(new Error(), null); + + const promise = getProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(500); + expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); + + describe('apiGetProjectsForUser()', () => { + it('returns validation error if user id not provided', (done) => { + const request = new Request(); + request.setParams({}); + const response = new Response(); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(422); + expect(response.json).toHaveBeenCalledWith({ + message: 'Username not provided' + }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + + it('returns 404 if user does not exist', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(null, null); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(404); + expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + + it('returns 500 on other errors', (done) => { + const request = new Request(); + request.setParams({ username: 'abc123' }); + const response = new Response(); + + UserMock + .expects('findOne') + .withArgs({ username: 'abc123' }) + .yields(new Error(), null); + + const promise = apiGetProjectsForUser(request, response); + + function expectations() { + expect(response.status).toHaveBeenCalledWith(500); + expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + + done(); + } + + promise.then(expectations, expectations).catch(expectations); + }); + }); +}); diff --git a/server/controllers/project.controller/createProject.js b/server/controllers/project.controller/createProject.js index 2cf34e32..e4040268 100644 --- a/server/controllers/project.controller/createProject.js +++ b/server/controllers/project.controller/createProject.js @@ -1,4 +1,5 @@ import Project from '../../models/project'; +import { toModel, FileValidationError, ProjectValidationError } from '../../domain-objects/Project'; export default function createProject(req, res) { let projectValues = { @@ -30,3 +31,67 @@ export default function createProject(req, res) { .then(populateUserData) .catch(sendFailure); } + +// TODO: What happens if you don't supply any files? +export function apiCreateProject(req, res) { + const params = Object.assign({ user: req.user._id }, req.body); + + function sendValidationErrors(err, type, code = 422) { + res.status(code).json({ + message: `${type} Validation Failed`, + detail: err.message, + errors: err.files, + }); + } + + // TODO: Error handling to match spec + function sendFailure(err) { + res.status(500).end(); + } + + function handleErrors(err) { + if (err instanceof FileValidationError) { + sendValidationErrors(err, 'File', err.code); + } else if (err instanceof ProjectValidationError) { + sendValidationErrors(err, 'Sketch', err.code); + } else { + sendFailure(); + } + } + + function checkUserHasPermission() { + if (req.user.username !== req.params.username) { + console.log('no permission'); + const error = new ProjectValidationError(`'${req.user.username}' does not have permission to create for '${req.params.username}'`); + error.code = 401; + + throw error; + } + } + + try { + checkUserHasPermission(); + + const model = toModel(params); + + return model.isSlugUnique() + .then(({ isUnique, conflictingIds }) => { + if (isUnique) { + return model.save() + .then((newProject) => { + res.status(201).json({ id: newProject.id }); + }); + } + + const error = new ProjectValidationError(`Slug "${model.slug}" is not unique. Check ${conflictingIds.join(', ')}`); + error.code = 409; + + throw error; + }) + .then(checkUserHasPermission) + .catch(handleErrors); + } catch (err) { + handleErrors(err); + return Promise.reject(err); + } +} diff --git a/server/controllers/project.controller/deleteProject.js b/server/controllers/project.controller/deleteProject.js new file mode 100644 index 00000000..dd980b23 --- /dev/null +++ b/server/controllers/project.controller/deleteProject.js @@ -0,0 +1,57 @@ +import isBefore from 'date-fns/is_before'; +import Project from '../../models/project'; +import { deleteObjectsFromS3, getObjectKey } from '../aws.controller'; +import createApplicationErrorClass from '../../utils/createApplicationErrorClass'; + +const ProjectDeletionError = createApplicationErrorClass('ProjectDeletionError'); + +function deleteFilesFromS3(files) { + deleteObjectsFromS3(files.filter((file) => { + if (file.url) { + if (!process.env.S3_DATE || ( + process.env.S3_DATE && + isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) { + return true; + } + } + return false; + }) + .map(file => getObjectKey(file.url))); +} + +export default function deleteProject(req, res) { + function sendFailure(error) { + res.status(error.code).json({ message: error.message }); + } + + function sendProjectNotFound() { + sendFailure(new ProjectDeletionError('Project with that id does not exist', { code: 404 })); + } + + function handleProjectDeletion(project) { + if (project == null) { + sendProjectNotFound(); + return; + } + + if (!project.user.equals(req.user._id)) { + sendFailure(new ProjectDeletionError('Authenticated user does not match owner of project', { code: 403 })); + return; + } + + deleteFilesFromS3(project.files); + + project.remove((removeProjectError) => { + if (removeProjectError) { + sendProjectNotFound(); + return; + } + + res.status(200).end(); + }); + } + + return Project.findById(req.params.project_id) + .then(handleProjectDeletion) + .catch(sendFailure); +} diff --git a/server/controllers/project.controller/getProjectsForUser.js b/server/controllers/project.controller/getProjectsForUser.js new file mode 100644 index 00000000..1d9a0e34 --- /dev/null +++ b/server/controllers/project.controller/getProjectsForUser.js @@ -0,0 +1,72 @@ +import Project from '../../models/project'; +import User from '../../models/user'; +import { toApi as toApiProjectObject } from '../../domain-objects/Project'; +import createApplicationErrorClass from '../../utils/createApplicationErrorClass'; + +const UserNotFoundError = createApplicationErrorClass('UserNotFoundError'); + +function getProjectsForUserName(username) { + return new Promise((resolve, reject) => { + User.findOne({ username }, (err, user) => { + if (err) { + reject(err); + return; + } + + if (!user) { + reject(new UserNotFoundError()); + return; + } + + Project.find({ user: user._id }) + .sort('-createdAt') + .select('name files id createdAt updatedAt') + .exec((innerErr, projects) => { + if (innerErr) { + reject(innerErr); + return; + } + + resolve(projects); + }); + }); + }); +} + +export default function getProjectsForUser(req, res) { + if (req.params.username) { + return getProjectsForUserName(req.params.username) + .then(projects => res.json(projects)) + .catch((err) => { + if (err instanceof UserNotFoundError) { + res.status(404).json({ message: 'User with that username does not exist.' }); + } else { + res.status(500).json({ message: 'Error fetching projects' }); + } + }); + } + + // could just move this to client side + res.status(200).json([]); + return Promise.resolve(); +} + +export function apiGetProjectsForUser(req, res) { + if (req.params.username) { + return getProjectsForUserName(req.params.username) + .then((projects) => { + const asApiObjects = projects.map(p => toApiProjectObject(p)); + res.json({ sketches: asApiObjects }); + }) + .catch((err) => { + if (err instanceof UserNotFoundError) { + res.status(404).json({ message: 'User with that username does not exist.' }); + } else { + res.status(500).json({ message: 'Error fetching projects' }); + } + }); + } + + res.status(422).json({ message: 'Username not provided' }); + return Promise.resolve(); +} diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js index a496373b..7e396288 100644 --- a/server/controllers/user.controller/__tests__/apiKey.test.js +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -1,73 +1,40 @@ /* @jest-environment node */ import last from 'lodash/last'; +import { Request, Response } from 'jest-express'; + +import User, { createMock, createInstanceMock } from '../../../models/user'; import { createApiKey, removeApiKey } from '../../user.controller/apiKey'; jest.mock('../../../models/user'); -/* - Create a mock object representing an express Response -*/ -const createResponseMock = function createResponseMock(done) { - const json = jest.fn(() => { - if (done) { done(); } - }); - - const status = jest.fn(() => ({ json })); - - return { - status, - json - }; -}; - -/* - Create a mock of the mongoose User model -*/ -const createUserMock = function createUserMock() { - const apiKeys = []; - let nextId = 0; - - apiKeys.push = ({ label, hashedKey }) => { - const id = nextId; - nextId += 1; - const publicFields = { id, label }; - const allFields = { ...publicFields, hashedKey }; - - Object.defineProperty(allFields, 'toObject', { - value: () => publicFields, - enumerable: false - }); - - return Array.prototype.push.call(apiKeys, allFields); - }; - - apiKeys.pull = ({ _id }) => { - const index = apiKeys.findIndex(({ id }) => id === _id); - return apiKeys.splice(index, 1); - }; - - return { - apiKeys, - save: jest.fn(callback => callback()) - }; -}; - -const User = require('../../../models/user').default; - describe('user.controller', () => { + let UserMock; + let UserInstanceMock; + beforeEach(() => { - User.__setFindById(null, null); + UserMock = createMock(); + UserInstanceMock = createInstanceMock(); }); + afterEach(() => { + UserMock.restore(); + UserInstanceMock.restore(); + }); + + describe('createApiKey', () => { it('returns an error if user doesn\'t exist', () => { const request = { user: { id: '1234' } }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, null); createApiKey(request, response); - expect(User.findById.mock.calls[0][0]).toBe('1234'); expect(response.status).toHaveBeenCalledWith(404); expect(response.json).toHaveBeenCalledWith({ error: 'User not found' @@ -75,10 +42,13 @@ describe('user.controller', () => { }); it('returns an error if label not provided', () => { - User.__setFindById(undefined, createUserMock()); - const request = { user: { id: '1234' }, body: {} }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, new User()); createApiKey(request, response); @@ -89,46 +59,59 @@ describe('user.controller', () => { }); it('returns generated API key to the user', (done) => { - let response; + const request = new Request(); + request.setBody({ label: 'my key' }); + request.user = { id: '1234' }; - const request = { - user: { id: '1234' }, - body: { label: 'my key' } - }; + const response = new Response(); - const user = createUserMock(); + const user = new User(); - const checkExpecations = () => { + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); + + UserInstanceMock.expects('save') + .yields(); + + function expectations() { const lastKey = last(user.apiKeys); expect(lastKey.label).toBe('my key'); expect(typeof lastKey.hashedKey).toBe('string'); - expect(response.json).toHaveBeenCalledWith({ - apiKeys: [ - { id: 0, label: 'my key', token: lastKey.hashedKey } - ] + const responseData = response.json.mock.calls[0][0]; + + expect(responseData.apiKeys.length).toBe(1); + expect(responseData.apiKeys[0]).toMatchObject({ + label: 'my key', + token: lastKey.hashedKey, + lastUsedAt: undefined, + createdAt: undefined }); done(); - }; + } - response = createResponseMock(checkExpecations); + const promise = createApiKey(request, response); - User.__setFindById(undefined, user); - - createApiKey(request, response); + promise.then(expectations, expectations).catch(expectations); }); }); describe('removeApiKey', () => { it('returns an error if user doesn\'t exist', () => { const request = { user: { id: '1234' } }; - const response = createResponseMock(); + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, null); removeApiKey(request, response); - expect(User.findById.mock.calls[0][0]).toBe('1234'); expect(response.status).toHaveBeenCalledWith(404); expect(response.json).toHaveBeenCalledWith({ error: 'User not found' @@ -140,10 +123,13 @@ describe('user.controller', () => { user: { id: '1234' }, params: { keyId: 'not-a-real-key' } }; - const response = createResponseMock(); + const response = new Response(); + const user = new User(); - const user = createUserMock(); - User.__setFindById(undefined, user); + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); removeApiKey(request, response); @@ -153,27 +139,41 @@ describe('user.controller', () => { }); }); - it.skip('removes key if it exists', () => { - const request = { - user: { id: '1234' }, - params: { keyId: 0 } - }; - const response = createResponseMock(); - - const user = createUserMock(); - + it('removes key if it exists', () => { + const user = new User(); user.apiKeys.push({ label: 'first key' }); // id 0 user.apiKeys.push({ label: 'second key' }); // id 1 - User.__setFindById(undefined, user); + const firstKeyId = user.apiKeys[0]._id.toString(); + const secondKeyId = user.apiKeys[1]._id.toString(); + + const request = { + user: { id: '1234' }, + params: { keyId: firstKeyId } + }; + const response = new Response(); + + UserMock + .expects('findById') + .withArgs('1234') + .yields(null, user); + + UserInstanceMock + .expects('save') + .yields(); removeApiKey(request, response); expect(response.status).toHaveBeenCalledWith(200); - expect(response.json).toHaveBeenCalledWith({ - apiKeys: [ - { id: 1, label: 'second key' } - ] + + const responseData = response.json.mock.calls[0][0]; + + expect(responseData.apiKeys.length).toBe(1); + expect(responseData.apiKeys[0]).toMatchObject({ + id: secondKeyId, + label: 'second key', + lastUsedAt: undefined, + createdAt: undefined }); }); }); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js index eed22e6f..5d45b123 100644 --- a/server/controllers/user.controller/apiKey.js +++ b/server/controllers/user.controller/apiKey.js @@ -19,67 +19,85 @@ function generateApiKey() { } export function createApiKey(req, res) { - User.findById(req.user.id, async (err, user) => { - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; + return new Promise((resolve, reject) => { + function sendFailure(code, error) { + res.status(code).json({ error }); + resolve(); } - if (!req.body.label) { - res.status(400).json({ error: 'Expected field \'label\' was not present in request body' }); - return; - } - - const keyToBeHashed = await generateApiKey(); - - const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); - - user.save((saveErr) => { - if (saveErr) { - res.status(500).json({ error: saveErr }); + User.findById(req.user.id, async (err, user) => { + if (!user) { + sendFailure(404, 'User not found'); return; } - const apiKeys = user.apiKeys - .map((apiKey, index) => { - const fields = apiKey.toObject(); - const shouldIncludeToken = index === addedApiKeyIndex - 1; + if (!req.body.label) { + sendFailure(400, 'Expected field \'label\' was not present in request body'); + return; + } - return shouldIncludeToken ? - { ...fields, token: keyToBeHashed } : - fields; - }); + const keyToBeHashed = await generateApiKey(); - res.json({ apiKeys }); + const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); + + user.save((saveErr) => { + if (saveErr) { + sendFailure(500, saveErr); + return; + } + + const apiKeys = user.apiKeys + .map((apiKey, index) => { + const fields = apiKey.toObject(); + const shouldIncludeToken = index === addedApiKeyIndex - 1; + + return shouldIncludeToken ? + { ...fields, token: keyToBeHashed } : + fields; + }); + + res.json({ apiKeys }); + resolve(); + }); }); }); } export function removeApiKey(req, res) { - User.findById(req.user.id, (err, user) => { - if (err) { - res.status(500).json({ error: err }); - return; - } - if (!user) { - res.status(404).json({ error: 'User not found' }); - return; - } - const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); - if (!keyToDelete) { - res.status(404).json({ error: 'Key does not exist for user' }); - return; + return new Promise((resolve, reject) => { + function sendFailure(code, error) { + res.status(code).json({ error }); + resolve(); } - user.apiKeys.pull({ _id: req.params.keyId }); - - user.save((saveErr) => { - if (saveErr) { - res.status(500).json({ error: saveErr }); + User.findById(req.user.id, (err, user) => { + if (err) { + sendFailure(500, err); return; } - res.status(200).json({ apiKeys: user.apiKeys }); + if (!user) { + sendFailure(404, 'User not found'); + return; + } + + const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + if (!keyToDelete) { + sendFailure(404, 'Key does not exist for user'); + return; + } + + user.apiKeys.pull({ _id: req.params.keyId }); + + user.save((saveErr) => { + if (saveErr) { + sendFailure(500, saveErr); + return; + } + + res.status(200).json({ apiKeys: user.apiKeys }); + resolve(); + }); }); }); } diff --git a/server/domain-objects/Project.js b/server/domain-objects/Project.js new file mode 100644 index 00000000..f0de023f --- /dev/null +++ b/server/domain-objects/Project.js @@ -0,0 +1,133 @@ +import isPlainObject from 'lodash/isPlainObject'; +import pick from 'lodash/pick'; +import Project from '../models/project'; +import createId from '../utils/createId'; +import createApplicationErrorClass from '../utils/createApplicationErrorClass'; +import createDefaultFiles from './createDefaultFiles'; + +export const FileValidationError = createApplicationErrorClass('FileValidationError'); +export const ProjectValidationError = createApplicationErrorClass('ProjectValidationError'); + +/** + * This converts between a mongoose Project model + * and the public API Project object properties + * + */ +export function toApi(model) { + return { + id: model.id, + name: model.name, + }; +} + +/** + * Transforms a tree of files matching the APIs DirectoryContents + * format into the data structure stored in mongodb + * + * - flattens the tree into an array of file/folders + * - each file/folder gets a generated BSON-ID + * - each folder has a `children` array containing the IDs of it's children + */ +function transformFilesInner(tree = {}, parentNode) { + const files = []; + const errors = []; + + Object.entries(tree).forEach(([name, params]) => { + const id = createId(); + const isFolder = params.files != null; + + if (isFolder) { + const folder = { + _id: id, + name, + fileType: 'folder', + children: [] // Initialise an empty folder + }; + + files.push(folder); + + // The recursion will return a list of child files/folders + // It will also push the child's id into `folder.children` + const subFolder = transformFilesInner(params.files, folder); + files.push(...subFolder.files); + errors.push(...subFolder.errors); + } else { + const file = { + _id: id, + name, + fileType: 'file' + }; + + if (typeof params.url === 'string') { + file.url = params.url; + } else if (typeof params.content === 'string') { + file.content = params.content; + } else { + errors.push({ name, message: 'missing \'url\' or \'content\'' }); + } + + files.push(file); + } + + // Push this child's ID onto it's parent's list + // of children + if (parentNode != null) { + parentNode.children.push(id); + } + }); + + return { files, errors }; +} + +export function transformFiles(tree = {}) { + const withRoot = { + root: { + files: tree + } + }; + + const { files, errors } = transformFilesInner(withRoot); + + if (errors.length > 0) { + const message = `${errors.length} files failed validation. See error.files for individual errors. + + Errors: + ${errors.map(e => `* ${e.name}: ${e.message}`).join('\n')} +`; + const error = new FileValidationError(message); + error.files = errors; + + throw error; + } + + return files; +} + +export function containsRootHtmlFile(tree) { + return Object.keys(tree).find(name => /\.html$/.test(name)) != null; +} + +/** + * This converts between the public API's Project object + * properties and a mongoose Project model + * + */ +export function toModel(object) { + let files = []; + let tree = object.files; + + if (isPlainObject(tree)) { + if (!containsRootHtmlFile(tree)) { + tree = Object.assign(createDefaultFiles(), tree); + } + + files = transformFiles(tree); + } else { + throw new FileValidationError('\'files\' must be an object'); + } + + const projectValues = pick(object, ['user', 'name', 'slug']); + projectValues.files = files; + + return new Project(projectValues); +} diff --git a/server/domain-objects/__test__/Project.test.js b/server/domain-objects/__test__/Project.test.js new file mode 100644 index 00000000..0f3c9dd9 --- /dev/null +++ b/server/domain-objects/__test__/Project.test.js @@ -0,0 +1,385 @@ +import find from 'lodash/find'; + +import { containsRootHtmlFile, toModel, transformFiles, FileValidationError } from '../Project'; + +jest.mock('../../utils/createId'); + +// TODO: File name validation +// TODO: File extension validation +// +describe('domain-objects/Project', () => { + describe('containsRootHtmlFile', () => { + it('returns true for at least one root .html', () => { + expect(containsRootHtmlFile({ 'index.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'another-one.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe(true); + expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe(true); + }); + + it('returns false anything else', () => { + expect(containsRootHtmlFile({ 'sketch.js': {} })).toBe(false); + }); + + it('ignores nested html', () => { + expect(containsRootHtmlFile({ + examples: { + files: { + 'index.html': {} + } + } + })).toBe(false); + }); + }); + + describe('toModel', () => { + it('filters extra properties', () => { + const params = { + name: 'My sketch', + extraThing: 'oopsie', + files: {} + }; + + const model = toModel(params); + + expect(model.name).toBe('My sketch'); + expect(model.extraThing).toBeUndefined(); + }); + + it('throws FileValidationError', () => { + const params = { + files: { + 'index.html': {} // missing content or url + } + }; + + expect(() => toModel(params)).toThrowError(FileValidationError); + }); + + it('throws if files is not an object', () => { + const params = { + files: [] + }; + + expect(() => toModel(params)).toThrowError(FileValidationError); + }); + + it('creates default index.html and dependent files if no root .html is provided', () => { + const params = { + files: {} + }; + + const { files } = toModel(params); + + expect(files.length).toBe(4); + expect(find(files, { name: 'index.html' })).not.toBeUndefined(); + expect(find(files, { name: 'sketch.js' })).not.toBeUndefined(); + expect(find(files, { name: 'style.css' })).not.toBeUndefined(); + }); + + it('does not create default files if any root .html is provided', () => { + const params = { + files: { + 'example.html': { + content: 'Hello!' + } + } + }; + + const { files } = toModel(params); + + expect(files.length).toBe(2); + expect(find(files, { name: 'example.html' })).not.toBeUndefined(); + expect(find(files, { name: 'index.html' })).toBeUndefined(); + expect(find(files, { name: 'sketch.js' })).toBeUndefined(); + expect(find(files, { name: 'style.css' })).toBeUndefined(); + }); + + it('does not overwrite default CSS and JS of the same name if provided', () => { + const params = { + files: { + 'sketch.js': { + content: 'const sketch = true;' + }, + 'style.css': { + content: 'body { outline: 10px solid red; }' + } + } + }; + + const { files } = toModel(params); + + expect(files.length).toBe(4); + expect(find(files, { name: 'index.html' })).not.toBeUndefined(); + + const sketchFile = find(files, { name: 'sketch.js' }); + expect(sketchFile.content).toBe('const sketch = true;'); + + const cssFile = find(files, { name: 'style.css' }); + expect(cssFile.content).toBe('body { outline: 10px solid red; }'); + }); + }); +}); + +describe('transformFiles', () => { + beforeEach(() => { + // eslint-disable-next-line global-require + const { resetMockCreateId } = require('../../utils/createId'); + + resetMockCreateId(); + }); + + it('creates an empty root with no data', () => { + const tree = {}; + + expect(transformFiles(tree)).toEqual([{ + _id: '0', + fileType: 'folder', + name: 'root', + children: [] + }]); + }); + + it('converts tree-shaped files into list', () => { + const tree = { + 'index.html': { + content: 'some contents', + } + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + content: 'some contents', + fileType: 'file', + name: 'index.html' + } + ]); + }); + + it('uses file url over content', () => { + const tree = { + 'script.js': { + url: 'http://example.net/something.js' + } + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + url: 'http://example.net/something.js', + fileType: 'file', + name: 'script.js' + } + ]); + }); + + it('creates folders', () => { + const tree = { + 'a-folder': { + files: {} + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + children: [], + fileType: 'folder', + name: 'a-folder' + } + ]); + }); + + it('walks the tree processing files', () => { + const tree = { + 'index.html': { + content: 'some contents', + }, + 'a-folder': { + files: { + 'data.csv': { + content: 'this,is,data' + }, + 'another.txt': { + content: 'blah blah' + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1', '2'] + }, + { + _id: '1', + name: 'index.html', + fileType: 'file', + content: 'some contents' + }, + { + _id: '2', + name: 'a-folder', + fileType: 'folder', + children: ['3', '4'] + }, + { + _id: '3', + name: 'data.csv', + fileType: 'file', + content: 'this,is,data' + }, + { + _id: '4', + name: 'another.txt', + fileType: 'file', + content: 'blah blah' + } + ]); + }); + + it('handles deep nesting', () => { + const tree = { + first: { + files: { + second: { + files: { + third: { + files: { + 'hello.js': { + content: 'world!' + } + } + } + } + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1'] + }, + { + _id: '1', + name: 'first', + fileType: 'folder', + children: ['2'] + }, + { + _id: '2', + name: 'second', + fileType: 'folder', + children: ['3'] + }, + { + _id: '3', + name: 'third', + fileType: 'folder', + children: ['4'] + }, + { + _id: '4', + name: 'hello.js', + fileType: 'file', + content: 'world!' + } + ]); + }); + + + it('allows duplicate names in different folder', () => { + const tree = { + 'index.html': { + content: 'some contents', + }, + 'data': { + files: { + 'index.html': { + content: 'different file' + } + } + }, + }; + + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: ['1', '2'] + }, + { + _id: '1', + name: 'index.html', + fileType: 'file', + content: 'some contents' + }, + { + _id: '2', + name: 'data', + fileType: 'folder', + children: ['3'] + }, + { + _id: '3', + name: 'index.html', + fileType: 'file', + content: 'different file' + } + ]); + }); + + it('validates files', () => { + const tree = { + 'index.html': {} // missing `content` + }; + + expect(() => transformFiles(tree)).toThrowError(FileValidationError); + }); + + it('collects all file validation errors', () => { + const tree = { + 'index.html': {}, // missing `content` + 'something.js': {} // also missing `content` + }; + + try { + transformFiles(tree); + + // Should not get here + throw new Error('should have thrown before this point'); + } catch (err) { + expect(err).toBeInstanceOf(FileValidationError); + expect(err.files).toEqual([ + { name: 'index.html', message: 'missing \'url\' or \'content\'' }, + { name: 'something.js', message: 'missing \'url\' or \'content\'' } + ]); + } + }); +}); diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js new file mode 100644 index 00000000..9164c12a --- /dev/null +++ b/server/domain-objects/createDefaultFiles.js @@ -0,0 +1,48 @@ +const defaultSketch = `function setup() { + createCanvas(400, 400); +} + +function draw() { + background(220); +}`; + +const defaultHTML = + ` + + + + + + + + + + + + + +`; + +const defaultCSS = + `html, body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} +`; + +export default function createDefaultFiles() { + return { + 'index.html': { + content: defaultHTML + }, + 'style.css': { + content: defaultCSS + }, + 'sketch.js': { + content: defaultSketch + } + }; +} diff --git a/server/models/__mocks__/project.js b/server/models/__mocks__/project.js index 7260fcfd..2590046f 100644 --- a/server/models/__mocks__/project.js +++ b/server/models/__mocks__/project.js @@ -11,6 +11,24 @@ export function createMock() { return sinon.mock(Project); } +// Wraps the Project.prototype i.e. the +// instance methods in a mock so +// Project.save() can be mocked +export function createInstanceMock() { + // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro + Object.defineProperty(Project.prototype, 'save', { + value: Project.prototype.save, + configurable: true, + }); + + Object.defineProperty(Project.prototype, 'remove', { + value: Project.prototype.remove, + configurable: true, + }); + + return sinon.mock(Project.prototype); +} + // Re-export the model, it will be // altered by mockingoose whenever // we call methods on the MockConfig diff --git a/server/models/__mocks__/user.js b/server/models/__mocks__/user.js index 585d8b67..fcd73f32 100644 --- a/server/models/__mocks__/user.js +++ b/server/models/__mocks__/user.js @@ -1,12 +1,31 @@ -let __err = null; -let __user = null; +import sinon from 'sinon'; +import 'sinon-mongoose'; -export default { - __setFindById(err, user) { - __err = err; - __user = user; - }, - findById: jest.fn(async (id, callback) => { - callback(__err, __user); - }) -}; +// Import the actual model to be mocked +const User = jest.requireActual('../user').default; + +// Wrap User in a sinon mock +// The returned object is used to configure +// the mocked model's behaviour +export function createMock() { + return sinon.mock(User); +} + +// Wraps the User.prototype i.e. the +// instance methods in a mock so +// User.save() can be mocked +export function createInstanceMock() { + // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro + Object.defineProperty(User.prototype, 'save', { + value: User.prototype.save, + configurable: true, + }); + + return sinon.mock(User.prototype); +} + + +// Re-export the model, it will be +// altered by mockingoose whenever +// we call methods on the MockConfig +export default User; diff --git a/server/models/project.js b/server/models/project.js index 8642bc90..bf8c992e 100644 --- a/server/models/project.js +++ b/server/models/project.js @@ -49,8 +49,43 @@ projectSchema.set('toJSON', { projectSchema.pre('save', function generateSlug(next) { const project = this; - project.slug = slugify(project.name, '_'); + + if (!project.slug) { + project.slug = slugify(project.name, '_'); + } + return next(); }); +/** + * Check if slug is unique for this user's projects + */ +projectSchema.methods.isSlugUnique = async function isSlugUnique(cb) { + const project = this; + const hasCallback = typeof cb === 'function'; + + try { + const docsWithSlug = await project.model('Project') + .find({ user: project.user, slug: project.slug }, '_id') + .exec(); + + const result = { + isUnique: docsWithSlug.length === 0, + conflictingIds: docsWithSlug.map(d => d._id) || [] + }; + + if (hasCallback) { + cb(null, result); + } + + return result; + } catch (err) { + if (hasCallback) { + cb(err, null); + } + + throw err; + } +}; + export default mongoose.model('Project', projectSchema); diff --git a/server/routes/api.routes.js b/server/routes/api.routes.js new file mode 100644 index 00000000..60be3494 --- /dev/null +++ b/server/routes/api.routes.js @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import passport from 'passport'; +import * as ProjectController from '../controllers/project.controller'; + +const router = new Router(); + +router.get( + '/:username/sketches', + passport.authenticate('basic', { session: false }), + ProjectController.apiGetProjectsForUser +); + +router.post( + '/:username/sketches', + passport.authenticate('basic', { session: false }), + ProjectController.apiCreateProject +); + +// NOTE: Currently :username will not be checked for ownership +// only the project's owner in the database. +router.delete( + '/:username/sketches/:project_id', + passport.authenticate('basic', { session: false }), + ProjectController.deleteProject +); + +export default router; diff --git a/server/scripts/examples-ml5.js b/server/scripts/examples-ml5.js index 0721fe54..51e1fe5d 100644 --- a/server/scripts/examples-ml5.js +++ b/server/scripts/examples-ml5.js @@ -1,11 +1,7 @@ import fs from 'fs'; import rp from 'request-promise'; import Q from 'q'; -import mongoose from 'mongoose'; -import objectID from 'bson-objectid'; -import shortid from 'shortid'; -import User from '../models/user'; -import Project from '../models/project'; +import { ok } from 'assert'; // TODO: Change branchName if necessary const branchName = 'release'; @@ -14,11 +10,20 @@ const baseUrl = 'https://api.github.com/repos/ml5js/ml5-examples/contents'; const clientId = process.env.GITHUB_ID; const clientSecret = process.env.GITHUB_SECRET; const editorUsername = process.env.ML5_EXAMPLES_USERNAME; +const personalAccessToken = process.env.EDITOR_API_ACCESS_TOKEN; +const editorApiUrl = process.env.EDITOR_API_URL; const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; -const requestOptions = { +ok(clientId, 'GITHUB_ID is required'); +ok(clientSecret, 'GITHUB_SECRET is required'); +ok(editorUsername, 'ML5_EXAMPLES_USERNAME is required'); +ok(personalAccessToken, 'EDITOR_API_ACCESS_TOKEN is required'); +ok(editorApiUrl, 'EDITOR_API_URL is required'); + +// +const githubRequestOptions = { url: baseUrl, qs: { client_id: clientId, @@ -29,14 +34,15 @@ const requestOptions = { json: true }; -const mongoConnectionString = process.env.MONGO_URL; -mongoose.connect(mongoConnectionString, { - useMongoClient: true -}); -mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); - process.exit(1); -}); +const editorRequestOptions = { + url: `${editorApiUrl}/${editorUsername}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from(`${editorUsername}:${personalAccessToken}`).toString('base64')}` + }, + json: true +}; /** * --------------------------------------------------------- @@ -51,12 +57,52 @@ function flatten(list) { return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); } +/** + * Fetch data for a single HTML/JS file, or return + * an url to the file's CDN location + */ +async function fetchFileContent(item) { + const { name } = item; + const file = { url: item.url }; + + // if it is an html or js file + if ( + (file.url != null && name.endsWith('.html')) || + name.endsWith('.js') + ) { + const options = Object.assign({}, githubRequestOptions); + options.url = `${file.url}`; + + if ( + options.url !== undefined || + options.url !== null || + options.url !== '' + ) { + file.content = await rp(options); + // NOTE: remove the URL property if there's content + // Otherwise the p5 editor will try to pull from that url + if (file.content !== null) delete file.url; + } + + return file; + // if it is NOT an html or js file + } + + if (file.url) { + const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${file.url.split(branchName)[1]}`; + file.url = cdnRef; + } + + return file; +} + + /** * STEP 1: Get the top level cateogories */ async function getCategories() { try { - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}/p5js${branchRef}`; const results = await rp(options); @@ -76,13 +122,18 @@ async function getCategoryExamples(sketchRootList) { const output = []; const sketchRootCategories = sketchRootList.map(async (categories) => { // let options = Object.assign({url: `${requestOptions.url}/${categories.path}${branchRef}`}, requestOptions) - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}${categories.path}${branchRef}`; // console.log(options) const sketchDirs = await rp(options); - const result = flatten(sketchDirs); - return result; + try { + const result = flatten(sketchDirs); + + return result; + } catch (err) { + return []; + } }); const sketchList = await Q.all(sketchRootCategories); @@ -107,7 +158,7 @@ async function traverseSketchTree(parentObject) { return output; } // let options = `https://api.github.com/repos/ml5js/ml5-examples/contents/${sketches.path}${branchRef}` - const options = Object.assign({}, requestOptions); + const options = Object.assign({}, githubRequestOptions); options.url = `${options.url}${parentObject.path}${branchRef}`; output.tree = await rp(options); @@ -124,7 +175,6 @@ async function traverseSketchTree(parentObject) { * @param {*} categoryExamples - all of the categories in an array */ async function traverseSketchTreeAll(categoryExamples) { - // const sketches = categoryExamples.map(async sketch => await traverseSketchTree(sketch)); const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch)); const result = await Q.all(sketches); @@ -139,37 +189,24 @@ function traverseAndFormat(parentObject) { const parent = Object.assign({}, parentObject); if (!parentObject.tree) { - const newid = objectID().toHexString(); // returns the files return { name: parent.name, - url: parent.download_url, - content: null, - id: newid, - _id: newid, - fileType: 'file' + url: parent.download_url }; } const subdir = parentObject.tree.map((item) => { - const newid = objectID().toHexString(); if (!item.tree) { // returns the files return { name: item.name, - url: item.download_url, - content: null, - id: newid, - _id: newid, - fileType: 'file' + url: item.download_url }; } const feat = { name: item.name, - id: newid, - _id: newid, - fileType: 'folder', children: traverseAndFormat(item) }; return feat; @@ -178,69 +215,27 @@ function traverseAndFormat(parentObject) { } /** - * Traverse the tree and flatten for project.files[] + * Traverse the tree and download all the content, + * transforming into an object keyed by file/directory name * @param {*} projectFileTree */ -function traverseAndFlatten(projectFileTree) { - const r = objectID().toHexString(); +async function traverseAndDownload(projectFileTree) { + return projectFileTree.reduce( + async (previousPromise, item, idx) => { + const result = await previousPromise; - const projectRoot = { - name: 'root', - id: r, - _id: r, - children: [], - fileType: 'folder' - }; - - let currentParent; - - const output = projectFileTree.reduce( - (result, item, idx) => { - if (idx < projectFileTree.length) { - projectRoot.children.push(item.id); - } - - if (item.fileType === 'file') { - if (item.name === 'sketch.js') { - item.isSelectedFile = true; - } - result.push(item); - } - - // here's where the magic happens *twinkles* - if (item.fileType === 'folder') { - // recursively go down the tree of children - currentParent = traverseAndFlatten(item.children); - // the above will return an array of the children files - // concatenate that with the results - result = result.concat(currentParent); // eslint-disable-line no-param-reassign - // since we want to get the children ids, - // we can map the child ids to the current item - // then push that to our result array to get - // our flat files array. - item.children = item.children.map(child => child.id); - result.push(item); + if (Array.isArray(item.children)) { + result[item.name] = { + files: await traverseAndDownload(item.children) + }; + } else { + result[item.name] = await fetchFileContent(item); } return result; }, - [projectRoot] + {} ); - - // Kind of hacky way to remove all roots other than the starting one - let counter = 0; - output.forEach((item, idx) => { - if (item.name === 'root') { - if (counter === 0) { - counter += 1; - } else { - output.splice(idx, 1); - counter += 1; - } - } - }); - - return output; } /** @@ -249,16 +244,14 @@ function traverseAndFlatten(projectFileTree) { * @param {*} sketch * @param {*} user */ -function formatSketchForStorage(sketch, user) { - const newProject = new Project({ - _id: shortid.generate(), +async function formatSketchForStorage(sketch, user) { + const newProject = { name: sketch.name, - user: user._id, - files: [] // <== add files to this array as file objects and add _id reference to children of root - }); + files: {} // <== add files to this object + }; let projectFiles = traverseAndFormat(sketch); - projectFiles = traverseAndFlatten(projectFiles); + projectFiles = await traverseAndDownload(projectFiles); newProject.files = projectFiles; return newProject; } @@ -271,68 +264,50 @@ function formatSketchForStorageAll(sketchWithItems, user) { sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user)); - return sketchList; + return Promise.all(sketchList); } /** - * Get all the content for the relevant files in project.files[] - * @param {*} projectObject + * Fetch a list of all projects from the API */ -async function fetchSketchContent(projectObject) { - const output = Object.assign({}, JSON.parse(JSON.stringify(projectObject))); +async function getProjectsList() { + const options = Object.assign({}, editorRequestOptions); + options.url = `${options.url}/sketches`; - const newFiles = output.files.map(async (item, i) => { - // if it is an html or js file - if ( - (item.fileType === 'file' && item.name.endsWith('.html')) || - item.name.endsWith('.js') - ) { - const options = Object.assign({}, requestOptions); - options.url = `${item.url}`; + const results = await rp(options); - if ( - options.url !== undefined || - options.url !== null || - options.url !== '' - ) { - item.content = await rp(options); - // NOTE: remove the URL property if there's content - // Otherwise the p5 editor will try to pull from that url - if (item.content !== null) delete item.url; - } - - return item; - // if it is NOT an html or js file - } - - if (item.url) { - const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${ - item.url.split(branchName)[1] - }`; - item.content = cdnRef; - item.url = cdnRef; - } - - return item; - }); - - output.files = await Q.all(newFiles); - return output; + return results.sketches; } /** - * STEP 5 - * Get all the content for the relevant files in project.files[] for all sketches - * @param {*} formattedSketchList + * Delete a project */ -async function fetchSketchContentAll(formattedSketchList) { - let output = formattedSketchList.slice(0); +async function deleteProject(project) { + const options = Object.assign({}, editorRequestOptions); + options.method = 'DELETE'; + options.url = `${options.url}/sketches/${project.id}`; - output = output.map(async item => fetchSketchContent(item)); + const results = await rp(options); - output = await Q.all(output); + return results; +} - return output; +/** + * Create a new project + */ +async function createProject(project) { + try { + const options = Object.assign({}, editorRequestOptions); + options.method = 'POST'; + options.url = `${options.url}/sketches`; + options.body = project; + + const results = await rp(options); + + return results; + } catch (err) { + throw err; + } } /** @@ -342,43 +317,34 @@ async function fetchSketchContentAll(formattedSketchList) { * @param {*} user */ async function createProjectsInP5User(filledProjectList, user) { - const userProjects = await Project.find({ user: user._id }); - const removeProjects = userProjects.map(async project => Project.remove({ _id: project._id })); - await Q.all(removeProjects); - console.log('deleted old projects!'); + console.log('Finding existing projects...'); - const newProjects = filledProjectList.map(async (project) => { - const item = new Project(project); - console.log(`saving ${project.name}`); - await item.save(); - }); - await Q.all(newProjects); - console.log(`Projects saved to User: ${editorUsername}!`); -} + const existingProjects = await getProjectsList(); -/** - * STEP 0 - * CHECK if user exists, ifnot create one - * - */ -async function checkP5User() { - const user = await User.findOne({ username: editorUsername }); + console.log(`Will delete ${existingProjects.length} projects`); - if (!user) { - const ml5user = new User({ - username: editorUsername, - email: process.env.ML5_EXAMPLES_EMAIL, - password: process.env.ML5_EXAMPLES_PASS - }); - - await ml5user.save((saveErr) => { - if (saveErr) throw saveErr; - console.log(`Created a user p5${ml5user}`); + try { + await Q.all(existingProjects.map(deleteProject)); + console.log('deleted old projects!'); + } catch (error) { + console.log('Problem deleting projects'); + console.log(error); + process.exit(1); + } + + try { + const newProjects = filledProjectList.map(async (project) => { + console.log(`saving ${project.name}`); + await createProject(project); }); + await Q.all(newProjects); + console.log(`Projects saved to User: ${editorUsername}!`); + } catch (error) { + console.log('Error saving projects'); + console.log(error); } } - /** * --------------------------------------------------------- * --------------------- main ------------------------------ @@ -394,18 +360,14 @@ async function checkP5User() { * Delete existing and save */ async function make() { - await checkP5User(); - // Get the user - const user = await User.findOne({ - username: editorUsername - }); // Get the categories and their examples const categories = await getCategories(); const categoryExamples = await getCategoryExamples(categories); + const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples); - const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user); - const filledProjectList = await fetchSketchContentAll(formattedSketchList); - await createProjectsInP5User(filledProjectList, user); + const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); + + await createProjectsInP5User(formattedSketchList); console.log('done!'); process.exit(); } @@ -418,20 +380,14 @@ async function make() { * Format the sketch files to be save to the db * Delete existing and save */ +// eslint-disable-next-line no-unused-vars async function test() { - await checkP5User(); - // Get the user - const user = await User.findOne({ - username: editorUsername - }); - // read from file while testing const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json')); - const formattedSketchList = formatSketchForStorageAll(examplesWithResourceTree, user); + const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); - const filledProjectList = await fetchSketchContentAll(formattedSketchList); - await createProjectsInP5User(filledProjectList, user); + await createProjectsInP5User(formattedSketchList); console.log('done!'); process.exit(); } diff --git a/server/server.js b/server/server.js index 4483b722..af6ea25a 100644 --- a/server/server.js +++ b/server/server.js @@ -16,6 +16,7 @@ import webpackHotMiddleware from 'webpack-hot-middleware'; import config from '../webpack/config.dev'; // Import all required modules +import api from './routes/api.routes'; import users from './routes/user.routes'; import sessions from './routes/session.routes'; import projects from './routes/project.routes'; @@ -51,7 +52,7 @@ const corsOriginsWhitelist = [ // Run Webpack dev server in development mode if (process.env.NODE_ENV === 'development') { const compiler = webpack(config); - app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config[0].output.publicPath })); + app.use(webpackDevMiddleware(compiler, { lazy: false, noInfo: true, publicPath: config[0].output.publicPath })); app.use(webpackHotMiddleware(compiler)); corsOriginsWhitelist.push(/localhost/); @@ -95,6 +96,7 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); +app.use('/api/v1', requestsOfTypeJSON(), api); app.use('/api', requestsOfTypeJSON(), users); app.use('/api', requestsOfTypeJSON(), sessions); app.use('/api', requestsOfTypeJSON(), files); diff --git a/server/utils/__mocks__/createId.js b/server/utils/__mocks__/createId.js new file mode 100644 index 00000000..919ea252 --- /dev/null +++ b/server/utils/__mocks__/createId.js @@ -0,0 +1,16 @@ +/** + * Creates an increasing numeric ID + * for testing + */ +let nextId = 0; + +export default function mockCreateId() { + const id = nextId; + nextId += 1; + + return String(id); +} + +export function resetMockCreateId() { + nextId = 0; +} diff --git a/server/utils/createApplicationErrorClass.js b/server/utils/createApplicationErrorClass.js new file mode 100644 index 00000000..f949866c --- /dev/null +++ b/server/utils/createApplicationErrorClass.js @@ -0,0 +1,33 @@ +/** + * This is the base class for custom errors in + * the application. + */ +export class ApplicationError extends Error { + constructor(message, extra = {}) { + super(); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + this.name = 'ApplicationError'; + this.message = message; + this.code = extra.code; + } +} + +/** + * Create a new custom error class e.g. + * const UserNotFoundError = createApplicationErrorClass('UserNotFoundError'); + * + * // Later + * throw new UserNotFoundError(`user ${user.name} not found`); + * + */ +export default function createApplicationErrorClass(name) { + return class extends ApplicationError { + constructor(...params) { + super(...params); + + this.name = name; + } + }; +} diff --git a/server/utils/createId.js b/server/utils/createId.js new file mode 100644 index 00000000..65b8290f --- /dev/null +++ b/server/utils/createId.js @@ -0,0 +1,8 @@ +import objectID from 'bson-objectid'; + +/** + * Creates a mongo ID + */ +export default function createId() { + return objectID().toHexString(); +} From 210e8b60bfb1c9d728ba93b41deff23f47568b2a Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 20:39:45 +0200 Subject: [PATCH 083/322] Public API: Namespace private and public APIs (#1148) * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour. * Splits private and public APIs The private API is under /editor and the public API under /api --- .env.example | 2 +- app.json | 2 +- server/server.js | 15 +++++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 7680fb90..0385cf26 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -API_URL=/api +API_URL=/editor AWS_ACCESS_KEY= AWS_REGION= AWS_SECRET_KEY= diff --git a/app.json b/app.json index a588258e..3eff82c9 100644 --- a/app.json +++ b/app.json @@ -16,7 +16,7 @@ ], "env": { "API_URL": { - "value": "/api" + "value": "/editor" }, "AWS_ACCESS_KEY": { "description": "AWS Access Key", diff --git a/server/server.js b/server/server.js index af6ea25a..a7acc387 100644 --- a/server/server.js +++ b/server/server.js @@ -97,16 +97,19 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); app.use('/api/v1', requestsOfTypeJSON(), api); -app.use('/api', requestsOfTypeJSON(), users); -app.use('/api', requestsOfTypeJSON(), sessions); -app.use('/api', requestsOfTypeJSON(), files); -app.use('/api', requestsOfTypeJSON(), projects); -app.use('/api', requestsOfTypeJSON(), aws); +app.use('/editor', requestsOfTypeJSON(), users); +app.use('/editor', requestsOfTypeJSON(), sessions); +app.use('/editor', requestsOfTypeJSON(), files); +app.use('/editor', requestsOfTypeJSON(), projects); +app.use('/editor', requestsOfTypeJSON(), aws); // This is a temporary way to test access via Personal Access Tokens // Sending a valid username: combination will // return the user's information. -app.get('/api/auth/access-check', passport.authenticate('basic', { session: false }), (req, res) => res.json(req.user)); +app.get( + '/api/v1/auth/access-check', + passport.authenticate('basic', { session: false }), (req, res) => res.json(req.user) +); app.use(assetRoutes); // this is supposed to be TEMPORARY -- until i figure out From 0ae7a9eebbd1c569e3108957d4d26c09d2e80df5 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Fri, 30 Aug 2019 22:15:13 +0200 Subject: [PATCH 084/322] Display Access Token tab depending on UI_ACCESS_TOKEN_ENABLED feature flag (#1149) --- .env.example | 1 + client/modules/User/pages/AccountView.jsx | 4 +++- server/views/index.js | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0385cf26..17b6fe5a 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,4 @@ PORT=8000 S3_BUCKET= S3_BUCKET_URL_BASE= SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production +UI_ACCESS_TOKEN_ENABLED=false diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 5648cee2..0cda4898 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -33,6 +33,8 @@ class AccountView extends React.Component { } render() { + const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED; + return (
    @@ -50,7 +52,7 @@ class AccountView extends React.Component {

    Account

    -

    Access Tokens

    + {accessTokensUIEnabled &&

    Access Tokens

    }
    diff --git a/server/views/index.js b/server/views/index.js index d4062061..0539b927 100644 --- a/server/views/index.js +++ b/server/views/index.js @@ -30,6 +30,8 @@ export function renderIndex() { window.process.env.CLIENT = true; window.process.env.LOGIN_ENABLED = ${process.env.LOGIN_ENABLED === 'false' ? false : true}; window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true}; + window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true}; + window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true}; From 83978acc1de55c3320bc7e77c0a82785998cc71a Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Mon, 8 Jul 2019 12:06:13 +0200 Subject: [PATCH 085/322] Adds Collections model and Editor API to manage collections - List any user's collections - Create new collection - Modify collection metadata - Delete collection - Add/remove any project to/from a collection --- .../addProjectToCollection.js | 74 +++++++++++++++++++ .../collection.controller/createCollection.js | 47 ++++++++++++ .../collection.controller/index.js | 6 ++ .../collection.controller/listCollections.js | 48 ++++++++++++ .../collection.controller/removeCollection.js | 34 +++++++++ .../removeProjectFromCollection.js | 62 ++++++++++++++++ .../collection.controller/updateCollection.js | 54 ++++++++++++++ server/models/collection.js | 48 ++++++++++++ server/routes/collection.routes.js | 20 +++++ server/server.js | 2 + 10 files changed, 395 insertions(+) create mode 100644 server/controllers/collection.controller/addProjectToCollection.js create mode 100644 server/controllers/collection.controller/createCollection.js create mode 100644 server/controllers/collection.controller/index.js create mode 100644 server/controllers/collection.controller/listCollections.js create mode 100644 server/controllers/collection.controller/removeCollection.js create mode 100644 server/controllers/collection.controller/removeProjectFromCollection.js create mode 100644 server/controllers/collection.controller/updateCollection.js create mode 100644 server/models/collection.js create mode 100644 server/routes/collection.routes.js diff --git a/server/controllers/collection.controller/addProjectToCollection.js b/server/controllers/collection.controller/addProjectToCollection.js new file mode 100644 index 00000000..6abf08bf --- /dev/null +++ b/server/controllers/collection.controller/addProjectToCollection.js @@ -0,0 +1,74 @@ +import Collection from '../../models/collection'; +import Project from '../../models/project'; + +export default function addProjectToCollection(req, res) { + const owner = req.user._id; + const { id: collectionId, projectId } = req.params; + + const collectionPromise = Collection.findById(collectionId).populate('items.project', '_id'); + const projectPromise = Project.findById(projectId); + + function sendFailure(code, message) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess(collection) { + res.status(200).json(collection); + } + + function updateCollection([collection, project]) { + if (collection == null) { + sendFailure(404, 'Collection not found'); + return null; + } + + if (project == null) { + sendFailure(404, 'Project not found'); + return null; + } + + if (!collection.owner.equals(owner)) { + sendFailure(403, 'User does not own this collection'); + return null; + } + + const projectInCollection = collection.items.find(p => p.project._id === project._id); + + if (projectInCollection) { + sendFailure(404, 'Project already in collection'); + return null; + } + + try { + collection.items.push({ project }); + + return collection.save(); + } catch (error) { + console.error(error); + sendFailure(500, error.message); + return null; + } + } + + function populateReferences(collection) { + return Collection.populate( + collection, + [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', select: ['username'] + } + } + ] + ); + } + + return Promise.all([collectionPromise, projectPromise]) + .then(updateCollection) + .then(populateReferences) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/createCollection.js b/server/controllers/collection.controller/createCollection.js new file mode 100644 index 00000000..9fd97be3 --- /dev/null +++ b/server/controllers/collection.controller/createCollection.js @@ -0,0 +1,47 @@ +import Collection from '../../models/collection'; + +export default function createCollection(req, res) { + const owner = req.user._id; + const { name, description, slug } = req.body; + + const values = { + owner, + name, + description, + slug + }; + + function sendFailure({ code = 500, message = 'Something went wrong' }) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess(newCollection) { + res.json(newCollection); + } + + function populateReferences(newCollection) { + return Collection.populate( + newCollection, + [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', select: ['username'] + } + } + ] + ); + } + + if (owner == null) { + sendFailure({ code: 404, message: 'No user specified' }); + return null; + } + + return Collection.create(values) + .then(populateReferences) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/index.js b/server/controllers/collection.controller/index.js new file mode 100644 index 00000000..b09db3f0 --- /dev/null +++ b/server/controllers/collection.controller/index.js @@ -0,0 +1,6 @@ +export { default as addProjectToCollection } from './addProjectToCollection'; +export { default as createCollection } from './createCollection'; +export { default as listCollections } from './listCollections'; +export { default as removeCollection } from './removeCollection'; +export { default as removeProjectFromCollection } from './removeProjectFromCollection'; +export { default as updateCollection } from './updateCollection'; diff --git a/server/controllers/collection.controller/listCollections.js b/server/controllers/collection.controller/listCollections.js new file mode 100644 index 00000000..c71041b3 --- /dev/null +++ b/server/controllers/collection.controller/listCollections.js @@ -0,0 +1,48 @@ +import Collection from '../../models/collection'; +import User from '../../models/user'; + +async function getOwnerUserId(req) { + if (req.params.username) { + const user = await User.findOne({ username: req.params.username }); + if (user && user._id) { + return user._id; + } + } else if (req.user._id) { + return req.user._id; + } + + return null; +} + +export default function listCollections(req, res) { + function sendFailure({ code = 500, message = 'Something went wrong' }) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess(collections) { + res.status(200).json(collections); + } + + function findCollections(owner) { + if (owner == null) { + sendFailure({ code: 404, message: 'User not found' }); + } + + return Collection.find({ owner }) + .populate([ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', select: ['username'] + } + } + ]); + } + + return getOwnerUserId(req) + .then(findCollections) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/removeCollection.js b/server/controllers/collection.controller/removeCollection.js new file mode 100644 index 00000000..dffff876 --- /dev/null +++ b/server/controllers/collection.controller/removeCollection.js @@ -0,0 +1,34 @@ +import Collection from '../../models/collection'; + + +export default function createCollection(req, res) { + const { id: collectionId } = req.params; + const owner = req.user._id; + + function sendFailure({ code = 500, message = 'Something went wrong' }) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess() { + res.status(200).json({ success: true }); + } + + function removeCollection(collection) { + if (collection == null) { + sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' }); + return null; + } + + return collection.remove(); + } + + function findCollection() { + // Only returned if owner matches current user + return Collection.findOne({ _id: collectionId, owner }); + } + + return findCollection() + .then(removeCollection) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/removeProjectFromCollection.js b/server/controllers/collection.controller/removeProjectFromCollection.js new file mode 100644 index 00000000..561a9ce7 --- /dev/null +++ b/server/controllers/collection.controller/removeProjectFromCollection.js @@ -0,0 +1,62 @@ +import Collection from '../../models/collection'; +import Project from '../../models/project'; + +export default function addProjectToCollection(req, res) { + const owner = req.user._id; + const { id: collectionId, projectId } = req.params; + + function sendFailure({ code = 500, message = 'Something went wrong' }) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess(collection) { + res.status(200).json(collection); + } + + function updateCollection(collection) { + if (collection == null) { + sendFailure({ code: 404, message: 'Collection not found' }); + return null; + } + + if (!collection.owner.equals(owner)) { + sendFailure({ code: 403, message: 'User does not own this collection' }); + return null; + } + + const project = collection.items.find(p => p.project._id === projectId); + + if (project != null) { + project.remove(); + return collection.save(); + } + + const error = new Error('not found'); + error.code = 404; + + throw error; + } + + function populateReferences(collection) { + return Collection.populate( + collection, + [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', select: ['username'] + } + } + ] + ); + } + + return Collection.findById(collectionId) + .populate('items.project', '_id') + .then(updateCollection) + .then(populateReferences) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/updateCollection.js b/server/controllers/collection.controller/updateCollection.js new file mode 100644 index 00000000..365b8d24 --- /dev/null +++ b/server/controllers/collection.controller/updateCollection.js @@ -0,0 +1,54 @@ +import omitBy from 'lodash/omitBy'; +import isUndefined from 'lodash/isUndefined'; +import Collection from '../../models/collection'; + +function removeUndefined(obj) { + return omitBy(obj, isUndefined); +} + +export default function createCollection(req, res) { + const { id: collectionId } = req.params; + const owner = req.user._id; + const { name, description, slug } = req.body; + + const values = removeUndefined({ + name, + description, + slug + }); + + function sendFailure({ code = 500, message = 'Something went wrong' }) { + res.status(code).json({ success: false, message }); + } + + function sendSuccess(collection) { + if (collection == null) { + sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' }); + return; + } + + res.json(collection); + } + + async function findAndUpdateCollection() { + // Only update if owner matches current user + return Collection.findOneAndUpdate( + { _id: collectionId, owner }, + values, + { new: true, runValidators: true, setDefaultsOnInsert: true } + ).populate([ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', select: ['username'] + } + } + ]).exec(); + } + + return findAndUpdateCollection() + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/models/collection.js b/server/models/collection.js new file mode 100644 index 00000000..2753496b --- /dev/null +++ b/server/models/collection.js @@ -0,0 +1,48 @@ +import mongoose from 'mongoose'; +import shortid from 'shortid'; +import slugify from 'slugify'; + +const { Schema } = mongoose; + +const collectedProjectSchema = new Schema( + { + project: { type: Schema.Types.ObjectId, ref: 'Project' }, + }, + { timestamps: true, _id: true, usePushEach: true } +); + +collectedProjectSchema.virtual('id').get(function getId() { + return this._id.toHexString(); +}); + +collectedProjectSchema.set('toJSON', { + virtuals: true +}); + +const collectionSchema = new Schema( + { + _id: { type: String, default: shortid.generate }, + name: { type: String, default: 'My collection' }, + description: { type: String }, + slug: { type: String }, + owner: { type: Schema.Types.ObjectId, ref: 'User' }, + items: { type: [collectedProjectSchema] } + }, + { timestamps: true, usePushEach: true } +); + +collectionSchema.virtual('id').get(function getId() { + return this._id; +}); + +collectionSchema.set('toJSON', { + virtuals: true +}); + +collectionSchema.pre('save', function generateSlug(next) { + const collection = this; + collection.slug = slugify(collection.name, '_'); + return next(); +}); + +export default mongoose.model('Collection', collectionSchema); diff --git a/server/routes/collection.routes.js b/server/routes/collection.routes.js new file mode 100644 index 00000000..d4f7ff9a --- /dev/null +++ b/server/routes/collection.routes.js @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import * as CollectionController from '../controllers/collection.controller'; +import isAuthenticated from '../utils/isAuthenticated'; + +const router = new Router(); + +// List collections +router.get('/collections', isAuthenticated, CollectionController.listCollections); +router.get('/:username/collections', CollectionController.listCollections); + +// Create, modify, delete collection +router.post('/collections', isAuthenticated, CollectionController.createCollection); +router.patch('/collections/:id', isAuthenticated, CollectionController.updateCollection); +router.delete('/collections/:id', isAuthenticated, CollectionController.removeCollection); + +// Add and remove projects to collection +router.post('/collections/:id/:projectId', isAuthenticated, CollectionController.addProjectToCollection); +router.delete('/collections/:id/:projectId', isAuthenticated, CollectionController.removeProjectFromCollection); + +export default router; diff --git a/server/server.js b/server/server.js index a7acc387..de850a23 100644 --- a/server/server.js +++ b/server/server.js @@ -21,6 +21,7 @@ import users from './routes/user.routes'; import sessions from './routes/session.routes'; import projects from './routes/project.routes'; import files from './routes/file.routes'; +import collections from './routes/collection.routes'; import aws from './routes/aws.routes'; import serverRoutes from './routes/server.routes'; import embedRoutes from './routes/embed.routes'; @@ -102,6 +103,7 @@ app.use('/editor', requestsOfTypeJSON(), sessions); app.use('/editor', requestsOfTypeJSON(), files); app.use('/editor', requestsOfTypeJSON(), projects); app.use('/editor', requestsOfTypeJSON(), aws); +app.use('/editor', requestsOfTypeJSON(), collections); // This is a temporary way to test access via Personal Access Tokens // Sending a valid username: combination will From 9c36f2b2e23061acdcafd310bdea2d9a80596bf0 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 9 Jul 2019 10:33:20 +0200 Subject: [PATCH 086/322] Adds collections Nav item behind a feature flag --- client/components/Nav.jsx | 12 ++++++++++++ server/views/index.js | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 31b7e5b8..8dab6854 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -574,6 +574,18 @@ class Nav extends React.PureComponent { My sketches
  • + {__process.env.UI_COLLECTIONS_ENABLED && +
  • + + My collections + +
  • + }
  • From 8ac95c0084039b423cfa150c129c82b3dba1f54d Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Sun, 8 Sep 2019 17:10:48 +0200 Subject: [PATCH 087/322] SketchList and AssetList set page title --- client/modules/User/pages/DashboardView.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index ba1b63f0..49b162b6 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { browserHistory } from 'react-router'; -import { Helmet } from 'react-helmet'; + import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import NavBasic from '../../../components/NavBasic'; @@ -68,10 +68,6 @@ class DashboardView extends React.Component { return (
    - - p5.js Web Editor | Account - -
    From d02a413bf30aef5d5236f633133cde341875734c Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 9 Jul 2019 10:35:24 +0200 Subject: [PATCH 088/322] Displays existing collection - List all collections for a given user - View an individual collection - Link to a sketch from a collection --- client/constants.js | 2 + client/modules/IDE/actions/collections.js | 34 ++ client/modules/IDE/components/Collection.jsx | 375 +++++++++++++++++ .../modules/IDE/components/CollectionList.jsx | 381 ++++++++++++++++++ client/modules/IDE/pages/IDEView.jsx | 22 +- client/modules/IDE/reducers/collections.js | 12 + client/modules/IDE/selectors/collections.js | 37 ++ .../User/components/DashboardTabSwitcher.jsx | 6 +- client/modules/User/pages/DashboardView.jsx | 23 +- client/reducers.js | 4 +- client/routes.jsx | 5 +- .../collectionForUserExists.js | 29 ++ .../collection.controller/index.js | 1 + server/routes/server.routes.js | 13 + 14 files changed, 921 insertions(+), 23 deletions(-) create mode 100644 client/modules/IDE/actions/collections.js create mode 100644 client/modules/IDE/components/Collection.jsx create mode 100644 client/modules/IDE/components/CollectionList.jsx create mode 100644 client/modules/IDE/reducers/collections.js create mode 100644 client/modules/IDE/selectors/collections.js create mode 100644 server/controllers/collection.controller/collectionForUserExists.js diff --git a/client/constants.js b/client/constants.js index 4dd57213..c4111b95 100644 --- a/client/constants.js +++ b/client/constants.js @@ -35,6 +35,8 @@ export const HIDE_EDIT_PROJECT_NAME = 'HIDE_EDIT_PROJECT_NAME'; export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; +export const SET_COLLECTIONS = 'SET_COLLECTIONS'; + export const DELETE_PROJECT = 'DELETE_PROJECT'; export const SET_SELECTED_FILE = 'SET_SELECTED_FILE'; diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js new file mode 100644 index 00000000..d3fffe6f --- /dev/null +++ b/client/modules/IDE/actions/collections.js @@ -0,0 +1,34 @@ +import axios from 'axios'; +import * as ActionTypes from '../../../constants'; +import { startLoader, stopLoader } from './loader'; + +const __process = (typeof global !== 'undefined' ? global : window).process; +const ROOT_URL = __process.env.API_URL; + +// eslint-disable-next-line +export function getCollections(username) { + return (dispatch) => { + dispatch(startLoader()); + let url; + if (username) { + url = `${ROOT_URL}/${username}/collections`; + } else { + url = `${ROOT_URL}/collections`; + } + axios.get(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.SET_COLLECTIONS, + collections: response.data + }); + dispatch(stopLoader()); + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + }); + }; +} diff --git a/client/modules/IDE/components/Collection.jsx b/client/modules/IDE/components/Collection.jsx new file mode 100644 index 00000000..22ed85b9 --- /dev/null +++ b/client/modules/IDE/components/Collection.jsx @@ -0,0 +1,375 @@ +import format from 'date-fns/format'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import InlineSVG from 'react-inlinesvg'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; +import * as ProjectActions from '../actions/project'; +import * as ProjectsActions from '../actions/projects'; +import * as CollectionsActions from '../actions/collections'; +import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import * as IdeActions from '../actions/ide'; +import { getCollection } from '../selectors/collections'; +import Loader from '../../App/components/loader'; +import Overlay from '../../App/components/Overlay'; + +const arrowUp = require('../../../images/sort-arrow-up.svg'); +const arrowDown = require('../../../images/sort-arrow-down.svg'); +const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); + +class CollectionItemRowBase extends React.Component { + constructor(props) { + super(props); + this.state = { + optionsOpen: false, + renameOpen: false, + renameValue: props.item.project.name, + isFocused: false + }; + } + + onFocusComponent = () => { + this.setState({ isFocused: true }); + } + + onBlurComponent = () => { + this.setState({ isFocused: false }); + setTimeout(() => { + if (!this.state.isFocused) { + this.closeAll(); + } + }, 200); + } + + openOptions = () => { + this.setState({ + optionsOpen: true + }); + } + + closeOptions = () => { + this.setState({ + optionsOpen: false + }); + } + + toggleOptions = () => { + if (this.state.optionsOpen) { + this.closeOptions(); + } else { + this.openOptions(); + } + } + + openRename = () => { + this.setState({ + renameOpen: true + }); + } + + closeRename = () => { + this.setState({ + renameOpen: false + }); + } + + closeAll = () => { + this.setState({ + renameOpen: false, + optionsOpen: false + }); + } + + handleRenameChange = (e) => { + this.setState({ + renameValue: e.target.value + }); + } + + handleRenameEnter = (e) => { + if (e.key === 'Enter') { + // TODO pass this func + this.props.changeProjectName(this.props.collection.id, this.state.renameValue); + this.closeAll(); + } + } + + resetSketchName = () => { + this.setState({ + renameValue: this.props.collection.name + }); + } + + handleDropdownOpen = () => { + this.closeAll(); + this.openOptions(); + } + + handleRenameOpen = () => { + this.closeAll(); + this.openRename(); + } + + handleSketchDownload = () => { + this.props.exportProjectAsZip(this.props.collection.id); + } + + handleSketchDuplicate = () => { + this.closeAll(); + this.props.cloneProject(this.props.collection.id); + } + + handleSketchShare = () => { + this.closeAll(); + this.props.showShareModal(this.props.collection.id, this.props.collection.name, this.props.username); + } + + handleSketchDelete = () => { + this.closeAll(); + if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) { + this.props.deleteProject(this.props.collection.id); + } + } + + render() { + const { item, username } = this.props; + const { renameOpen, optionsOpen, renameValue } = this.state; + const sketchOwnerUsername = item.project.user.username; + const userIsOwner = this.props.user.username === sketchOwnerUsername; + const sketchUrl = `/${item.project.user.username}/sketches/${item.project.id}`; + + const dropdown = ( + + + {optionsOpen && +
      + {userIsOwner && +
    • + +
    • } +
    + } + + ); + + return ( + + + + {renameOpen ? '' : item.project.name} + + {renameOpen + && + e.stopPropagation()} + /> + } + + {format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')} + {sketchOwnerUsername} + {/* + {format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')} + {format(new Date(itm.updatedAt), 'MMM D, YYYY h:mm A')} + {(collection.items || []).length} + {dropdown} + */} + ); + } +} + +CollectionItemRowBase.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteProject: PropTypes.func.isRequired, + showShareModal: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + changeProjectName: PropTypes.func.isRequired +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); +} + +const CollectionItemRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionItemRowBase); + +class Collection extends React.Component { + constructor(props) { + super(props); + this.props.getCollections(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); + } + + getTitle() { + if (this.props.username === this.props.user.username) { + return 'p5.js Web Editor | My collections'; + } + return `p5.js Web Editor | ${this.props.username}'s collections`; + } + + getCollectionName() { + return this.props.collection.name; + } + + hasCollection() { + return !this.props.loading && this.props.collection != null; + } + + hasCollectionItems() { + return this.hasCollection() && this.props.collection.items.length > 0; + } + + _renderLoader() { + if (this.props.loading) return ; + return null; + } + + _renderCollectionMetadata() { + return ( +
    +

    {this.props.collection.description}

    +
    + ); + } + + _renderEmptyTable() { + if (!this.hasCollectionItems()) { + return (

    No sketches in collection.

    ); + } + return null; + } + + _renderFieldHeader(fieldName, displayName) { + const { field, direction } = this.props.sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + return ( + + + + ); + } + + render() { + const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + const title = this.hasCollection() ? this.getCollectionName() : null; + + return ( + +
    + + {this.getTitle()} + + {this._renderLoader()} + {this.hasCollection() && this._renderCollectionMetadata()} + {this._renderEmptyTable()} + {this.hasCollectionItems() && + + + + {this._renderFieldHeader('name', 'Name')} + {this._renderFieldHeader('createdAt', 'Date Added')} + {this._renderFieldHeader('user', 'Owner')} + + + + + {this.props.collection.items.map(item => + ())} + +
    } +
    +
    + ); + } +} + +Collection.propTypes = { + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + getCollections: PropTypes.func.isRequired, + collection: PropTypes.shape({}).isRequired, // TODO + username: PropTypes.string, + loading: PropTypes.bool.isRequired, + toggleDirectionForField: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired +}; + +Collection.defaultProps = { + username: undefined +}; + +function mapStateToProps(state, ownProps) { + return { + user: state.user, + collection: getCollection(state, ownProps.collectionId), + sorting: state.sorting, + loading: state.loading, + project: state.project + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions), dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Collection); diff --git a/client/modules/IDE/components/CollectionList.jsx b/client/modules/IDE/components/CollectionList.jsx new file mode 100644 index 00000000..2b071847 --- /dev/null +++ b/client/modules/IDE/components/CollectionList.jsx @@ -0,0 +1,381 @@ +import format from 'date-fns/format'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import InlineSVG from 'react-inlinesvg'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; +import * as ProjectActions from '../actions/project'; +import * as ProjectsActions from '../actions/projects'; +import * as CollectionsActions from '../actions/collections'; +import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import * as IdeActions from '../actions/ide'; +import getSortedCollections from '../selectors/collections'; +import Loader from '../../App/components/loader'; + +const arrowUp = require('../../../images/sort-arrow-up.svg'); +const arrowDown = require('../../../images/sort-arrow-down.svg'); +const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); + +class CollectionListRowBase extends React.Component { + constructor(props) { + super(props); + this.state = { + optionsOpen: false, + renameOpen: false, + renameValue: props.collection.name, + isFocused: false + }; + } + + onFocusComponent = () => { + this.setState({ isFocused: true }); + } + + onBlurComponent = () => { + this.setState({ isFocused: false }); + setTimeout(() => { + if (!this.state.isFocused) { + this.closeAll(); + } + }, 200); + } + + openOptions = () => { + this.setState({ + optionsOpen: true + }); + } + + closeOptions = () => { + this.setState({ + optionsOpen: false + }); + } + + toggleOptions = () => { + if (this.state.optionsOpen) { + this.closeOptions(); + } else { + this.openOptions(); + } + } + + openRename = () => { + this.setState({ + renameOpen: true + }); + } + + closeRename = () => { + this.setState({ + renameOpen: false + }); + } + + closeAll = () => { + this.setState({ + renameOpen: false, + optionsOpen: false + }); + } + + handleRenameChange = (e) => { + this.setState({ + renameValue: e.target.value + }); + } + + handleRenameEnter = (e) => { + if (e.key === 'Enter') { + // TODO pass this func + this.props.changeProjectName(this.props.collection.id, this.state.renameValue); + this.closeAll(); + } + } + + resetSketchName = () => { + this.setState({ + renameValue: this.props.collection.name + }); + } + + handleDropdownOpen = () => { + this.closeAll(); + this.openOptions(); + } + + handleRenameOpen = () => { + this.closeAll(); + this.openRename(); + } + + handleSketchDownload = () => { + this.props.exportProjectAsZip(this.props.collection.id); + } + + handleSketchDuplicate = () => { + this.closeAll(); + this.props.cloneProject(this.props.collection.id); + } + + handleSketchShare = () => { + this.closeAll(); + this.props.showShareModal(this.props.collection.id, this.props.collection.name, this.props.username); + } + + handleSketchDelete = () => { + this.closeAll(); + if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) { + this.props.deleteProject(this.props.collection.id); + } + } + + render() { + const { collection, username } = this.props; + const { renameOpen, optionsOpen, renameValue } = this.state; + const userIsOwner = this.props.user.username === this.props.username; + + const dropdown = ( + + + {optionsOpen && +
      + {userIsOwner && +
    • + +
    • } +
    + } + + ); + + return ( + + + + {renameOpen ? '' : collection.name} + + {renameOpen + && + e.stopPropagation()} + /> + } + + {format(new Date(collection.createdAt), 'MMM D, YYYY h:mm A')} + {format(new Date(collection.updatedAt), 'MMM D, YYYY h:mm A')} + {(collection.items || []).length} + {dropdown} + ); + } +} + +CollectionListRowBase.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteProject: PropTypes.func.isRequired, + showShareModal: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + changeProjectName: PropTypes.func.isRequired +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); +} + +const CollectionListRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); + +class CollectionList extends React.Component { + constructor(props) { + super(props); + this.props.getCollections(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); + } + + getTitle() { + if (this.props.username === this.props.user.username) { + return 'p5.js Web Editor | My collections'; + } + return `p5.js Web Editor | ${this.props.username}'s collections`; + } + + hasCollections() { + return !this.props.loading && this.props.collections.length > 0; + } + + _renderLoader() { + if (this.props.loading) return ; + return null; + } + + _renderEmptyTable() { + if (!this.props.loading && this.props.collections.length === 0) { + return (

    No collections.

    ); + } + return null; + } + + _renderFieldHeader(fieldName, displayName) { + const { field, direction } = this.props.sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + return ( + + + + ); + } + + render() { + const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + return ( +
    + + {this.getTitle()} + + {this._renderLoader()} + {this._renderEmptyTable()} + {this.hasCollections() && + + + + {this._renderFieldHeader('name', 'Name')} + {this._renderFieldHeader('createdAt', 'Date Created')} + {this._renderFieldHeader('updatedAt', 'Date Updated')} + {this._renderFieldHeader('numItems', '# sketches')} + + + + + {this.props.collections.map(collection => + ())} + +
    } +
    + ); + } +} + +const ProjectShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string.isRequired + }).isRequired, +}); + +const ItemsShape = PropTypes.shape({ + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + project: ProjectShape +}); + +CollectionList.propTypes = { + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + getCollections: PropTypes.func.isRequired, + collections: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + items: PropTypes.arrayOf(ItemsShape), + })).isRequired, + username: PropTypes.string, + loading: PropTypes.bool.isRequired, + toggleDirectionForField: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + owner: PropTypes.shape({ + id: PropTypes.string + }) + }) +}; + +CollectionList.defaultProps = { + project: { + id: undefined, + owner: undefined + }, + username: undefined +}; + +function mapStateToProps(state) { + return { + user: state.user, + collections: getSortedCollections(state), + sorting: state.sorting, + loading: state.loading, + project: state.project + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions), + dispatch + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index b0be087e..e97a8d88 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -314,12 +314,12 @@ class IDEView extends React.Component { {( ( (this.props.preferences.textOutput || - this.props.preferences.gridOutput || - this.props.preferences.soundOutput + this.props.preferences.gridOutput || + this.props.preferences.soundOutput ) && - this.props.ide.isPlaying + this.props.ide.isPlaying ) || - this.props.ide.isAccessibleOutputPlaying + this.props.ide.isAccessibleOutputPlaying ) }
    @@ -350,14 +350,14 @@ class IDEView extends React.Component { - { this.props.ide.modalIsVisible && + {this.props.ide.modalIsVisible && } - { this.props.ide.newFolderModalVisible && + {this.props.ide.newFolderModalVisible && } - { this.props.location.pathname === '/feedback' && + {this.props.location.pathname === '/feedback' && } - { this.props.ide.shareModalVisible && + {this.props.ide.shareModalVisible && } - { this.props.ide.keyboardShortcutVisible && + {this.props.ide.keyboardShortcutVisible && } - { this.props.ide.errorType && + {this.props.ide.errorType && } - { this.props.ide.helpType && + {this.props.ide.helpType && { + switch (action.type) { + case ActionTypes.SET_COLLECTIONS: + return action.collections; + default: + return state; + } +}; + +export default sketches; diff --git a/client/modules/IDE/selectors/collections.js b/client/modules/IDE/selectors/collections.js new file mode 100644 index 00000000..3ba90241 --- /dev/null +++ b/client/modules/IDE/selectors/collections.js @@ -0,0 +1,37 @@ +import { createSelector } from 'reselect'; +import differenceInMilliseconds from 'date-fns/difference_in_milliseconds'; +import find from 'lodash/find'; +import orderBy from 'lodash/orderBy'; +import { DIRECTION } from '../actions/sorting'; + +const getCollections = state => state.collections; +const getField = state => state.sorting.field; +const getDirection = state => state.sorting.direction; + +const getSortedCollections = createSelector( + getCollections, + getField, + getDirection, + (collections, field, direction) => { + if (field === 'name') { + if (direction === DIRECTION.DESC) { + return orderBy(collections, 'name', 'desc'); + } + return orderBy(collections, 'name', 'asc'); + } + const sortedCollections = [...collections].sort((a, b) => { + const result = + direction === DIRECTION.ASC + ? differenceInMilliseconds(new Date(a[field]), new Date(b[field])) + : differenceInMilliseconds(new Date(b[field]), new Date(a[field])); + return result; + }); + return sortedCollections; + } +); + +export function getCollection(state, id) { + return find(getCollections(state), { id }); +} + +export default getSortedCollections; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 8143759f..4b657b01 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -4,6 +4,7 @@ import { Link } from 'react-router'; const TabKey = { assets: 'assets', + collections: 'collections', sketches: 'sketches', }; @@ -30,8 +31,9 @@ Tab.propTypes = { const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => (
      - Sketches - {isOwner && Assets} + Sketches + Collections + {isOwner && Assets}
    ); diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index ba1b63f0..3b6344b0 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -8,6 +8,7 @@ import { updateSettings, initiateVerification, createApiKey, removeApiKey } from import NavBasic from '../../../components/NavBasic'; import AssetList from '../../IDE/components/AssetList'; +import CollectionList from '../../IDE/components/CollectionList'; import SketchList from '../../IDE/components/SketchList'; import DashboardTabSwitcher, { TabKey } from '../components/DashboardTabSwitcher'; @@ -35,11 +36,13 @@ class DashboardView extends React.Component { browserHistory.push('/'); } - selectedTabName() { + selectedTabKey() { const path = this.props.location.pathname; if (/assets/.test(path)) { return TabKey.assets; + } else if (/collections/.test(path)) { + return TabKey.collections; } return TabKey.sketches; @@ -57,12 +60,20 @@ class DashboardView extends React.Component { return this.props.user.username === this.props.params.username; } - navigationItem() { - + renderContent(tabKey, username) { + switch (tabKey) { + case TabKey.assets: + return ; + case TabKey.collections: + return ; + case TabKey.sketches: + default: + return ; + } } render() { - const currentTab = this.selectedTabName(); + const currentTab = this.selectedTabKey(); const isOwner = this.isOwner(); const { username } = this.props.params; @@ -82,9 +93,7 @@ class DashboardView extends React.Component {
    - { - currentTab === TabKey.sketches ? : - } + {this.renderContent(currentTab, username)}
    diff --git a/client/reducers.js b/client/reducers.js index 057dbd62..b14fd59f 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -12,6 +12,7 @@ import console from './modules/IDE/reducers/console'; import assets from './modules/IDE/reducers/assets'; import sorting from './modules/IDE/reducers/sorting'; import loading from './modules/IDE/reducers/loading'; +import collections from './modules/IDE/reducers/collections'; const rootReducer = combineReducers({ form, @@ -26,7 +27,8 @@ const rootReducer = combineReducers({ toast, console, assets, - loading + loading, + collections }); export default rootReducer; diff --git a/client/routes.jsx b/client/routes.jsx index 89db6977..6910d923 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -41,8 +41,9 @@ const routes = store => ( - - + + + diff --git a/server/controllers/collection.controller/collectionForUserExists.js b/server/controllers/collection.controller/collectionForUserExists.js new file mode 100644 index 00000000..e2881fd4 --- /dev/null +++ b/server/controllers/collection.controller/collectionForUserExists.js @@ -0,0 +1,29 @@ +import Collection from '../../models/collection'; +import User from '../../models/user'; + +export default function collectionForUserExists(username, collectionId, callback) { + function sendFailure() { + callback(false); + } + + function sendSuccess(collection) { + callback(collection != null); + } + + function findUser() { + return User.findOne({ username }); + } + + function findCollection(owner) { + if (owner == null) { + throw new Error('User not found'); + } + + return Collection.findOne({ _id: collectionId, owner }); + } + + return findUser() + .then(findCollection) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/index.js b/server/controllers/collection.controller/index.js index b09db3f0..8cb9e368 100644 --- a/server/controllers/collection.controller/index.js +++ b/server/controllers/collection.controller/index.js @@ -1,4 +1,5 @@ export { default as addProjectToCollection } from './addProjectToCollection'; +export { default as collectionForUserExists } from './collectionForUserExists'; export { default as createCollection } from './createCollection'; export { default as listCollections } from './listCollections'; export { default as removeCollection } from './removeCollection'; diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 41d7dc60..29877809 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -3,6 +3,7 @@ import { renderIndex } from '../views/index'; import { get404Sketch } from '../views/404Page'; import { userExists } from '../controllers/user.controller'; import { projectExists, projectForUserExists } from '../controllers/project.controller'; +import { collectionForUserExists } from '../controllers/collection.controller'; const router = new Router(); @@ -111,4 +112,16 @@ router.get('/:username/sketches', (req, res) => { )); }); +router.get('/:username/collections', (req, res) => { + userExists(req.params.username, exists => ( + exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) + )); +}); + +router.get('/:username/collections/:id', (req, res) => { + collectionForUserExists(req.params.username, req.params.id, exists => ( + exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) + )); +}); + export default router; From dcf65c6f46d02484976244bc27bf760f022a5404 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 9 Jul 2019 18:24:09 +0200 Subject: [PATCH 089/322] Create Collection --- client/constants.js | 4 + client/modules/IDE/actions/collections.js | 77 +++++++++++ .../modules/IDE/components/CollectionList.jsx | 19 ++- client/modules/IDE/reducers/collections.js | 12 ++ client/modules/IDE/reducers/project.js | 10 +- .../{IDE => User}/components/Collection.jsx | 0 .../User/components/CollectionCreate.jsx | 124 ++++++++++++++++++ client/modules/User/pages/CollectionView.jsx | 114 ++++++++++++++++ client/routes.jsx | 2 + client/styles/base/_base.scss | 7 +- client/styles/components/_forms.scss | 13 ++ client/utils/generateRandomName.js | 12 ++ server/routes/server.routes.js | 12 ++ 13 files changed, 394 insertions(+), 12 deletions(-) rename client/modules/{IDE => User}/components/Collection.jsx (100%) create mode 100644 client/modules/User/components/CollectionCreate.jsx create mode 100644 client/modules/User/pages/CollectionView.jsx create mode 100644 client/utils/generateRandomName.js diff --git a/client/constants.js b/client/constants.js index c4111b95..cd90d4c9 100644 --- a/client/constants.js +++ b/client/constants.js @@ -36,6 +36,10 @@ export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; export const SET_COLLECTIONS = 'SET_COLLECTIONS'; +export const CREATE_COLLECTION = 'CREATED_COLLECTION'; + +export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION'; +export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION'; export const DELETE_PROJECT = 'DELETE_PROJECT'; diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index d3fffe6f..17f0b872 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -32,3 +32,80 @@ export function getCollections(username) { }); }; } + +export function createCollection(collection) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections`; + return axios.post(url, collection, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.CREATE_COLLECTION + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} + +export function addToCollection(collectionId, projectId) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; + return axios.post(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.ADD_TO_COLLECTION, + payload: response.data + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} + +export function removeFromCollection(collectionId, projectId) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; + return axios.delete(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.REMOVE_FROM_COLLECTION, + payload: response.data + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} diff --git a/client/modules/IDE/components/CollectionList.jsx b/client/modules/IDE/components/CollectionList.jsx index 2b071847..9da68917 100644 --- a/client/modules/IDE/components/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList.jsx @@ -21,6 +21,10 @@ const arrowDown = require('../../../images/sort-arrow-down.svg'); const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); class CollectionListRowBase extends React.Component { + static projectInCollection(project, collection) { + return collection.items.find(item => item.project.id === project.id) != null; + } + constructor(props) { super(props); this.state = { @@ -134,6 +138,14 @@ class CollectionListRowBase extends React.Component { } } + handleCollectionAdd = () => { + this.props.addToCollection(this.props.collection.id, this.props.project.id); + } + + handleCollectionRemove = () => { + this.props.removeFromCollection(this.props.collection.id, this.props.project.id); + } + render() { const { collection, username } = this.props; const { renameOpen, optionsOpen, renameValue } = this.state; @@ -198,6 +210,8 @@ class CollectionListRowBase extends React.Component { } CollectionListRowBase.propTypes = { + addToCollection: PropTypes.func.isRequired, + removeFromCollection: PropTypes.func.isRequired, collection: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired @@ -215,7 +229,7 @@ CollectionListRowBase.propTypes = { }; function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); + return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions), dispatch); } const CollectionListRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); @@ -279,6 +293,9 @@ class CollectionList extends React.Component { {this.getTitle()} + + New collection + {this._renderLoader()} {this._renderEmptyTable()} {this.hasCollections() && diff --git a/client/modules/IDE/reducers/collections.js b/client/modules/IDE/reducers/collections.js index fbbaefcc..4c4027b1 100644 --- a/client/modules/IDE/reducers/collections.js +++ b/client/modules/IDE/reducers/collections.js @@ -4,6 +4,18 @@ const sketches = (state = [], action) => { switch (action.type) { case ActionTypes.SET_COLLECTIONS: return action.collections; + + // The API returns the complete new collection + // with the items added or removed + case ActionTypes.ADD_TO_COLLECTION: + case ActionTypes.REMOVE_FROM_COLLECTION: + return state.map((collection) => { + if (collection.id === action.payload.id) { + return action.payload; + } + + return collection; + }); default: return state; } diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 0779c0f5..2eb19d4d 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -1,14 +1,8 @@ -import friendlyWords from 'friendly-words'; import * as ActionTypes from '../../../constants'; - -const generateRandomName = () => { - const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; - const obj = friendlyWords.objects[Math.floor(Math.random() * friendlyWords.objects.length)]; - return `${adj} ${obj}`; -}; +import { generateProjectName } from '../../../utils/generateRandomName'; const initialState = () => { - const generatedString = generateRandomName(); + const generatedString = generateProjectName(); const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1); return { name: generatedName, diff --git a/client/modules/IDE/components/Collection.jsx b/client/modules/User/components/Collection.jsx similarity index 100% rename from client/modules/IDE/components/Collection.jsx rename to client/modules/User/components/Collection.jsx diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx new file mode 100644 index 00000000..3542e6a3 --- /dev/null +++ b/client/modules/User/components/CollectionCreate.jsx @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { connect } from 'react-redux'; +import { browserHistory } from 'react-router'; +import { bindActionCreators } from 'redux'; +import * as CollectionsActions from '../../IDE/actions/collections'; + +import { generateCollectionName } from '../../../utils/generateRandomName'; + +class CollectionCreate extends React.Component { + constructor() { + super(); + + const name = generateCollectionName(); + + this.state = { + generatedCollectionName: name, + collection: { + name, + description: '' + } + }; + } + + getTitle() { + return 'p5.js Web Editor | Create collection'; + } + + handleTextChange = field => (evt) => { + this.setState({ + collection: { + ...this.state.collection, + [field]: evt.target.value, + } + }); + } + + handleCreateCollection = (event) => { + event.preventDefault(); + + this.props.createCollection(this.state.collection) + .then(({ id, owner }) => { + browserHistory.replace(`/${owner.username}/collections/${id}`); + }) + .catch((error) => { + console.error('Error creating collection', error); + this.setState({ + creationError: error, + }); + }); + } + + render() { + const { generatedCollectionName, creationError } = this.state; + const { name, description } = this.state.collection; + + const invalid = name === '' || name == null; + + return ( +
    + + {this.getTitle()} + + +
    + {creationError && Couldn't create collection} +

    + + + {invalid && Collection name is required} +

    +

    + +