This commit is contained in:
Belen Curcio
2017-04-05 12:08:07 -03:00
41 changed files with 931 additions and 189 deletions
+15
View File
@@ -1,3 +1,18 @@
# excluded because we'll likely need to rebuild this.
node_modules
# scripts are used during development and testing, not
# production.
scripts
# documentation should not be visable in production.
docs
# static assets are rebuild in the docker container.
dist
# tests are not run in the docker container.
test
# we won't use the .git folder in production.
.git
+1 -4
View File
@@ -2,14 +2,11 @@ root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+2
View File
@@ -1,3 +1,5 @@
dist
client/lib
**/*.html
plugins/
plugins/**/node_modules
+4 -11
View File
@@ -1,20 +1,13 @@
node_modules
npm-debug.log*
dist
!dist/coral-admin
dist/coral-admin/bundle.js
test/e2e/reports
.DS_Store
*.iml
*.swp
dump.rdb
.env
*.cfg
.idea/
coverage/
.tags
.tags1
# remove plugin folders
# plugins
# plugins.json
*.swp
plugins.json
plugins
!plugins/coral-plugin-respect
+10 -5
View File
@@ -5,16 +5,21 @@ RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Setup the environment
ENV NODE_ENV production
ENV PATH /usr/src/app/bin:$PATH
ENV TALK_PORT 5000
EXPOSE 5000
# Install app dependencies
COPY package.json yarn.lock /usr/src/app/
RUN yarn install --production
# Bundle app source
COPY . /usr/src/app
# Install app dependencies and build static assets.
RUN yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn install --production && \
yarn cache clean
# Ensure the runtime of the container is in production mode.
ENV NODE_ENV production
CMD ["yarn", "start"]
+14
View File
@@ -0,0 +1,14 @@
FROM coralproject/talk:latest
# Bundle app source
ONBUILD COPY . /usr/src/app
# At this stage, we need to install the development dependancies again because
# we need to have webpack available. We then build the new dependancies and
# clear out the development dependancies again. After this we of course need to
# clear out the yarn cache, this saves quite a lot of size.
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
NODE_ENV=production cli plugins reconcile && \
NODE_ENV=production yarn build && \
NODE_ENV=production yarn install --production && \
yarn cache clean
+119 -1
View File
@@ -9,7 +9,7 @@ All plugins must be registered in the root file `plugins.json`.
The format for this file is thus:
```js
```json
{
"server": [
"people"
@@ -22,6 +22,42 @@ name in the `plugins/` folder. For example, the above `plugins.json` would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
External plugins can be resolved by running:
```bash
./bin/cli plugins reconcile
```
This will also traverse into local plugin folders and install their
dependancies. _Note that if the plugin is already installed and available in the
node_modules folder, it will not be fetched again unless there is a version
mismatch._
## Plugin Dependencies
From your plugins you may import any component of server code relative to the
project root. An example could be:
```js
const cache = require('services/cache');
```
You may also include additional external depenancies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Server Plugins
### Specification
@@ -170,6 +206,88 @@ If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `router`
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
```
The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications. We also provide the authorization
middleware via:
```js
const authorization = require('middleware/authorization');
module.exports = {
router(router) {
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
res.json({people: [{name: 'SECRET PEOPLE'}]});
});
}
}
```
#### Field: `passport`
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
},
router(router) {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
}
};
```
This is a full example including the routes hook to add the required components
to the application router to support a different auth strategy.
### Full Example
Contents of `plugins.json`:
+1 -1
View File
@@ -1,6 +1,6 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
+1 -1
View File
@@ -3,7 +3,7 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const passport = require('./services/passport');
const {passport} = require('./services/passport');
const session = require('express-session');
const enabled = require('debug').enabled;
const RedisStore = require('connect-redis')(session);
+1
View File
@@ -13,6 +13,7 @@ program
.command('setup', 'setup the application')
.command('jobs', 'work with the job queues')
.command('users', 'work with the application auth')
.command('plugins', 'provides utilities for interacting with the plugin system')
.parse(process.argv);
/**
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
// Interface heavily inspired by the yarn package manager:
// https://yarnpkg.com/
const program = require('./commander');
// Make things colorful!
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs');
const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const {plugins, itteratePlugins, isInternal} = require('../plugins');
function existsInNodeModules(name) {
try {
resolve.sync(name, {basedir: dir});
return true;
} catch (e) {
return false;
}
}
function versionMatch(name, version) {
try {
let matched = false;
resolve.sync(name, {
basedir: dir,
packageFilter: (pkg) => {
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
matched = true;
}
return pkg;
}
});
return matched;
} catch (e) {
return false;
}
}
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
const fetchable = [];
const local = [];
const upgradable = [];
if (!quiet) {
console.log();
console.log(' +local (l) packages in your project');
console.log(' +external (e) packages are external');
console.log(' +outofdate (oe) packages are external but are out of date');
console.log(' +missing (m) packages are not found');
console.log();
}
for (let i in plugins) {
let section = itteratePlugins(plugins[i]);
for (let j in section) {
let {name, version} = section[j];
let namespaced = name.charAt(0) === '@';
let dep = name.split('/')
.slice(0, namespaced ? 2 : 1)
.join('/');
// Ignore relative modules, which aren't installed by NPM
if (!dep.match(EXTERNAL) && !namespaced) {
return;
}
if (isInternal(dep)) {
if (!quiet) {
console.log(` l ${name}`);
}
local.push({name, version});
continue;
}
if (!existsInNodeModules(dep)) {
if (!quiet) {
console.log(` m ${name}`);
}
fetchable.push({name, version});
} else if (!versionMatch(dep, version)) {
// A plugin was found, yet the current version does not match the
// current version installed. We should warn if upgradeRemote is
// not enabled that it is currently not supported.
if (!upgradeRemote) {
if (!quiet) {
console.warn(` oe ${name} (package upgrade may be required)`.bgRed);
}
continue;
}
console.log(` oe ${name} (package upgrade may be required)`);
upgradable.push({name, version});
} else {
if (!quiet) {
console.log(` e ${name}`);
}
if (upgradeRemote) {
upgradable.push({name, version});
}
}
}
}
if (!quiet) {
console.log();
}
return {local, fetchable, upgradable};
}
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
if (fetchable.length > 0) {
console.log(`$ yarn add ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
if (!dryRun) {
let args = [
'add',
...fetchable.map(({name, version}) => `${name}@${version}`)
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
}
console.log(output.stdout.toString());
}
}
if (upgradable.length > 0) {
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
if (!dryRun) {
let args = [
'upgrade',
...upgradable.map(({name, version}) => `${name}@${version}`)
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
}
console.log(output.stdout.toString());
}
}
return {upgradable, fetchable};
}
async function reconcileLocalPlugins({skipRemote, dryRun}) {
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
const {local} = reconcilePackages({quiet: true});
for (let i in local) {
let {name} = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
}
let wd = path.join(dir, 'plugins', name);
console.log(`$ cd ${wd.cyan} && yarn`);
if (!dryRun) {
let args = [];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd
});
if (output.status) {
throw new Error('Could not install local plugin dependencies, errors occured during install');
}
console.log(output.stdout.toString());
}
}
}
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
let startTime = new Date();
// 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});
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
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 endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
console.log(`✨ Done in ${totalTime}s.`);
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('list')
.description('')
.action(reconcilePackages);
program
.command('reconcile')
.description('reconciles local plugin dependencies and downloads external plugins')
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
}
+4 -3
View File
@@ -1,6 +1,6 @@
machine:
node:
version: 7.6
version: 7
services:
- docker
- redis
@@ -20,6 +20,7 @@ dependencies:
# - sudo service mongod restart
# Install node dependencies.
- yarn --version
- yarn
cache_directories:
- ~/.cache/yarn
@@ -47,9 +48,9 @@ deployment:
release:
tag: /v[0-9]+(\.[0-9]+)*/
commands:
- bash ./scripts/deploy.sh
- bash ./scripts/docker.sh deploy
latest:
branch: master
commands:
- bash ./scripts/deploy.sh
- bash ./scripts/docker.sh deploy
-23
View File
@@ -1,23 +0,0 @@
# Coral Admin
This app handles moderation for Talk (and maybe more later on)
## Installation
$ yarn install
$ cp config.sample.json config.json
Then change `config.json` to adjust it to your project
## Building for production
$ yarn build
The public folder has everything you need for deployment. You can just copy that folder to your favorite static web server
## Development
$ yarn start
A development server will be running at http://localhost:4132
@@ -5,6 +5,7 @@
.Comment {
margin-bottom: 15px;
position: relative;
}
.pendingComment {
+2 -5
View File
@@ -148,8 +148,7 @@ class Comment extends React.Component {
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
<div>
<div className={highlighted === comment.id ? 'highlighted-comment' : ''}>
<AuthorName
author={comment.user}/>
{ isStaff(comment.tags)
@@ -192,7 +191,6 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
</IfUserCanModifyBest>
</ActionButton>
<Slot fill="Comment.Detail" commentId={comment.id} />
</div>
<div className="commentActionsRight comment__action-container">
<ActionButton>
@@ -246,8 +244,7 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
comment={reply}
/>;
comment={reply} />;
})
}
{
+80 -7
View File
@@ -6,16 +6,17 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations';
const lang = new I18n(translations);
import {TabBar, Tab, TabContent, Spinner} from 'coral-ui';
import {TabBar, Tab, TabContent, Spinner, Button} from 'coral-ui';
const {logout, showSignInDialog, requestConfirmEmail} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {updateCountCache} from 'coral-framework/actions/asset';
import {updateCountCache, viewAllComments} from 'coral-framework/actions/asset';
import {notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
@@ -31,7 +32,7 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
import Comment from './Comment';
import HighlightedComment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
@@ -69,10 +70,30 @@ class Embed extends Component {
pym.sendMessage('childReady');
}
componentWillUnmount () {
clearInterval(this.state.countPoll);
}
componentWillReceiveProps (nextProps) {
const {loadAsset} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
const {getCounts, updateCountCache} = this.props;
const {asset} = nextProps.data;
updateCountCache(asset.id, asset.commentCount);
this.setState({
countPoll: setInterval(() => {
const {asset} = this.props.data;
getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
});
}, NEW_COMMENT_COUNT_POLL_INTERVAL)
});
}
}
@@ -80,7 +101,7 @@ class Embed extends Component {
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
}
}
@@ -98,6 +119,8 @@ class Embed extends Component {
const {closedAt, countCache = {}} = this.props.asset;
const {loading, asset, refetch, comment} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
const highlightedComment = comment && comment.parent ? comment.parent : comment;
const openStream = closedAt === null;
@@ -125,6 +148,16 @@ class Embed extends Component {
<Tab>{lang.t('MY_COMMENTS')}</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{
highlightedComment &&
<Button
cStyle='darkGrey'
style={{float: 'right'}}
onClick={() => {
this.props.viewAllComments();
this.props.data.refetch();
}}>{lang.t('showAllComments')}</Button>
}
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
<TabContent show={activeTab === 0}>
{
@@ -171,9 +204,11 @@ class Embed extends Component {
offset={signInOffset}/>}
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
{loggedIn && <ModerationLink assetId={asset.id} isAdmin={isAdmin} />}
{/* the highlightedComment is isolated after the user followed a permalink */}
{
highlightedComment &&
<Comment
highlightedComment
? <HighlightedComment
refetch={refetch}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
@@ -230,6 +265,43 @@ class Embed extends Component {
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
loadMore={this.props.loadMore}/>
comment={highlightedComment} />
: <div>
<NewCount
commentCount={asset.commentCount}
countCache={countCache[asset.id]}
loadMore={this.props.loadMore}
firstCommentDate={firstCommentDate}
assetId={asset.id}
updateCountCache={this.props.updateCountCache}
/>
<div className="embed__stream">
<Stream
open={openStream}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
setActiveReplyBox={this.setActiveReplyBox}
activeReplyBox={this.state.activeReplyBox}
asset={asset}
currentUser={user}
postLike={this.props.postLike}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
addCommentTag={this.props.addCommentTag}
removeCommentTag={this.props.removeCommentTag}
loadMore={this.props.loadMore}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
</div>
<LoadMore
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
loadMore={this.props.loadMore} />
</div>
}
</TabContent>
<TabContent show={activeTab === 1}>
<ProfileContainer
@@ -266,6 +338,7 @@ const mapDispatchToProps = dispatch => ({
editName: (username) => dispatch(editName(username)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
updateCountCache: (id, count) => dispatch(updateCountCache(id, count)),
viewAllComments: () => dispatch(viewAllComments()),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
});
@@ -279,5 +352,5 @@ export default compose(
addCommentTag,
removeCommentTag,
deleteAction,
queryStream,
queryStream
)(Embed);
+3 -3
View File
@@ -5,7 +5,7 @@ import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments';
import {Button} from 'coral-ui';
const lang = new I18n(translations);
const loadMoreComments = (assetId, comments, loadMore, parentId) => {
const loadMoreComments = (assetId, comments, loadMore, parentId, replyCount) => {
let cursor = null;
if (comments.length) {
@@ -15,7 +15,7 @@ const loadMoreComments = (assetId, comments, loadMore, parentId) => {
}
loadMore({
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
limit: parentId ? replyCount : ADDTL_COMMENTS_ON_LOAD_MORE,
cursor,
asset_id: assetId,
parent_id: parentId,
@@ -48,7 +48,7 @@ class LoadMore extends React.Component {
<Button
onClick={() => {
this.initialState = false;
loadMoreComments(assetId, comments, loadMore, parentId);
loadMoreComments(assetId, comments, loadMore, parentId, replyCount);
}}>
{topLevel ? lang.t('viewMoreComments') : this.replyCountFormat(replyCount)}
</Button>
-21
View File
@@ -1,6 +1,5 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comments';
class Stream extends React.Component {
@@ -27,26 +26,6 @@ class Stream extends React.Component {
this.state = {activeReplyBox: '', countPoll: null};
}
componentDidMount() {
const {asset, getCounts, updateCountCache} = this.props;
updateCountCache(asset.id, asset.commentCount);
// Note: Apollo's built-in polling doesn't work with fetchMore queries, so a
// setInterval is being used instead.
this.setState({
countPoll: setInterval(() => getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
}), NEW_COMMENT_COUNT_POLL_INTERVAL),
});
}
componentWillUnmount() {
clearInterval(this.state.countPoll);
}
render () {
const {
comments,
+1 -3
View File
@@ -16,6 +16,7 @@ body {
font-size: 14px;
margin: 0px;
padding: 0px 0px 50px 0px;
height: auto !important;
}
.expandForSignin {
@@ -306,10 +307,7 @@ button.comment__action-button[disabled],
display: none;
background-color: white;
border: 1px solid black;
width: calc(100% - 15px);
position: absolute;
top: 70px;
right: 0;
padding: 5px;
}
+9
View File
@@ -74,6 +74,15 @@ function configurePymParent(pymParent, asset_url) {
snackbar.style.opacity = 0;
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function () {
window.history.replaceState(
{},
document.title,
location.origin + location.pathname + location.search
);
});
pymParent.onMessage('coral-alert', function (message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
+35
View File
@@ -1,6 +1,7 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
@@ -49,3 +50,37 @@ export const updateOpenStatus = status => dispatch => {
dispatch(updateOpenStream({closedAt: new Date().getTime()}));
}
};
function removeParam(key, sourceURL) {
let rtn = sourceURL.split('?')[0];
let param;
let params_arr = [];
let queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : '';
if (queryString !== '') {
params_arr = queryString.split('&');
for (let i = params_arr.length - 1; i >= 0; i -= 1) {
param = params_arr[i].split('=')[0];
if (param === key) {
params_arr.splice(i, 1);
}
}
rtn = `${rtn}?${params_arr.join('&')}`;
}
return rtn;
}
export const viewAllComments = () => {
// remove the comment_id url param
const modifiedUrl = removeParam('comment_id', location.href);
try {
// "window" here refers to the embedded iframe
window.history.replaceState({}, document.title, modifiedUrl);
// also change the parent url
pym.sendMessage('coral-view-all-comments');
} catch (e) { /* not sure if we're worried about old browsers */ }
return {type: actions.VIEW_ALL_COMMENTS};
};
@@ -9,3 +9,5 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const UPDATE_COUNT_CACHE = 'UPDATE_COUNT_CACHE';
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
@@ -5,6 +5,7 @@ import GET_COUNTS from './getCounts.graphql';
import MY_COMMENT_HISTORY from './myCommentHistory.graphql';
import uniqBy from 'lodash/uniqBy';
import sortBy from 'lodash/sortBy';
import isNil from 'lodash/isNil';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
@@ -20,6 +21,7 @@ function getQueryVariable(variable) {
return null;
}
// get the counts of the top-level comments
export const getCounts = (data) => ({asset_id, limit, sort}) => {
return data.fetchMore({
query: GET_COUNTS,
@@ -41,23 +43,48 @@ export const getCounts = (data) => ({asset_id, limit, sort}) => {
});
};
// handle paginated requests for more Comments pertaining to the Asset
export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, sort}, newComments) => {
return data.fetchMore({
query: LOAD_MORE,
variables: {
limit,
cursor,
parent_id,
asset_id,
sort
limit, // how many comments are we returning
cursor, // the date of the first/last comment depending on the sort order
parent_id, // if null, we're loading more top-level comments, if not, we're loading more replies to a comment
asset_id, // the id of the asset we're currently on
sort // CHRONOLOGICAL or REVERSE_CHRONOLOGICAL
},
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => {
let updatedAsset;
if (parent_id) {
if (!isNil(oldData.comment)) { // loaded replies on a highlighted (permalinked) comment
let comment = {};
if (oldData.comment && oldData.comment.parent) {
// put comments (replies) onto the oldData.comment.parent object
// the initial comment permalinked was a reply
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.parent.replies], 'id');
comment.parent = {...oldData.comment.parent, replies: sortBy(uniqReplies, 'created_at')};
} else if (oldData.comment) {
// put the comments (replies) directly onto oldData.comment
// the initial comment permalinked was a top-level comment
const uniqReplies = uniqBy([...new_top_level_comments, ...oldData.comment.replies], 'id');
comment.replies = sortBy(uniqReplies, 'created_at');
}
updatedAsset = {
...oldData,
comment: {
...oldData.comment,
...comment
}
};
} else if (parent_id) { // If loading more replies
// If loading more replies
updatedAsset = {
...oldData,
asset: {
@@ -76,9 +103,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
})
}
};
} else {
} else { // If loading more top-level comments
// If loading more top-level comments
updatedAsset = {
...oldData,
asset: {
@@ -94,8 +120,11 @@ export const loadMore = (data) => ({limit, cursor, parent_id = null, asset_id, s
});
};
// load the comment stream.
export const queryStream = graphql(STREAM_QUERY, {
options: () => {
// where the query string is from the embeded iframe url
let comment_id = getQueryVariable('comment_id');
let has_comment = comment_id != null;
@@ -1,10 +1,18 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
# the comment here is for loading one comment and it's children, probably after following a permalink
# $has_comment is derived from the comment_id query param in the iframe url,
# which is in turn pulled from the host page url
comment(id: $comment_id) @include(if: $has_comment) {
...commentView
replyCount
replies {
...commentView
}
parent {
...commentView
replyCount
replies {
...commentView
}
+2
View File
@@ -13,6 +13,7 @@
"error": "Usernames can contain letters, numbers and _ only"
},
"viewMoreComments": "view more comments",
"showAllComments": "Show all comments",
"viewReply": "view reply",
"viewAllRepliesInitial": "view all {0} replies",
"viewAllReplies": "view {0} replies",
@@ -56,6 +57,7 @@
"newCount": "Ver {0} {1} más",
"comment": "commentario",
"comments": "commentarios",
"showAllComments": "Mostrar todos los comentarios",
"error": {
"emailNotVerified": "E-mail {0} no verificado.",
"email": "No es un e-mail válido",
@@ -23,6 +23,10 @@ class PermalinkButton extends React.Component {
}
toggle () {
// I wish I could position this with a stylesheet, but top-level comments with
// nested replies throws everything off, as well as very long comments
this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
this.setState({popoverOpen: !this.state.popoverOpen});
}
@@ -48,11 +52,16 @@ class PermalinkButton extends React.Component {
const {copySuccessful, copyFailure} = this.state;
return (
<div className={`${name}-container`}>
<button onClick={this.toggle} className={`${name}-button`}>
<button
ref={ref => this.linkButton = ref}
onClick={this.toggle}
className={`${name}-button`}>
{lang.t('permalink.permalink')}
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
</button>
<div className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
<div
ref={ref => this.popover = ref}
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
<input
className={`${name}-copy-field`}
type='text'
+2 -9
View File
@@ -4,22 +4,15 @@
box-sizing: border-box;
/* box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3); */
border: solid 1px rgba(153, 153, 153, 0.33);
width: auto;
margin: 0 auto;
left: 0;
margin: 0 5px;
top: 0;
left: 0;
width: 100%;
margin: 0;
margin-top: -13px;
&::before {
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 8.69em;
right: 7em;
bottom: -20px;
z-index: 2;
}
@@ -29,7 +22,7 @@
border: 10px solid transparent;
border-top-color: rgba(153, 153, 153, 0.33);
position: absolute;
right: 8.69em;
right: 7em;
bottom: -21px;
z-index: 1;
}
+1 -1
View File
@@ -1,7 +1,7 @@
const loaders = require('./loaders');
const mutators = require('./mutators');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:graph:context');
/**
+1 -1
View File
@@ -8,7 +8,7 @@ const Metrics = require('./metrics');
const Settings = require('./settings');
const Users = require('./users');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
let loaders = [
+1 -1
View File
@@ -5,7 +5,7 @@ const Comment = require('./comment');
const Action = require('./action');
const User = require('./user');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
let mutators = [
+1 -1
View File
@@ -20,7 +20,7 @@ const UserError = require('./user_error');
const User = require('./user');
const ValidationUserError = require('./validation_user_error');
const plugins = require('../../plugins');
const plugins = require('../../services/plugins');
// Provide the core resolvers.
let resolvers = {
+1 -1
View File
@@ -2,7 +2,7 @@ const {makeExecutableSchema} = require('graphql-tools');
const {maskErrors} = require('graphql-errors');
const {decorateWithHooks} = require('./hooks');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
const resolvers = require('./resolvers');
const typeDefs = require('./typeDefs');
+1 -1
View File
@@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const {mergeStrings} = require('gql-merge');
const debug = require('debug')('talk:graph:typeDefs');
const plugins = require('../plugins');
const plugins = require('../services/plugins');
/**
* Plugin support requires us to merge the type definitions from the loaded
+6
View File
@@ -49,11 +49,14 @@
},
"homepage": "https://github.com/coralproject/talk#readme",
"dependencies": {
"app-module-path": "^2.2.0",
"bcrypt": "^1.0.2",
"body-parser": "^1.17.1",
"cli-table": "^0.3.1",
"colors": "^1.1.2",
"commander": "^2.9.0",
"connect-redis": "^3.1.0",
"cross-spawn": "^5.1.0",
"csurf": "^1.9.0",
"dataloader": "^1.3.0",
"debug": "^2.6.3",
@@ -80,6 +83,7 @@
"mongoose": "^4.9.1",
"morgan": "^1.8.1",
"natural": "^0.4.0",
"node-emoji": "^1.5.1",
"node-fetch": "^1.6.3",
"nodemailer": "^2.6.4",
"parse-duration": "^0.1.1",
@@ -89,6 +93,8 @@
"react-apollo": "^1.0.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"uuid": "^2.0.3"
},
+101 -9
View File
@@ -1,5 +1,10 @@
const fs = require('fs');
const path = require('path');
const resolve = require('resolve');
const debug = require('debug')('talk:plugins');
// Add support for require rewriting.
require('app-module-path').addPath(__dirname);
let plugins = {};
@@ -16,17 +21,95 @@ try {
}
}
/**
* isInternal checks to see if a given plugin is internal, and returns true
* if it is.
*
* @param {String} name
* @returns {Boolean}
*/
function isInternal(name) {
const internalPluginPath = path.join(__dirname, 'plugins', name);
// Check to see if this plugin exists internally, because if it doesn't, it is
// external.
return fs.existsSync(internalPluginPath);
}
/**
* Returns the plugin path for the given plugin name.
*
* @param {any} name
* @returns
*/
function pluginPath(name) {
if (isInternal(name)) {
return path.join(__dirname, 'plugins', name);
}
try {
return resolve.sync(name, {basedir: process.cwd()});
} catch (e) {
return undefined;
}
}
function itteratePlugins(plugins) {
return plugins.map((p) => {
let plugin = {};
// This checks to see if the structure for this entry is an object:
//
// {"people": "^1.2.0"}
//
// otherwise it's checked whether it matches the local version:
//
// "people"
//
if (typeof p === 'object') {
plugin.name = Object.keys(p).find((name) => name !== null);
plugin.version = p[plugin.name];
} else if (typeof p === 'string') {
plugin.name = p;
plugin.version = `file:./plugins/${plugin.name}`;
} else {
throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`);
}
// Get the path for the plugin.
plugin.path = pluginPath(plugin.name);
return plugin;
});
}
/**
* Stores a reference to a section for a section of Plugins.
*/
class PluginSection {
constructor(plugin_names) {
this.plugins = plugin_names.map((plugin_name) => {
let plugin = require(`./plugins/${plugin_name}`);
constructor(plugins) {
this.plugins = itteratePlugins(plugins).map((plugin) => {
if (typeof plugin.path === 'undefined') {
throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`);
}
// Ensure we have a default plugin name, but allow the name to be
// overrided by the plugin.
plugin.name = plugin.name || plugin_name;
try {
plugin.module = require(plugin.path);
} catch (e) {
if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) {
console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`));
throw e;
}
console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`));
throw e;
}
if (isInternal(plugin.name)) {
debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`);
} else {
debug(`loading external plugin '${plugin.name}' from '${plugin.path}'`);
}
return plugin;
});
@@ -38,8 +121,11 @@ class PluginSection {
*/
hook(hook) {
return this.plugins
.filter((plugin) => hook in plugin)
.map((plugin) => ({plugin, [hook]: plugin[hook]}));
.filter(({module}) => hook in module)
.map((plugin) => ({
plugin,
[hook]: plugin.module[hook]
}));
}
}
@@ -78,4 +164,10 @@ class PluginManager {
}
}
module.exports = new PluginManager(plugins);
module.exports = {
plugins,
PluginManager,
isInternal,
pluginPath,
itteratePlugins
};
+1 -49
View File
@@ -1,7 +1,6 @@
const express = require('express');
const passport = require('../../../services/passport');
const {passport, HandleAuthCallback, HandleAuthPopupCallback} = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
const router = express.Router();
@@ -34,53 +33,6 @@ router.delete('/', authorization.needed(), (req, res) => {
// PASSPORT ROUTES
//==============================================================================
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Local auth endpoint, will recieve a email and password
*/
+10
View File
@@ -1,5 +1,7 @@
const express = require('express');
const path = require('path');
const plugins = require('../services/plugins');
const debug = require('debug')('talk:routes');
const router = express.Router();
@@ -22,4 +24,12 @@ if (process.env.NODE_ENV !== 'production') {
});
}
// Inject server route plugins.
plugins.get('server', 'router').forEach((plugin) => {
debug(`added plugin '${plugin.plugin.name}'`);
// Pass the root router to the plugin to mount it's routes.
plugin.router(router);
});
module.exports = router;
+24 -11
View File
@@ -2,9 +2,7 @@
set -e
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
# Sourced from https://segment.com/blog/ci-at-segment/
# Inspired by https://segment.com/blog/ci-at-segment/
deploy_tag() {
# Find our individual versions from the tags
@@ -28,6 +26,7 @@ deploy_tag() {
do
echo "==> tagging $version"
docker tag coralproject/talk:latest coralproject/talk:$version
docker tag coralproject/talk:latest-onbuild coralproject/talk:$version-onbuild
done
# Push each of the tags to docker hub, including latest
@@ -35,21 +34,35 @@ deploy_tag() {
do
echo "==> pushing $version"
docker push coralproject/talk:$version
docker push coralproject/talk:$version-onbuild
done
}
deploy_latest() {
echo "==> pushing latest"
docker push coralproject/talk:latest
docker push coralproject/talk:latest-onbuild
}
# build the repo
docker build -t coralproject/talk .
# build the repo, including the onbuild tagged versions.
docker build -t coralproject/talk:latest -f Dockerfile .
docker build -t coralproject/talk:latest-onbuild -f Dockerfile.onbuild .
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
if [ "$1" = "deploy" ]
then
deploy_tag
else
deploy_latest
fi
if [[ -n "$DOCKER_EMAIL" && -n "$DOCKER_USER" && -n "$DOCKER_PASS" ]]
then
# Log the Docker Daemon in
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
fi
# deploy based on the env
if [ -n "$CIRCLE_TAG" ]
then
deploy_tag
else
deploy_latest
fi
fi
+63 -1
View File
@@ -7,6 +7,7 @@ const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const errors = require('../errors');
const debug = require('debug')('talk:passport');
const plugins = require('./plugins');
//==============================================================================
// SESSION SERIALIZATION
@@ -27,6 +28,52 @@ passport.deserializeUser((id, done) => {
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Validates that a user is allowed to login.
* @param {User} user the user to be validated
@@ -329,4 +376,19 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET &&
console.error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
}
module.exports = passport;
// Inject server route plugins.
plugins.get('server', 'passport').forEach((plugin) => {
debug(`added plugin '${plugin.plugin.name}'`);
// Pass the passport.js instance to the plugin to allow it to inject it's
// functionality.
plugin.passport(passport);
});
module.exports = {
passport,
ValidateUserLogin,
HandleFailedAttempt,
HandleAuthCallback,
HandleAuthPopupCallback
};
+3
View File
@@ -0,0 +1,3 @@
const {plugins, PluginManager} = require('../plugins');
module.exports = new PluginManager(plugins);
+54 -5
View File
@@ -194,6 +194,10 @@ apollo-client@^1.0.0, apollo-client@^1.0.0-rc.9:
"@types/graphql" "^0.9.0"
"@types/isomorphic-fetch" "0.0.33"
app-module-path@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
"apparatus@>= 0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/apparatus/-/apparatus-0.0.9.tgz#37dcd25834ad0b651076596291db823eeb1908bd"
@@ -1715,7 +1719,7 @@ colors@1.0.3:
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"
@@ -1963,6 +1967,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
create-hash "^1.1.0"
inherits "^2.0.1"
cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
lru-cache "^4.0.1"
shebang-command "^1.2.0"
which "^1.2.9"
crypt@~0.0.1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
@@ -4865,6 +4877,13 @@ lru-cache@2, lru-cache@^2.5.0:
version "2.7.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
lru-cache@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
dependencies:
pseudomap "^1.0.1"
yallist "^2.0.0"
lru-cache@~2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
@@ -5253,6 +5272,12 @@ node-dir@^0.1.10:
dependencies:
minimatch "^3.0.2"
node-emoji@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1"
dependencies:
string.prototype.codepointat "^0.2.0"
node-fetch@^1.0.1, node-fetch@^1.3.3, node-fetch@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04"
@@ -6373,6 +6398,10 @@ ps-tree@^1.0.1:
dependencies:
event-stream "~3.3.0"
pseudomap@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
public-encrypt@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@@ -7014,9 +7043,11 @@ resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
resolve@^1.1.6, resolve@^1.1.7:
version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235"
dependencies:
path-parse "^1.0.5"
restore-cursor@^1.0.1:
version "1.0.1"
@@ -7183,6 +7214,16 @@ sha.js@^2.3.6:
dependencies:
inherits "^2.0.1"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
dependencies:
shebang-regex "^1.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
shelljs@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.0.tgz#ce1ed837b4b0e55b5ec3dab84251ab9dbdc0c7ec"
@@ -7451,6 +7492,10 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
string.prototype.codepointat@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -8081,7 +8126,7 @@ which@1.1.1:
dependencies:
is-absolute "^0.1.7"
which@^1.1.1:
which@^1.1.1, which@^1.2.9:
version "1.2.12"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
dependencies:
@@ -8183,6 +8228,10 @@ y18n@^3.2.0, y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
yallist@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yargs-parser@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4"