mirror of
https://github.com/wassname/talk.git
synced 2026-07-10 03:31:22 +08:00
Merge pull request #458 from coralproject/docker-plugins
Plugin Router/Passport/Deployment
This commit is contained in:
@@ -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,3 +1,5 @@
|
||||
dist
|
||||
client/lib
|
||||
**/*.html
|
||||
plugins/
|
||||
plugins/**/node_modules
|
||||
+1
-9
@@ -1,20 +1,12 @@
|
||||
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
|
||||
*.swp
|
||||
plugins
|
||||
plugins.json
|
||||
|
||||
+10
-5
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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`:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
Executable
+297
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -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');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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": "^0.10.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
@@ -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,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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
Regular → Executable
+24
-11
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const {plugins, PluginManager} = require('../plugins');
|
||||
|
||||
module.exports = new PluginManager(plugins);
|
||||
@@ -189,6 +189,10 @@ apollo-client@^0.8.3:
|
||||
"@types/graphql" "^0.8.0"
|
||||
"@types/isomorphic-fetch" "0.0.30"
|
||||
|
||||
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"
|
||||
@@ -1641,7 +1645,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"
|
||||
|
||||
@@ -1883,6 +1887,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"
|
||||
@@ -4763,6 +4775,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"
|
||||
@@ -5151,6 +5170,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"
|
||||
@@ -6263,6 +6288,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"
|
||||
@@ -6487,11 +6516,7 @@ react-apollo@^0.10.0:
|
||||
optionalDependencies:
|
||||
react-dom "0.14.x || 15.* || ^15.0.0"
|
||||
|
||||
"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@^15.3.1:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f"
|
||||
|
||||
react-dom@^15.4.2:
|
||||
"react-dom@0.14.x || 15.* || ^15.0.0", react-dom@^15.3.1, react-dom@^15.4.2:
|
||||
version "15.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
|
||||
dependencies:
|
||||
@@ -6561,15 +6586,7 @@ react-tagsinput@^3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.14.0.tgz#6729f8d5b313ed8fbb35496205ec2a97654af6bc"
|
||||
|
||||
react@^15.3.1:
|
||||
version "15.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e"
|
||||
dependencies:
|
||||
fbjs "^0.8.4"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react@^15.4.2:
|
||||
react@^15.3.1, react@^15.4.2:
|
||||
version "15.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
|
||||
dependencies:
|
||||
@@ -6914,9 +6931,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"
|
||||
@@ -7083,6 +7102,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"
|
||||
@@ -7351,6 +7380,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"
|
||||
@@ -7971,7 +8004,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:
|
||||
@@ -8073,6 +8106,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"
|
||||
|
||||
Reference in New Issue
Block a user