diff --git a/bin/cli b/bin/cli index 6237d4740..b61c529c5 100755 --- a/bin/cli +++ b/bin/cli @@ -41,20 +41,3 @@ if (!commands.includes(command)) { } process.exit(1); } - -// /** -// * When this process exists, check to see if we have a running command, if we do -// * check to see if it is still running. If it is, then kill it with a SIGINT -// * signal. This is for the use case where we want to kill the process that is -// * labeled with the PID written out by the parent process. -// */ -// process.once('exit', () => { -// if ( - -// // program.runningCommand && -// program.runningCommand.killed === false && -// program.runningCommand.exitCode === null -// ) { -// program.runningCommand.kill('SIGINT'); -// } -// }); diff --git a/bin/cli-plugins b/bin/cli-plugins index a99e97649..883682ede 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -235,7 +235,7 @@ async function reconcileLocalPlugins({ skipRemote, dryRun }) { if (output.status) { throw new Error( - 'Could not install local plugin dependencies, errors occured during install' + 'Could not install local plugin dependencies, errors occurred during install' ); } @@ -253,59 +253,61 @@ async function reconcilePluginDeps({ dryRun, upgradeRemote, }) { - let startTime = new Date(); + try { + let startTime = new Date(); - // We don't need to do anything if we skip everything.... - if (skipLocal && skipRemote) { - return; - } + // We don't need to do anything if we skip everything.... + if (skipLocal && skipRemote) { + return; + } - // Traverse local plugins and install dependencies if enabled. - if (!skipLocal) { - await reconcileLocalPlugins({ skipRemote, dryRun }); - } + // Traverse local plugins and install dependencies if enabled. + if (!skipLocal) { + await reconcileLocalPlugins({ skipRemote, dryRun }); + } - // Locate any external plugins and install them. - if (!skipRemote) { - let results = []; - try { - results = await reconcileRemotePlugins({ + // Locate any external plugins and install them. + if (!skipRemote) { + const results = await reconcileRemotePlugins({ skipLocal, skipRemote, dryRun, upgradeRemote, }); - } catch (e) { - throw e; + + let status; + if (dryRun) { + status = '[dry-run] success'.green; + } else { + status = 'success'.green; + } + + let message; + if (results.upgradable.length === 0 && results.fetchable.length === 0) { + message = 'Already up-to-date.'; + } else if (results.upgradable.length === 0) { + message = `Fetched ${results.fetchable.length} new plugins.`; + } else if (results.fetchable.length === 0) { + message = `Upgraded ${results.upgradable.length} new plugins.`; + } else { + message = `Fetched ${results.fetchable.length} new plugins, upgraded ${ + results.upgradable.length + } plugins.`; + } + + console.log(`\n${status} ${message}`); } - let status; - if (dryRun) { - status = '[dry-run] success'.green; - } else { - status = 'success'.green; - } + let endTime = new Date(); - let message; - if (results.upgradable.length === 0 && results.fetchable.length === 0) { - message = 'Already up-to-date.'; - } else if (results.upgradable.length === 0) { - message = `Fetched ${results.fetchable.length} new plugins.`; - } else if (results.fetchable.length === 0) { - message = `Upgraded ${results.upgradable.length} new plugins.`; - } else { - message = `Fetched ${results.fetchable.length} new plugins, upgraded ${ - results.upgradable.length - } plugins.`; - } - - console.log(`\n${status} ${message}`); + let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed( + 2 + ); + console.log(`✨ Done in ${totalTime}s.`); + } catch (err) { + console.error(err); + process.exit(1); } - - let endTime = new Date(); - - let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2); - console.log(`✨ Done in ${totalTime}s.`); } async function createSeedPlugin() { diff --git a/bin/util.js b/bin/util.js index 0a9adb966..e80f123a5 100644 --- a/bin/util.js +++ b/bin/util.js @@ -63,5 +63,6 @@ process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2')); // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { - throw err; + console.error(err); + process.exit(1); }); diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index d226ac744..71d2ed1a3 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -2,7 +2,6 @@ import React from 'react'; import cn from 'classnames'; import PropTypes from 'prop-types'; import capitalize from 'lodash/capitalize'; -import { getErrorMessages } from 'coral-framework/utils'; import styles from './UserDetail.css'; import AccountHistory from './AccountHistory'; import { Slot } from 'coral-framework/components'; @@ -29,43 +28,23 @@ import UserInfoTooltip from './UserInfoTooltip'; class UserDetail extends React.Component { rejectThenReload = async info => { - try { - await this.props.rejectComment(info); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.rejectComment(info); + this.props.data.refetch(); }; acceptThenReload = async info => { - try { - await this.props.acceptComment(info); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.acceptComment(info); + this.props.data.refetch(); }; bulkAcceptThenReload = async () => { - try { - await this.props.bulkAccept(); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.bulkAccept(); + this.props.data.refetch(); }; bulkRejectThenReload = async () => { - try { - await this.props.bulkReject(); - this.props.data.refetch(); - } catch (err) { - console.error(err); - this.props.notify('error', getErrorMessages(err)); - } + await this.props.bulkReject(); + this.props.data.refetch(); }; changeTab = tab => { @@ -94,6 +73,16 @@ class UserDetail extends React.Component { ); } + renderError() { + return ( + + +
{this.props.data.error.message}
+
+
+ ); + } + getActionMenuLabel() { const { root: { user } } = this.props; @@ -345,6 +334,10 @@ class UserDetail extends React.Component { } render() { + if (this.props.data.error) { + return this.renderError(); + } + if (this.props.loading) { return this.renderLoading(); } @@ -371,7 +364,6 @@ UserDetail.propTypes = { selectedCommentIds: PropTypes.array.isRequired, viewUserDetail: PropTypes.any.isRequired, loadMore: PropTypes.any.isRequired, - notify: PropTypes.func.isRequired, showSuspendUserDialog: PropTypes.func, showBanUserDialog: PropTypes.func, unbanUser: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/containers/BanUserDialog.js b/client/coral-admin/src/containers/BanUserDialog.js index d2dc642a7..3617902ff 100644 --- a/client/coral-admin/src/containers/BanUserDialog.js +++ b/client/coral-admin/src/containers/BanUserDialog.js @@ -10,8 +10,6 @@ import { } from 'coral-framework/graphql/mutations'; import { compose } from 'react-apollo'; import t from 'coral-framework/services/i18n'; -import { getErrorMessages } from 'coral-framework/utils'; -import { notify } from 'coral-framework/actions/notification'; class BanUserDialogContainer extends Component { banUser = async () => { @@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component { banUser, setCommentStatus, hideBanUserDialog, - notify, } = this.props; - try { - await banUser({ id: userId, message: '' }); - hideBanUserDialog(); - if (commentId && commentStatus && commentStatus !== 'REJECTED') { - await setCommentStatus({ commentId, status: 'REJECTED' }); - } - } catch (err) { - notify('error', getErrorMessages(err)); + await banUser({ id: userId, message: '' }); + hideBanUserDialog(); + if (commentId && commentStatus && commentStatus !== 'REJECTED') { + await setCommentStatus({ commentId, status: 'REJECTED' }); } }; @@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({ ...bindActionCreators( { hideBanUserDialog, - notify, }, dispatch ), }); export default compose( + connect(mapStateToProps, mapDispatchToProps), withBanUser, - withSetCommentStatus, - connect(mapStateToProps, mapDispatchToProps) + withSetCommentStatus )(BanUserDialogContainer); diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index e058faad6..f03d48f52 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -11,7 +11,6 @@ import { import { compose, gql } from 'react-apollo'; import t, { timeago } from 'coral-framework/services/i18n'; import withQuery from 'coral-framework/hocs/withQuery'; -import { getErrorMessages } from 'coral-framework/utils'; import get from 'lodash/get'; import { notify } from 'coral-framework/actions/notification'; @@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component { notify, } = this.props; hideSuspendUserDialog(); - try { - await suspendUser({ id: userId, message, until }); - notify( - 'success', - t('suspenduser.notify_suspend_until', username, timeago(until)) - ); - if (commentId && commentStatus && commentStatus !== 'REJECTED') { - await setCommentStatus({ commentId, status: 'REJECTED' }); - } - } catch (err) { - notify('error', getErrorMessages(err)); + await suspendUser({ id: userId, message, until }); + notify( + 'success', + t('suspenduser.notify_suspend_until', username, timeago(until)) + ); + if (commentId && commentStatus && commentStatus !== 'REJECTED') { + await setCommentStatus({ commentId, status: 'REJECTED' }); } }; diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index bd34ac4d8..fb1508a2f 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -24,7 +24,6 @@ import { } from 'coral-framework/graphql/mutations'; import UserDetailComment from './UserDetailComment'; import update from 'immutability-helper'; -import { notify } from 'coral-framework/actions/notification'; import { showBanUserDialog } from 'actions/banUserDialog'; import { showSuspendUserDialog } from 'actions/suspendUserDialog'; @@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component { acceptComment={this.acceptComment} rejectComment={this.rejectComment} loading={loading} + error={this.props.data && this.props.data.error} loadMore={this.loadMore} {...this.props} /> @@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({ viewUserDetail, hideUserDetail, toggleSelectAllCommentInUserDetail, - notify, }, dispatch ), diff --git a/client/coral-admin/src/routes/Community/components/FlaggedUser.css b/client/coral-admin/src/routes/Community/components/FlaggedUser.css index 75a91ce29..436f7307d 100644 --- a/client/coral-admin/src/routes/Community/components/FlaggedUser.css +++ b/client/coral-admin/src/routes/Community/components/FlaggedUser.css @@ -10,7 +10,7 @@ margin: 0 auto; position: relative; transition: background 200ms, box-shadow 200ms, margin-bottom 200ms; - padding: 10px 0 0; + padding: 10px 0 10px; min-height: 220px; &:hover { diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js index ddd684f87..60ae4ee99 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -18,7 +18,6 @@ import { handleFlaggedAccountsChange } from '../graphql'; import { notify } from 'coral-framework/actions/notification'; import { isFlaggedUserDangling } from '../utils'; import t from 'coral-framework/services/i18n'; -import { notifyOnMutationError } from 'coral-framework/hocs'; import FlaggedAccounts from '../components/FlaggedAccounts'; import FlaggedUser from '../containers/FlaggedUser'; @@ -300,7 +299,6 @@ const mapDispatchToProps = dispatch => export default compose( connect(null, mapDispatchToProps), withApproveUsername, - notifyOnMutationError(['approveUsername']), withQuery( gql` query TalkAdmin_Community_FlaggedAccounts { diff --git a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js index f9ada0945..325eadfb5 100644 --- a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js +++ b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js @@ -4,8 +4,6 @@ import { hideRejectUsernameDialog } from '../../../actions/community'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { compose } from 'react-apollo'; -import { notify } from 'coral-framework/actions/notification'; -import { notifyOnMutationError } from 'coral-framework/hocs'; const mapStateToProps = state => ({ user: state.community.user, @@ -16,13 +14,11 @@ const mapDispatchToProps = dispatch => bindActionCreators( { handleClose: hideRejectUsernameDialog, - notify, }, dispatch ); export default compose( connect(mapStateToProps, mapDispatchToProps), - withRejectUsername, - notifyOnMutationError(['rejectUsername']) + withRejectUsername )(RejectUsernameDialog); diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index 0d9d5faf9..ce33fa1f4 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; import { withQuery, withMergedSettings } from 'coral-framework/hocs'; import { Spinner } from 'coral-ui'; -import { notify } from 'coral-framework/actions/notification'; import PropTypes from 'prop-types'; import { withUpdateSettings } from 'coral-framework/graphql/mutations'; -import { getErrorMessages, getDefinitionName } from 'coral-framework/utils'; +import { getDefinitionName } from 'coral-framework/utils'; import StreamSettings from './StreamSettings'; import TechSettings from './TechSettings'; import ModerationSettings from './ModerationSettings'; @@ -16,22 +15,21 @@ import Configure from '../components/Configure'; class ConfigureContainer extends Component { savePending = async () => { - try { - await this.props.updateSettings(this.props.pending); - this.props.clearPending(); - } catch (err) { - this.props.notify('error', getErrorMessages(err)); - } + await this.props.updateSettings(this.props.pending); + this.props.clearPending(); }; render() { + if (this.props.data.error) { + return
{this.props.data.error.message}
; + } + if (this.props.data.loading) { return ; } return ( ({ variables: {}, + fetchPolicy: 'network-only', }), } ); @@ -81,7 +80,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, clearPending, setActiveSection, }, @@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch => ); export default compose( + connect(mapStateToProps, mapDispatchToProps), withUpdateSettings, withConfigureQuery, - connect(mapStateToProps, mapDispatchToProps), withMergedSettings('root.settings', 'pending', 'mergedSettings') )(ConfigureContainer); @@ -99,7 +97,6 @@ ConfigureContainer.propTypes = { updateSettings: PropTypes.func.isRequired, clearPending: PropTypes.func.isRequired, setActiveSection: PropTypes.func.isRequired, - notify: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, data: PropTypes.object.isRequired, root: PropTypes.object.isRequired, diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index b5820287b..c0e2e60a6 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -292,10 +292,6 @@ class ModerationContainer extends Component { const { root, root: { asset, settings }, data } = this.props; const assetId = getAssetId(this.props); - if (data.error) { - return
Error
; - } - if (assetId) { if (asset === null) { // Not found. @@ -303,6 +299,10 @@ class ModerationContainer extends Component { } } + if (data.error) { + return
{data.error.message}
; + } + if (data.loading && data.networkStatus !== 3) { // loading. return ; diff --git a/client/coral-auth-callback/src/index.js b/client/coral-auth-callback/src/index.js new file mode 100644 index 000000000..b7f3ed226 --- /dev/null +++ b/client/coral-auth-callback/src/index.js @@ -0,0 +1,14 @@ +document.addEventListener('DOMContentLoaded', () => { + // Get the auth element and parse it as JSON by decoding it. + const auth = document.getElementById('auth'); + const doc = document.implementation.createHTMLDocument(''); + doc.body.innerHTML = auth.innerText; + + // Set the item in localStorage. + localStorage.setItem('auth', doc.body.textContent); + + // Close the window. + setTimeout(() => { + window.close(); + }, 50); +}); diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js index 2c4a42153..7e11b1311 100644 --- a/client/coral-embed-stream/src/tabs/configure/components/Settings.js +++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js @@ -57,7 +57,7 @@ class Settings extends React.Component { {this.props.data.error.message}; + } + return ( { - try { - await this.props.updateAssetSettings( - this.props.asset.id, - this.props.pending - ); - this.props.clearPending(); - } catch (err) { - this.props.notify('error', getErrorMessages(err)); - } + await this.props.updateAssetSettings( + this.props.asset.id, + this.props.pending + ); + this.props.clearPending(); }; render() { @@ -98,7 +90,6 @@ SettingsContainer.propTypes = { mergedSettings: PropTypes.object.isRequired, updateAssetSettings: PropTypes.func.isRequired, clearPending: PropTypes.func.isRequired, - notify: PropTypes.func.isRequired, updatePending: PropTypes.func.isRequired, canSave: PropTypes.bool.isRequired, }; @@ -135,7 +126,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, clearPending, updatePending, }, @@ -143,9 +133,9 @@ const mapDispatchToProps = dispatch => ); const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), withSettingsFragments, withUpdateAssetSettings, - connect(mapStateToProps, mapDispatchToProps), withMergedSettings('asset.settings', 'pending', 'mergedSettings') ); diff --git a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js index ed1fe73f9..3190ab155 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js +++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import LoadMore from './LoadMore'; import NewCount from './NewCount'; import { TransitionGroup } from 'react-transition-group'; -import { forEachError } from 'coral-framework/utils'; import Comment from '../containers/Comment'; import NoComments from './NoComments'; @@ -91,11 +90,8 @@ class AllCommentsPane extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index f4e8404e4..cd0fab9e9 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -24,7 +24,6 @@ import { EditableCommentContent } from './EditableCommentContent'; import { getActionSummary, iPerformedThisAction, - forEachError, isCommentActive, getShallowChanges, } from 'coral-framework/utils'; @@ -261,11 +260,8 @@ export default class Comment extends React.Component { loadingState: 'success', }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); emit('ui.Comment.showMoreReplies', { id }); return; diff --git a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js index bfd2c8bad..f4cfc42e8 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js +++ b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js @@ -6,7 +6,6 @@ import styles from './Comment.css'; import { CountdownSeconds } from './CountdownSeconds'; import { getEditableUntilDate } from './util'; import { can } from 'coral-framework/services/perms'; -import { forEachError } from 'coral-framework/utils'; import { Icon } from 'coral-ui'; import t from 'coral-framework/services/i18n'; @@ -80,7 +79,7 @@ export class EditableCommentContent extends React.Component { this.setState({ loadingState: 'loading' }); - const { editComment, notify, stopEditing } = this.props; + const { editComment, stopEditing } = this.props; if (typeof editComment !== 'function') { return; } @@ -95,7 +94,6 @@ export class EditableCommentContent extends React.Component { } } catch (error) { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => notify('error', msg)); } }; diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index d82f49498..6288a2b6b 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { StreamError } from './StreamError'; +import StreamError from './StreamError'; import Comment from '../containers/Comment'; import BannedAccount from '../../../components/BannedAccount'; import ChangeUsername from '../containers/ChangeUsername'; diff --git a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js index 0955de2ef..79d6b03fc 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js +++ b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from './StreamError.css'; -export const StreamError = ({ children }) => ( +export default ({ children }) => (
{children}
); diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index 70cbadae5..51b4d9d9e 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -38,6 +38,7 @@ import { insertFetchedCommentsIntoEmbedQuery, nest, } from '../../../graphql/utils'; +import StreamError from '../components/StreamError'; const { showSignInDialog, editName } = authActions; const { notify } = notificationActions; @@ -208,6 +209,10 @@ class StreamContainer extends React.Component { } render() { + if (this.props.data.error) { + return {this.props.data.error.message}; + } + if ( !this.props.asset || (this.props.asset.comment === undefined && !this.props.asset.comments) @@ -424,7 +429,8 @@ export default compose( withEmit, connect(mapStateToProps, mapDispatchToProps), withPostComment, - withPostFlag, + // `talk-plugin-flags` has a custom error handling logic. + withPostFlag({ notifyOnError: false }), withPostDontAgree, withDeleteAction, withEditComment diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index 275f6df42..174f25a63 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -6,4 +6,3 @@ export { default as withEmit } from './withEmit'; export { default as excludeIf } from './excludeIf'; export { default as connect } from './connect'; export { default as withMergedSettings } from './withMergedSettings'; -export { default as notifyOnMutationError } from './notifyOnMutationError'; diff --git a/client/coral-framework/hocs/notifyOnMutationError.js b/client/coral-framework/hocs/notifyOnMutationError.js deleted file mode 100644 index c9429ad15..000000000 --- a/client/coral-framework/hocs/notifyOnMutationError.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { compose } from 'react-apollo'; -import { notify } from 'coral-framework/actions/notification'; -import { forEachError } from 'coral-framework/utils'; -import { withProps } from 'recompose'; - -const notifyOnMutationError = keys => - compose( - connect(null, dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ) - ), - withProps(ownProps => - keys.reduce((props, key) => { - props[key] = async (...args) => { - try { - return await ownProps[key](...args); - } catch (e) { - forEachError(e, ({ msg }) => { - ownProps.notify('error', msg); - }); - throw e; - } - }; - return props; - }, {}) - ) - ); - -export default notifyOnMutationError; diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js index b72e56f40..dae38071f 100644 --- a/client/coral-framework/hocs/withMutation.js +++ b/client/coral-framework/hocs/withMutation.js @@ -4,11 +4,16 @@ import merge from 'lodash/merge'; import uniq from 'lodash/uniq'; import flatten from 'lodash/flatten'; import isEmpty from 'lodash/isEmpty'; -import { getDefinitionName, getResponseErrors } from '../utils'; +import { + getDefinitionName, + getResponseErrors, + getErrorMessages, +} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import hoistStatics from 'recompose/hoistStatics'; import union from 'lodash/union'; +import { notify } from 'coral-framework/actions/notification'; class ResponseErrors extends Error { constructor(errors) { @@ -27,11 +32,7 @@ class ResponseError { } } -/** - * Exports a HOC with the same signature as `graphql`, that will - * apply mutation options registered in the graphRegistry. - */ -export default (document, config = {}) => +const createHOC = (document, config, { notifyOnError = true }) => hoistStatics(WrappedComponent => { config = { ...config, @@ -46,10 +47,18 @@ export default (document, config = {}) => graphql: PropTypes.object, }; + static propTypes = { + notify: PropTypes.func, + }; + get graphqlRegistry() { return this.context.graphql.registry; } + notifyErrors(messages) { + this.context.store.dispatch(notify('error', messages)); + } + resolveDocument(documentOrCallback) { return this.context.graphql.resolveDocument( documentOrCallback, @@ -165,6 +174,11 @@ export default (document, config = {}) => variables, error, }); + + // Show errors as notifications. + if (notifyOnError) { + this.notifyErrors(getErrorMessages(error)); + } throw error; }); }; @@ -213,3 +227,18 @@ export default (document, config = {}) => } }; }); + +/** + * Exports a HOC with the same signature as `graphql`, that will + * apply mutation options registered in the graphRegistry. + * + * The returned HOC accepts a settings object with the following properties: + * notifyOnError: show a notification to the user when an error occured. + * Defaults to true. + */ +export default (document, config = {}) => settingsOrComponent => { + if (typeof settingsOrComponent === 'function') { + return createHOC(document, config, {})(settingsOrComponent); + } + return createHOC(document, config, settingsOrComponent); +}; diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index d5609c340..c4b9074b4 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -9,6 +9,8 @@ import PropTypes from 'prop-types'; import hoistStatics from 'recompose/hoistStatics'; import { getOperationName } from 'apollo-client/queries/getFromAST'; import throttle from 'lodash/throttle'; +import get from 'lodash/get'; +import { notify } from 'coral-framework/actions/notification'; const withSkipOnErrors = reducer => (prev, action, ...rest) => { if ( @@ -36,21 +38,23 @@ function networkStatusToString(networkStatus) { return 'ready'; case 8: return 'error'; + default: + throw new Error(`Unknown network status ${networkStatus}`); } - throw new Error(`Unknown network status ${networkStatus}`); } -/** - * Exports a HOC with the same signature as `graphql`, that will - * apply query options registered in the graphRegistry. - */ -export default (document, config = {}) => +const createHOC = (document, config, { notifyOnError = true }) => hoistStatics(WrappedComponent => { return class WithQuery extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, graphql: PropTypes.object, client: PropTypes.object, + store: PropTypes.object, + }; + + static propTypes = { + notify: PropTypes.func, }; // Lazily resolve fragments from graphRegistry to support circular dependencies. @@ -166,10 +170,24 @@ export default (document, config = {}) => return () => this.client.networkInterface.unsubscribe(id); }; + notifyErrors(messages) { + this.context.store.dispatch(notify('error', messages)); + } + nextData(data) { this.apolloData = data; this.emitWhenNeeded(data); + if ( + get(data, 'error.message') && + get(this, 'data.error.message') !== get(data, 'error.message') + ) { + // Show errors as notifications. + if (notifyOnError) { + this.notifyErrors(data.error.message); + } + } + // If data was previously set, we update it in a immutable way. if (this.data) { if (this.data.loading && !data.loading) { @@ -319,3 +337,18 @@ export default (document, config = {}) => } }; }); + +/** + * Exports a HOC with the same signature as `graphql`, that will + * apply query options registered in the graphRegistry. + * + * The returned HOC accepts a settings object with the following properties: + * notifyOnError: show a notification to the user when an error occured. + * Defaults to true. + */ +export default (document, config = {}) => settingsOrComponent => { + if (typeof settingsOrComponent === 'function') { + return createHOC(document, config, {})(settingsOrComponent); + } + return createHOC(document, config, settingsOrComponent); +}; diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js index fb9d8de17..e1bcf7f13 100644 --- a/client/coral-settings/containers/ProfileContainer.js +++ b/client/coral-settings/containers/ProfileContainer.js @@ -65,6 +65,10 @@ class ProfileContainer extends Component { const { me } = this.props.root; const loading = this.props.data.loading; + if (this.props.data.error) { + return
{this.props.data.error.message}
; + } + if (!auth.loggedIn) { return ; } diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js index a8da78f3f..8eac1ff2f 100644 --- a/client/talk-plugin-commentbox/CommentBox.js +++ b/client/talk-plugin-commentbox/CommentBox.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import { can } from 'coral-framework/services/perms'; -import { forEachError } from 'coral-framework/utils'; import Slot from 'coral-framework/components/Slot'; import { connect } from 'react-redux'; @@ -93,9 +92,8 @@ class CommentBox extends React.Component { commentPostedHandler(); } }) - .catch(err => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(err, ({ msg }) => notify('error', msg)); }); }; diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js index 64841a893..b2048e105 100644 --- a/client/talk-plugin-flags/components/FlagButton.js +++ b/client/talk-plugin-flags/components/FlagButton.js @@ -60,9 +60,10 @@ export default class FlagButton extends Component { }); }; - onPopupContinue = () => { + onPopupContinue = async () => { const { postFlag, postDontAgree, id, author_id } = this.props; const { itemType, reason, step, message } = this.state; + let failed = false; switch (step) { case 0: @@ -75,13 +76,9 @@ export default class FlagButton extends Component { return; } break; - } - - // Proceed to the next step or close the menu if we've reached the end - if (step + 1 >= this.props.getPopupMenu.length) { - this.closeMenu(); - } else { - this.setState({ step: step + 1 }); + case this.props.getPopupMenu.length: + this.closeMenu(); + return; } // If itemType and reason are both set, post the action @@ -96,43 +93,46 @@ export default class FlagButton extends Component { break; } - if (itemType === 'COMMENTS') { - this.setState({ localPost: 'temp' }); - } - let action = { item_id, item_type: itemType, message, }; + if (reason === REASONS.comment.noagree) { - postDontAgree(action) - .then(({ data }) => { - if (itemType === 'COMMENTS') { - this.setState({ localPost: data.createDontAgree.dontagree.id }); - } - }) - .catch(err => { - this.props.notify('error', getErrorMessages(err)); - console.error(err); - }); - } else { - postFlag({ ...action, reason }) - .then(({ data }) => { - if (itemType === 'COMMENTS') { - this.setState({ localPost: data.createFlag.flag.id }); - } - }) - .catch(errors => { - forEachError(errors, ({ error, msg }) => { - if (error.translation_key === 'ALREADY_EXISTS') { - msg = t('already_flagged_username'); - } - this.props.notify('error', msg); + const result = await postDontAgree(action); + try { + if (itemType === 'COMMENTS') { + this.setState({ + localPost: result.data.createDontAgree.dontagree.id, }); + } + } catch (err) { + this.props.notify('error', getErrorMessages(err)); + console.error(err); + failed = true; + } + } else { + try { + const result = await postFlag({ ...action, reason }); + if (itemType === 'COMMENTS') { + this.setState({ localPost: result.data.createFlag.flag.id }); + } + } catch (errors) { + forEachError(errors, ({ error, msg }) => { + if (error.translation_key === 'ALREADY_EXISTS') { + msg = t('already_flagged_username'); + } + this.props.notify('error', msg); }); + failed = true; + } } } + + if (!failed) { + this.setState({ step: step + 1 }); + } }; onPopupOptionClick = sets => e => { diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js index a56350c4a..a17a347c5 100644 --- a/client/talk-plugin-history/CommentHistory.js +++ b/client/talk-plugin-history/CommentHistory.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Comment from './Comment'; import LoadMore from './LoadMore'; -import { forEachError } from 'plugin-api/beta/client/utils'; class CommentHistory extends React.Component { state = { @@ -16,11 +15,8 @@ class CommentHistory extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - forEachError(error, ({ msg }) => { - this.props.notify('error', msg); - }); }); }; @@ -55,7 +51,6 @@ class CommentHistory extends React.Component { CommentHistory.propTypes = { comments: PropTypes.object.isRequired, loadMore: PropTypes.func, - notify: PropTypes.func, link: PropTypes.func, data: PropTypes.object, root: PropTypes.object, diff --git a/graph/connectors.js b/graph/connectors.js index a27b9d38c..976f05b71 100644 --- a/graph/connectors.js +++ b/graph/connectors.js @@ -24,6 +24,7 @@ const Limit = require('../services/limit'); const Mailer = require('../services/mailer'); const Metadata = require('../services/metadata'); const Migration = require('../services/migration'); +const Moderation = require('../services/moderation'); const Mongoose = require('../services/mongoose'); const Passport = require('../services/passport'); const Plugins = require('../services/plugins'); @@ -62,6 +63,7 @@ const connectors = { Mailer, Metadata, Migration, + Moderation, Mongoose, Passport, Plugins, diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index 6106f819c..7e0607287 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -1,13 +1,11 @@ const errors = require('../../errors'); const ActionModel = require('../../models/action'); -const AssetsService = require('../../services/assets'); const ActionsService = require('../../services/actions'); const TagsService = require('../../services/tags'); const CommentsService = require('../../services/comments'); const KarmaService = require('../../services/karma'); const merge = require('lodash/merge'); -const linkify = require('linkify-it')().tlds(require('tlds')); -const Wordlist = require('../../services/wordlist'); + const { CREATE_COMMENT, SET_COMMENT_STATUS, @@ -15,10 +13,6 @@ const { EDIT_COMMENT, } = require('../../perms/constants'); const debug = require('debug')('talk:graph:mutators:comment'); -const { - DISABLE_AUTOFLAG_SUSPECT_WORDS, - IGNORE_FLAGS_AGAINST_STAFF, -} = require('../../config'); const resolveTagsForComment = async ( { user, loaders: { Tags } }, @@ -188,279 +182,27 @@ const createComment = async ( return comment; }; -/** - * Filters the comment object and outputs wordlist results. - * @param {Object} context graphql context - * @param {String} body body of a comment - * @param {String} [asset_id] id of asset comment is posted on - * @return {Object} resolves to the wordlist results - */ -const filterNewComment = async (context, { body, asset_id }) => { - // Load the settings. - const [settings, asset] = await Promise.all([ - context.loaders.Settings.load(), - context.loaders.Assets.getByID.load(asset_id), - ]); - - // Create a new instance of the Wordlist. - const wl = new Wordlist(); - - // Load the wordlist. - wl.upsert(settings.wordlist); - - // Load the wordlist and filter the comment content. - return [ - // Scan the word. - wl.scan('body', body), - - // Return the asset's settings. - await AssetsService.rectifySettings(asset, settings), - ]; -}; - -/** - * moderationPhases is an array of phases carried out in order until a status is - * returned. - */ -const moderationPhases = [ - // This phase checks to see if the comment is long enough. - (context, comment) => { - // Check to see if the body is too short, if it is, then complain about it! - if (comment.body.length < 2) { - throw errors.ErrCommentTooShort; - } - }, - - // This phase checks to see if the asset being processed is closed or not. - (context, comment, { asset }) => { - // Check to see if the asset has closed commenting... - if (asset.isClosed) { - throw new errors.ErrAssetCommentingClosed(asset.closedMessage); - } - }, - - // This phase checks the comment against the wordlist. - (context, comment, { wordlist }) => { - // Decide the status based on whether or not the current asset/settings - // has pre-mod enabled or not. If the comment was rejected based on the - // wordlist, then reject it, otherwise if the moderation setting is - // premod, set it to `premod`. - if (wordlist.banned) { - // Add the flag related to Trust to the comment. - return { - status: 'REJECTED', - actions: [ - { - action_type: 'FLAG', - user_id: null, - group_id: 'BANNED_WORD', - metadata: {}, - }, - ], - }; - } - - // If the comment has a suspect word or a link, we need to add a - // flag to it to indicate that it needs to be looked at. - // Otherwise just return the new comment. - - // If the wordlist has matched the suspect word filter and we haven't disabled - // auto-flagging suspect words, then we should flag the comment! - if (wordlist.suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) { - // TODO: this is kind of fragile, we should refactor this to resolve - // all these const's that we're using like 'COMMENTS', 'FLAG' to be - // defined in a checkable schema. - return { - actions: [ - { - action_type: 'FLAG', - user_id: null, - group_id: 'SUSPECT_WORD', - metadata: {}, - }, - ], - }; - } - }, - - // This phase checks to see if the comment's length exceeds maximum. - (context, comment, { assetSettings: { charCountEnable, charCount } }) => { - // Reject if the comment is too long - if (charCountEnable && comment.body.length > charCount) { - // Add the flag related to Trust to the comment. - return { - status: 'REJECTED', - actions: [ - { - action_type: 'FLAG', - user_id: null, - group_id: 'BODY_COUNT', - metadata: { - count: comment.body.length, - }, - }, - ], - }; - } - }, - - // If a given user is a staff member, always approve their comment. - context => { - if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) { - return { - status: 'ACCEPTED', - }; - } - }, - - // This phase checks the comment if it has any links in it if the check is - // enabled. - (context, comment, { assetSettings: { premodLinksEnable } }) => { - if (premodLinksEnable && linkify.test(comment.body)) { - // Add the flag related to Trust to the comment. - return { - status: 'SYSTEM_WITHHELD', - actions: [ - { - action_type: 'FLAG', - user_id: null, - group_id: 'LINKS', - metadata: { - links: comment.body, - }, - }, - ], - }; - } - }, - - // This phase checks to see if the user making the comment is allowed to do so - // considering their reliability (Trust) status. - context => { - if (context.user && context.user.metadata) { - // If the user is not a reliable commenter (passed the unreliability - // threshold by having too many rejected comments) then we can change the - // status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's - // comments away from the public eye until a moderator can manage them. This of - // course can only be applied if the comment's current status is `NONE`, - // we don't want to interfere if the comment was rejected. - if ( - KarmaService.isReliable('comment', context.user.metadata.trust) === - false - ) { - // Add the flag related to Trust to the comment. - return { - status: 'SYSTEM_WITHHELD', - actions: [ - { - action_type: 'FLAG', - user_id: null, - group_id: 'TRUST', - metadata: { - trust: context.user.metadata.trust, - }, - }, - ], - }; - } - } - }, - - // This phase checks to see if the comment was already prescribed a status. - (context, comment) => { - // If the status was already defined, don't redefine it. It's only defined - // when specific external conditions exist, we don't want to override that. - if (comment.status && comment.status.length > 0) { - return { - status: comment.status, - }; - } - }, - - // This phase checks to see if the settings have premod enabled, if they do, - // the comment is premod, otherwise, it's just none. - (context, comment, { assetSettings: { moderation } }) => { - // If the settings say that we're in premod mode, then the comment is in - // premod status. - if (moderation === 'PRE') { - return { - status: 'PREMOD', - }; - } - - return { - status: 'NONE', - }; - }, -]; - -/** - * This resolves a given comment's status and actions. - * @param {Object} context graphql context - * @param {String} body body of the comment - * @param {String} [asset_id] asset for the comment - * @param {Object} [wordlist={}] the results of the wordlist scan - * @return {Promise} resolves to the comment's status and actions - */ -const resolveCommentModeration = async (context, comment) => { - // First we filter the comment contents to ensure that we note any validation - // issues. - let [wordlist, settings] = await filterNewComment(context, comment); - - // Get the asset from the loader. - const asset = await context.loaders.Assets.getByID.load(comment.asset_id); - if (!asset) { - // And leave now if this asset wasn't found. - throw errors.ErrNotFound; - } - - // Combine the asset and the settings to get the asset settings. - const assetSettings = await AssetsService.rectifySettings(asset, settings); - - let actions = comment.actions || []; - - // Loop over all the moderation phases and see if we've resolved the status. - for (const phase of moderationPhases) { - const result = await phase(context, comment, { - asset, - assetSettings, - settings, - wordlist, - }); - - if (result) { - if (result.actions) { - actions.push(...result.actions); - } - - // If this result contained a status, then we've finished resolving - // phases! - if (result.status) { - return { status: result.status, actions }; - } - } - } -}; - /** * createPublicComment is designed to create a comment from a public source. It * validates the comment, and performs some automated moderator actions based on * the settings. - * @param {Object} context the graphql context + * @param {Object} ctx the graphql context * @param {Object} commentInput the new comment to be created * @return {Promise} resolves to a new comment */ -const createPublicComment = async (context, comment) => { +const createPublicComment = async (ctx, comment) => { + const { connectors: { services: { Moderation } } } = ctx; + // We then take the wordlist and the comment into consideration when // considering what status to assign the new comment, and resolve the new // status to set the comment to. - let { actions, status } = await resolveCommentModeration(context, comment); + let { actions, status } = await Moderation.process(ctx, comment); // Assign status to comment. comment.status = status; // Then we actually create the comment with the new status. - const result = await createComment(context, comment); + const result = await createComment(ctx, comment); // Create all the actions that were determined during the moderation check // phase. @@ -522,18 +264,20 @@ const setStatus = async ({ user, loaders: { Comments } }, { id, status }) => { * @param {Object} edit describes how to edit the comment * @param {String} edit.body the new Comment body */ -const edit = async (context, { id, asset_id, edit: { body } }) => { +const edit = async (ctx, { id, asset_id, edit: { body } }) => { + const { connectors: { services: { Moderation } } } = ctx; + // Build up the new comment we're setting. We need to check this with // moderation now. let comment = { id, asset_id, body }; // Determine the new status of the comment. - const { actions, status } = await resolveCommentModeration(context, comment); + const { actions, status } = await Moderation.process(ctx, comment); // Execute the edit. comment = await CommentsService.edit({ id, - author_id: context.user.id, + author_id: ctx.user.id, body, status, }); @@ -543,7 +287,7 @@ const edit = async (context, { id, asset_id, edit: { body } }) => { await createActions(comment.id, actions); // Publish the edited comment via the subscription. - context.pubsub.publish('commentEdited', comment); + ctx.pubsub.publish('commentEdited', comment); return comment; }; diff --git a/models/asset.js b/models/asset.js index 68ca08bbf..6fdea3b78 100644 --- a/models/asset.js +++ b/models/asset.js @@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; const uuid = require('uuid'); const TagLinkSchema = require('./schema/tag_link'); +const get = require('lodash/get'); const AssetSchema = new Schema( { @@ -45,8 +46,8 @@ const AssetSchema = new Schema( // the base settings from the base Settings object. This is to be accessed // always after running `rectifySettings` against it. settings: { - type: Schema.Types.Mixed, default: {}, + type: Object, }, // Tags are added by the self or by administrators. @@ -85,9 +86,12 @@ AssetSchema.index( * Returns true if the asset is closed, false else. */ AssetSchema.virtual('isClosed').get(function() { - return Boolean( - this.closedAt && this.closedAt.getTime() <= new Date().getTime() - ); + const closedAt = get(this, 'closedAt', null); + if (closedAt === null) { + return false; + } + + return closedAt.getTime() <= new Date().getTime(); }); const Asset = mongoose.model('Asset', AssetSchema); diff --git a/package.json b/package.json index 90abdf2e7..17af19867 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "mocha-junit-reporter": "^1.12.1", "nightwatch": "^0.9.16", "nodemon": "^1.11.0", - "pre-git": "^3.16.0", + "pre-commit": "^1.2.2", "selenium-standalone": "^6.11.0", "sinon": "^3.2.1", "sinon-chai": "^2.13.0", @@ -220,24 +220,8 @@ "engines": { "node": "^8" }, - "config": { - "pre-git": { - "pre-commit": [ - "yarn lint", - "yarn test:client", - "yarn test:server" - ], - "pre-push": [ - "yarn lint", - "yarn test:client", - "yarn test:server" - ], - "post-commit": [], - "post-checkout": [], - "post-merge": [] - } - }, - "release": { - "analyzeCommits": "simple-commit-message" + "pre-commit": { + "silent": false, + "run": ["lint", "test:client", "test:server"] } } diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index f5a0167b7..6020d4002 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -9,11 +9,7 @@ import withFragments from 'coral-framework/hocs/withFragments'; import withMutation from 'coral-framework/hocs/withMutation'; import { notify } from 'coral-framework/actions/notification'; import { capitalize } from 'coral-framework/helpers/strings'; -import { - getMyActionSummary, - getTotalActionCount, - getErrorMessages, -} from 'coral-framework/utils'; +import { getMyActionSummary, getTotalActionCount } from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import * as PropTypes from 'prop-types'; import { getDefinitionName } from '../utils'; @@ -282,7 +278,6 @@ export default (reaction, options = {}) => }) .catch(err => { this.duringMutation = false; - this.props.notify('error', getErrorMessages(err)); throw err; }); }; @@ -307,7 +302,6 @@ export default (reaction, options = {}) => }) .catch(err => { this.duringMutation = false; - this.props.notify('error', getErrorMessages(err)); throw err; }); }; diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js index 7b95a5e6c..958028a4b 100644 --- a/plugin-api/beta/client/hocs/withTags.js +++ b/plugin-api/beta/client/hocs/withTags.js @@ -1,13 +1,11 @@ import React from 'react'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import { compose, gql } from 'react-apollo'; import { getDisplayName } from 'coral-framework/helpers/hoc'; import { capitalize } from 'coral-framework/helpers/strings'; import { withAddTag, withRemoveTag } from 'coral-framework/graphql/mutations'; import withFragments from 'coral-framework/hocs/withFragments'; -import { notify } from 'coral-framework/actions/notification'; -import { getErrorMessages, isTagged } from 'coral-framework/utils'; +import { isTagged } from 'coral-framework/utils'; import hoistStatics from 'recompose/hoistStatics'; import { getDefinitionName } from '../utils'; @@ -38,7 +36,7 @@ export default (tag, options = {}) => loading = false; postTag = () => { - const { comment, asset, notify } = this.props; + const { comment, asset } = this.props; if (this.loading) { return; @@ -59,13 +57,12 @@ export default (tag, options = {}) => }) .catch(err => { this.loading = false; - notify('error', getErrorMessages(err)); throw err; }); }; deleteTag = () => { - const { comment, asset, notify } = this.props; + const { comment, asset } = this.props; if (this.loading) { return; @@ -84,7 +81,6 @@ export default (tag, options = {}) => }) .catch(err => { this.loading = false; - notify('error', getErrorMessages(err)); throw err; }); }; @@ -114,9 +110,6 @@ export default (tag, options = {}) => user: state.auth.user, }); - const mapDispatchToProps = dispatch => - bindActionCreators({ notify }, dispatch); - const enhance = compose( withFragments({ ...fragments, @@ -146,7 +139,7 @@ export default (tag, options = {}) => }), withAddTag, withRemoveTag, - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, null) ); WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`; diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.js b/plugins/talk-plugin-featured-comments/client/components/ModTag.js index 420ee9417..659b2cea4 100644 --- a/plugins/talk-plugin-featured-comments/client/components/ModTag.js +++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.js @@ -64,7 +64,6 @@ export default class ModTag extends React.Component { ModTag.propTypes = { alreadyTagged: PropTypes.bool, deleteTag: PropTypes.func, - notify: PropTypes.func, openFeaturedDialog: PropTypes.func, comment: PropTypes.object, asset: PropTypes.object, diff --git a/plugins/talk-plugin-featured-comments/client/components/TabPane.js b/plugins/talk-plugin-featured-comments/client/components/TabPane.js index 084de7d0a..9bdba6fe8 100644 --- a/plugins/talk-plugin-featured-comments/client/components/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/components/TabPane.js @@ -1,7 +1,6 @@ import React from 'react'; import Comment from '../containers/Comment'; import LoadMore from './LoadMore'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; class TabPane extends React.Component { state = { @@ -15,9 +14,8 @@ class TabPane extends React.Component { .then(() => { this.setState({ loadingState: 'success' }); }) - .catch(error => { + .catch(() => { this.setState({ loadingState: 'error' }); - this.props.notify('error', getErrorMessages(error)); }); }; diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js index 1f210cb95..7c8f94fb3 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js @@ -13,8 +13,8 @@ const mapDispatchToProps = dispatch => ); const enhance = compose( - withTags('featured'), - connect(null, mapDispatchToProps) + connect(null, mapDispatchToProps), + withTags('featured') ); export default enhance(ModActionButton); diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js index 990a8a2e9..42c653418 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js +++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js @@ -3,12 +3,10 @@ import { withTags, connect } from 'plugin-api/beta/client/hocs'; import { gql, compose } from 'react-apollo'; import { bindActionCreators } from 'redux'; import { openFeaturedDialog } from '../actions'; -import { notify } from 'plugin-api/beta/client/actions/notification'; const mapDispatchToProps = dispatch => bindActionCreators( { - notify, openFeaturedDialog, }, dispatch @@ -24,8 +22,8 @@ const fragments = { `, }; const enhance = compose( - withTags('featured', { fragments }), - connect(null, mapDispatchToProps) + connect(null, mapDispatchToProps), + withTags('featured', { fragments }) ); export default enhance(ModTag); diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js index 5bf92fb0a..297f5aedf 100644 --- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js +++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js @@ -4,7 +4,6 @@ import { compose, gql } from 'react-apollo'; import TabPane from '../components/TabPane'; import { withFragments, connect } from 'plugin-api/beta/client/hocs'; import Comment from '../containers/Comment'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import { viewComment } from 'coral-embed-stream/src/actions/stream'; import { appendNewNodes, @@ -81,7 +80,6 @@ const mapDispatchToProps = dispatch => bindActionCreators( { viewComment, - notify, }, dispatch ); diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js index 733dab756..5aa834034 100644 --- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js +++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js @@ -10,21 +10,16 @@ import { bindActionCreators } from 'redux'; import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions'; import { notify } from 'plugin-api/beta/client/actions/notification'; import { t } from 'plugin-api/beta/client/services'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; class IgnoreUserConfirmationContainer extends React.Component { ignoreUser = () => { const { ignoreUser, notify, comment, closeMenu } = this.props; - ignoreUser(comment.user.id) - .then(() => { - notify( - 'success', - t('talk-plugin-ignore-user.notify_success', comment.user.username) - ); - }) - .catch(err => { - notify('error', getErrorMessages(err)); - }); + ignoreUser(comment.user.id).then(() => { + notify( + 'success', + t('talk-plugin-ignore-user.notify_success', comment.user.username) + ); + }); closeMenu(); }; diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js index 70cb2ce4b..1f7bed3bb 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js @@ -1,23 +1,16 @@ import React from 'react'; import { compose } from 'react-apollo'; -import { bindActionCreators } from 'redux'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import ApproveCommentAction from '../components/ApproveCommentAction'; -import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs'; +import { withSetCommentStatus } from 'plugin-api/beta/client/hocs'; class ApproveCommentActionContainer extends React.Component { approveComment = async () => { - const { setCommentStatus, comment, hideMenu, notify } = this.props; + const { setCommentStatus, comment, hideMenu } = this.props; - try { - await setCommentStatus({ - commentId: comment.id, - status: 'ACCEPTED', - }); - } catch (err) { - notify('error', getErrorMessages(err)); - } + await setCommentStatus({ + commentId: comment.id, + status: 'ACCEPTED', + }); hideMenu(); }; @@ -32,17 +25,6 @@ class ApproveCommentActionContainer extends React.Component { } } -const mapDispatchToProps = dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ); - -const enhance = compose( - connect(null, mapDispatchToProps), - withSetCommentStatus -); +const enhance = compose(withSetCommentStatus); export default enhance(ApproveCommentActionContainer); diff --git a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js index 6d64addca..8dd3cce7d 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js @@ -3,19 +3,16 @@ import PropTypes from 'prop-types'; import { compose } from 'react-apollo'; import { bindActionCreators } from 'redux'; import { closeBanDialog, closeMenu } from '../actions'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import { connect, withSetCommentStatus, withBanUser, } from 'plugin-api/beta/client/hocs'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; import BanUserDialog from '../components/BanUserDialog'; class BanUserDialogContainer extends React.Component { banUser = async () => { const { - notify, authorId, commentId, commentStatus, @@ -25,23 +22,19 @@ class BanUserDialogContainer extends React.Component { banUser, } = this.props; - try { - await banUser({ - id: authorId, - message: '', + await banUser({ + id: authorId, + message: '', + }); + + closeMenu(); + closeBanDialog(); + + if (commentStatus !== 'REJECTED') { + await setCommentStatus({ + commentId: commentId, + status: 'REJECTED', }); - - closeMenu(); - closeBanDialog(); - - if (commentStatus !== 'REJECTED') { - await setCommentStatus({ - commentId: commentId, - status: 'REJECTED', - }); - } - } catch (err) { - notify('error', getErrorMessages(err)); } }; @@ -70,7 +63,6 @@ const mapStateToProps = ({ talkPluginModerationActions: state }) => ({ const mapDispatchToProps = dispatch => bindActionCreators( { - notify, closeBanDialog, closeMenu, }, diff --git a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js index e49fdddba..54ca0420d 100644 --- a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js +++ b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js @@ -1,23 +1,16 @@ import React from 'react'; import { compose } from 'react-apollo'; -import { bindActionCreators } from 'redux'; -import { getErrorMessages } from 'plugin-api/beta/client/utils'; -import { notify } from 'plugin-api/beta/client/actions/notification'; import RejectCommentAction from '../components/RejectCommentAction'; -import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs'; +import { withSetCommentStatus } from 'plugin-api/beta/client/hocs'; class RejectCommentActionContainer extends React.Component { rejectComment = async () => { - const { setCommentStatus, comment, hideMenu, notify } = this.props; + const { setCommentStatus, comment, hideMenu } = this.props; - try { - await setCommentStatus({ - commentId: comment.id, - status: 'REJECTED', - }); - } catch (err) { - notify('error', getErrorMessages(err)); - } + await setCommentStatus({ + commentId: comment.id, + status: 'REJECTED', + }); hideMenu(); }; @@ -27,17 +20,6 @@ class RejectCommentActionContainer extends React.Component { } } -const mapDispatchToProps = dispatch => - bindActionCreators( - { - notify, - }, - dispatch - ); - -const enhance = compose( - connect(null, mapDispatchToProps), - withSetCommentStatus -); +const enhance = compose(withSetCommentStatus); export default enhance(RejectCommentActionContainer); diff --git a/public/javascripts/admin.js b/public/javascripts/admin.js deleted file mode 100644 index 2c8b7c42a..000000000 --- a/public/javascripts/admin.js +++ /dev/null @@ -1,8 +0,0 @@ -function showError(error) { - try { - let err = JSON.parse(error); - $('.error-console').text(err.message).addClass('active'); - } catch (err) { - $('.error-console').text(error).addClass('active'); - } -} diff --git a/public/javascripts/auth-callback.js b/public/javascripts/auth-callback.js deleted file mode 100644 index f91f4d743..000000000 --- a/public/javascripts/auth-callback.js +++ /dev/null @@ -1,4 +0,0 @@ -document.addEventListener('DOMContentLoaded', function(event) { - localStorage.setItem('auth', document.getElementById('auth').innerText); - setTimeout(function() { window.close(); }, 50); -}); \ No newline at end of file diff --git a/services/moderation/index.js b/services/moderation/index.js new file mode 100644 index 000000000..4d87e190f --- /dev/null +++ b/services/moderation/index.js @@ -0,0 +1,130 @@ +const errors = require('../../errors'); +const get = require('lodash/get'); + +// Load in the phases to use. +const { + wordlist, + commentLength, + assetClosed, + karma, + staff, + links, + premod, +} = require('./phases'); + +// This phase checks to see if the comment was already prescribed a status. This +// essentially provides a hook for plugins to inject their own comments. +const applyPreexisting = (ctx, comment) => { + const status = get(comment, 'status'); + + // If the status was already defined, don't redefine it. It's only defined + // when specific external conditions exist, we don't want to override that. + if (status) { + return { + status, + }; + } +}; + +// Applies the defaulted status. +const applyStatus = status => () => ({ status }); + +/** + * phases is an array of moderation phases carried out in order until a status is + * returned. + */ +const phases = [ + commentLength, + assetClosed, + wordlist, + staff, + links, + karma, + applyPreexisting, + premod, + applyStatus('NONE'), +]; + +/** + * compose will create a moderation pipeline for which is executable with the + * passed actions. + * + * @param {Array} phases the set of moderation phases to pass the comment and + * their options through. + */ +const compose = phases => async (ctx, comment, options) => { + const actions = get(comment, 'actions', []); + + // Loop over all the moderation phases and see if we've resolved the status. + for (const phase of phases) { + const result = await phase(ctx, comment, options); + if (result) { + if (result.actions) { + actions.push(...result.actions); + } + + // If this result contained a status, then we've finished resolving + // phases! + if (result.status) { + return { status: result.status, actions }; + } + } + } +}; + +/** + * fetchOptions will generate the options used by the moderation service to + * determine the end status. + * + * @param {Object} ctx graph context + * @param {Object} comment comment object to use + */ +const fetchOptions = async (ctx, comment) => { + const { + connectors: { services: { Assets: AssetsService } }, + loaders: { Settings, Assets }, + } = ctx; + + // Load the settings. + const settings = await Settings.load(); + + // Pull the asset id out of the comment. + const assetID = get(comment, 'asset_id', null); + if (assetID === null) { + // And leave now if this asset wasn't found. + throw errors.ErrNotFound; + } + + // Load the asset. + const asset = await Assets.getByID.load(assetID); + if (!asset) { + // And leave now if this asset wasn't found. + throw errors.ErrNotFound; + } + + // Combine the asset and the settings to get the asset settings. + asset.settings = await AssetsService.rectifySettings(asset, settings); + + // Create the options that will be consumed by the phases. + return { + asset, + settings, + }; +}; + +/** + * process the comment and return moderation details. + * + * @param {Object} ctx graphql context + * @param {Object} comment comment to perform the moderation phases on + */ +const process = async (ctx, comment) => { + // Fetch the options to use for the moderation phases. + const options = await fetchOptions(ctx, comment); + + // Compose a moderation pipeline from the moderation phases and execute it on + // the comment. + return compose(phases)(ctx, comment, options); +}; + +module.exports.process = process; diff --git a/services/moderation/phases/assetClosed.js b/services/moderation/phases/assetClosed.js new file mode 100644 index 000000000..075028764 --- /dev/null +++ b/services/moderation/phases/assetClosed.js @@ -0,0 +1,9 @@ +const { ErrAssetCommentingClosed } = require('../../../errors'); + +// This phase checks to see if the asset being processed is closed or not. +module.exports = (ctx, comment, { asset }) => { + // Check to see if the asset has closed commenting... + if (asset.isClosed) { + throw new ErrAssetCommentingClosed(asset.closedMessage); + } +}; diff --git a/services/moderation/phases/commentLength.js b/services/moderation/phases/commentLength.js new file mode 100644 index 000000000..925115326 --- /dev/null +++ b/services/moderation/phases/commentLength.js @@ -0,0 +1,31 @@ +const { ErrCommentTooShort } = require('../../../errors'); + +// This phase checks to see if the comment is long enough. +module.exports = ( + ctx, + comment, + { asset: { settings: { charCountEnable, charCount } } } +) => { + // Check to see if the body is too short, if it is, then complain about it! + if (comment.body.length < 2) { + throw ErrCommentTooShort; + } + + // Reject if the comment is too long + if (charCountEnable && comment.body.length > charCount) { + // Add the flag related to Trust to the comment. + return { + status: 'REJECTED', + actions: [ + { + action_type: 'FLAG', + user_id: null, + group_id: 'BODY_COUNT', + metadata: { + count: comment.body.length, + }, + }, + ], + }; + } +}; diff --git a/services/moderation/phases/index.js b/services/moderation/phases/index.js new file mode 100644 index 000000000..a1c2e7bf5 --- /dev/null +++ b/services/moderation/phases/index.js @@ -0,0 +1,7 @@ +module.exports.wordlist = require('./wordlist'); +module.exports.commentLength = require('./commentLength'); +module.exports.assetClosed = require('./assetClosed'); +module.exports.karma = require('./karma'); +module.exports.staff = require('./staff'); +module.exports.links = require('./links'); +module.exports.premod = require('./premod'); diff --git a/services/moderation/phases/karma.js b/services/moderation/phases/karma.js new file mode 100644 index 000000000..ad55b37a7 --- /dev/null +++ b/services/moderation/phases/karma.js @@ -0,0 +1,33 @@ +const get = require('lodash/get'); + +// This phase checks to see if the user making the comment is allowed to do so +// considering their reliability (Trust) status. +module.exports = ctx => { + const { connectors: { services: { Karma } } } = ctx; + const trust = get(ctx, 'user.metadata.trust', null); + + if (trust !== null) { + // If the user is not a reliable commenter (passed the unreliability + // threshold by having too many rejected comments) then we can change the + // status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's + // comments away from the public eye until a moderator can manage them. This of + // course can only be applied if the comment's current status is `NONE`, + // we don't want to interfere if the comment was rejected. + if (Karma.isReliable('comment', trust) === false) { + // Add the flag related to Trust to the comment. + return { + status: 'SYSTEM_WITHHELD', + actions: [ + { + action_type: 'FLAG', + user_id: null, + group_id: 'TRUST', + metadata: { + trust, + }, + }, + ], + }; + } + } +}; diff --git a/services/moderation/phases/links.js b/services/moderation/phases/links.js new file mode 100644 index 000000000..0aed4ecd9 --- /dev/null +++ b/services/moderation/phases/links.js @@ -0,0 +1,26 @@ +const linkify = require('linkify-it')().tlds(require('tlds')); + +// This phase checks the comment if it has any links in it if the check is +// enabled. +module.exports = ( + ctx, + comment, + { asset: { settings: { premodLinksEnable } } } +) => { + if (premodLinksEnable && linkify.test(comment.body)) { + // Add the flag related to Trust to the comment. + return { + status: 'SYSTEM_WITHHELD', + actions: [ + { + action_type: 'FLAG', + user_id: null, + group_id: 'LINKS', + metadata: { + links: comment.body, + }, + }, + ], + }; + } +}; diff --git a/services/moderation/phases/premod.js b/services/moderation/phases/premod.js new file mode 100644 index 000000000..cbd3627b1 --- /dev/null +++ b/services/moderation/phases/premod.js @@ -0,0 +1,15 @@ +// This phase checks to see if the settings have premod enabled, if they do, +// the comment is premod, otherwise, it's just none. +module.exports = (ctx, comment, { asset: { settings: { moderation } } }) => { + // If the settings say that we're in premod mode, then the comment is in + // premod status. + if (moderation === 'PRE') { + return { + status: 'PREMOD', + }; + } + + return { + status: 'NONE', + }; +}; diff --git a/services/moderation/phases/staff.js b/services/moderation/phases/staff.js new file mode 100644 index 000000000..4b6b74946 --- /dev/null +++ b/services/moderation/phases/staff.js @@ -0,0 +1,10 @@ +const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../../config'); + +// If a given user is a staff member, always approve their comment. +module.exports = ctx => { + if (IGNORE_FLAGS_AGAINST_STAFF && ctx.user && ctx.user.isStaff()) { + return { + status: 'ACCEPTED', + }; + } +}; diff --git a/services/moderation/phases/wordlist.js b/services/moderation/phases/wordlist.js new file mode 100644 index 000000000..b71e0361e --- /dev/null +++ b/services/moderation/phases/wordlist.js @@ -0,0 +1,56 @@ +const { DISABLE_AUTOFLAG_SUSPECT_WORDS } = require('../../../config'); + +// This phase checks the comment against the wordlist. +module.exports = async (ctx, comment, { settings }) => { + const { connectors: { services: { Wordlist } } } = ctx; + + // Create a new instance of the Wordlist. + const wl = new Wordlist(); + + // Load the wordlist. + wl.upsert(settings.wordlist); + + // Scan the comment body for wordlist violations. + const { banned = null, suspect = null } = wl.scan('body', comment.body); + + // Decide the status based on whether or not the current asset/settings + // has pre-mod enabled or not. If the comment was rejected based on the + // wordlist, then reject it, otherwise if the moderation setting is + // premod, set it to `premod`. + if (banned) { + // Add the flag related to Trust to the comment. + return { + status: 'REJECTED', + actions: [ + { + action_type: 'FLAG', + user_id: null, + group_id: 'BANNED_WORD', + metadata: {}, + }, + ], + }; + } + + // If the comment has a suspect word or a link, we need to add a + // flag to it to indicate that it needs to be looked at. + // Otherwise just return the new comment. + + // If the wordlist has matched the suspect word filter and we haven't disabled + // auto-flagging suspect words, then we should flag the comment! + if (suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) { + // TODO: this is kind of fragile, we should refactor this to resolve + // all these const's that we're using like 'COMMENTS', 'FLAG' to be + // defined in a checkable schema. + return { + actions: [ + { + action_type: 'FLAG', + user_id: null, + group_id: 'SUSPECT_WORD', + metadata: {}, + }, + ], + }; + } +}; diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js index 8a5a98aa1..720297e12 100644 --- a/test/server/graph/mutations/createComment.js +++ b/test/server/graph/mutations/createComment.js @@ -49,6 +49,9 @@ describe('graph.mutations.createComment', () => { return graphql(schema, query, {}, context).then( ({ data, errors }) => { + if (errors) { + console.error(errors); + } expect(errors).to.be.undefined; if (error) { expect(data.createComment).to.have.property('comment').null; @@ -98,7 +101,9 @@ describe('graph.mutations.createComment', () => { async () => { const context = new Context({ user }); const { data, errors } = await graphql(schema, query, {}, context); - + if (errors) { + console.error(errors); + } expect(errors).to.be.undefined; if (error) { expect(data.createComment).to.have.property('comment').null; diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs index 0bc54990b..f8f6c62f2 100644 --- a/views/admin/confirm-email.ejs +++ b/views/admin/confirm-email.ejs @@ -20,9 +20,17 @@ - - - + diff --git a/webpack.config.js b/webpack.config.js index d46c2cb7d..3367727f5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,7 +19,11 @@ const targetPlugins = manager.section('targets').plugins; debug(`Using ${pluginsPath} as the plugin configuration path`); -const buildTargets = ['coral-admin', 'coral-docs']; +const buildTargets = [ + 'coral-admin', + 'coral-docs', + { name: 'coral-auth-callback', disablePolyfill: true }, +]; const buildEmbeds = ['stream']; @@ -156,9 +160,14 @@ const config = { modules: [ path.resolve(__dirname, 'plugins'), path.resolve(__dirname, 'client'), - ...buildTargets.map(target => - path.join(__dirname, 'client', target, 'src') - ), + ...buildTargets.map(target => { + if (typeof target !== 'string') { + target = target.name; + } + + return path.join(__dirname, 'client', target, 'src'); + }), + ...buildEmbeds.map(embed => path.join(__dirname, 'client', `coral-embed-${embed}`, 'src') ), @@ -276,10 +285,19 @@ module.exports = [ // All framework targets/embeds/plugins. applyConfig([ // Load in all the targets. - ...buildTargets.map(target => ({ - name: `${target}/bundle`, - path: path.join(__dirname, 'client/', target, '/src/index'), - })), + ...buildTargets.map(target => { + let disablePolyfill = false; + if (typeof target !== 'string') { + disablePolyfill = target.disablePolyfill; + target = target.name; + } + + return { + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index'), + disablePolyfill, + }; + }), // Load in all the embeds. ...buildEmbeds.map(embed => ({ diff --git a/yarn.lock b/yarn.lock index 4d5d3f52d..38f5963ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -212,14 +212,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" -always-error@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/always-error/-/always-error-1.0.0.tgz#95c84042cfa86f38c86ca6c2cc42c0a0103441b2" - -am-i-a-dependency@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/am-i-a-dependency/-/am-i-a-dependency-1.1.2.tgz#f9d3422304d6f642f821e4c407565035f6167f1f" - amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -230,10 +222,6 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - ansi-escapes@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" @@ -242,10 +230,6 @@ ansi-escapes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" -ansi-regex@^1.0.0, ansi-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-1.1.1.tgz#41c847194646375e6a1a5d10c3ca054ef9fc980d" - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -447,7 +431,7 @@ arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" -asap@^2.0.0, asap@~2.0.3: +asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1249,15 +1233,11 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@2.9.24: - version "2.9.24" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c" - bluebird@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" -bluebird@3.5.1, bluebird@^3.0.6, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0: +bluebird@^3.0.6, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1629,14 +1609,6 @@ chain-function@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" -chalk@2.3.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1647,6 +1619,14 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + change-emitter@^0.1.2: version "0.1.6" resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" @@ -1665,32 +1645,10 @@ charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" -chdir-promise@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/chdir-promise/-/chdir-promise-0.4.1.tgz#1888bb33719699c9fb72138c07556503c4913e85" - dependencies: - check-more-types "2.24.0" - debug "2.6.8" - lazy-ass "1.6.0" - q "1.5.0" - spots "0.5.0" - check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" -check-more-types@2.23.0: - version "2.23.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.23.0.tgz#6226264d30b1095aa1c0a5b874edbdd5d2d0a66f" - -check-more-types@2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" - -check-more-types@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.3.0.tgz#b8397c69dc92a3e645f18932c045b09c74419ec4" - cheerio@^0.20.0: version "0.20.0" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.20.0.tgz#5c710f2bab95653272842ba01c6ea61b3545ec35" @@ -1779,28 +1737,18 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - dependencies: - restore-cursor "^1.0.1" - cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" dependencies: restore-cursor "^2.0.0" -cli-table@0.3.1, cli-table@^0.3.1: +cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" dependencies: colors "1.0.3" -cli-width@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-1.1.1.tgz#a4d293ef67ebb7b88d4a4d42c0ccf00c4d1e366d" - cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -1911,14 +1859,10 @@ colors@1.0.3, colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@1.1.2, colors@^1.1.2, colors@~1.1.2: +colors@^1.1.2, colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" -colors@~0.6.0-1: - version "0.6.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" - combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" @@ -1931,10 +1875,6 @@ combined-stream@~0.0.4: dependencies: delayed-stream "0.0.5" -commander@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -1951,10 +1891,6 @@ commander@^2.11.0, commander@^2.9.0: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" -commander@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" - commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -2072,19 +2008,6 @@ content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" -conventional-commit-message@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/conventional-commit-message/-/conventional-commit-message-1.1.0.tgz#ece8c661a168e983692e1d5a14875acb59510f6a" - dependencies: - check-more-types "2.3.0" - cz-conventional-changelog "1.1.5" - lazy-ass "1.3.0" - word-wrap "1.1.0" - -conventional-commit-types@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz#5db95739d6c212acbe7b6f656a11b940baa68946" - convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -2374,26 +2297,6 @@ cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" -cz-conventional-changelog@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.1.5.tgz#0a4d1550c4e2fb6a3aed8f6cd858c21760e119b8" - dependencies: - word-wrap "^1.0.3" - -cz-conventional-changelog@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz#2f4bc7390e3244e4df293e6ba351e4c740a7c764" - dependencies: - conventional-commit-types "^2.0.0" - lodash.map "^4.5.1" - longest "^1.0.1" - right-pad "^1.0.1" - word-wrap "^1.0.3" - -d3-helpers@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/d3-helpers/-/d3-helpers-0.3.0.tgz#4b31dce4a2121a77336384574d893fbed5fb293d" - d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -3156,10 +3059,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -3317,13 +3216,6 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3400,16 +3292,6 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" -find-parent-dir@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" - -find-up@2.1.0, find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - dependencies: - locate-path "^2.0.0" - find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3417,12 +3299,11 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -findup@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/findup/-/findup-0.1.5.tgz#8ad929a3393bac627957a7e5de4623b06b0e2ceb" +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" dependencies: - colors "~0.6.0-1" - commander "~2.1.0" + locate-path "^2.0.0" flat-cache@^1.2.1: version "1.3.0" @@ -3683,57 +3564,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -ggit@1.23.1: - version "1.23.1" - resolved "https://registry.yarnpkg.com/ggit/-/ggit-1.23.1.tgz#e513c2f222a6249a46e4d0df354b8604b5baeedb" - dependencies: - always-error "1.0.0" - bluebird "3.5.0" - chdir-promise "0.4.1" - check-more-types "2.24.0" - cli-table "0.3.1" - colors "1.1.2" - commander "2.11.0" - d3-helpers "0.3.0" - debug "2.6.8" - find-up "2.1.0" - glob "7.1.2" - lazy-ass "1.6.0" - lodash "3.10.1" - moment "2.18.1" - optimist "0.6.1" - pluralize "6.0.0" - q "2.0.3" - quote "0.4.0" - ramda "0.24.1" - semver "5.4.1" - -ggit@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/ggit/-/ggit-2.4.0.tgz#b99d981f3ede2a3a8a8e4bbff578bc277d400588" - dependencies: - always-error "1.0.0" - bluebird "3.5.1" - chdir-promise "0.4.1" - check-more-types "2.24.0" - cli-table "0.3.1" - colors "1.1.2" - commander "2.11.0" - d3-helpers "0.3.0" - debug "3.1.0" - find-up "2.1.0" - glob "7.1.2" - lazy-ass "1.6.0" - lodash "4.17.4" - moment "2.19.1" - moment-timezone "0.5.13" - optimist "0.6.1" - pluralize "7.0.0" - q "2.0.3" - quote "0.4.0" - ramda "0.25.0" - semver "5.4.1" - git-up@^2.0.0: version "2.0.9" resolved "https://registry.yarnpkg.com/git-up/-/git-up-2.0.9.tgz#219bfd27c82daeead8495beb386dc18eae63636d" @@ -3797,17 +3627,6 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^5.0.3: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -3818,6 +3637,17 @@ glob@^5.0.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + 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" + global-dirs@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -4204,10 +4034,6 @@ hpkp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672" -hr@0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/hr/-/hr-0.1.3.tgz#d9aa30f5929dabfd0b65ba395938a3e184dbcafe" - hsts@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" @@ -4418,44 +4244,6 @@ inquirer-autocomplete-prompt@^0.12.1: inquirer "3.2.0" run-async "^2.3.0" -inquirer-confirm@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/inquirer-confirm/-/inquirer-confirm-0.2.2.tgz#6f406d037bf9d9e455ef0f953929f357fe9a8848" - dependencies: - bluebird "2.9.24" - inquirer "0.8.2" - -inquirer@0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" - dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" - cli-width "^2.0.0" - figures "^1.3.5" - lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" - through "^2.3.6" - -inquirer@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.8.2.tgz#41586548e1c5d9b3f81df7325034baacab6f58ab" - dependencies: - ansi-regex "^1.1.1" - chalk "^1.0.0" - cli-width "^1.0.1" - figures "^1.3.5" - lodash "^3.3.1" - readline2 "^0.1.1" - rx "^2.4.3" - through "^2.3.6" - inquirer@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.0.tgz#45b44c2160c729d7578c54060b3eed94487bb42b" @@ -4475,7 +4263,7 @@ inquirer@3.2.0: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@3.3.0, inquirer@^3.0.6, inquirer@^3.2.2: +inquirer@^3.0.6, inquirer@^3.2.2: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: @@ -5417,31 +5205,12 @@ kue@0.11.6: optionalDependencies: reds "^0.2.5" -largest-semantic-change@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/largest-semantic-change/-/largest-semantic-change-1.0.0.tgz#25dc538bdaaa8bbdc30276b1ebf902d47a34bf0e" - dependencies: - check-more-types "2.23.0" - lazy-ass "1.5.0" - latest-version@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" dependencies: package-json "^4.0.0" -lazy-ass@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.3.0.tgz#7d0d14eef3ec9702c6f30c60ea81f1a8d3f900fb" - -lazy-ass@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.5.0.tgz#ca15be243c7c475b8565cdbfa0f9c2f374f2a01d" - -lazy-ass@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" - lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -5732,10 +5501,6 @@ lodash.keysin@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-4.2.0.tgz#8cc3fb35c2d94acc443a1863e02fa40799ea6f28" -lodash.map@^4.5.1: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5796,11 +5561,7 @@ lodash.values@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" -lodash@3.10.1, lodash@^3.3.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - -lodash@4.17.4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: +lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -6115,24 +5876,14 @@ mocha@^3.1.2: mkdirp "0.5.1" supports-color "3.1.2" -moment-timezone@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90" - dependencies: - moment ">= 2.9.0" - -moment@2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" - -moment@2.19.1, moment@^2.10.3: - version "2.19.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" - -moment@2.x.x, "moment@>= 2.9.0": +moment@2.x.x: version "2.19.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.4.tgz#17e5e2c6ead8819c8ecfad83a0acccb312e94682" +moment@^2.10.3: + version "2.19.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" + moment@^2.18.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" @@ -6232,14 +5983,6 @@ murmurhash-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" -mute-stream@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.4.tgz#a9219960a6d5d5d046597aee51252c6655f7177e" - -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6678,10 +6421,6 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: dependencies: wrappy "1" -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -6728,6 +6467,10 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -7038,21 +6781,13 @@ platform@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.4.tgz#6f0fb17edaaa48f21442b3a975c063130f1c3ebd" -pluralize@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-6.0.0.tgz#d9b51afad97d3d51075cc1ddba9b132cacccb7ba" - -pluralize@7.0.0, pluralize@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" - pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" -pop-iterate@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3" +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" popsicle@^6.2.0: version "6.2.2" @@ -7477,24 +7212,13 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^4.4.0" -pre-git@^3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/pre-git/-/pre-git-3.16.0.tgz#a7656bc5f277185fd213c78f39f24f2cb603eb61" +pre-commit@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6" dependencies: - bluebird "3.5.1" - chalk "2.3.0" - check-more-types "2.24.0" - conventional-commit-message "1.1.0" - cz-conventional-changelog "2.1.0" - debug "2.6.9" - ggit "2.4.0" - inquirer "3.3.0" - lazy-ass "1.6.0" - require-relative "0.8.7" - shelljs "0.7.8" - simple-commit-message "3.3.2" - validate-commit-msg "2.14.0" - word-wrap "1.2.3" + cross-spawn "^5.0.1" + spawn-sync "^1.0.15" + which "1.2.x" prebuild-install@^2.3.0: version "2.3.0" @@ -7785,18 +7509,10 @@ q@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -q@1.5.0, q@^1.1.2: +q@^1.1.2: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" -q@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134" - dependencies: - asap "^2.0.0" - pop-iterate "^1.0.1" - weak-map "^1.0.5" - qs@6.5.1, qs@^6.1.0, qs@^6.2.0, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -7832,10 +7548,6 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" -quote@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01" - raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -7846,14 +7558,10 @@ railroad-diagrams@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" -ramda@0.24.1, ramda@^0.24.1: +ramda@^0.24.1: version "0.24.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" -ramda@0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" - randexp@^0.4.2: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" @@ -8159,21 +7867,6 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" -readline2@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-0.1.1.tgz#99443ba6e83b830ef3051bfd7dc241a82728d568" - dependencies: - mute-stream "0.0.4" - strip-ansi "^2.0.1" - -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -8425,10 +8118,6 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" -require-relative@0.8.7: - version "0.8.7" - resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" - require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -8467,13 +8156,6 @@ resolve@^1.1.7: dependencies: path-parse "^1.0.5" -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -8495,10 +8177,6 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -right-pad@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" - rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -8525,12 +8203,6 @@ rst-selector-parser@^2.2.3: lodash.flattendeep "^4.4.0" nearley "^2.7.10" -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - dependencies: - once "^1.3.0" - run-async@^2.2.0, run-async@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -8553,14 +8225,6 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" - -rx@^2.4.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566" - safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -8640,15 +8304,11 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -semver-regex@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" - "semver@2 || 3 || 4 || 5", semver@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" -semver@5.4.1, semver@^5.0.3, semver@^5.1.0, semver@^5.4.1: +semver@^5.0.3, semver@^5.1.0, semver@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -8764,7 +8424,7 @@ shell-quote@^1.6.1: array-reduce "~0.0.0" jsonify "~0.0.0" -shelljs@0.7.8, shelljs@^0.7.0: +shelljs@^0.7.0: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" dependencies: @@ -8780,22 +8440,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -simple-commit-message@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/simple-commit-message/-/simple-commit-message-3.3.2.tgz#52bdadb7f4f680d8b29c07af1826a6611f7ee783" - dependencies: - am-i-a-dependency "1.1.2" - check-more-types "2.24.0" - debug "2.6.9" - ggit "1.23.1" - hr "0.1.3" - inquirer "0.12.0" - inquirer-confirm "0.2.2" - largest-semantic-change "1.0.0" - lazy-ass "1.6.0" - semver "5.4.1" - word-wrap "1.2.3" - simple-get@^1.4.2: version "1.4.3" resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-1.4.3.tgz#e9755eda407e96da40c5e5158c9ea37b33becbeb" @@ -8956,6 +8600,13 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -8976,10 +8627,6 @@ split@0.3: dependencies: through "2" -spots@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/spots/-/spots-0.5.0.tgz#b7aa0f1ac389a5a6d57c21e98da1d53839405fe1" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -9108,12 +8755,6 @@ stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" -strip-ansi@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-2.0.1.tgz#df62c1aa94ed2f114e1d0f21fd1d50482b79a60e" - dependencies: - ansi-regex "^1.0.0" - strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -9738,15 +9379,6 @@ v8flags@^2.1.1: dependencies: user-home "^1.1.1" -validate-commit-msg@2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/validate-commit-msg/-/validate-commit-msg-2.14.0.tgz#e5383691012cbb270dcc0bc2a4effebe14890eac" - dependencies: - conventional-commit-types "^2.0.0" - find-parent-dir "^0.3.0" - findup "0.1.5" - semver-regex "1.0.0" - validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -9807,10 +9439,6 @@ watchpack@^1.4.0: chokidar "^1.7.0" graceful-fs "^4.1.2" -weak-map@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/weak-map/-/weak-map-1.0.5.tgz#79691584d98607f5070bd3b70a40e6bb22e401eb" - webidl-conversions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506" @@ -9898,6 +9526,12 @@ which@1, which@^1.2.12, which@^1.2.9: dependencies: isexe "^2.0.0" +which@1.2.x: + version "1.2.14" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" @@ -9940,14 +9574,6 @@ with@^5.0.0: acorn "^3.1.0" acorn-globals "^3.0.0" -word-wrap@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6" - -word-wrap@1.2.3, word-wrap@^1.0.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"